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
- Level 1: system context
- Level 2: containers
- Level 3: components
- Level 4: code
- Building-block view
- Runtime view
- Deployment view
- Updating this page
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.