Status: agreed MVP design, implemented through v0.8. This document is the source of truth for the architecture. It supersedes the original free-form spec. Load-bearing decisions that are expensive to change later are marked [LOCKED]; everything else is revisable. The MVP vertical slice in §8 has shipped (provenance spine, import + FastAPI + domain-entity + library-surface
- event-handler + call-graph extractors plus the grounded process-path builder over them — with cross-file grounding, the sandboxed openapi and griffe oracles, the read-only MCP server, pgvector embeddings/search, the RAG A/B gate plus a nightly LLM-judged A/B, LLM-grounded descriptions through the whole-repo overview and process-path labels, incremental re-index, and published Docker images); items still labelled deferred below remain so.
knowbase turns a git repository into a versioned, provenance-grounded Knowledge Layer served to humans and AI agents (via MCP). It is not RAG-over-code and not an AI code generator.
The product value is not in embeddings, OpenAI/Anthropic, or any vector DB — those are replaceable adapters. The value is:
- Automatically extracting durable knowledge from a codebase.
- Keeping it current relative to git history.
- Versioning knowledge per commit/branch.
- Serving it to people and AI with provenance and trust metadata.
The one risk that matters: learning to stably extract useful knowledge instead of generating a large volume of useless AI documentation. The architecture below is organized around de-risking exactly this, not around building the perfect storage layer first.
- Multi-language analysis (Python only for now).
- Multi-provider LLM/embedding abstraction beyond a thin replaceable adapter.
- A separate graph database, a 3-system storage triad, PR-scoped knowledge spaces, and a branch-inheritance protocol (replaced by content-addressing — see §6).
| # | Decision | Status |
|---|---|---|
| D1 | Core language: Python (daemon, CLI, MCP server, orchestration). | [LOCKED] |
| D2 | First analyzed language: Python. First targets: web/API services + libraries/SDK. | [LOCKED] |
| D3 | Reuse existing code-intelligence (tree-sitter now; SCIP/scip-python deferred behind an interface). Do not hand-write AST analyzers. | [LOCKED] |
| D4 | De-risk extraction quality first. Provenance spine + deterministic extractors + eval before any infrastructure build-out. | [LOCKED] |
| D5 | Provenance spine is the core invariant: no knowledge unit is stored unless it is bound to ≥1 exact code span. | [LOCKED] |
| D6 | Span identity ≠ span location (see §5). Identity is structural + symbol-path based and excludes file_path; byte/line offsets are stored per occurrence (per-SHA). |
[LOCKED] |
| D7 | Single PostgreSQL store. No separate graph DB; the invalidation DAG is shallow and handled with edge tables (+ recursive CTE once the semantic layer exists). | [LOCKED] |
One data structure closes three requirements at once:
Provenance graph — every knowledge artifact is bound to the exact code spans it was derived from, with extraction method and confidence.
It gives us, from a single mechanism:
- Anti-hallucination — an artifact that cannot be grounded in ≥1 span is not stored. This is the line between knowledge and hallucination. Enforced at write time.
- Incremental update — a diff maps to changed spans → we invalidate exactly the artifacts derived from them (transitively, once artifact→artifact edges exist).
- Consumer trust — MCP returns
{knowledge, source_spans@sha, method, confidence, freshness}, so an agent can weigh deterministic facts against LLM summaries.
Categories differ by orders of magnitude in extraction difficulty and ground-truth availability. We tier by method, and we build strictly bottom-up.
| Knowledge | Method | LLM? | Trust | Ground truth |
|---|---|---|---|---|
| API contracts (web routes / lib public surface) | parse routes / framework openapi / griffe | no | high | exact & free (app.openapi(), public interface) |
| Domain entities | AST (pydantic/SQLAlchemy/dataclass) | no | high | high |
| Dependency graph | tree-sitter imports + grimp resolution | no | high | hand-labeled + dynamic-import fixtures |
| Architecture | abstraction over call graph | partial | medium | partial |
| Business processes | call-graph trace + grounded labeling | yes | low | sub-property gates only (§9) |
| ADRs | git/PR history mining (NOT from code) | yes | low | sub-property gates only (§9) |
Two reframings carried from review:
- ADRs are not extractable from code. Code shows "we use Kafka now", not "we switched
because RabbitMQ couldn't handle load". ADR candidates are mined from git history + commit
messages + PR descriptions. Separate pipeline — local commit history shipped (slice 1,
kb mine); PR-description mining shipped (slice 2,kb mine --prs— strictly opt-in: the only networked step in the toolchain, off the index/serve paths and OFF by default;kb.git.forgefetches a squash-merged commit's PR title/body from api.github.com). PR text is context for the LLM and a payload fact — never grounding; and because PR text is MUTABLE after merge, a digest of the capped text is folded intoframework_versions(identity-bearing, the sink-registry precedent) so an edited PR yields a NEW artifact id instead of a silent payload swap. A failed fetch degrades to the byte-identical plain artifact, counted per run. The D5 grounding bridge: adecisionartifact is grounded on the spans its source commit changed (present at the commit, absent at its first parent — the commit's diff is the decision's footprint in code), so even prose-born knowledge points at exact code spans. The commit message is stored verbatim in the payload as a fact — it is immutable and pinned by the sha in the logical key (decision:{sha}), but it is context, never grounding. Claims pass the same deterministicvalidate_claimsfloor askb describe(a claim must cite an identifier of the changed code); a commit with no surviving claim stores nothing. - A business process is a named real path, never free generation. Statically trace the call graph from an entrypoint to side-effecting sinks; the LLM may only name/summarize a path already materialized in the provenance graph. A deterministic validator drops any claim citing spans/symbols/effects not on the path. If it can't be grounded, it isn't stored.
This is the single most expensive-to-change decision (the canonical serializer must be locked
before any extractor writes data). Review found a fatal contradiction in the naive version
(hash on normalized text and file_path, while reporting exact byte ranges that shift per
commit and break on file moves). Resolution:
A span corresponds to a named, symbol-bearing tree-sitter node: module, class, function/method, and import statements (a dedicated span kind). Not per-line, not whole-file. Imports get their own span kind so the dependency extractor can ground each edge.
span_id = sha256(
normalization_version || # lets the rule evolve safely
lang ||
span_kind || # module | class | function | import | ...
fq_symbol_path || # fully-qualified from package root, e.g. "shop.orders.OrderService.create"
structural_fingerprint # tree-sitter node normalized (whitespace/comments stripped)
)
file_pathis NOT in identity. Moving a file keeps identity stable as long as the fully-qualified symbol path is stable → cross-branch dedup survives refactors.- Reformatting changes neither
fq_symbol_pathnorstructural_fingerprint→ identity stable → no spurious invalidation cascade. - Renaming a symbol does change identity — semantically it is a change. Snapshot-diff
continuity across renames is a separate concern handled by
logical_keyrename tracking (designed in §6, deferred in build).
Offsets/line numbers and the raw text live on span_occurrence(span_id, sha, …) and are
re-resolved on every commit, even when span_id is unchanged. This is what makes "dedup"
and "exact file:line@sha provenance" both correct simultaneously.
artifact_id = sha256(
artifact_id_version || # bumped when THIS rule changes (mirror of NORMALIZATION_VERSION)
canonical(sorted derived_from span_ids) ||
logical_key || # rule v2 (bug-fix): distinct units may share their entire span set
extractor_id || extractor_version ||
prompt_version || # '' for deterministic extractors
model_id || # '' for deterministic; REQUIRED for llm_grounded ← review fix
framework_versions_subset # only for extractors whose output depends on it (API/entity), NOT imports
)
- Rule v2 (
ARTIFACT_ID_VERSION = 2):logical_keyjoined the hash. v1 derived identity from spans + extractor only, so two DIFFERENT artifacts of one extractor sharing their whole evidence set — mutually referencing entities (Order ↔ LineItem), stacked registrations, mutually recursive callers — silently collided into one digest and the second payload was lost on write. Same-input reproducibility and cross-branch dedup are unaffected (same key + same spans ⇒ same id). - Identical inputs on any branch/commit collapse to the same
artifact_id→ free dedup and free "branch inheritance" without an inheritance protocol. An unchanged span on a new branch re-hashes to the existing artifact and is simply re-pointed, never recomputed and never re-LLM'd. model_idis in the key for LLM artifacts — otherwise a model swap (same prompt) silently reuses stale labels.framework_versionsis in the key only where output depends on it (FastAPI/entity extractors), not for the import graph (which doesn't depend on fastapi/pydantic versions).
A snapshot is a git-tree-like manifest: snapshot_entry(sha, logical_key → artifact_id).
logical_key is the stable human identity per kind (api:GET /orders, entity:shop.Order,
import:shop.orders→shop.billing). Two commits map the same logical thing to different
artifact_id versions; unchanged units keep the same id.
Open seam —
logical_keyrename tracking: who mintslogical_keyand how it survives a route/class/function rename. Without it, snapshot diffs become churn ("everything deleted+added"). Designed here, deferred in build (single-SHA MVP doesn't exercise it).
git diff → changed span_ids → join artifact_derived_from → affected artifacts re-extract
for the new SHA. Artifacts whose inputs didn't actually change re-hash to an existing row and
are re-pointed with zero recompute. In the MVP the DAG is one hop (span → artifact), so a
plain join suffices; the recursive-CTE UP-walk over artifact_depends_on arrives with the
semantic layer (deferred).
Eight tables. The load-bearing edge is artifact ||--|{ artifact_derived_from — one-or-more, the
≥ 1-grounding invariant in cardinality form. Location lives per-SHA in span_occurrence; rag_chunk
is the deliberately separate RAG baseline arm (raw source windows, no provenance).
%%{init: {'theme':'base','themeVariables':{'background':'#FAF3E7','primaryColor':'#FAF3E7','primaryTextColor':'#071812','primaryBorderColor':'#1C4434','lineColor':'#3C5B4E','secondaryColor':'#EDE3D2','tertiaryColor':'#F3EAD9','textColor':'#1C4434','fontFamily':'ui-sans-serif, system-ui, sans-serif'}}}%%
erDiagram
commit_ref ||--o{ span_occurrence : "at sha"
commit_ref ||--o{ snapshot_entry : "snapshot"
branch_ref }o--|| commit_ref : "points at"
code_span ||--o{ span_occurrence : "located per-SHA"
code_span ||--o{ artifact_derived_from : "grounds"
artifact ||--|{ artifact_derived_from : "≥1 derived_from"
artifact ||--o{ snapshot_entry : "appears in"
commit_ref ||--o{ rag_chunk : "RAG arm (no provenance)"
artifact additionally carries the derived embedding vector(384) + embedding_model_id columns
(not part of the content-addressed artifact_id; recomputable from payload + model).
%%{init: {'theme':'base','themeVariables':{'background':'#FAF3E7','primaryColor':'#FAF3E7','primaryTextColor':'#071812','primaryBorderColor':'#1C4434','lineColor':'#3C5B4E','secondaryColor':'#EDE3D2','tertiaryColor':'#F3EAD9','textColor':'#1C4434','fontFamily':'ui-sans-serif, system-ui, sans-serif'}}}%%
flowchart TD
I1["1 · INGEST<br/>repo @ SHA → commit_ref (+parents);<br/>on a new commit, diff vs prior indexed SHA"]
I2["2 · STRUCTURE<br/>tree-sitter → spans (per-SHA location);<br/>upsert code_span (identity) + span_occurrence"]
I3["3 · INVALIDATE<br/>changed span_ids → affected artifacts<br/>(one-hop join in MVP)"]
I4["4 · EXTRACT (deterministic, each ≥1 derived_from, confidence=1.0)<br/>(a) import/dependency graph — spine bring-up<br/>(b) API contract (FastAPI) + library surface (griffe) — thesis test"]
I5["5 · SNAPSHOT<br/>write snapshot_entry(sha, logical_key → artifact_id)"]
I6["6 · EVAL GATE<br/>deterministic tiers run in CI as HARD gates (§9)"]
I7["7 · SERVE<br/>read-only MCP: find_provenance / get_knowledge / search_knowledge,<br/>stamped provenance + method + confidence + freshness"]
EMB["EMBED (separate kb embed pass)<br/>pgvector artifact embeddings → search_knowledge"]
I1 --> I2 --> I3 --> I4 --> I5 --> I6 --> I7
I5 -. "after index" .-> EMB
EMB -.-> I7
[deferred] recursive (artifact→artifact) invalidation. ADR mining (slice 1) has since shipped as
kb mine — a separate key-gated pass beside kb describe, walking indexed first-parent history
and grounding each decision on its commit's changed spans. kb watch is the live
trigger over the incremental core: poll a LOCAL branch ref (no network/credentials) → index each
new first-parent commit incrementally → advance the branch_ref cursor per commit (crash-safe
resume; force-push/--max-catchup degrade to one explicit-parent index of the head). (EMBED + search_knowledge and
the runtime openapi oracle shipped in v0.2; the oracle stays eval-only. The LLM-grounded SEMANTIC
EXTRACT — kb describe — and diff-based incremental re-index have since shipped: index_commit
can reuse the spans of files unchanged since an already-indexed parent and parse only the diff, with
extraction still run fully so the snapshot is identical to a full re-index — INVALIDATE is now wired
into the index path, not just the invalidation query.)
Goal: prove durably-extracted knowledge > RAG on a question RAG actually fumbles, while
exercising the provenance discipline end-to-end. (Review caveat: the import graph alone proves
the plumbing, not the thesis — what depends on X is answered exactly by grep/grimp/an IDE.
So the import graph is the spine bring-up, and the API contract is the honest thesis
test.)
- tree-sitter structural layer → content-addressed spans (identity §5, location per-SHA).
- (a) Import/dependency extractor: spans from tree-sitter, edge resolution via grimp.
- (b) API-contract extractor: FastAPI routes (route → handler → params/response),
grounded across files, scored against
app.openapi()(free exact oracle). - (c) Library public-API-surface extractor (shipped): one
public_symbolper name a package exposes from its__init__.py(__all__-authoritative;__init__re-exports resolved cross-file to the defining function/class). Extractor is tree-sitter-static (never imports user code); scored against griffe as a dev-only independent static oracle (never onindex). - (d) Event-handler extractor (shipped): one
event_handlerper handler carrying decorator registrations — pydantic@field_validator/@model_validator, FastAPI@app.on_event, SQLAlchemy@event.listens_for— AND module-level call-formevent.listen(Target, "e", fn)registrations (familysqlalchemy_listen;fnresolved same-module or via the calls.py import tables — the handler may live in another module than the listen; grounded additionally on the registering file's module span, roleregistration_site; the barefrom sqlalchemy.event import listen [as alias]form is accepted when the import table proves the called name ISsqlalchemy.event.listen). Stacked decorators and call sites aggregate inpayload.registrations; one artifact per handler. Grounded on the handler span + cross-file on the listened-to class; hand-labeled Tier-1 gate incl. a three-file provenance case (handler + target class + registration site). Known gaps (asserted):listen(...)inside a function/class body (conditional), lambda/attributefn, lifespan, pydantic-v1, dynamic names. - (e) Call-graph edge extractor (shipped): one
call_edgeper RESOLVED caller→callee pair, three deterministic tiers (same-module; imported — cross-file;self.method of the same class); precision-first — only first-party-resolved edges are emitted, recall bounded by documented gaps (dynamic/attribute calls, inheritance); grounded on caller + callee def spans; hand-labeled Tier-1 gate incl. a mutual-recursion identity-v2 regression. The deterministic foundation for §14 item 2. - Single Postgres with the
≥1 derived_frominvariant enforced at write. - Adversarial fixture: an ungrounded artifact that the write-time check must reject — so the anti-hallucination discipline is tested in the MVP, not deferred with the LLM.
- Read-only MCP —
find_provenance(file:line@sha)andget_knowledge(target, token_budget)(the MVP pair), plussearch_knowledge(query, k, token_budget)added in v0.2 once pgvector ranking existed. - Eval Tier-1 (import + API oracles) and Tier-4 (one-hop invalidation set-equality) as CI gates.
- A frozen, documented pgvector-RAG baseline + a small fixed question set for the first honest A/B (cross-file contract questions).
artifact_depends_on + recursive UP-walk + CYCLE clause; pgvector/HNSW + embeddings + the
EMBED stage; tsvector/pg_search/BM25/RRF; snapshot Merkle-root; the runtime app.openapi()
sandbox oracle (eval-only, later milestone — it executes user code); the grounded
business-process/LLM layer (call-graph slicing, sink registry, labeler, validator); ADR mining
(both slices since shipped — kb mine / kb mine --prs);
multi-branch dedup, mutable branch pointers exercise; eval Tiers 2/3 (build when the artifacts
they score exist; stub the Tier-3 question schema now); GC; SCIP/scip-python upgrade.
Eval is co-equal with extraction, weighted to cheap/exact tiers that gate CI from week 1.
- Tier 1 — deterministic self-checking (HARD GATE).
- Imports: extracted edges vs a hand-labeled fixture import list + a deliberate
dynamic-import fixture. (Note: grimp and
importlib.find_specshare import-resolution machinery and are not independent oracles — the hand-labeled fixture is the real oracle; the grimp-vs-importlib cross-check only catches consumer bugs.) - API contract: extracted routes/params/schemas vs
app.openapi()/get_openapi( routes=app.routes), compared $ref-resolved, order-insensitive, encoding the oracle's documented blind spots (Mount sub-apps, WebSocket,include_in_schema=False). - Library surface: extracted public-API surface vs griffe (an INDEPENDENT static engine,
dev-only) —
__all__-authoritative,__init__re-exports resolved cross-file; canonicalized to the top package's functions/classes (Scope A), with third-party / dynamic-__all__re-exports grounded-but-flagged as known gaps. Entities use a hand-labeled oracle (no independent one exists); the library surface has griffe, so the gate is independent like the API one.
- Imports: extracted edges vs a hand-labeled fixture import list + a deliberate
dynamic-import fixture. (Note: grimp and
- Tier 4 — incremental-invalidation regression (HARD GATE). Per SHA-pair, assert
invalidated_set == expectedexactly (over-invalidation and stale-survival both fail). Separate version-bump invalidation (full) from content-diff invalidation (minimal). - Incremental re-index equivalence (HARD GATE). An incremental re-index (reuse unchanged files'
spans from the parent snapshot, parse only the diff) must yield the identical
{logical_key: artifact_id}snapshot as a full re-index of the same tree — verified across two distinct SHAs in one database (artifact ids are content-addressed) — and must provably skip the parse for unchanged files (parsed_files/reused_filescounters). A missing/unindexed parent or a first-party-root change falls back to a full index. - Tier 2 — golden curated repos (TRACKED, non-gating). 3–5 SHA-pinned permissive Python repos; one held out and never used for tuning (the real trust signal). Report per-repo, never just the mean.
- Tier 3 — downstream vs RAG (TRACKED, after MCP). Fixed question set; coding agent
answers with knowbase-MCP vs a frozen, peer-reviewed pgvector-RAG baseline (same
Postgres, same model). Pre-register the win threshold. Metrics: grounded-answer accuracy,
hallucination rate (claims with no provenance), tokens-to-answer, tool round-trips.
Implemented: the deterministic cross-file recall gate (
tier3_rag_test, HARD) plus an optional, key-gated, NON-gating LLM-judged A/B (kb.llm+tier3_llm_judge_test, run nightly): an answerer answers each question from knowbase context vs RAG context, and a judge scores accuracy against hand-written gold + hallucination, comparing to a pre-registered threshold (printed, never asserted).
Invariants asserted as exact ground truth every run: every artifact has ≥1 derived_from
row (zero orphans); re-running an extractor on the same span identity+version yields an
identical artifact_id (reproducibility). For the semantic layer (later): 100% of stored LLM
claims survive independent span re-validation, and adversarially-injected ungrounded steps are
rejected. Verbalized LLM confidence is never used as the score.
Semantic-layer hard floor (review fix): the thesis-bearing semantic extractor must have a deterministic sub-property gate, not only subjective Tier-2/LLM-judge: every claimed sink in a process summary is a real sink-registry match on the path; path endpoints are real entrypoints/sinks. Confidence must honestly count unknown-unknowns (edges never discovered by the ~70%-recall call-graph engine), not only "unresolved on the path it found".
Implemented: the
kb describedescriber enforces this floor —kb.extract.semantic.grounding.validate_claimsdrops any claim whose cited symbol is absent from the target's grounding spans; a target with no surviving claim is not stored. It coversapi_route/entity/event_handlerartifacts, per-module (file) descriptions, per-package architecture overviews (a package is grounded on its own and its direct-child modules' spans; the overview synthesizes the import graph + public surface + member-module summaries as context, but claims still validate against code spans, so provenance stays code-grounded), and the whole-repo overview (onedesc:repoper snapshot, grounded on the bounded top-level surface — top-level modules plus each top package's own bounded set,queries.repo_target; it runs under its OWN system prompt that demands synthesis of the just-written package/module summaries — the repo's substance lives in those facts, not in its mostly-empty top-level spans — with a per-span body cap so no single large top file can monopolize the prompt, while claims still validate against the grounding spans). The gate is deterministic, sosemantic_grounding_testenforces it in CI (stub LLM, no API key), including an adversarial fabricated claim that must be dropped — on the artifact, module, package, event-handler, and repo paths — and a bounded-grounding proof (a grandchild module never grounds the repo overview).Implemented (deterministic process slice):
process_pathartifacts satisfy the floor by construction — every sink claim IS a registry match on the materialized path and every endpoint IS an extracted entrypoint. Theirconfidenceis 1.0: a found path is machine-checked exact (thecall_edgeprecedent); the unknown-unknowns of the bounded-recall call graph are priced into the LLM-labeled process artifact (confidence < 1.0 there), with incompleteness carried as explicit payload facts here, never a fudged scalar.Implemented (LLM labeling):
kb describenow labels eachprocess_pathas a fourth describe slice — the label's claims validate against the path's own spans (fabricated claims dropped; a path with no surviving claim stores nothing), and the stored description ships withconfidence = kept / (kept + dropped + 1)— Laplace add-one: the +1 prices unknown-unknowns, so an llm_grounded artifact stays < 1.0 by construction even when a disciplined model fabricates nothing (dogfooding showed the plain ratio degenerating to 1.0 across the board); 1.0 remains reserved for the deterministic layer. Gated insemantic_grounding_teston the stub LLM (no API key), adversarial fabricated claim included.Implemented (ADR mining, slice 1):
kb minereuses the same floor for history-born knowledge — adecisionartifact's claims validate against the spans its source commit changed (§4's D5 bridge), so a fabricated symbol dies deterministically and a commit whose changed code carries no cited identifier stores nothing.confidence = kept / (kept + dropped + 1)(Laplace add-one, as above), counted, never verbalized; trust stays LOW (§4). Merge commits are skipped (their prose belongs to the PR slice), docs-only commits never pay an LLM call, a root commit is mined against the empty tree, and an oversized grounding set is capped with a retro-flag (grounding_capped+total_changed_spans) — bounded, never silent. Re-mining skips stored decisions (LLM-cost idempotency); no-decision commits are re-asked on purpose — a "nothing here" marker would itself be an ungrounded artifact, which D5 forbids. Gated inadr_mining_test(stub LLM, no API key).
Read-only in the MVP. The interesting part is the query shape ("give knowledge relevant to
this target within a token budget"), not the transport. Every response unit always carries
provenance(file:line@sha) + extraction_method(deterministic|llm_grounded) + confidence + freshness(current|stale@sha), with a deterministic tie-break for reproducible eval.
- Shipped tools:
find_provenance,get_knowledge(budget-trimmed, ranked), andsearch_knowledge(cosine-ranked over pgvector embeddings; added v0.2 — see §9 Tier-3). - Deferred:
expand_knowledge, write/mutation tools, auth/multi-tenant, async tasks, subscriptions.
Freshness semantics (open): one
artifact_idlegitimately appears in many snapshots, so "the unit's provenance SHA" is ambiguous. Freshness must be defined per-snapshot (or via per-SHA span occurrence), and likely precomputed per commit to avoid an O(repo) query.
| Module | Responsibility | Key tech |
|---|---|---|
kb.structural |
Parse Python without executing it; enumerate symbols/imports/call-sites with per-SHA byte/line ranges; compute content-addressed span identity; incremental reparse. Hidden behind a StructuralIndex/PathEngine interface so a SCIP backend can replace tree-sitter later. |
tree-sitter + tree-sitter-python (canonical bindings) |
kb.extract.deterministic |
No-LLM extractors → exact artifacts (confidence=1.0): import graph; FastAPI API contract (static, cross-file grounded); domain entities (pydantic/dataclass/SQLAlchemy, static, cross-file links to referenced entities, hand-labeled gate); library public-API surface (static tree-sitter, cross-file __init__ re-export resolution, independent griffe-oracle gate); event handlers (pydantic validators / FastAPI on_event / SQLAlchemy listens_for + module-level call-form listen, static, cross-file target + registration-site grounding, hand-labeled gate); call-graph edges (per-edge artifacts, three resolution tiers, caller+callee span grounding, hand-labeled gate); process paths (paths.PathEngine — the first shipped increment of the PathEngine seam — BFS to sink-registry matches, multi-file grounded, registry digest identity-bearing). |
grimp, tree-sitter queries; griffe (dev-only oracle) |
kb.introspect |
Eval-only runtime oracle: runs a FastAPI app in a network-blocked sandbox and emits app.openapi() for the Tier-1 API gate. Never on the index path. |
subprocess sandbox, fastapi |
kb.embed |
Replaceable embedding adapters + snapshot population for search_knowledge. Torch isolated behind the embed extra and a lazy import. |
sentence-transformers (default), OpenAI (optional), pgvector |
kb.rag |
Frozen pgvector RAG-over-source baseline — the "other arm" of the knowledge-vs-RAG A/B (no provenance/grounding). | deterministic line-window chunker, pgvector |
kb.git |
Ingest commits/branches; diff SHAs → changed byte ranges → changed span_ids. kb.git.forge: GitHub PR-text adapter (stdlib urllib; feeds opt-in kb mine --prs — the only networked module; fail-soft). |
pygit2 |
kb.store |
Single source of truth: content-addressed spans/artifacts, provenance edges, snapshot manifests. Enforces the ≥1-derived_from invariant at write. | PostgreSQL 17, psycopg 3, SQLAlchemy Core, alembic |
kb.eval |
Tiered eval; deterministic tiers gate CI. | pytest over SHA-pinned golden repos |
kb.mcp |
Read-only MCP server; provenance-carrying records; budget-aware assembly. | FastMCP (pinned), Pydantic models |
kb.daemon |
Orchestration + CLI: index a repo @ SHA (full or incremental), run extractors in order, write snapshot, host MCP; kb watch polls a local branch ref and indexes new first-parent commits incrementally (branch_ref cursor, per-commit crash-safe advance). |
typer |
kb.extract.semantic |
Shipped: kb describe — LLM-grounded NL descriptions of routes/entities/modules, per-package architecture overviews (a package grounded on its own + direct-child modules' spans; the overview synthesizes the import graph + public surface + member-module summaries as context, claims code-grounded), process-path labels (one per materialized process_path, grounded on every span along the path, confidence < 1.0), event-handler descriptions, and the whole-repo overview (one desc:repo per snapshot, grounded on the bounded top-level surface via queries.repo_target, synthesizing package summaries + cross-package imports + artifact counts as context), each claim validated against the target's spans by a deterministic sub-property gate (grounding.validate_claims); separate key-gated pass, never on index. kb mine — ADR-candidate mining over local first-parent history: one decision per mined commit, grounded on the commit's changed spans (role changed), message verbatim in the payload, same claim floor; --prs enriches squash-merged commits with PR title/body (digest identity-bearing in framework_versions; enriched prompt pinned by its own prompt_version). |
thin LLM adapter (kb.llm); PathEngine + YAML sink registry live in kb.extract.deterministic.paths |
Review fact-checked these against current (2026) sources. Caveats are first-class.
- tree-sitter + tree-sitter-python — mature, fast, error-tolerant, no project setup. Syntactic only (no type/reference resolution); raw byte offsets shift on edits → offsets live per-occurrence, identity is structural (§5).
- grimp (import graph) — Rust-core, queryable. Gives module-level edges with at most a
line number, NOT byte spans → import-statement spans come from tree-sitter; grimp is
used only for edge resolution. Needs the first-party package resolvable on
sys.path→ not "zero setup" for arbitrary repos; the truly zero-setup path is tree-sitter import parsing, grimp as an enhanced (env-dependent) resolver. - FastAPI
app.openapi()— free exact oracle for the API contract. Runtime mode executes user code → deferred, eval-only, sandboxed (subprocess, no network, resource limits) or an opt-inknowbase introspectthe user runs in their own venv. The serving extractor is static (tree-sitter). - griffe (static) — ready-made library public-API-surface analyzer (signatures, types,
docstrings) with built-in API-diff. Used as the dev-only independent oracle for the
library-surface Tier-1 gate (
allow_inspection=False→ never executes code); the serving extractor is tree-sitter-static and never imports it (mirrors how fastapi powers the openapi oracle while the route extractor stays static). - PostgreSQL 17 — single store. We avoid Apache AGE because the invalidation DAG is shallow (1–4 hops) and a single store is simpler — not because of a "~40× faster" figure (that number traces to one alpha-era microbenchmark of ~1.86×; struck). Recursive CTEs are adequate for this shape.
- FastMCP — chosen on feature set (auth, middleware, structured output, transports). v3.0
is very recently released (was beta in Jan 2026) → pin
fastmcp>=3.0,<4and keep the official SDK FastMCP 1.0 as a genuine fallback. (Vendor "powers ~70% of MCP servers / ~1M downloads/day" claims are self-reported and not relied upon.) - scip-python / SCIP (deferred) — the precise cross-reference/typed-edge upgrade, kept
behind
PathEngine/StructuralIndex(thesymbol_idcolumn is reserved). Caveat: low velocity, small team, no tagged GitHub releases (v0.6.6 is the npm version), needs Node + an activated venv, and the Python consumer will likely compile the.protoitself. This strengthens keeping it deferred behind an interface. - The shipped deterministic
call_edgeextractor takes the tree-sitter-call-sites road: it emits only resolved edges (precision-first) with recall bounded by documented gaps (dynamic dispatch, attribute calls, inheritance) — the "always quote recall, not just precision" rule; PyCG-class resolvers stay deferred behindPathEngine. - PyCG (call graph, deferred semantic layer) — archived/unmaintained; ~99% precision is
paired with only ~70% recall (≈30% of real edges missed → "incomplete path" is the common
case). Treat as a known-temporary default strictly behind
PathEngine; evaluate maintained alternatives (Jarvis, Scalpel, or a tree-sitter-call-sites + scip-python-resolved-refs hybrid). Always quote recall, not just precision.
- Canonical serializer stability — lock one normalized serializer for span identity; fold
normalization_versioninto the hash so it can evolve; assert reproducibility as a CI invariant. logical_keyrename/move tracking — undesigned; needed before multi-commit snapshot diffs are meaningful.- Static-analysis ceiling — dynamic/conditional imports, dynamic route/task registration,
dynamic dispatch/DI/
getattr, metaprogramming are invisible. Surface as an explicit incomplete confidence signal; probe with dedicated fixtures; never imply completeness. - Daemon concurrency/transaction model — commit arriving mid-extraction, partial-snapshot visibility to MCP reads, crash recovery mid-snapshot, content-addressed upsert races. Undesigned.
- Unparseable-file handling — a syntax error on a branch (common mid-refactor) must be a recorded gap, not a silent recall loss.
- Monorepo / namespace-package first-party boundaries —
src-layout, multiple top-level packages, namespace packages; affects import-extractor correctness directly. - Freshness across branches — see §10.
__init__.pyre-exports / star imports / relative imports — break the 1:1 "edge → one import span" mapping for the very first extractor; needs an explicit story.- GC under cross-branch sharing — one
artifact_idis referenced by many snapshots; GC needs reference counting across all retained snapshots + a retention policy. Deferred.
- Second deterministic family: entities (pydantic/dataclass/SQLAlchemy) — shipped (static
tree-sitter, hand-labeled Tier-1 gate); events — shipped (decorator registrations for
pydantic/FastAPI/SQLAlchemy plus module-level call-form
event.listen— v2, static tree-sitter, hand-labeled Tier-1 gate; the bare from-import form is resolved via the import table; function-body/lambda forms and lifespan stay documented gaps). - The one grounded business-process extractor (named real path + labeler + validator +
deterministic sub-property gate) — shipped: the
call_edgeextractor + Tier-1 calls gate; theprocess_pathbuilder (PathEngine BFS + sink registry +.kb/sinks.yamloverride, multi-file grounded paths, Tier-1 processes gate); and the LLM labeler (kb describe'sprocess_pathslice — span validation against the path's own spans IS the binding validator, gated insemantic_grounding_test). - Recursive invalidation (
artifact_depends_on), multi-branch dedup, freshness precompute. - Embeddings +
search_knowledge(shipped v0.2); thenpg_search/BM25 + RRF if vector ranking is insufficient. - ADR mining from git/PR history — both slices shipped:
kb mine(local commit history, decisions grounded on the source commit's changed spans,adr_mining_testgate) andkb mine --prs(opt-in GitHub PR-text enrichment viakb.git.forge; PR-text digest is identity-bearing). - SCIP/scip-python precise-reference backend behind
PathEngine. - Scale: GC/retention, read replicas, monorepo boundaries, runtime-oracle sandbox hardening.
License: AGPLv3 (already chosen) — classic open-core posture. Likely model: OSS core (self-host, single repo) / commercial (hosted, org-wide cross-repo knowledge, PR bot, SSO, scale). Honest moat note: if extraction quality is the value and it's open source, the defensibility is the eval/tuning flywheel (eval sets, tuned prompts/pipelines) + hosted graph at monorepo scale + integrations — not the code itself.