Repository navigation
DevCouncil v0.2.2
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 entiredevmap-storesuite 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. AUTOINCREMENTwas an unguarded correctness requirement. v22 digests are
taken overpathsandunresolved_textsids, 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.shwhilecargo test --workspacestayed 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-clifixture 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_loopthat 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 turneddevmap 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.2reports 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 ofdb.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.jsonandrepo_map.jsonare 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 lettingVecbe 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. selfin 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-48cd3c7run 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, andcomparison.mdnow 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.