Skip to content

feat(codemap): a code map plane — files, imports, importers, and the issues touching them - #1

Merged
sorcerai merged 44 commits into
mainfrom
feat/codemap
Sep 6, 2026
Merged

sorcerai merged 44 commits into
mainfrom
feat/codemap

Conversation

@sorcerai

@sorcerai sorcerai commented Sep 6, 2026

Copy link
Copy Markdown
Owner

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> and bd update --claim print a CODE section: each linked file with its layer, one-line summary, imports, importers, and the other open issues touching the same files.
  • bd prime carries a bounded repo shape (layers, hot files) plus two instructions that replace exploring.
  • Claude Code PreToolUse / PostToolUse hooks (installed by bd setup claude) inject a four-line file briefing before a Read/Edit and record edited files against the in-progress issue.
  • A managed post-commit git 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 explain read a knowledge graph nothing generated, bd arch draft had 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 is docs/superpowers/specs/2026-09-05-codemap-design.md.

How it is built

  • Storage: migration 0067 adds code_nodes, code_edges, issue_files, keyed by the repository fingerprint bd init already records. Three roles (codemapops.Indexer, Reader, IssueFiles) follow engdocs/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.
  • Scouts: deterministic Go (go list + go/parser) and Rust (cargo metadata + mod/use resolution). Unresolved references are counted, never guessed.
  • Summaries: opt-in only (--summaries), routed through agy, pinned to the file's git blob so a rescan marks them stale rather than silently keeping them.
  • Cache: a derived .beads/codemap.cache.json so the PreToolUse hook never opens the database.
  • HTTP: four operations written spec-first in openapi.v0.yaml, handlers call roles only.

Verification

Measured on this repository (2906 nodes, 9174 edges):

Operation Time
bd codemap build (full) 7-16 s
bd update --claim with the CODE section 0.29 s
PreToolUse hook, own work 17 ms
go build -tags gms_pure_go ./... && go vet -tags gms_pure_go ./...
go test -tags gms_pure_go ./codemapops/ ./internal/codemap/... ./internal/storage/codemapops/ \
  ./internal/agyclient/ ./backend/conformance/ ./internal/storage/issueops/ -count=1
BEADS_TEST_EMBEDDED_DOLT=1 go test -tags gms_pure_go ./cmd/bd/ -run 'TestCodemap|TestShowPrintsCode|TestShowBranch|TestClaimPrints' -count=1
BEADS_TEST_PROXIED_SERVER=1 go test -tags gms_pure_go ./cmd/bd/ -run TestProxiedServerShow -count=1 -timeout 30m
go test -tags gms_pure_go ./internal/storage/dolt/ ./internal/storage/uow/ -run 'CodeMap|IssueFiles' -count=1

A full go test ./... on this branch leaves 23 packages failing. Each one was re-run standalone against main at a95db254e and fails identically there: cmd/bd/doctor, internal/fdhygiene, internal/remotecache, internal/storage/dolt (exceeds a 20-minute cap on both), test/docsync, and two cmd/bd tests (TestCorruptMetadataDiagnosticsRunAndDataFailsLoud, TestEmbeddedCreateStorageClass). scripts and test/testmainconvention fail 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 prime naming bd codemap build. Nothing indexes implicitly, no summary is ever generated without --summaries, and BD_NO_CODEMAP=1 silences all three hooks.

Notes for review

  • bd setup claude now writes two hook entries it did not write before.
  • Until a released bd is upgraded, a checkout of this branch prints unknown hook: post-commit after each commit; the managed hook name is new.
  • bd codemap show --json and the code block on bd show --json use snake_case members.
  • Follow-up work is filed as 28 open children of epic beads-20x.9.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ecAiHvkZLYEZc9A1UiyQC

sorcerai and others added 30 commits September 5, 2026 16:33
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
…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
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
sorcerai and others added 14 commits September 5, 2026 20:40
…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
@sorcerai
sorcerai merged commit 1e9c848 into main Sep 6, 2026
110 of 120 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant