feat(codemap): a code map plane — files, imports, importers, and the issues touching them - #1
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Adds Graph.ExternalPackages so an incremental scan can name packages it imports but did not rescan without failing edge-endpoint validation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…ode context) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
A rescan of a package the scout found deleted arrives as ApplyInTx with Only set and no nodes, which built "id NOT IN ()" and failed to parse on Dolt. The NOT IN clause is now added only when it has operands, so an empty keep list means "delete everything in these packages". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…hree roles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…r accessors) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Teach both decorator censuses the codemapops facade package, and enumerate CodeMapIndexer, CodeMapReader and IssueFiles in every table that lists roles. The storage file gets three fake-store fields (initialised, so the UNWRAPPED rows can actually fail), three accessors, ten methods on the shared sentinel, and rows in the wrapped/unwrapped and error-propagation tables. All three recurse UNWRAPPED: the hook vocabulary is on_create/on_update/on_close and each hands a script an ISSUE, which an apply, a summary pass and a Record binding an already-existing issue to a path do not produce. The pass-through paragraph now names them so that reads as a decision. The telemetry file gets the same, wrapped: telemetry spans reads too. issue_roles_external_test.go gets the three layering pins, read-shape. timedProvider declares the three accessors over the WRAPPER rather than recursing, or an HTTP request reaching a code-map role would bind to the untimed provider and log uow_ms=0.000. Binding-test floor raised 15 -> 29, the verified accessor count. Prose counts derived rather than incremented: the census is 31 roles today, and the files said twenty-eight before this change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…with three wirings Twenty-two contract cases across the three code-map roles, each citing the leaf doc line it pins, wired on all three legs (dolt, embeddeddolt, uow) through the accessors. Every case owns its repo id, so the suites are order-independent on the legs with no per-test branch. Also required by gates the brief did not anticipate: codemapops joins facadePackages (the census cross-check already failed without it, which left the leaf's ten methods outside the exhaustiveness gate), and the role bundle gains three fields and three dispatch rows so an out-of-tree backend running RunRoleContracts sees these cases too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Completes Task 11: ScanAll's error branches had no runnable check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
PackageDirsFor returns repo-relative directories, but GoScout passed `only` straight to `go list` as import-path patterns and RustScout matched it against crate names. Neither accepted a directory, so an incremental scan built from changed files returned an empty graph with no error. Go prefixes a non-import-path scope with "./"; Rust also matches a crate whose directory contains the requested path. An explicitly empty scope now scans nothing instead of falling back to the whole module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
An incremental scan kept every workspace member in the module lookup but indexed only the scanned crate's files, so a `use` into an unscanned crate resolved to nothing and incremented Dropped. A path that resolves in a full scan was reported as unresolvable. A crate now records whether this scan read it, and a use path naming an unscanned crate is out of scope: neither an edge nor a drop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
build merges every detected scout's graph into ONE codemapops.Graph and applies it once with Only == nil. Applying per language would make each whole-repo apply delete the previous language's nodes; Lang already distinguishes them on every node, so the merge loses nothing. refresh reads the last indexed commit from Reader.Shape, diffs it against HEAD, and rescans only the packages owning the changed files, one Apply per language scoped by Only. refreshCodemap is a function rather than cobra-inline because record-commit calls exactly this next. status reports "up to date" or "behind HEAD" plus counts, stale summaries, cache age and detected languages. A commit count it cannot compute (after a rebase, say) is omitted rather than reported as zero. Dogfooded on this repository at 9b810d5: 2906 nodes, 9174 edges, one language, 5m20s wall for build and 0.5s for status. Two deviations from the brief, both deliberate: - The read-only subcommands are NOT added to readOnlyCommands. That map is keyed on cmd.Name() globally, and roughly fourteen commands are already named "status" (bd dolt status, bd github status, bd epic status, ...) plus an existing bd stale and a top-level bd link. Adding those names would flip every one of them into read-only store mode and change runsPostCommandMaintenance for them. Read-only registration buys a read-only store handle, not correctness, so omitting it costs nothing and avoids a cross-cutting behavior change. - The build test expects SIX nodes on the go-mini fixture, not the five in the brief. The go scout excludes gen/gen.pb.go as generated but still emits the empty package node that contained it. TestGoScoutMini never caught this because it asserts the five nodes it wants are present rather than pinning the total. Left as-is: the scout is out of scope here, and the empty package node is harmless to every verb in this commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
show tries a file first because a path is the common spelling, and falls back to a package when the map has no file by that name. Issues are recorded against file paths, so the ISSUES section appears only on the file branch. deps walks imports (or importers, with --reverse) breadth-first, one Reader call per node, dispatching on node kind because a file's imports are packages. A visited set cuts cycles; a neighbour the map cannot expand is a leaf rather than a failed walk. link does NOT validate paths at the front door. RecordInTx normalizes every path and refuses one that escapes the repository before it probes the issue or writes a row, so the rule stays with the storage that enforces it instead of being copied here — and cmd/bd may not import internal/storage/codemapops anyway. renderRefList caps a list at 20 lines with an exact count and a pointer to --json. Package-scoped lists are routinely huge: bd codemap show on a file in cmd/bd reports 580 sibling test files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…d packages Two review findings. FINDING 1 — build spent ~95% of its wall time in an O(files) sequence of store transactions. cache.Build looped every path calling Reader.FileContext then IssueFiles.ByPath, each its own withConn transaction: ~5560 of them on this repository, against a scout that takes 1.4s and one Apply. cache.BuildFromGraph replaces it and reads nothing per file. Imports and importers come from the scanned graph's own edges (file->package imports; importers = whatever imports the file's package). What it still needs from the store arrives whole: the previous cache carries Summary and Layer forward (a scan produces neither; only Task 21's summarizer will), ONE Reader.Stale call marks which of those went out of date, and openIssueLinks maps paths to open issues. openIssueLinks is two steps because neither role method is both cheap and correct on its own: ByIssue has no repository filter, so it can only propose candidate paths, and ByPath is authoritative but would cost a call per file. Asking ByPath only about paths some open issue touched keeps the repository scoping the old per-file build had, at O(open issues + touched paths). rebuildCache now takes the applied graph plus a `full` flag. full says the graph covers the whole repository, which is what decides whether a previous cache entry the graph does not mention is a file outside this scan's scope (keep) or one that left the tree (drop). cache.Build and its store-fake test are deleted; nothing else called them. Dogfooded on this repository: before 5m20.41s after 7.08s FINDING 2 — refresh left orphan nodes when a package directory was deleted. `only := packagePathsOf(g)` scoped the apply to what the scan RETURNED. A deleted directory yields a `go list -e` placeholder that goscout skips at its `p.Dir == ""` guard, so the package never reached Only and the scoped delete never removed its rows. Graph grows ScopedPackages: the package paths the scan was ASKED about, including ones that no longer exist. The go scout derives them from the `only` list rather than from `go list` output, because the whole point is naming a package the toolchain can no longer see. The rust scout reports the crate names its scope matched. refresh applies Only = union(ScopedPackages, packagePathsOf(g)). Verified by mutation: with the union reverted to packagePathsOf alone, TestCodemapRefreshDeletedPackage fails and prints the orphaned package still carrying b/b.go and b/b_test.go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Regression from the previous commit. BuildFromGraph derived entry.Importers purely from the scanned graph's edges, which is the whole answer only for a whole-repository build. On refresh the graph holds just the rescanned packages, so when package P is rescanned and an unmodified package A imports it, A's edges are absent and P's cache entry lost that importer — silently, overwriting a previously correct entry on every refresh touching P. Importers for a rescanned file are now the graph's edges UNION the names in the previous entry that this scan had no say about: not a node in the graph (there the scan is authoritative, so a file that dropped the import must not come back) and not deleted by the commit range (there the commit is). The deleted set is passed into BuildFromGraph rather than stripped from prev beforehand, because a deleted path has to be filtered out of the importer lists INSIDE surviving entries, not just from prev's own keys. Imports need none of this: a file's imports come from its own source, which a rescan just read. Both halves are mutation-verified. With the union reverted to the graph's edges alone, TestBuildFromGraphKeepsUnscannedImporters reports the importers as [] and TestCodemapRefreshKeepsCrossPackageImporters fails end to end with "refresh of package a dropped the importer living in package b: []". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
`bd codemap record-commit <rev>` reads the issue ids out of a commit message, records the files that commit touched against each of them with source=commit, and then runs the same incremental rescan `bd codemap refresh` does, so a commit leaves the map describing the tree it made. post-commit joins the managed git hooks. Its codemap half never fails the hook — git has already written the commit by then — while the chained hook's exit code is propagated exactly as every other managed hook does, so installing beads cannot silently disarm a user's existing post-commit. It stays out of the way entirely when BD_NO_CODEMAP=1 or the repository has no code map cache yet, and bounds the subprocess with BD_CODEMAP_HOOK_TIMEOUT (default 10s). The three copies of the managed hook-name list (install, status, uninstall) are now one: two local literals that had to be edited in lockstep are pointed at managedHookNames, and a new test pins that every managed name has a `bd hooks run` case and a line in all three help blocks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
`bd codemap-hook pre-tool` tells the agent what the map already knows
about a file before it opens it — one header line, its imports and
importers, the open issues on it, and how to ask for more — at most once
per file per session. `bd codemap-hook post-tool` records that the issue
being worked touched a file the agent just edited.
Neither may ever fail a tool call: every path ends in "{}" on stdout and
exit 0, and a failure is at most one line on stderr. post-tool records
ONLY while the active issue is in_progress; an open issue that merely
happens to be the last one touched is not what an edit is about, and
attributing files to it would poison the link table quietly.
codemap-hook is a parent with two subcommands rather than one command
taking an argument, so the two halves can differ in what they may cost:
commandOptsOutOfStore walks the ancestor chain, so the skip-store
annotation on pre-tool exempts it without exempting its write-path
sibling. pre-tool therefore opens no store at all — it reads the derived
JSON cache and runs one `git rev-parse --show-toplevel`, which is what
keeps it correct in a worktree and in a repository whose .beads is a
redirect (this one is). The session marker is checked before the cache is
read, because in a long session it is the common answer and the cache is
the expensive part.
Measured on this repository with a 1.4 MB cache: 87 ms wall on a first
read, 66 ms on a repeat; in a small scratch repo, 59 ms. The hook's OWN
work is 17-18 ms of that in both. The remainder is bd's process startup
floor, which is 69 ms here and 42 ms there for `bd version` itself, so
the brief's 50 ms is not reachable as total wall time by any command.
`bd setup claude` now registers both hooks with matchers — Read|Edit|
Write|MultiEdit for pre-tool, Edit|Write|MultiEdit|NotebookEdit for
post-tool — and removeClaude and the legacy settings.local.json sweep
take them away again. addHookCommand gained a matcher parameter;
its two existing callers pass "".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
… hook list
Two gaps found reviewing the two commits before this one.
BD_NO_CODEMAP gated only the post-commit git hook. An opt-out named for
the feature that silences one of its three hooks is a trap, so both
Claude Code tool hooks now check it too and answer "{}" without decoding
their payload or touching the store.
cmd/bd/doctor keeps its own copy of the managed hook-name list — a
different package cannot import cmd/bd's — and it drives `bd doctor`'s
hook-migration plan. Without post-commit there, doctor plans a migration
that silently omits the hook `bd hooks install` now writes.
Checked and left alone: doctor/git.go and doctor/quick.go list a
recommended SUBSET that already omits post-checkout and
prepare-commit-msg, so adding post-commit there would be a new warning,
not a fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…tool hooks for plugin users
The 2s post-tool budget did not cover the thing a tool call actually waits
for. The context was created inside the handler, but the store opened in
PersistentPreRun before RunE could read stdin, so an embedded-Dolt open —
seconds on a cold workspace — sat structurally outside the deadline.
post-tool now carries the same skip-store annotation pre-tool has and opens
the store itself, inside the bounded context, through
ensureStoreActiveWithContext: the lazy-open path ensureDirectMode already
uses. Nothing from the pre-run's 300-line block is duplicated, and
PersistentPostRunE still auto-commits and closes the lazily-opened store, so
the record is not lost. BD_CODEMAP_TOOL_TIMEOUT makes the budget settable,
which is how the new test proves the open is inside it: at 1ms the hook
answers {} and records nothing, and the same payload at the normal budget
records — so the first half is a timeout, not a broken hook.
Under skipStore the pre-run never sets proxiedServerMode, so a
proxied-server workspace now logs one line and records nothing instead of
recording. Called out in the report; restoring it is ~6 lines if wanted.
Both bd setup claude registrations sat inside the else of the pluginManaged
branch. Only SessionStart is plugin-managed and the plugin ships no tool
hooks, so that left plugin users with no code map integration rather than
avoiding a duplicate. Registered in both branches.
Minors: XDG_CACHE_HOME in the test env so marker isolation does not rest on
HOME alone; the unused codemapHookMarkerDirOverride deleted; WaitDelay on
the record-commit subprocess so an inherited pipe cannot outlive the
deadline; stdin wired through runBDEnvRaw, without which the budget test
would have passed vacuously.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
post-tool skips the root pre-run so its budget can cover the store open, which also skipped the config resolution that picks direct-vs-proxied. A proxied workspace therefore took the direct arm and died on "proxy server store should be uow provider". ensureRecorderStore now picks the route itself: the direct lazy open as before, or newProxiedServerUOWProvider on the same bounded context — the constructor PersistentPreRun uses. It sets proxiedServerMode and cmdCtx.ProxiedServerMode because usesProxiedServer reads them and openIssueFiles asks it which accessor to use; PersistentPostRunE closes the provider under the same flag, so cleanup is the existing path. The proxied harness (BEADS_TEST_PROXIED_SERVER=1, a real Dolt server via testcontainers) made a real integration test cheap, so TestPostToolRecordsOnProxiedServer is that rather than a branch check. It was run, and it fails with the proxied branch forced off. Writing it surfaced two defects it does not fix, both in the report: .beads/last-touched is never written on a proxied workspace, so post-tool's active issue is empty there regardless of route; and bd codemap files/link cannot run proxied at all, because resolveCodemapIssueID calls ensureDirectMode unconditionally. The test seeds last-touched by hand and reads back through the route-agnostic `who`. Three BD_DEBUG lines added at post-tool's silent returns. A hook that must stay quiet is hard to diagnose; that is how the first defect was found. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
… route-agnostically Two defects the proxied post-tool test surfaced, fixed where all callers route through rather than where the symptom showed. .beads/last-touched is local per-clone state, independent of which store route a command took, but four proxied front doors never wrote it: create, update, close and link. Each now records exactly what its direct twin does — create the new id, update the first id that actually updated, close the first settled id with a claimed-next overwriting it, link the from-side id. ready, heartbeat and comment already did; assign, note, priority and tag have no proxied twin. This is what left post-tool's active issue empty on a proxied workspace no matter which store it opened. resolveCodemapIssueID called ensureDirectMode unconditionally, so bd codemap files and bd codemap link could not run proxied at all. It now makes the same route fork resolveLabelTarget makes, reusing the exact-then-wisp resolver every proxied front door already shares. That resolver was called resolveLabelTargetProxied and is not label-specific, so it is renamed resolveIssueIDProxied rather than copied. TestPostToolRecordsOnProxiedServer stops seeding last-touched by hand: it claims through the proxied route and asserts the file appears, then writes with codemap link and reads back with codemap files, both proxied. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
… direct route Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…ts store Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
… flag `bd show` gains the CODE block after the related section: one row per file the issue touches, its layer and summary, the import fan-out under it, and the other issues on those same files, deduplicated by issue id. The JSON payload carries the same context under a `code` key, set only when the issue has linked files, so an issue with none is byte-identical to before. `bd update` gains `--files a,b`, which records manual links on every id that updated, and prints the CODE block after a `--claim` on both the direct and the proxied route. The global `--quiet` suppresses the block. Every read here is best effort: no repository, no map or a role error is a debug line and no output, never a failed show or claim. Success criterion 1 measured on the embedded store: `bd update <id> --claim` with a built map and a linked file runs in 0.29 s wall (0.10 s user, 0.06 s system), against a 2 s target. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
`bd prime` gains a `## Code map` section between the memories block and the command reference, on both the CLI and the MCP output: a freshness line, the layers with their package counts, the highest fan-in files with their one-line summaries, and the two instructions that say what to do with any of it. Capped by `codemap.prime.max_lines` (default 20), with the header, freshness and both instructions always surviving the cap; layers are dropped before hot files. `codemap.prime.max_lines` is registered in `recognizedConfigKeys` so `bd config set` does not warn on it, and in `yamlOnlyKeys` for the same reason the prime memory caps live there: prime reads it at session start, before the database is necessarily reachable. Without the second, the value lands somewhere prime cannot see and the knob silently does nothing. A repository with no map gets the one line that names the fix. Every other failure renders nothing: prime runs from a session-start hook and must degrade the way the memory read already does, including the second store-nil check after ensureStoreActiveForPrime returns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
`bd codemap build --summaries` and `bd codemap refresh --summaries` now summarize the files whose summary is missing or stale, in batches of 25, via the `agy` CLI. `--max-files N` bounds a pass. Without the flag nothing calls a model; with `BD_CODEMAP_FAKE_AGY=<path>` the "model" is that file's contents, which is how the end-to-end test runs without one. The model's answer is verified, never trusted: an item whose path is not in the batch, whose summary is empty or over 160 bytes, whose layer is outside the repository's vocabulary, or that carries more than five tags is DROPPED and counted with a reason. It is never retried — a malformed answer is the model's opinion, not a transport failure, and only a transport failure earns the one retry per batch. The checks are a strict superset of the store's because SetSummaries errors the whole write on one bad item instead of refusing the row, so one long summary would otherwise lose the 24 good ones beside it. Each summary is pinned to the blob it describes, so the store refuses one whose file moved under it, and the derived cache is patched for exactly the paths that landed rather than rebuilt (a rebuild needs a scan). The layer vocabulary is the repository's own top-level directories plus a fixed set, stored once in `codemap.<repo_id>.layers` so a renamed directory cannot change what an existing summary was allowed to be called. `callAgWithFallback`/`callAgOnce` move out of `bd arch draft` into internal/agyclient as `Call`, unchanged in behavior and now with a test for the fallback order; `--model` is still not sent, since agy's is broken in print mode, so `codemap.summaries.model` is the label recorded on the row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Three fixes from review of the summarizer pass. A candidate file is now stat'd before it is read: over 512 KiB, or binary (a NUL byte in the first 8 KiB), and it is skipped with a warning. The prompt carries 60 lines of it, so reading and regex-scanning a multi-megabyte generated table to throw it away was pure cost. A batch the model answers with prose used to count as ONE drop, so "25 written, 1 dropped" could hide 25 unsummarized files. ParseResponse now reports one drop per file the response said nothing about — which closes the same hole on the ordinary path, where a model that answers about 1 of 25 files previously reported nothing dropped at all. The layer vocabulary and the model label are read and written through the route fork the `bd config` verbs use rather than the global store, which was nil (and so silently no-op) on the proxied-server route. Also: layer matching trims and folds case, storing the vocabulary's spelling; the progress line names BD_CODEMAP_FAKE_AGY when the fake is active; the report line reads "N of M written"; and `codemap.` joins the recognized config prefixes so setting the layers key by hand does not warn. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
`bd codemap export` projects the indexed map into the Understand-Anything document `bd explain` already reads, written atomically to .understand-anything/knowledge-graph.json by default. The derived cache is the enumerator, as it is for build and refresh, and every edge endpoint names a node the document carries. `bd explain` now seeds its associated files from the issue-file links before it greps git log, and — for the workspace this process's store is bound to, where no exported document exists — fills each file's layer, summary and connections from the code map itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Four operations, spec first: GET /v0/beads/codemap/files/{path} (the file's
context plus the open issues on it), GET /v0/beads/codemap/shape, and the
issue's own file collection under GET and POST /v0/beads/issues/{id}/files.
The path occupies one segment, so its separators arrive percent-encoded, and
repo_id is a required query parameter wherever the row or the read is keyed by
repository.
The handlers call the code-map roles and nothing else: no path normalization,
no escaping-path check, no issue-id resolution, no "is it indexed" probe. The
one value narrowed at the edge is source, which must be manual — the other
three sources are observations of a working tree this server cannot see.
New problem code codemap_not_built, frozen to 409 and naming the command that
fixes it; storage.ErrNotFound is a 404 with this plane's own sentence, and the
role's ErrValidation is a 400.
The codemapops wire structs gain snake_case JSON tags, and seven of them are
x-go-type-pinned so TestWireTagBijection checks the document against the struct
in both directions. `bd show --json`'s code payload moves to those names with
them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
`bd update --claim --json` marshals codemapops.IssueCodeContext, whose members became snake_case with the wire tags, and this test still asked for `Files` and `Path` — so it decoded an empty list and failed the assertion it exists to make. Adds the shape read's 409 case and the empty-`paths` refusal to the HTTP tests. The latter needed a named refusal at the edge: the role rejects an empty list as a request-wide validation error that cannot say which member to change, and the document already promises `minItems: 1`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Adds the code-map reference at docs/reference/codemap.md and the decision record at engdocs/adr/0004-code-map.md, plus an AGENT_INSTRUCTIONS.md paragraph, the docs.json nav entry and a CHANGELOG Unreleased entry. The reference is written to docs/reference/, NOT docs/cli-reference/. That tree and docs/CLI_REFERENCE.md are generated by scripts/generate-cli-docs.sh from the release pinned in docs/cli-docs.pin (v1.2.2), which has no codemap command, and docsmint prunes pages for commands absent from the pinned tree. A hand-written page there would be deleted by the next regen and would fail check-cli-docs-drift.sh. Both generated surfaces pick up the codemap family at the next pin bump. Every documented command, flag and JSON shape was verified against a binary built from this checkout and against the embedded tests; every example is one the tests actually run. The 50 ms hook criterion is documented as it measured: unmet as total wall time, unmeetable by any bd command given a 42-69 ms process floor, and met as the hook's 17-18 ms marginal cost. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
An incremental apply declares the packages it imports but did not rescan in Graph.ExternalPackages. idOfPath had no node for them, so it fell back to a FILE id: the edge dangled, and the orphan prune in step 4 deleted it. A refresh of one package therefore stripped every import edge into every package outside its scope, silently, commit by commit through the post-commit hook, while the derived cache still showed the edges. ApplyInTx now builds the path->kind map once — the graph's own nodes, then each ExternalPackages entry as a package — and idOfPath reads it, which also drops the O(nodes x edges) scan the old resolution did per endpoint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
The proxied default renderer appended the bare detail view and rendered the text without ever calling the two CODE helpers, so an issue's linked files appeared on `bd update --claim` and then vanished on `bd show` of the same issue. Both helpers reach the code map through its own accessor, so parity is one call on each path, placed where the direct route places it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Spec §9.4 defines a `branch` linkage source that nothing produced. It is computed at the cmd layer, in memory, on every CODE read: when the checked-out branch name contains the issue id, the branch's diff against its base joins the context as SourceBranch, with FileContext filled in where the map knows the path. Never persisted. A branch is a guess that expires with the branch, so a Record call would leave it in the store as an observation. Recorded links win on a path they already hold, and the renderer already marks the inferred ones ◑. The branch test and the base resolution are `bd explain`'s, so the two surfaces infer the same set from the same repository. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
…in go list's env Three small ones. refreshCodemap returned early over a commit that changed no file, leaving last_sha where it was: `status` reported "behind HEAD" forever and every later refresh re-ran the same diff from the same stale base. An apply with a non-nil EMPTY Only now records the head and touches no row, which is the only write that commit justifies. A docs-only commit already reached an apply through its ScopedPackages, so the case that broke is the empty diff; both are pinned. The go scout's `go list` now runs with GOPROXY=off and GOTOOLCHAIN=local. A code map is a read of the tree in front of it, so a scan that reaches the network or downloads a toolchain is a surprise, not a feature. agyclient keeps passing the prompt as argv: `agy --help` documents stdin only for --input-format stream-json, and --print takes the prompt as its value, so switching would be a guess about another tool's contract. Recorded at the call site with the ceiling and the upgrade path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
Migration 0067 added issue_files with ON DELETE CASCADE from issues, but the issueops.DeleteCascadeTables issue-plane list and the dolt sweeper's mirrored sweptTables list weren't updated, so a sweep or tracked delete would leave issue_files rows uncommitted in Dolt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a code map plane to beads: a per-repository index of packages and files, the import edges between them, and the issues that touch each file. Agents get "what does this issue touch, what depends on it, who else is working here" handed to them instead of grepping for it.
Surfaces it where work actually starts:
bd show <id>andbd update --claimprint aCODEsection: each linked file with its layer, one-line summary, imports, importers, and the other open issues touching the same files.bd primecarries a bounded repo shape (layers, hot files) plus two instructions that replace exploring.PreToolUse/PostToolUsehooks (installed bybd setup claude) inject a four-line file briefing before a Read/Edit and record edited files against the in-progress issue.post-commitgit hook links a commit's files to the issue IDs in its message and refreshes the map incrementally.New command family:
bd codemap build | refresh | status | show | deps | stale | files | who | link | export | record-commit.Why
Re-discovering the same structural facts is the largest token sink in an agent session, and it produces nothing durable. Everything needed already existed in the repo but was never wired to the agent's turn:
bd explainread a knowledge graph nothing generated,bd arch drafthad a dependency scout used only for ARCH.md, and issues carried no file linkage at all.Design decisions and their trade-offs are recorded in
engdocs/adr/0004-code-map.md; the full spec isdocs/superpowers/specs/2026-09-05-codemap-design.md.How it is built
0067addscode_nodes,code_edges,issue_files, keyed by the repository fingerprintbd initalready records. Three roles (codemapops.Indexer,Reader,IssueFiles) followengdocs/ADDING_AN_ISSUEOPS_ROLE.md, share one transaction-level body, and are wired on the server-backed store, the embedded store and the unit-of-work provider with conformance contracts on all three legs.go list+go/parser) and Rust (cargo metadata+ mod/use resolution). Unresolved references are counted, never guessed.--summaries), routed throughagy, pinned to the file's git blob so a rescan marks them stale rather than silently keeping them..beads/codemap.cache.jsonso thePreToolUsehook never opens the database.openapi.v0.yaml, handlers call roles only.Verification
Measured on this repository (2906 nodes, 9174 edges):
bd codemap build(full)bd update --claimwith the CODE sectionPreToolUsehook, own workA full
go test ./...on this branch leaves 23 packages failing. Each one was re-run standalone againstmainata95db254eand fails identically there:cmd/bd/doctor,internal/fdhygiene,internal/remotecache,internal/storage/dolt(exceeds a 20-minute cap on both),test/docsync, and twocmd/bdtests (TestCorruptMetadataDiagnosticsRunAndDataFailsLoud,TestEmbeddedCreateStorageClass).scriptsandtest/testmainconventionfail only in a checkout that has stale.worktrees/*directories and pass in a clean worktree of this branch.Behaviour for repositories without a map
Unchanged, except one line in
bd primenamingbd codemap build. Nothing indexes implicitly, no summary is ever generated without--summaries, andBD_NO_CODEMAP=1silences all three hooks.Notes for review
bd setup claudenow writes two hook entries it did not write before.bdis upgraded, a checkout of this branch printsunknown hook: post-commitafter each commit; the managed hook name is new.bd codemap show --jsonand thecodeblock onbd show --jsonuse snake_case members.beads-20x.9.🤖 Generated with Claude Code
https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC