Explanation: architecture
kuda answers one question — "what does this project look like, right now, in a form an agent can load?" — and treats a wrong answer as worse than none.
The pipeline
repos ──► extract ──► summarize ──► index ──► store ──► serve
│ │ │ │ │
adapters: adapters: graph + sqlite cli |
manifest template cards + yaml mcp | acp
treesitter (llm planned) provenance
lspExtract is mechanical — no model, fully rerunnable. It pulls symbols, package structure, and import edges.
Summarize turns each module's extraction into an L1 card — committed YAML humans review in diffs.
Index merges everything into the symbol/dependency graph with per-node provenance.
Serve is read-side only: find, dependents, refs, card, map, status — plus the MCP and ACP wire surfaces.
The tiers of memory
- L0 repo card (
.kuda/index.yaml) — the ~1k-token always-load blob: purpose, module map, commands, provenance. - L1 module cards (
.kuda/modules/*.yaml) — fetched on demand when planning touches a module. - L1.5 file cards (
.kuda/files/<module>/*.yaml) — per-source-filepurposewritten by the semantic tier (opt-in,[summarize] file_cards = true). Files are chunked with a code-aware splitter (tree-sitter boundaries via text-splitter), fed to the agent, and hash-gated per file. - L2 graph (
.kuda/index.sqlite) — symbols, edges both directions, per-file hashes; gitignored, rebuildable, the query substrate. - L3 — the source itself. Memory is an index into code, never a replacement.
Committed L0/L1/L1.5 + ignored L2 is deliberate: cards are reviewed prose, the graph is derived state.
Incremental sync
sync is diff-driven end to end:
- One walk groups manifests + source files by module (manifest package names, not dir names —
kuda-docs, notdocs). - Every file gets an in-process git blob hash — no subprocess per file.
- Hashes are compared against the store's
filestable — unchanged modules are never re-parsed; a changed file re-parses only its own module. - Cards whose provenance matches are reused verbatim — the summarizer (and the agent, in
acpmode) is only invoked for drift.
Steady-state on a warm index is sub-second; agent spend is per-drift, never per-sync. Deleted files and vanished modules are detected by the same diff — their cards and store rows are removed.
Extraction tiers
Best-available wins, configured per repo via [extract].kinds:
manifest— free, no parser: package graph + module boundaries from Cargo.toml/package.json/pyproject/go.mod.treesitter— grammar+query data fromdata/langs/*: symbols anduse/import edges in ~any language.lsp—documentSymbolagainst a real server (lsp_server = "rust-analyzer"); the strongest signal where a server exists. Absent server = silent skip.
See adding a language — knowledge lives in descriptor files; adding one is a data change.
Language as data
Per-language knowledge — grammar refs, symbol queries, manifest names — lives in data/langs/*, not in extractor code. The adapters' vocabulary (language() registries, SymbolKind, EdgeKind) is the only fixed part.
Ports and adapters
kuda-core holds domain types only (Symbol, ModuleCard, Provenance, Claim-free config). Every concern is a trait crate + adapter crates:
kuda-extract→kuda-manifest,kuda-treesitter,kuda-lspkuda-store→kuda-store-sqlitekuda-summarize→kuda-template, (kuda-llmplanned)kuda-vcs→kuda-git;kuda-config→kuda-config-toml;kuda-secret→kuda-keyring,kuda-dotenv
New capability = new adapter crate, not a core rewrite.
Trust model
Every artifact carries provenance {files, hashes, commit_sha, generator} — kuda status re-hashes and reports drift per card. Cards end with verify: — the files an agent should open before acting on the card. Memory is a hypothesis that narrows the search space; the code remains the only truth. mimus audit independently re-verifies card provenance — memory with an external falsifier.