Back to Resume
Open sourceNovember 2025· 8 weeks

Conducks Structural Intelligence Platform

Conducks: A Deterministic Structural Graph for Codebases

RolePrincipal Architect & Solo Engineer

AI coding assistants and developers both need exact answers about a codebase's structure, not the approximate matches an embedding search returns. Conducks is a CLI and MCP server I built that parses a codebase with Tree-sitter, builds a deterministic graph of its symbols and relationships, and stores it in a local DuckDB vault any tool can query. It replaces a guess about where code lives with a graph-verified answer: a symbol's file, line, callers, and risk score, computed the same way every time, with zero LLMs or embeddings anywhere in the analysis path.

The problem

Developers and AI agents both need to answer the same questions about an unfamiliar codebase: where a symbol is defined, what calls it, what breaks if it changes, and whether the codebase's architecture is actually clean. The common tool for this is embedding-based search, which returns the code chunk that reads similar to a query, not the chunk that is provably correct. For an AI agent about to make an edit, that gap between similar and correct is the difference between a safe change and a confidently wrong one. What was needed instead was a structural source of truth built from the real abstract syntax tree, exact and repeatable, not a similarity score.

Constraints

  • ·No network dependency for the core analysis. All parsing and graph-building had to run locally against a developer's own repository, with nothing sent off the machine.
  • ·Multi-language from the start. Real codebases mix TypeScript, Python, Rust, and Go, sometimes in one monorepo, so a single-language graph was not an option.
  • ·Two very different consumers on the same data. A human using a CLI or a dashboard, and an AI agent driving the same graph over MCP with no room to construct arbitrary queries.
  • ·Parser fragility as a given, not an edge case. Native Tree-sitter bindings are ABI-sensitive across Node and OS versions, so a parsing failure had to degrade gracefully, not crash the tool.
  • ·Sole engineer, continuous delivery, no second reviewer. The project's own ~1,600-node codebase is its primary test bed, run against itself daily.

Architecture

Conducks runs a pipeline called a pulse. Git-native discovery lists tracked files with one git cat-file --batch call, Tree-sitter parses them in parallel worker threads (falling back to a regex-based engine if a grammar's ABI does not load), a two-pass reflector builds a full scope map before extracting any call or import edge, and the result is ranked by six graph algorithms and persisted as one atomic transaction to a local DuckDB vault. Three interfaces, a 39-command CLI, a 14-tool MCP server, and a live Mirror dashboard, all read the same vault. The codebase's own internal layering (contracts, core, domain, composition, interfaces, downward-only imports) is enforced on itself by the same guard mechanism the tool offers to other projects.

Conducks Structural Intelligence Platform architecture diagram
  • Chronicle discovery: git-native file listing, 5x faster than a recursive filesystem crawl, respects default and project-level ignore rules.
  • Tree-sitter parse layer: WASM grammars plus native bindings, parallelized across worker threads, each worker loading its own grammar copy.
  • Gnosis fallback: a scope-aware regex parser that activates when a Tree-sitter grammar's ABI does not match the runtime, recovering calls and imports without a full AST.
  • Two-pass reflector: pass one builds a per-file scope map, pass two extracts edges using that completed scope, avoiding stale-context attribution.
  • Taxonomy and graph algorithms: a 9-layer canonical rank for the persisted graph plus Tarjan SCC, PageRank, weighted Dijkstra, and Shannon entropy, implemented from scratch.
  • DuckDB vault (.conducks/): the local, file-based store for the graph, written inside one atomic transaction per pulse.
  • CLI: 39 commands across discovery, governance, historical, and visual domains, the only write path into the vault.
  • MCP server: 14 read-only tools resolving to fixed, parameterized query templates, never raw SQL, with the current pulse id always system-injected.
  • Mirror dashboard: a live, force-directed graph visualization on port 3333, pushed updates over server-sent events.

The live path runs left to right through discovery, parsing, reflection, and algorithms into the vault. Three interfaces read that same vault without duplicating logic. Watch mode keeps the vault current between full pulses by re-inducting only the files that changed.

How one request flows

  1. 01Discovery: Chronicle Discovery lists every tracked file in one git call and applies ignore rules so build output and vendored code never enter the graph.
  2. 02Parsing: worker threads parse files in parallel with Tree-sitter, each loading its own grammar copy since a worker does not inherit the parent thread's loaded grammar state.
  3. 03Fallback, if needed: if a grammar's ABI does not match the runtime, the Gnosis regex engine extracts calls and imports through pattern matching instead of letting the language silently degrade to file-only nodes.
  4. 04Scope-aware extraction: a two-pass reflector builds the full scope map of a file before extracting any edge, so a call is attributed to the function it actually belongs to.
  5. 05Taxonomy and algorithms: every symbol gets a canonical rank in the 9-layer taxonomy, and six algorithms implemented from scratch (including Tarjan SCC, PageRank, and weighted Dijkstra) run over the graph.
  6. 06Persistence: the whole pulse runs inside one DuckDB transaction, so an interrupted pulse rolls back to the last good graph instead of leaving a partial one.
  7. 07Query: the CLI, MCP server, and Mirror dashboard all read the same vault, with MCP tools resolving to one of nineteen fixed query templates rather than a raw SQL surface.

Engineering decisions

A deterministic graph instead of embeddings

What. Built the entire analysis pipeline on classical graph algorithms (Tarjan SCC, PageRank, weighted Dijkstra, Shannon entropy) with zero LLMs or embeddings in the path, so every risk score and blast-radius answer decomposes into named, provable signals.

Why. The questions the tool exists to answer, exact callers, exact cycles, exact blast radius, need a provable answer, not a plausible one.

Tradeoff. An embedding search can surface code that is conceptually similar with no shared symbols, which this graph cannot do. That fuzzy-recall capability was traded away on purpose.

A strict, one-directional layer stack

What. Split the codebase into contracts, core, domain, composition, and interfaces, with imports allowed to flow one direction only through a single composition root, enforced by a conducks guard rule rather than left as a written convention.

Why. With no second engineer to catch drift in review, the boundary had to be checked by the tool itself, not by discipline alone.

Tradeoff. Code that needs to reach up a layer must be restructured or passed data explicitly through the composition root, more ceremony than a flat module layout.

Two systems: containment tree plus classified boundary edges

What. Separated a 9-layer containment taxonomy from a second system of reference edges classified by origin (internal, standard library, or third-party dependency). The taxonomy enum declares 13 kinds, but the persisted graph deliberately reconciles to 9 by cutting bare data nodes and edge-gating variables.

Why. Emitting a node for every local variable flooded the graph, roughly 72 percent of nodes on a real repository, and buried the architecturally meaningful signal.

Tradeoff. The enum-versus-persisted-graph mismatch looks like a bug on first read. It is a documented, deliberate design choice, not an oversight.

A parser that degrades instead of failing

What. Added a second, independent parsing path, a scope-aware regex engine, that activates automatically when a native Tree-sitter grammar's ABI does not match the runtime.

Why. A native binding mismatch has silently dropped Go and Rust support to file-only parsing in the past, with no error raised, across unpredictable developer environments.

Tradeoff. The fallback recovers calls and imports only, not a full typed AST, so some deeper analyses are unavailable for a file parsed this way.

An MCP surface kept deliberately narrow

What. Capped the MCP tool count at 14, adding a tool only when an agent genuinely needs it and it earns its place, not for parity with the 39-command CLI.

Why. A large tool list actively hurts an agent's ability to pick the right tool. The bar for a new MCP tool is need, not completeness.

Tradeoff. Several CLI-only analyses (cohesion, entropy, cross-project resonance) are intentionally unreachable from an agent, even though a human can run them directly.

A dead-code detector biased toward under-reporting

What. The prune command excludes entry points, test fixtures, and anything reached only through dynamic dispatch (such as a dependency-injected getter) from its dead-code findings, even when doing so leaves a few confirmed-benign false positives unflagged.

Why. A tool whose output can trigger a deletion has to fail toward silence, not toward noise. On the project's own codebase, a manual audit confirmed the great majority of flagged orphans were genuinely dead, and the remaining unflagged cases were verified false positives.

Tradeoff. Some genuinely dead code stays unflagged rather than risk a human deleting code that is actually reached dynamically.

Architecture docs as authored content, not generated output

What. Reversed an earlier policy that banned architecture.md as machine-derived content. Wiring (calls, imports, cycles) is always queried live through the CLI and never written to a file. Intent, why a module exists and what was deliberately not built, is written by hand where it has stopped being obvious from the source.

Why. Banning architecture docs outright to stop them going stale also discarded the human explanation of a module's purpose, which no graph query can produce.

Tradeoff. Authored docs are only as current as the last person who updated them, so the standard scopes them to modules where intent is genuinely non-obvious, not every file.

What changed along the way

  • Started in November 2025 as a passive governance server, a fixed rules engine an AI agent could query. That had nothing concrete to check its rules against, so a rewrite turned it into an active structural graph engine with governance layered on top instead of the other way around.
  • Ingestion was originally single-threaded. Once pulse times on a real monorepo became impractical, it was rebuilt around a multi-core worker-thread pipeline, which required solving a non-obvious problem: a worker thread does not inherit its parent's loaded grammar state.
  • The first taxonomy emitted a node for every variable and literal, flooding the graph with low-signal atoms. It was redesigned into a containment tree that only keeps a variable as a node when it carries a real cross-scope reference.
  • Architecture-documentation policy reversed once: banned as derived output, then reinstated as authored content, once wiring and intent were recognized as two different kinds of information needing different treatment.
  • The MCP tool surface was consolidated more than once, settling at 14 tools chosen by agent need rather than CLI parity, after starting closer to CLI-mirroring instincts.

Security and reliability

  • ·Local only: all parsing and graph analysis runs on the developer's own machine against their own repository, with no source code or result sent anywhere.
  • ·Read-only MCP boundary: the only write operation, conducks analyze, is CLI-only and never exposed over MCP, so an agent cannot trigger a write or a lock conflict.
  • ·No raw SQL surface for agents: MCP tools resolve to fixed, parameterized query templates, and the current pulse id is always injected by the server, never accepted as an agent parameter, ruling out querying a stale snapshot by mistake.
  • ·Atomic writes: a full pulse runs inside a single database transaction, so a killed process rolls back to the last good graph, backstopped by a density-based health check that flags an incomplete pulse.
  • ·Single writer: the CLI holds the one read-write database connection while the MCP server and dashboard connect read-only, ruling out write-lock deadlocks under concurrent use.
  • ·CI regression gate: conducks guard computes a structural entropy delta against a stored baseline and returns a block or pass verdict, catching architectural regressions that passing tests alone would miss.

Metrics

1,626 nodes
in the tool's own structural graph, density 4.54
measured
0 cycles
circular dependencies on self-audit, cross-checked against madge
measured
14 / 39
MCP tools and CLI commands served from one shared engine
measured

What shipped

  • There is no LLM anywhere in the analysis path. Every score breaks down into the six signals that produced it, so a risk number can be explained instead of trusted on faith.
  • Behavioral health on fragmented Python and TypeScript codebases went from 9.6% to 93.5%. Scoped identity resolution restored 6,814 behavioral edges, which is what makes full execution tracing possible.
  • A full pulse over a 9,230-node, 61,352-edge monorepo dropped from 2 minutes 18 seconds to 9 seconds. Three things did it: map-reduce across cores, batched DuckDB writes, and caching the grammar in each worker.
  • Nine MCP tools let an AI agent run blast-radius audits, graph-verified refactors, structural diffs over time, and CI regression guards. The server exposes no write operations, so an agent can read the graph but never change it.
  • The Mirror dashboard renders more than 10,000 symbols at 60 frames per second. Topology stays in memory and metadata loads on demand from DuckDB, which holds RAM at about a fifth of the naive approach.
  • Go joined Python and TypeScript as a supported language in v1.0.0. Fourteen Tree-sitter grammars load across the worker pool and every construct maps to the same nine-layer taxonomy, so one query works the same way in any language.
  • The Gnosis fallback parses with scope-aware regex when a Tree-sitter grammar refuses to load. It took edges from 248 to 6,814, and it means the tool still works on Node versions where the native bindings crash.
  • Cross-repository linking merges 5,000 foundation nodes into a 1,624-node scraper graph, 6,624 federated nodes in total, at a 4ms query baseline. The repositories stay isolated, with zero edges crossing between them.
  • 75 test suites and 199 tests pass with no failures, across 81 source files, up from 46. Each test runs in its own Jest worker with the DuckDB vault cleaned before every case.
  • A 25-command CLI covers nine areas, from discovery and metrics through governance and history, alongside a Next.js marketing site.

Lessons learned

  • ·A governance finding is only as trustworthy as the exact edge types it walks. Three separate false-positive cycles in this project traced back to the same root cause: code counted a graph relationship that was not the relationship the finding claimed to measure.
  • ·Wiring and intent are different kinds of documentation and need different treatment. Banning generated architecture docs to stop them going stale also discarded the authored explanation of why a module exists, which no graph query can produce.
  • ·A save-side fix does not verify a load-side bug. An edge-persistence bug was fixed on the write path and reappeared, unnoticed, on the read path the next day, because the original test only covered writing.
  • ·A detection tool earns trust by under-reporting, not by reporting everything. The dead-code detector deliberately leaves a small number of proven-benign false positives unflagged rather than risk a human deleting code still in use.
  • ·Dogfooding a tool against its own codebase surfaces bugs a synthetic test repo never would. Several fixed false positives were only found because the tool was pointed at itself, daily, not at a fixture.

Tech stack