Skip to content

C4 and arc42 views

Audience: anyone who needs the whole system on one page, from who uses it down to the classes on the query path. overview.md covers the design principles, the module table and the data model. This page adds the four C4 zoom levels and the arc42 building-block, runtime and deployment views, and it links back to overview.md instead of repeating it.

The diagrams use C4 notation inside ordinary Mermaid flowcharts. Mermaid has a C4Context syntax, but it is still experimental and lays out poorly at this size. Every box names a real file, class or workflow, and diagrams.yml parses every block on each push. Dashed lines and dashed boxes are optional: nothing fails without them.

Contents


Repository layout

The top-level folders fall into four groups. Product is what a learner runs, assurance proves it still works, knowledge explains it, and operations runs the course on GitHub. The nanorag/ package is the only code the other folders import.

nanorag/            the toolkit: 19 modules, numpy + pandas + matplotlib only
  corpus.py           fact graph → documents, eval questions, personas, MultiHop-RAG loader
  chunking.py         7 strategies behind the STRATEGIES registry; stable chunk ids
  embed.py            BaseEmbedder → LsaEmbedder (default), Hashing, SentenceTransformers, Bedrock
  store.py            InMemoryIndex: sqlite :memory:, FTS5, vectors, NSW graph, ACL, aliases
  retrieve.py         lexical, dense, grep and hybrid retrievers; rrf; weighted fusion; rerankers; packing
  context.py          build_prompt, PackedContext, budget slices, cacheable prefix
  generate.py         Extractive (default), Ungrounded (fault fixture), Bedrock, Anthropic
  pipeline.py         RagPipeline.run / variant, evaluate(), scorecard, compare_runs
  metrics.py          evidence and full-chain recall, nDCG, MRR, κ, paired bootstrap
  judge.py            rubrics, HeuristicJudge, BedrockJudge, verbosity and position probes
  agent.py            AgenticSearch: decompose → tool → sufficiency → stop
  costs.py            token categories, PromptCache, latency model, unit economics
  trace.py            Trace, TraceStore, diff_traces
  bedrock.py          Knowledge Base retriever, preflight, LOCAL_TO_AWS
  bootstrap.py        seeds the RNG, detects optional extras
  catalog.py, trees.py, viz.py, tables.py   teaching layer: decision trees as data, figures, tables
notebooks/          00 start here … 09 capstone, executed nightly in CI
labs/               L01–L12 and C01–C08: brief, starter, checks, reference, meta each
  _registry.py, _harness.py, test_labs.py   prerequisite DAG, public and hidden checks, meta-tests
tests/              invariants: recall ceiling, ACL isolation, determinism, stable ids
scripts/            CLIs: run_eval, lab, lab_sandbox, lab_submission, tracker, boards, pulse,
                    setup_github, seed_content, create_thread, harvest_faq, sync_wiki, …
docs/               MkDocs source, numbered by purpose (00 orientation … 90 reference)
threads/            JSON specs for seeded Discussions
wiki/               mirror of the live GitHub wiki, which is canonical
tools/              validate-mermaid.mjs, check_links.py
.github/            18 workflows, eval-baseline.json, issue, PR and discussion templates
.devcontainer/      Codespaces image: Python 3.12 bookworm + Node 20, asserts FTS5 on create
.agents/skills/     skills for coding agents; .claude/skills is a symlink to it
AGENTS.md           instructions for every coding agent; CLAUDE.md imports it
Makefile, pyproject.toml, mkdocs.yml, uv.lock, eval-report.json, CITATION.cff
flowchart TB
    subgraph product["Product: what a learner runs"]
        PKG["nanorag/<br/>toolkit package · 19 modules"]
        NB["notebooks/<br/>10 executable lessons"]
        LAB["labs/<br/>12 labs + 8 challenges"]
    end
    subgraph assurance["Assurance: what proves it still works"]
        TST["tests/<br/>pytest invariants"]
        GATE[".github/eval-baseline.json<br/>+ eval-report.json"]
        TOOLS["tools/<br/>mermaid + link validators"]
    end
    subgraph knowledge["Knowledge: what explains it"]
        DOCS["docs/<br/>00 orientation … 90 reference"]
        WIKI["wiki/<br/>mirror of the live wiki"]
        THR["threads/<br/>discussion seed specs · JSON"]
    end
    subgraph ops["Operations: what runs it on GitHub"]
        SCR["scripts/<br/>eval · lab · GitHub ops CLIs"]
        GH[".github/<br/>18 workflows · templates"]
        DEV[".devcontainer/<br/>Codespaces image"]
        MK["Makefile · pyproject.toml<br/>mkdocs.yml · uv.lock"]
        AGT["AGENTS.md · .agents/skills/<br/>instructions + skills for coding agents"]
    end

    NB -->|imports| PKG
    LAB -->|reference solutions import| PKG
    TST -->|asserts invariants of| PKG
    TST -->|executes| NB
    SCR -->|run_eval.py drives| PKG
    SCR -->|lab.py runs checks in| LAB
    SCR -->|create_thread.py posts| THR
    GATE -.->|compared by run_eval.py| SCR
    GH -->|invokes| SCR
    GH -->|invokes| TST
    GH -->|validates| TOOLS
    TOOLS -->|parse| DOCS
    DOCS -->|mkdocs build| GH
    DEV -->|pip install -e .dev| PKG
    SCR -->|sync_wiki.py seeds| WIKI

    classDef p fill:#FBF1E2,stroke:#B87A12,color:#101318
    classDef a fill:#E9F3EE,stroke:#3F8F6E,color:#101318
    classDef k fill:#EAF4F7,stroke:#2F8CA3,color:#101318
    classDef o fill:#EFEDFB,stroke:#6C5CE0,color:#101318
    class PKG,NB,LAB p
    class TST,GATE,TOOLS a
    class DOCS,WIKI,THR k
    class SCR,GH,DEV,MK,AGT o

Level 1: system context

Who uses nanorag, and what does it reach outside itself?

Nothing outside nanorag is needed at runtime. PyPI is needed once, at install time, and GitHub hosts the course around the code. Amazon Bedrock, the Claude API, Hugging Face and MultiHop-RAG connect through the interfaces the offline defaults already implement: Hit, BaseEmbedder, .rerank() and BaseGenerator. Swapping one in changes the retriever or the reader. The harness, the metrics and the eval set stay the same.

flowchart TB
    L(["Learner<br/>[Person]<br/>runs notebooks and labs,<br/>posts submissions"])
    F(["Faculty / maintainer<br/>[Person]<br/>sets assignments, reviews PRs,<br/>curates Discussions"])
    R(["Recruiter / interviewer<br/>[Person]<br/>reads ADRs, traces, portfolio"])

    SYS["<b>nanorag</b><br/>[Software System]<br/>Offline, deterministic RAG stack +<br/>curriculum: BM25, dense, ANN, fusion,<br/>reranking, packing, judging, eval gate"]

    GH["GitHub<br/>[External System]<br/>Actions · Discussions · Issues ·<br/>Projects v2 · Pages · Wiki · Codespaces"]
    BR["Amazon Bedrock<br/>[External · optional]<br/>KB retrieve · Titan embed ·<br/>rerank · Converse"]
    AN["Claude API<br/>[External · optional]<br/>generator · judge"]
    HF["Hugging Face<br/>[External · optional]<br/>sentence-transformers"]
    MH["MultiHop-RAG<br/>[External dataset · optional]"]
    PY["PyPI<br/>[External]<br/>numpy · pandas · matplotlib"]

    L -->|"Run All in JupyterLab /<br/>python scripts/lab.py"| SYS
    L -->|"asks, submits labs<br/>in Discussions"| GH
    F -->|"issues, reviews,<br/>office hours"| GH
    R -->|"reads docs + decisions"| GH
    GH -->|"CI, eval gate, sandboxed lab runs,<br/>Pages deploy, board automation"| SYS
    SYS -.->|"boto3 · same Hit / Embedder /<br/>Reranker / Generator seams"| BR
    SYS -.->|"anthropic SDK"| AN
    SYS -.->|"model download"| HF
    SYS -.->|"load_multihop_rag()"| MH
    SYS -->|"pip install"| PY

    classDef person fill:#101318,stroke:#101318,color:#FFFFFF
    classDef sys fill:#2F8CA3,stroke:#3A414B,color:#FFFFFF
    classDef ext fill:#6B7480,stroke:#3A414B,color:#FFFFFF
    classDef opt fill:#F6F4EF,stroke:#C9C4B8,color:#3A414B,stroke-dasharray: 4 3
    class L,F,R person
    class SYS sys
    class GH,PY ext
    class BR,AN,HF,MH opt

Level 2: containers

A container here is anything that runs or holds data on its own. The retrieval store and the trace store live inside the Python process, but they have their own schema and lifecycle, so they get their own boxes. The lab sandbox and the eval gate runner exist only on a GitHub runner.

flowchart TB
    L(["Learner [Person]"])
    F(["Faculty [Person]"])

    subgraph B["nanorag [Software System boundary]"]
        direction TB
        NB["Notebooks<br/>[Container: Jupyter · 10 .ipynb]<br/>lessons: code → measure → decide"]
        LABCLI["Lab simulator<br/>[Container: Python CLI · scripts/lab.py]<br/>list · next · run · status · verify"]
        PKG["nanorag toolkit<br/>[Container: Python 3.10+ library]<br/>corpus → chunk → embed → index →<br/>retrieve → rerank → pack → generate → judge"]
        DB[("Retrieval store<br/>[Container: sqlite3 :memory:]<br/>chunks · FTS5 · vectors BLOB ·<br/>aliases · index_versions")]
        TS[("Trace store<br/>[Container: in-process dict]<br/>replayable per-query traces")]
        EVAL["Eval gate runner<br/>[Container: Python · scripts/run_eval.py]<br/>scorecard vs committed baseline"]
        SBX["Lab sandbox<br/>[Container: Docker python:3.12-slim]<br/>--network=none · read-only · 30 s kill"]
        OPS["Community ops<br/>[Container: Python CLIs · scripts/*]<br/>boards · tracker · pulse · threads · FAQ"]
        SITE["Docs site<br/>[Container: MkDocs Material]<br/>docs/ + executed notebooks"]
    end

    GH["GitHub platform<br/>[External]"]
    BR["Amazon Bedrock<br/>[External · optional]"]
    AN["Claude API<br/>[External · optional]"]

    L -->|"Run All"| NB
    L -->|"CLI"| LABCLI
    L -->|"posts code in<br/>submit / arena thread"| GH
    F -->|"PRs, issues"| GH
    NB -->|"import nanorag"| PKG
    LABCLI -->|"load starter + checks"| PKG
    PKG -->|"SQL: FTS5 MATCH, bm25(),<br/>upsert, alias swap"| DB
    PKG -->|"put / diff"| TS
    EVAL -->|"quickstart + evaluate"| PKG
    GH -->|"runs on PR / nightly"| EVAL
    GH -->|"discussion event →<br/>submission.py"| SBX
    SBX -->|"lab_sandbox.py checks"| PKG
    GH -->|"GraphQL via gh / PROJECT_TOKEN"| OPS
    OPS -->|"boards, comments, threads"| GH
    GH -->|"deploy-pages"| SITE
    PKG -.->|"boto3"| BR
    PKG -.->|"anthropic"| AN

    classDef person fill:#101318,stroke:#101318,color:#FFFFFF
    classDef cont fill:#2F8CA3,stroke:#3A414B,color:#FFFFFF
    classDef store fill:#6C5CE0,stroke:#3A414B,color:#FFFFFF
    classDef ext fill:#6B7480,stroke:#3A414B,color:#FFFFFF
    classDef opt fill:#F6F4EF,stroke:#C9C4B8,color:#3A414B,stroke-dasharray: 4 3
    class L,F person
    class NB,LABCLI,PKG,EVAL,SBX,OPS,SITE cont
    class DB,TS store
    class GH ext
    class BR,AN opt
Container Technology Entry point State
nanorag toolkit Python ≥3.10, numpy, pandas, matplotlib nanorag.quickstart() none; seeded with bootstrap.SEED
Retrieval store sqlite3 :memory:, FTS5 with tokenchars '_-' store.InMemoryIndex gone when the kernel stops
Trace store in-process trace.TraceStore per session
Notebooks JupyterLab make lab outputs stripped before commit
Lab simulator Python CLI scripts/lab.py progress derived from the checks, never stored
Eval gate runner Python CLI scripts/run_eval.py .github/eval-baseline.json
Lab sandbox Docker python:3.12-slim scripts/lab_sandbox.py read-only, tmpfs /tmp
Community ops Python + gh GraphQL scripts/pulse.py, tracker.py, boards.py, … GitHub Projects v2 and Discussions
Docs site MkDocs Material + nbconvert mkdocs build GitHub Pages

Level 3: components

Inside the toolkit

These are the layers from the module map. Arrows follow the data: a document becomes chunks, then vectors, then index rows, then hits, packed context, an answer, a trace and finally metrics. pipeline is the only module that touches every layer on the query path.

flowchart TB
    subgraph PKG["nanorag toolkit [Container]"]
        direction TB
        subgraph data["Data"]
            CO["corpus<br/>[Component]<br/>fact graph → 484 docs,<br/>243 eval questions, personas"]
            CH["chunking<br/>[Component]<br/>7 strategies · STRATEGIES registry ·<br/>stable chunk ids"]
            EM["embed<br/>[Component]<br/>BaseEmbedder: LSA · Hashing ·<br/>SentenceTransformers · Bedrock"]
        end
        subgraph storage["Storage"]
            ST["store.InMemoryIndex<br/>[Component]<br/>lexical · exact_vector · ann_vector ·<br/>ACL pre-filter · versions · tombstones"]
            TR["trace<br/>[Component]<br/>Trace · TraceStore · diff_traces"]
        end
        subgraph query["Query path"]
            RE["retrieve<br/>[Component]<br/>HybridRetriever · rrf · weighted_fusion ·<br/>ProxyCrossEncoder · pack_context"]
            CX["context<br/>[Component]<br/>build_prompt · PackedContext ·<br/>cacheable_prefix"]
            GE["generate<br/>[Component]<br/>Extractive · Ungrounded ·<br/>Bedrock · Anthropic"]
            AG["agent<br/>[Component]<br/>AgenticSearch · decompose ·<br/>choose_tool · sufficiency_check"]
        end
        subgraph measure["Measurement"]
            PI["pipeline<br/>[Component]<br/>RagPipeline · variant · evaluate ·<br/>scorecard · compare_runs"]
            ME["metrics<br/>[Component]<br/>recall · nDCG · MRR · κ ·<br/>paired_bootstrap"]
            JU["judge<br/>[Component]<br/>Rubric · HeuristicJudge · BedrockJudge ·<br/>bias probes"]
            CS["costs<br/>[Component]<br/>Rates · PromptCache ·<br/>latency_model · unit_economics"]
        end
        subgraph teach["Teaching + integration"]
            CA["catalog + trees<br/>[Component]<br/>decision trees / matrices as data"]
            VT["viz + tables<br/>[Component]<br/>numbered figures and tables"]
            BE["bedrock<br/>[Component]<br/>KB retriever · preflight ·<br/>LOCAL_TO_AWS map"]
            BO["bootstrap + quickstart<br/>[Component]<br/>seeds RNG, detects extras,<br/>builds a live system"]
        end
    end
    DB[("sqlite3 :memory:<br/>[Container]")]
    BR["Amazon Bedrock<br/>[External · optional]"]

    BO --> CO & CH & EM & ST & PI
    CO -->|"Document, Chunk"| CH
    CH -->|"texts"| EM
    EM -->|"vectors + embedder_tag"| ST
    ST <-->|"SQL"| DB
    RE -->|"lexical / exact / ann"| ST
    RE -->|"encode_queries"| EM
    PI --> RE
    PI --> CX
    PI --> GE
    PI --> TR
    PI --> ME
    PI --> CS
    AG --> RE
    AG --> CX
    GE --> CX
    JU --> ME
    CA --> VT
    BE -.->|"returns store.Hit"| ST
    BE -.-> BR
    EM -.-> BR
    GE -.-> BR

    classDef c fill:#2F8CA3,stroke:#3A414B,color:#FFFFFF
    classDef s fill:#6C5CE0,stroke:#3A414B,color:#FFFFFF
    classDef opt fill:#F6F4EF,stroke:#C9C4B8,color:#3A414B,stroke-dasharray: 4 3
    class CO,CH,EM,ST,TR,RE,CX,GE,AG,PI,ME,JU,CS,CA,VT,BE,BO c
    class DB s
    class BR opt

Inside the GitHub automation

Each workflow is a thin wrapper around a script, and every script that writes to GitHub goes through boards.py or gh.py. PROJECT_TOKEN goes only to the steps that write to the user-owned Projects boards.

flowchart LR
    subgraph WF["GitHub Actions workflows [Container]"]
        direction TB
        DL["discussion-lab.yml<br/>[Component]<br/>triage → execute → post"]
        LW["labs.yml<br/>[Component]<br/>PR: verify DAG, meta-tests"]
        PU["pulse.yml<br/>[Component]<br/>every 6 h"]
        FQ["faq.yml<br/>[Component]<br/>weekly → PR"]
        PA["project-automation.yml<br/>[Component]<br/>issues/PRs → board"]
    end
    subgraph SC["scripts/ [Container]"]
        direction TB
        SB["lab_sandbox.py<br/>[Component]<br/>runs checks, prints<br/>@@LAB-REPORT@@ JSON"]
        LS["lab_submission.py<br/>[Component]<br/>report → reply.md"]
        TK["tracker.py<br/>[Component]<br/>Hands-on Tracker rows"]
        BD["boards.py<br/>[Component]<br/>Projects v2 GraphQL helpers"]
        PL["pulse.py<br/>[Component]<br/>Repo Pulse board"]
        HF["harvest_faq.py +<br/>discussion_map.py"]
        CT["create_thread.py ·<br/>grow_thread.py · seed_content.py ·<br/>setup_github.py"]
        GHX["gh.py<br/>[Component]<br/>thin gh api wrapper"]
    end
    subgraph LABS["labs/ [Container]"]
        REG["_registry.py<br/>meta.json → DAG"]
        HAR["_harness.py<br/>public / hidden checks"]
        LX["L01…L12 · C01…C08<br/>brief · starter · checks · reference"]
    end
    THR[("threads/*.json<br/>discussion specs")]
    GH["GitHub GraphQL / REST<br/>[External]"]

    DL -->|"docker, no network"| SB
    SB --> REG & HAR
    HAR --> LX
    DL --> LS
    DL -->|"PROJECT_TOKEN"| TK
    LW -->|"PROJECT_TOKEN"| TK
    TK --> BD
    PU --> PL --> BD
    FQ --> HF
    CT --> THR
    BD & HF & CT --> GHX --> GH

    classDef c fill:#2F8CA3,stroke:#3A414B,color:#FFFFFF
    classDef s fill:#6C5CE0,stroke:#3A414B,color:#FFFFFF
    classDef ext fill:#6B7480,stroke:#3A414B,color:#FFFFFF
    class DL,LW,PU,FQ,PA,SB,LS,TK,BD,PL,HF,CT,GHX,REG,HAR,LX c
    class THR s
    class GH ext

Level 4: code

Query-path classes

The swappable interfaces are BaseEmbedder, BaseGenerator and the reranker protocol. The reranker protocol has no base class in the code; it is duck-typed, and the diagram draws it as an interface. RagPipeline receives its index and embedder and never imports store, so a Bedrock Knowledge Base retriever that returns Hit can replace the local index.

classDiagram
    direction LR
    class RagPipeline {
        +index
        +embedder
        +cfg: RetrievalConfig
        +generator
        +reranker
        +trace_store: TraceStore
        +name: str
        +variant(name, **cfg_updates) RagPipeline
        +run(query, qid, persona, acl_groups) Trace
    }
    class RetrievalConfig {
        +n_candidates = 100
        +k = 8
        +evidence_token_cap = 6000
        +fusion: rrf|weighted|dense|lexical
        +alpha
        +rerank: none|cross|late
        +rerank_depth = 50
        +ann, ef_search
        +acl_groups, filter_mode: pre|post
    }
    class HybridRetriever {
        +index
        +embedder
        +search(query, cfg) List~Hit~
    }
    class InMemoryIndex {
        +db: sqlite3.Connection
        +upsert(chunks, vectors, version, embedder_tag) int
        +tombstone(chunk_ids, version) int
        +set_alias(alias, version)
        +lexical(query, n, acl_groups) List~Hit~
        +exact_vector(qvec, n, acl_groups) List~Hit~
        +ann_vector(qvec, n, ef_search) List~Hit~
        +mixed_version_check(version) dict
    }
    class Hit {
        +chunk_id
        +score
        +rank
        +method
        +doc_id, text, title, acl
    }
    class BaseEmbedder {
        <<abstract>>
        +info: EmbedderInfo
        +fit(texts)
        +encode_documents(texts)
        +encode_queries(texts)
    }
    class LsaEmbedder
    class HashingEmbedder
    class SentenceTransformersEmbedder
    class BedrockEmbedder
    class Reranker {
        <<interface>>
        +rerank(query, hits, depth) List~Hit~
    }
    class ProxyCrossEncoder {
        +fit(pairs, labels)
        +rerank(query, hits, depth)
    }
    class LateInteractionReranker
    class NoReranker
    class BedrockReranker
    class BaseGenerator {
        <<abstract>>
        +generate(query, packed) Answer
    }
    class ExtractiveGenerator
    class UngroundedGenerator
    class BedrockGenerator
    class AnthropicGenerator
    class PackedContext {
        +blocks
        +resolve(sid) chunk_id
    }
    class Answer {
        +text
        +citations
        +sufficient
        +usage
    }
    class Trace {
        +trace_id, qid, query
        +candidates, reranked, packed
        +stage_ms, k_collapse
        +answer, citations, usage
    }
    class TraceStore {
        +put(trace)
    }
    class AgenticSearch {
        +p: RagPipeline
        +budget: AgentBudget
        +run(question, acl_groups) AgentResult
    }

    RagPipeline *-- RetrievalConfig
    RagPipeline *-- HybridRetriever
    RagPipeline o-- Reranker
    RagPipeline o-- BaseGenerator
    RagPipeline o-- TraceStore
    HybridRetriever --> InMemoryIndex
    HybridRetriever --> BaseEmbedder
    InMemoryIndex ..> Hit : returns
    BaseEmbedder <|-- LsaEmbedder
    BaseEmbedder <|-- HashingEmbedder
    BaseEmbedder <|-- SentenceTransformersEmbedder
    BaseEmbedder <|-- BedrockEmbedder
    Reranker <|.. ProxyCrossEncoder
    Reranker <|.. LateInteractionReranker
    Reranker <|.. NoReranker
    Reranker <|.. BedrockReranker
    BaseGenerator <|-- ExtractiveGenerator
    BaseGenerator <|-- UngroundedGenerator
    BaseGenerator <|-- BedrockGenerator
    BaseGenerator <|-- AnthropicGenerator
    BaseGenerator ..> PackedContext : reads
    BaseGenerator ..> Answer : returns
    RagPipeline ..> Trace : produces
    TraceStore o-- Trace
    AgenticSearch --> RagPipeline

Retrieval store schema

Taken from store.SCHEMA. Blue/green deployment is rows in index_versions plus a pointer in aliases. embedder_tag is the only thing that catches a mixed-encoder index, because cosine returns well-formed scores for vectors from two different models.

erDiagram
    index_versions ||--o{ chunks : "holds"
    aliases }o--|| index_versions : "points at"
    chunks ||--|| chunks_fts : "mirrored into"

    chunks {
        int row_id PK
        text chunk_id "doc_id + ordinal + content hash"
        text index_version "UNIQUE with chunk_id"
        text doc_id
        int ordinal
        int n_chunks
        text title
        text heading
        text source
        text published
        text tenant
        text acl "json array of groups"
        text content_hash
        text embedder_tag "pinned encoder identity"
        text text
        blob vector "float32"
        int tombstoned "soft delete"
    }
    chunks_fts {
        text text "FTS5 indexed"
        text title "FTS5 indexed"
        text heading "FTS5 indexed"
        text chunk_id "UNINDEXED"
        text index_version "UNINDEXED"
    }
    aliases {
        text name PK "e.g. live"
        text index_version
        text swapped_at
    }
    index_versions {
        text index_version PK
        text embedder_tag
        text chunker
        text created_at
        text note
    }

Building-block view

Level 1 is the repository layout diagram. Level 2 opens the package and shows its real import graph, read from the from .x import lines. There are no cycles, and seven leaf modules import nothing else from the package, so each can be read or replaced on its own. The seams where new techniques plug in are in overview.md.

flowchart LR
    subgraph leaf["Leaf modules: import nothing from the package"]
        direction TB
        corpus
        embed
        store
        metrics
        costs
        trace
        viz
    end
    chunking --> corpus
    chunking --> embed
    context --> chunking
    retrieve --> chunking
    retrieve --> embed
    retrieve --> store
    generate --> context
    generate --> embed
    agent --> context
    agent --> embed
    agent --> retrieve
    judge --> embed
    judge --> metrics
    pipeline --> retrieve
    pipeline --> context
    pipeline --> generate
    pipeline --> trace
    pipeline --> metrics
    pipeline --> costs
    bedrock --> store
    catalog --> trees
    trees --> tables
    trees --> viz
    tables --> viz
    bootstrap --> viz
    init["__init__ · quickstart"] -.->|wires| pipeline
    init -.-> store
    init -.-> corpus

    classDef lf fill:#E9F3EE,stroke:#3F8F6E,color:#101318
    classDef mid fill:#EAF4F7,stroke:#2F8CA3,color:#101318
    classDef top fill:#FBF1E2,stroke:#B87A12,color:#101318
    class corpus,embed,store,metrics,costs,trace,viz lf
    class chunking,context,retrieve,generate,judge,bedrock,tables,trees mid
    class pipeline,agent,catalog,bootstrap,init top

Runtime view

Six runtime paths matter. Two run in the learner's kernel (R1, R2), two run on GitHub (R3, R4), and two are control loops inside the toolkit (R5, R6).

R1: cold start with quickstart()

sequenceDiagram
    autonumber
    participant U as Notebook / run_eval.py
    participant Q as nanorag.quickstart
    participant C as corpus
    participant K as chunking
    participant E as LsaEmbedder
    participant I as InMemoryIndex
    participant P as RagPipeline

    U->>Q: quickstart(**TUNED)
    Q->>C: build_corpus()
    C-->>Q: CorpusBundle: 484 docs, 243 questions, personas
    Q->>K: chunk_corpus(docs, "structural")
    K-->>Q: 2,430 Chunk with stable ids
    Q->>E: fit(whole documents)
    Note over E: SVD latent space fitted on docs,<br/>then chunks encoded into it
    Q->>E: encode_documents(chunk texts)
    E-->>Q: float32 matrix, tag lsa@1.0:d96
    Q->>I: upsert(chunks, vecs, "v1", embedder_tag)
    Note over I: rows + FTS5 mirror written,<br/>ACL cache cleared
    Q->>I: set_alias("live", "v1")
    Q->>P: RagPipeline(idx, emb, RetrievalConfig)
    P-->>U: bundle, idx, pipe  ·  about 0.6 s

R2: one query through RagPipeline.run()

Each stage is timed into stage_ms. trace_id is a UUID5 of the pipeline name, the qid and the query, so re-running a query produces the same id and two runs can be diffed with diff_traces.

sequenceDiagram
    autonumber
    participant U as Caller
    participant P as RagPipeline.run
    participant H as HybridRetriever
    participant I as InMemoryIndex
    participant R as ProxyCrossEncoder
    participant X as pack_context + build_prompt
    participant G as ExtractiveGenerator
    participant T as TraceStore

    U->>P: run(query, qid, acl_groups)
    P->>H: search(query, cfg)
    Note over H: legs run one after the other
    H->>I: LexicalRetriever: lexical(query, N=100, acl)
    I-->>H: FTS5 MATCH ranked by bm25()
    H->>I: DenseRetriever: exact_vector or ann_vector(qvec, N, acl)
    I-->>H: cosine over the visible id set
    H->>H: weighted_fusion alpha=0.2 or rrf k=60
    H-->>P: candidates
    P->>R: rerank(query, candidates, depth=50)
    R-->>P: reordered by 8 pair features
    P->>X: dedup, order, pack k=8 within 6,000 tokens
    X-->>P: PackedContext with S# citation ids
    P->>G: generate(query, packed)
    G-->>P: Answer text, citations, usage
    P->>T: put(Trace with stage_ms per stage)
    P-->>U: Trace

R3: the release gate on a pull request

The tolerances live in scripts/run_eval.py: evidence recall may drop by 0.02, full-chain recall by 0.03 and answer correctness by 0.03. The frozen slice is reported on every run and is never gated or tuned against. ADR-0008 records why the gate is in CI.

sequenceDiagram
    autonumber
    actor Dev as Contributor
    participant GH as GitHub PR
    participant WF as eval-regression.yml
    participant RE as scripts/run_eval.py
    participant NR as nanorag
    participant BL as .github/eval-baseline.json

    Dev->>GH: open / push PR
    GH->>WF: pull_request event, also nightly 04:42 UTC
    WF->>RE: run_eval.py  (all 243 questions)
    RE->>NR: quickstart(**TUNED) then evaluate(pipe, questions, personas)
    NR-->>RE: per-question rows then summarize()
    RE->>BL: read prior metrics
    alt a gated metric drops beyond tolerance
        RE-->>WF: exit 1, RELEASE BLOCKED
    else within tolerance
        RE-->>WF: exit 0
    end
    WF->>RE: run_eval.py --slice frozen  (reported, never tuned against)
    WF->>GH: upsert "Eval gate" scorecard comment
    WF-->>GH: check fails if step 1 failed
    Note over Dev,BL: A deliberate change re-baselines in the same PR:<br/>python scripts/run_eval.py --baseline

R4: a lab submission from a Discussion comment

Anyone can comment on a public discussion, so the comment body is untrusted. The job that runs the code has no token and no network. The job that holds a write token never sees the code; it reads a JSON report. The body reaches disk through an environment variable and never appears on a shell command line. The security header of .github/workflows/discussion-lab.yml has the full reasoning.

sequenceDiagram
    autonumber
    actor L as Learner
    participant D as GitHub Discussions
    participant T as job triage<br/>no permissions
    participant X as job execute<br/>no token
    participant S as Docker sandbox<br/>network none
    participant P as job post<br/>discussions write
    participant B as Hands-on Tracker board

    L->>D: comment with fenced code in "[submit · L03]" or "[arena · C02]"
    D->>T: discussion_comment event
    T->>T: lab id from title, body shaped like a submission?
    T-->>X: lab = L03
    X->>X: body via env var into submission.md, extract code block
    X->>S: docker run --read-only --cap-drop=ALL, 30 s kill
    S->>S: lab_sandbox.py runs public + hidden checks
    S-->>X: stdout line prefixed @@LAB-REPORT@@
    X-->>P: report.json artifact
    P->>P: lab_submission.py renders reply.md
    P->>B: tracker.py with PROJECT_TOKEN
    P->>D: addDiscussionComment as a threaded reply
    D-->>L: pass / fail with measurements

R5: the agentic search loop

stateDiagram-v2
    direction LR
    [*] --> Decompose: question
    Decompose --> ChooseTool: sub-questions pending
    ChooseTool --> Retrieve: lexical, dense or hybrid
    Retrieve --> Sufficiency: append new hits with provenance
    Sufficiency --> Done: sufficient
    Sufficiency --> ChooseTool: missing sub-questions, re-anchor on original
    Retrieve --> Stopped: repeat detector
    Sufficiency --> Stopped: no new info, turn cap, wall clock, token budget
    Done --> Synthesise
    Stopped --> Synthesise: partial answer with a stated gap
    Synthesise --> [*]: AgentResult + turns

R6: the index lifecycle

The incremental path handles an edited document. The rebuild path handles an encoder, chunker or analyzer change. ADR-0004 explains why stable chunk ids keep the incremental path cheap.

stateDiagram-v2
    direction LR
    [*] --> Live: set_alias live to v1
    Live --> Incremental: document edited
    state Incremental {
        [*] --> HashDiff
        HashDiff --> Skip: content_hash unchanged
        HashDiff --> Upsert: changed, new chunk_id
        Upsert --> Tombstone: orphaned chunks
        Tombstone --> Compact
        Skip --> [*]
        Compact --> [*]
    }
    Incremental --> Live
    Live --> Rebuild: encoder, chunker or analyzer changed
    state Rebuild {
        [*] --> BuildV2: v2 alongside v1
        BuildV2 --> Shadow: evaluate frozen slice
        Shadow --> Swap: clean
        Shadow --> KeepV1: regression
        Swap --> [*]
        KeepV1 --> [*]
    }
    Rebuild --> Live
    Live --> Poisoned: new-encoder vectors written into v1
    Poisoned --> Live: mixed_version_check blocks release

Deployment view

There is no server. Production is whatever runs the Python kernel: a laptop or a Codespace. GitHub runs everything else, which is testing the code, publishing the docs and running the course.

flowchart TB
    subgraph DEV["Learner machine or GitHub Codespace [Node]"]
        direction TB
        subgraph CTR["Dev container: python:1-3.12-bookworm + Node 20 + gh [Execution env]"]
            JL["JupyterLab :8888"]
            KER["Python kernel<br/>nanorag + numpy · pandas · matplotlib"]
            SQL[("sqlite3 :memory:<br/>FTS5 compiled in, asserted at create")]
            CLI["scripts/lab.py · make check"]
        end
    end

    subgraph RUN["GitHub-hosted runner: ubuntu-latest [Node]"]
        direction TB
        CI["CI · Tests py3.10-3.12 · Coverage · CodeQL"]
        NBW["Notebooks · matrix of 10, nightly 03:17"]
        EG["Eval gate · PR + nightly 04:42"]
        LBW["Labs · Diagrams · Markdown · Links"]
        PGB["Pages build<br/>mkdocs + nbconvert --execute"]
        subgraph SBX["Docker: python:3.12-slim [Execution env]"]
            LSB["lab_sandbox.py<br/>no network · read-only · uid 65534<br/>256 MB · 1 CPU · 64 pids"]
        end
        OPS["pulse every 6 h · faq weekly ·<br/>stale · welcome · labeler · board"]
    end

    subgraph GHP["GitHub platform [Node]"]
        REPO[("Repository main")]
        DISC["Discussions"]
        PROJ["Projects v2<br/>delivery · tracker · pulse · lifecycle"]
        PAGES["GitHub Pages<br/>akash-coded.github.io/nanorag"]
        WIKI["Wiki<br/>canonical, mirrored in wiki/"]
    end

    subgraph AWS["AWS account + region [Node · optional]"]
        BRK["Bedrock runtime<br/>Titan embed · rerank · Converse"]
        KB["Bedrock Knowledge Base"]
    end
    ANT["api.anthropic.com [optional]"]

    KER --- SQL
    JL --> KER
    CLI --> KER
    DEV -->|"git push / PR"| REPO
    REPO -->|"workflow triggers"| RUN
    DISC -->|"discussion_comment"| SBX
    OPS -->|"PROJECT_TOKEN"| PROJ
    PGB -->|"deploy-pages"| PAGES
    EG -->|"scorecard comment"| REPO
    KER -.->|"boto3, AWS credentials"| BRK
    KER -.-> KB
    KER -.->|"ANTHROPIC_API_KEY"| ANT
    CLI -.->|"make wiki-pull"| WIKI

    classDef node fill:#F6F4EF,stroke:#3A414B,color:#101318
    classDef run fill:#2F8CA3,stroke:#3A414B,color:#FFFFFF
    classDef store fill:#6C5CE0,stroke:#3A414B,color:#FFFFFF
    classDef sand fill:#CF4F35,stroke:#3A414B,color:#FFFFFF
    classDef opt fill:#F6F4EF,stroke:#C9C4B8,color:#3A414B,stroke-dasharray: 4 3
    class JL,KER,CLI,CI,NBW,EG,LBW,PGB,OPS run
    class SQL,REPO store
    class LSB sand
    class DISC,PROJ,PAGES,WIKI run
    class BRK,KB,ANT opt
Workflow Trigger What it checks or does
ci.yml push, PR ruff; pytest on Python 3.10, 3.11 and 3.12; install on a fresh machine
notebooks.yml push, PR, nightly 03:17 UTC all ten notebooks execute, one matrix job each
eval-regression.yml PR, nightly 04:42 UTC release gate and frozen slice; posts the scorecard on the PR
labs.yml PR lab DAG is valid; references pass and starters fail; updates the tracker
discussion-lab.yml discussion, discussion comment sandboxed lab grading and a threaded reply
pages.yml push to main touching docs, notebooks or the package MkDocs site plus executed notebooks, deployed to Pages
diagrams.yml, markdown.yml, link-check.yml push, PR; links also weekly every Mermaid block parses; markdownlint; no dead relative links
coverage.yml, codeql.yml main and PR; CodeQL also weekly coverage with bedrock.py omitted; security scan
pulse.yml, faq.yml every 6 hours; weekly Repo Pulse board; FAQ and discussion map opened as a PR
project-automation.yml, labeler.yml, semantic-pr.yml, welcome.yml, stale.yml issues, PRs, weekly board routing, labels, PR-title check, greetings, stale sweep

Updating this page

Change a diagram in the same PR as the code it describes. The usual triggers are a new module or seam implementation (Level 3, Level 4 and the import graph), a new workflow (the deployment table) and a new external dependency (Level 1). Run node tools/validate-mermaid.mjs or make check before pushing. Node colours come from the palette in nanorag/viz.py, as in the README and overview.md.