Skip to content

DevCouncil v0.2.2

Choose a tag to compare

@github-actions github-actions released this 14 Sep 22:41

DevCouncil v0.2.2 Release Notes

DevCouncil v0.2.2 and DevMap v0.2.2 are the same product version. The npm package,
the Go host (devcouncil / dev), and the Rust workspace (devmap, dcstore,
dcverify, dcgrep) all report 0.2.2.

This is a performance and durability release. It makes both halves of the build
materially faster — the resolver's import pass and the store's write path — makes
the store materially smaller, and closes five correctness gaps that audits of
those paths uncovered, four of which no existing test could see.

The store schema advances from 20 to 22. Existing stores migrate in place on
first open; no rebuild is required.

Highlights

1. Go import resolution stopped rescanning the repository

Three call sites answered "which files sit directly in this directory" by
scanning every key of file_symbols and calling parent_dir on each:
go_files_in_dir, the package-suffix fallback in resolve_go_import, and
files_in_dir_with_extensions. Go import resolution calls the first up to four
times per import, so the cost was O(imports × files in the repository) — and
parent_dir allocates twice per call, through Path::parent and
to_string_lossy().replace().

Measured with sample(1) on a 1,942-Go-file tree: 623 of the 825 samples in
resolver:index were this scan, 267 of them inside parent_dir alone, and the
pass was 29% of an incremental build.

One files_by_dir index, grouped once per pass at the point where
file_symbols is already complete, turns all three scans into lookups:

before after
resolver:index 1.058 s 0.212 s 5.0x
touch build (total) — — −29%
cold build (total) — — −13%

code_graph.json and repo_map.json are byte-identical across five corpora.
The index is built from file_symbols rather than from the extractions, so it
holds exactly the population the scans walked — file_symbols takes an entry per
extraction unconditionally, including files that declare no symbol.

The remaining go_package_dirs walk is still O(files), but it is reached only by
the suffix tier and measures 1.4% of the build. It was left alone deliberately
rather than traded for a second structure to keep in sync.

It was found with sample(1) after two hand-instrumented hypotheses had each
blamed the wrong loop — the second because the timer's span silently included
the import loop it was meant to exclude.

2. The ledger stores integers where it stored strings

unresolved_rows repeated three text columns on every row — the source file, the
reason, and the classification — for what is, on a large repository, hundreds of
thousands of rows drawn from a handful of distinct values. Schema v22 interns
those three columns into a shared unresolved_texts pool and stores three
integer ids in their place. Schema v21 drops two indexes that no production
statement filters on.

Measured on this repository (1,094 files), min of five runs, interleaved, against
a binary built from the previous tip:

phase before after
persist:write (cold) 0.7525 s 0.5423 s 1.39x
unresolved (cold) 0.4279 s 0.2264 s 1.89x
commit (cold) 0.0878 s 0.0665 s 1.32x
total (cold) 1.9852 s 1.7924 s 1.11x
persist:write (warm) 0.2017 s 0.1750 s 1.15x
unresolved (warm) 0.1256 s 0.1001 s 1.26x
total (warm) 0.7104 s 0.6666 s 1.07x

On-disk store size falls from 155.1 MiB to 129.9 MiB for the same content.

3. What this costs the reader, stated plainly

Interning is a trade, not a free win. The generation_unresolved view now
resolves three INTEGER PRIMARY KEY lookups per row that the flat table did not
perform at all:

query over the view v20 v22
COUNT(*) 0.015 s 0.047 s 3.19x
project all six columns 0.108 s 0.157 s 1.45x
the same, ordered 0.111 s 0.149 s 1.34x

This is accepted rather than unnoticed. No production statement in the workspace
scans the whole view, no crate outside devmap-store reads the relation, and the
two callers that touch it are test-only or LIMITed. The measurements and the
reason the three joins are deliberately not LEFT joins — an inner join makes
a row with a dangling id fail a count, where a left join would hand the reader a
plausible NULL — are recorded next to the migration in rust/devmap-store/src/schema.rs.

4. Four correctness gaps closed, each demonstrated red

Thirteen tests were added. Each one was shown to fail against mutated code rather
than assumed to be load-bearing.

  • The per-row path memo had no multi-file fixture. Every fixture row in the
    ledger suite carried the same source file, so replacing the memo's guard with
    true — which attributes every unresolved call in a repository to the first
    row's file — left the entire devmap-store suite green. Found by mutation
    testing, not by inspection; a classification memo three lines above it had the
    same shape and had been checked by hand.
  • AUTOINCREMENT was an unguarded correctness requirement. v22 digests are
    taken over paths and unresolved_texts ids, so an id reused after a prune
    could match different text and leave stale rows live. Schema checks verify
    column names, not constraints; there is now a behavioural test that inserts,
    deletes, and re-inserts, and requires the second id to exceed the first.
  • The gate scripts embed SQL that no test parsed. A migration broke
    memory_model_probe.sh while cargo test --workspace stayed green across 303
    test binaries. Every SQL statement the gate scripts embed is now parsed against
    the current schema, over a glob of the scripts rather than a hand-maintained
    list.
  • A devmap-cli fixture wrote a column v22 drops, which the migration would
    have surfaced only at gate time.

5. The differential write's carry-forward guards got tests

save_generation_timed's carry-forward is an optimisation carrying a correctness
obligation: every row it declines to rewrite, it must instead be able to reuse
exactly. Four of its decisions guard against a caller whose affected_paths,
deleted_paths, and extraction slice do not agree with each other — and each was
reachable by mutation with the whole suite green across 101 test binaries. A guard
nothing exercises is indistinguishable from a guard that is wrong.

The guards are not hypothetical: the two writers disagree about what a
differential write even contains. devmap build hands over an extraction for
every scanned file, while the daemon hands over only the files it re-read. Every
carry-forward has to be right for both shapes, and the partial shape — the
daemon's — was the one no test reached.

A new suite names the mutation each test refuses. It is gated on the parse
feature rather than listed as feature-off-safe, because with parse off no
payload identity can be computed, nothing is ever carried, and the write is
refused outright before reaching the decision under test — the tests would not
merely fail to compile, they would assert something else.

6. The daemon says when it is serving, instead of being guessed at

Daemon::wait_until_serving is a latched watch channel tripped the moment the
IPC endpoint is live. The socket-path polling it replaces was wrong in two ways
that cost a gate its credibility:

  • It cannot see a daemon that stopped. A run_loop that returned an error or
    panicked before binding leaves the path missing for exactly the same reason a
    slow startup does, so the waiter blamed the clock. Measured on an idle machine,
    a poll bounded at 500 × 20 ms turned devmap IPC endpoint is already active —
    a daemon that failed in milliseconds and said precisely why — into 11.5 s of
    waiting followed by "it did not bind in time".
  • It cannot see an endpoint that came up and was released between two polls,
    which every short-idle daemon in this crate's tests does by design.

The signal is latched rather than levelled and never cleared, because the
endpoint's release is an ordinary part of a daemon's life — every retirement
path unlinks the socket. What a caller needs to know is that startup reached a
live endpoint, which is a fact about the past and cannot stop being true.

It is not a substitute for watching the task: a caller that wants a bounded wait
still has to race this against the handle run_loop runs on, because a daemon
that died before binding never reaches the signal at all. serving_or_dead — in
the file's test module for unit tests, and in tests/support/mod.rs for the
integration tests, which cannot see inside the library — is that race written out.

7. Cross-repository benchmarks published

benchmarks/results/competition/20260913-multirepo/REPORT.md records a
head-to-head comparison of DevMap against five graph tools across four
repositories, with per-run stdout, stderr, and SHA-256 artifact manifests
retained alongside the report. benchmarks/results/map/ adds cross-repository
map timings for the same four repositories.

Upgrade

Stores at schema 20 or 21 migrate in place on first open. The whole migration
ladder runs inside a single immediate transaction, and the v22 rung refuses and
rolls back if the row count changes across it.

Upgrade equivalence was verified end to end on four repositories spanning Go,
TypeScript, TSX, Python, Rust, Svelte, and JavaScript: the previous binary wrote
a store, the new binary migrated it in place and built over it, and the result
was compared against a cold build from scratch. All four were byte-identical,
across 1,214,003 ledger rows in total — DevCouncil 181,749, GitPulse 403,913,
Manvi 83,533, scholarlm 544,808.

After these notes were first written

Eleven commits landed between d5017d6, where the notes above were written, and
the tagged tree. Four are safety snapshots of uncommitted work, absorbed so that
nothing was lost. The rest are recorded here because two of them changed shipped
behaviour.

The safety merge regressed seven files, and left main red

6b9bf01 absorbed the pre-consolidation snapshot 4d04186 taking the
snapshot's side on conflicts. That was deliberate, and that commit's own message
says so — but the snapshot was the older half, so seven files went backwards.

rust/devmap-cli/src/integrate.rs came out of the merge blended: it kept the
new clap::ValueEnum derive but lost #[value(name = "opencode")] and regained
the hand-written Host::parse match. clap's default rename then spelled the
variant open-code while as_str and parse still said opencode, so
devmap integrate --help advertised a host name the integrator refuses. The
test that pins those two together —
integrate_advertises_every_host_the_integrator_accepts — was left in place by
the merge and had been failing from that commit onward:

the help offers a different set of hosts than `Host` accepts
  left:  ["cursor", "claude", "codex", "antigravity", "open-code", "warp"]
  right: ["cursor", "claude", "codex", "antigravity", "opencode",  "warp"]

e143b72 restores 017da09's content for exactly those seven files, leaving
the merge in history so the snapshot stays reachable. It also brings back seven
Go tests the merge had deleted — five in host_selection_test.go, two in
host_documents_test.go — the detailed TASK-P7-9 entry in docs/TODO.md, and
the build-48cd3c7 benchmark prose that had reverted to v0.2.1 claims.

The guides were rewritten

705c42e committed a rewrite of ten guides that had been sitting uncommitted in
the main checkout. fa6d01b adds the docs/README.md index those guides link
to: nine of them carry a Documentation index link that resolved to nothing
until it landed. fa6d01b also corrects docs/project-status.md, which claimed
the npm launcher was still at 0.2.1 — scripts/check-release.mjs reports 0.2.2
consistent across every version-bearing surface.

Verification

  • Full gate suite green: 9 of 9 gates, 292 test binaries, 3,253 tests, 0 failures
    (rust/verify.sh, exit 0). Step 9, mutation testing, is opt-in and was not run
    in this pass; the mutation figures below are the measurements recorded when the
    store change was made.
  • Determinism, self-build, memory-model, growth-plateau, and
    incremental-versus-cold equivalence gates all pass.
  • Release identity: node scripts/check-release.mjs --tag v0.2.2 reports the
    product version consistent across all six version-bearing surfaces.
  • Mutation testing (store): 15 of 15 mutants caught in schema.rs; in the changed
    functions of db.rs, 78 mutants tested and 64 caught. Of the 14 survivors,
    three were in new code and all three were the one memo guard — two are now
    killed by the added test and the third only disables the memo, which is an
    equivalent mutant. The remaining 11 pre-date this change and are tracked
    separately.
  • The resolver index was verified by output equality, not by timing alone:
    code_graph.json and repo_map.json are byte-identical to the previous
    binary's across five corpora.

Re-verified at the release tip

Everything above was measured at d5017d6, where these notes were written. The
tree that ships is eleven commits later, and 6b9bf01 left it red in between, so
the correctness gates were re-run from a fully cleaned rust/target at the
release tip:

gate result
cargo fmt --all -- --check exit 0
cargo clippy --workspace --all-targets -- -D warnings exit 0
cargo test --workspace exit 0 — 292 test binaries and 12 doc-test targets, 3,253 passed, 0 failed, 8 ignored, all 304 result lines ok
go test ./... (Go host) exit 0 — 27 packages, 0 failures
node scripts/check-release.mjs --tag v0.2.2 product version 0.2.2 consistent

Exit codes were read from unpiped runs. A first pass had piped cargo test into
tail, which discards the exit status and hides all but the last few result
lines — the numbers above come from the re-run, not from that one.

Not re-run at the tip: rust/verify.sh gates 4–8 — determinism, self-build,
memory-model, growth-plateau and incremental-versus-cold equivalence — and gate
9, mutation testing. Those gates resolve their paths relative to rust/, which
the harness running this pass could not change into. They passed at d5017d6
and nothing in the eleven intervening commits touches the resolver, the store,
or the write path: the changes are the seven restored files, ten rewritten
guides, a documentation index, and a Cargo.lock correction. That is an
argument from blast radius, not a measurement, and is recorded as such.

After the tag: consolidation of 2026-09-14

The v0.2.2 tag points at 1d68048 and has not moved. Four commits landed on
main after it, consolidating two branches and the release's own benchmark
evidence:

commit what
986df9e the 20260914-v0.2.2 benchmark campaign and the guides rewritten on it
f491566 merge of claude/devmap-unresolved-calls-ff83ca — a call is one attribution site
4656167 these notes, through the section above
f883618 merge of claude/devmap-field-types — declared field types in every grammar
095d418 the rest of these notes, from The second branch onward

The locally installed binary is built from f883618 and reports devmap 0.2.2 (store schema 22, code graph schema 2, build f883618543e8). The product version
is unchanged; the build hash is ahead of the tag by four commits. All four
analysis components — dcstore, dcverify, dcgrep, devmap — were rebuilt
from that commit and installed to ~/.local/bin, each passing the install
script's health check.

The two documentation commits are deliberately not rebuilt for: a notes-only
change cannot alter an instruction, and rebuilding would move the embedded build
hash printed above and make it wrong. So the installed binary is the last commit
that changed code, not the tip.

main is the only branch. It is 33 commits ahead of origin/main and
unpushed.

The last branch was merged

f491566 merges claude/devmap-unresolved-calls-ff83ca: five commits,
1,842 insertions across devmap-extract and devmap-resolve, three of the five
commits and 48 of the added test functions being tests. It closes four
attribution defects in the call extractor and resolver:

  • A call was counted as two attribution sites. One call produced two entries
    in the unresolved ledger, inflating the coverage warning DevMap reports about
    itself, and letting Vec be attributed as a value rather than a type.
  • A receiver was reduced through an operator that is not part of it, so the
    wrong expression was attributed as the callee's receiver.
  • self in a type's own body now resolves to that type.
  • A function named like a type declared elsewhere no longer borrows that
    type's resolution.

Two of the added suites exist because the prelude-receiver vetoes they cover were
passing vacuously; they now assert over something.

What it changed on this repository, measured

Two generations of this repository's own store bracket the merge — #2570 built
at 13:16 by the previous binary, #2571 built at 14:47 by this one — over an
identical corpus: 1,132 files and 11,674 symbols in both.

#2570 before #2571 after
Calls edges 15,823 15,823 unchanged
Contains / Imports / MemberOf / Implements 12,679 12,679 unchanged
References edges 9,259 7,292 −1,967 (−21%)
Unresolved ledger rows 183,777 133,505 −50,272 (−27%)

The entire edge delta is in References. No call edge and no structural edge
was lost
— which is the claim that matters, because a fix that removes
double-counting and a fix that loses resolution look identical in a total.

The ledger moves the same way, by classification:

classification before after
uninferred_receiver 104,351 66,602 −36%
external 53,863 40,041 −26%
builtin 20,924 22,456 +7%
module_path 829 596 −28%
local_binding, no_namesake, unresolved, host_global 3,810 3,810 unchanged

builtin rising while everything else falls is the Vec fix showing up:
prelude-type receivers that were previously miscounted as uninferred or external
are now classified as what they are. The four classifications that depend on
neither receiver inference nor attribution counting are byte-stable.

A cold build into a throwaway store reproduces 133,505 exactly, so the figure
is a property of the binary and not of the incremental path.

This closes the gap the section above left open

Re-verified at the release tip, above, recorded that rust/verify.sh gates 4–8
were not re-run at the tagged tree, and that their continued validity rested
on blast radius — on the claim that nothing in the intervening commits touched
the resolver, the store, or the write path.

That argument does not extend across this merge, because this merge is a change
to the resolver and the extractor. It does not need to. The full gate chain was
run against the branch tip 8e9ec20 and returned ALL GATES GREEN, gates 1–8
executed, gate 9 skipped as opt-in:

gate result at 8e9ec20
1–3 fmt, clippy, tests green
4 determinism — two clean builds, identical digests green, digest 6f624b6b98b0
5 self-build gates, release green — 2,624 ms, db 110 MiB cold, 1,103 files, peak RSS 599 MiB
6 memory-model probe green — 664,371 milli-B/candidate at-cap against a 1,200,000 cap; above-cap leg emitted 0 fan-out edges
7 growth gate — repeated builds plateau green, 2 generations retained
8 incremental-vs-cold equivalence green, 5 cycles
9 mutation testing skipped (opt-in, --mutants)

The result transfers to the merged tree as a measurement rather than an
inference: git diff 8e9ec20 f491566 -- rust/ is empty. The shipped Rust
tree is byte-identical to the tree the gates ran against, and the only difference
between the two commits is 990 benchmark and documentation files.

So gates 4–8 are now measured for the shipping resolver, which is what the
earlier section said it could not claim. Gate 9 remains unrun, as before.

The benchmark evidence the guides cite is now in the repository

986df9e commits the 20260914-v0.2.2 campaign — 990 files, every run's stdout
and stderr beside the report — and rewrites the comparison guides on it.

The campaign re-runs the four-repository matrix against the shipping v0.2.2
binary and adds what 20260913-multirepo could not: a controlled v0.2.1/v0.2.2
A/B on the same corpora in the same session. All 180 matrix measurements
succeeded; none were discarded.

Two constraints are carried in the report rather than smoothed over:

  • The competitor builds are identical to the prior report's — each was verified
    byte-for-byte against the sha256 the build-48cd3c7 run recorded, after the
    2026-09-13 cleanup deleted the pinned copies. The corpora are not: three of
    the four repositories advanced past the commits that report measured. So
    competitor numbers here are not a controlled A/B against the prior report,
    and only the v0.2.1/v0.2.2 section is.
  • The raw numbers appear to show a 3× query speedup against the previous
    report. The controlled A/B shows none. The difference was campaign
    conditions, not the release, and comparison.md now says so at the point where
    a reader comparing the two reports would otherwise draw the wrong conclusion.

What the A/B does establish for v0.2.2 against v0.2.1, same machine, same session:

v0.2.1 → v0.2.2
Cold index 1.14–1.40× faster on all four repositories
One-file edit 1.37× faster on the largest corpus; unchanged elsewhere
Store on disk 16–19% smaller on all four
Extracted graph identical on all four
Query latency unchanged — every cell within measurement spread
Caller accuracy unchanged — 5/5 in both
Unchanged refresh ~12% slower on the smallest corpus — a real regression, ~5 ms

The regression is listed because it is real, not because it is material.

The second branch: declared field types in every grammar

f883618 merges claude/devmap-field-types (601ec22). langfields makes the
declared-field-type arm one module covering eighteen grammar keys — C, C++, CUDA,
Objective-C, C#, Dart, Go, Java, Kotlin, Pascal, PHP, Python, Rust, Scala,
Solidity, Swift, TypeScript and TSX, with ArkTS and Metal reaching it through the
grammars they borrow. Only Rust and Go emitted it before, each with its own owner
walk and type reducer; both originals are deleted rather than left beside the new
module, and treesitter.rs loses 96 lines and two single-caller helpers.

This merge did not carry the verification the previous one did, and that is
worth stating rather than blurring.
It was merged at its author's session's
request while that session's own mutation pass was still running: 64 of 99
mutants scored, 58 caught, 5 surviving, 1 unviable. Those five survivors were
unresolved at the time of writing. Gate 9 is the mutation gate and is skipped in
the run below, so nothing here addresses them — they are resolved in Third
consolidation
below, where the completed 99-mutant pass supersedes this partial
one.

What was run, against the merge commit f883618 itself:

gate result
1 cargo fmt --all -- --check green
2 cargo clippy --workspace --all-targets -- -D warnings green
3 cargo test --workspace green — 3,319 passed, 0 failed, 311 of 311 result lines ok
4 determinism green, digest 6f624b6b98b0 — unchanged from the previous merge
5 self-build gates, release green — 1,930 ms, db 112 MiB cold, 1,135 files, peak RSS 610 MiB
6 memory-model probe green — 659,456 milli-B/candidate at-cap against a 1,200,000 cap
7 growth gate green, 2 generations retained
8 incremental-vs-cold equivalence green, 5 cycles
9 mutation testing skipped (opt-in) — the 5 survivors above are addressed in Third consolidation, not here

Exit code 0, read from an unpiped run. The pass/fail counts are summed from the
311 individual result lines rather than taken from the banner, because a green
banner over a piped run is how this repository has hidden a red test before.

Its headline claim did not reproduce here

The branch message states that "57% of this repository's remaining unexplained
receivers … are a bare identifier of exactly this shape."
Measured on this
repository, cold-built by the binary before and after the merge:

before f883618 after
uninferred_receiver 66,602 66,682 +80
Calls edges 15,823 15,918 +95
files 1,132 1,135 +3
symbols 11,674 11,708 +34

This is not a controlled A/B. The merge adds three files of its own —
langfields.rs and two test files — so the corpus is not identical across the
two builds, and those three files account for some unknown part of both the +95
call edges and the +80 receivers. The previous binary is no longer on disk to
rebuild the identical file set against.

What can be said: the unexplained-receiver count on this repository did not
fall
. Whether the change delivers on the 306-file Swift corpus the branch also
cites is unmeasured here — that corpus was not built in this pass. The 57% figure
describes the shape of the remaining receivers, which is an opportunity size
and not a claim that this change resolves them; the two should not be read as the
same number.

Repository and install state

main is the only branch. Both consolidated branches were deleted after merging;
safety/tip-main-20260914-consolidation,
safety/tip-claude-devmap-unresolved-calls-ff83ca-20260914 and
safety/tip-claude-devmap-field-types-20260914 mark the pre-merge tips.

Four worktrees remain, all detached and holding no unique work: every modified
file in each was byte-compared against main and found identical, twice — once
before the first merge and again after the second.

The Go host (devcouncil / dev, 0.2.2) was not rebuilt: git diff 1d68048 f883618 -- backend/ is empty, so the installed host is already built from
identical source.

About 39 GB of stale build output was removed — a 17 GB rust/target/debug tree
and 22 GB of benchmark scratch. The benchmark scratch stays gone; the debug tree
does not, because the gate run above rebuilt it from scratch immediately
afterwards. Net reclaimed on disk is therefore the 22 GB of scratch, and
rust/target is back to roughly its previous size holding a valid cache rather
than a stale one. That is the intended end state: the repository's own hygiene
rule is to reuse a valid cache, not to clear it after every build.

The pinned competitor executables under
.devcouncil/benchmarks/20260914-v0.2.2/tools were kept: they carry a
DO-NOT-DELETE-LIGHTLY.md recording that the 2026-09-13 cleanup deleted the
previous copy and cost the next run a ~150 MB re-fetch and a Graphify venv
rebuild. Everything removed is regenerated by a rerun, and every number derived
from it is already committed under benchmarks/results/competition/.

Third consolidation — the mutation gate closed, and the 57% measured

a62ae50 merges claude/devmap-field-types-mutants: three commits on top of
the field-types merge above, finishing the mutation pass that merge was admitted
without. The merge is clean and genuinely disjoint — the branch touches
langfields.rs and its test registry, main's 991 changed paths are benchmark
results and docs, and neither side edited a file the other touched.

The mutation gate this release left open is now closed

The section above records the field-types merge as carrying five surviving
mutants, unresolved
, scored from a partial run — 64 of 99 — that was still
going when the merge landed. That run has since completed, and the survivors are
resolved.

cargo mutants -f devmap-extract/src/langfields.rs --test-package devmap-extract,
99 mutants, 32 minutes:

before 5f62986 after
caught 86 94
missed 12 4
unviable 1 1

98 of the 99 are viable and 94 + 4 = 98, so every viable mutant in the field
reader now either dies or is an equivalent mutation
. The eight that were real
gaps are closed by assertions in a_declared_field_has_a_type, each confirmed by
hand: apply the mutant, watch the suite go red, restore. The after-column is
re-readable in rust/mutants.out/{caught,missed,unviable}.txt; the before-column
is this session's record, as the run directory only retains the latest pass.

The four survivors are equivalent, each argued at its own site by 3462e4c:

survivor equivalent because
is_type_bearing -> true Objective-C states the type as the first named child, so the predicate cannot change the answer on any shape the grammar produces — four measured
delete the collection arm redundant with _ => None; kept so a later edit moving array_type into the wrapper arm cannot reverse the refusal silently
delete the builtin arm likewise redundant; the edit it guards against is moving those kinds into the leaf arm, which measured 696 wrong rows on this repository
delete ! in Go's qualified_type filter falls through to bare_leaf, which reduces pkg.Lease past its dot to the same Lease

A survivor names a line, not a construct. 436:28 delete ! was first read as
the ! in the generic-head fallback one arm above it, and judged a real gap on
that basis. It is the ! in the Go arm. The difference was settled by making the
site observable rather than by reading it: replacing the generic fallback with a
panic! proved that Java's generic_type does reach it, so that ! is live and
its own mutant is genuinely caught. rust/mutants.out/diff/ holds the applied
patch for every mutant and is the fact; the summary line is a label.

What a controlled A/B says about the 57%

The section above measured uninferred_receiver at 66,602 -> 66,682 across the
field-types merge and labelled the comparison uncontrolled, because that merge
adds three files of its own to the corpus. That reservation was correct, and
the conflation it was reacting to was real: the branch message read as a promise
of a reduction. The module doc now states the 57% as an opportunity size — the
share of remaining receivers that have the shape a field type could answer — and
not as a predicted drop. Every Type reference the reader emits is itself an
attribution site, so the raw receiver count can rise while unexplained falls.

Held to a fixed file set, which is what the section above could not do:

controlled A/B, 1,106-file corpus baseline after
unexplained call sites 106,217 68,608
net resolution rate 19.2% 25.4%
MarkDev edges 32,928 33,092
MarkDev non-exempt dead symbols 4 4, byte-identical

Measured again at this tip by the binary installed below — a different, larger
corpus
of 1,135 files, because this merge brings main's benchmark results in,
so these are absolute figures and not the delta above:

analysis_json, generation 1 value
resolved sites 23,719
unresolved sites 133,817
explained sites 64,518
unexplained 69,299
gross_permille / net_permille 150 / 254

net_permille 254 is the 25.4% above, reproduced by the shipped binary rather
than restated from a notebook.

The release gate had a blind spot, and it is closed

release.yml publishes docs/releases/<tag>.md from the tagged commit —
actions/checkout takes github.ref, and the release targets github.sha. So
notes edited after a tag is cut are invisible to the release, and no CI gate
can see it
: at the tagged commit the older notes are internally consistent,
mention the version, and sit under the size cap, so every check passes while the
wrong text ships. Found here concretely — v0.2.2 named a commit whose notes
were 277 lines against 653 at the tip.

That makes it a pre-push check by construction rather than by oversight.
tagNamesHead() is the pure decision, tested three ways, and ci:local calls
it with the shas git reports. The same step also runs
check-release.mjs --tag, the path release.yml takes on a tag push and which
ci:local never exercised — it only ever ran the bare pull_request form, so a
tag-identity failure could only be discovered in CI, after the push.

Reviewed at the same time and left alone, because each was already sound: the
npm publish job is gated to refs/tags/v so a branch push skips rather than
failing its GITHUB_REF_NAME comparison; it runs npm ping so an unreachable
registry cannot look like an unpublished version; release.yml is idempotent
through gh release view/edit/upload --clobber; notes travel by
--notes-file and never through the 48 KB GITHUB_ENV; and fail-fast: true
plus two --artifacts sweeps mean a missing platform fails loudly instead of
publishing a partial set.

Repository and install state after the third consolidation

main is again the only branch. claude/devmap-field-types-mutants was merged
as a62ae50 and deleted; safety/tip-main-20260914-consolidation2 and
safety/tip-claude-devmap-field-types-mutants-20260914 mark the two pre-merge
tips, so either side is recoverable without the reflog.

The merge was taken into the lane and fast-forwarded onto main, not pushed
over it. main is checked out in the primary working tree, which had three live
sessions in it at the time; receive.denyCurrentBranch is unset, so a plain push
is refused by design, and git update-ref would have moved the ref while leaving
those trees stale — a state whose next git add -A silently reverts the merge.
updateInstead is the mechanism that refuses on a dirty tree and otherwise syncs
it, and a fast-forward adding two files of comments is the smallest move it can
be asked to make.

All four components were rebuilt from a62ae50 and installed to ~/.local/bin,
each passing the install script's health check: dcstore, dcverify, dcgrep,
devmap. The binary reports:

devmap 0.2.2 (store schema 22, code graph schema 2, build a62ae50e0b72)

Unlike the previous round, that hash is the merge commit rather than the last
commit to change code, because the merge is the last such commit. The notes
commit that follows it is not rebuilt for, on the same rule as before: a
notes-only change cannot alter an instruction, and rebuilding would move the
embedded hash printed above and make it wrong.

Gates on the merged tree, before installing: all nine green, exit 0, 3,321
passed, 0 failed
, counted from 311 of 311 test result: lines rather than the
banner. The self-build scans 1,135 files now rather than 1,106 — this merge
brings main's benchmark results into the tree.

13 GiB reclaimed. Seven superseded experiment target directories from this
lane's A/B and mutation work were removed, along with rust/mutants.out.old.
Three things were deliberately kept:

  • rust/target — a valid cache, and the repository's hygiene rule is to
    reuse one rather than clear it after every build. Clearing it reclaims
    nothing durable, as the previous round's notes record: the 17 GB it freed
    came straight back on the next gate run.
  • rust/mutants.out — the run artifacts the mutation section above cites.
    Deleting them would leave that section's after-column unre-readable.
  • .devcouncil/benchmarks/20260914-v0.2.2/tools — the pinned competitor
    executables, which are not this session's to remove and carry their own
    DO-NOT-DELETE-LIGHTLY.md.

The primary working tree's own 18 GB rust/target was left alone for the same
reason the ref was moved carefully: three live sessions were building in it.