What is missing
If src/model.h changes while a build is running, some object files end up compiled against the old
Symbol layout and some against the new one. Their mtimes are newer than the header, so make
concludes everything is up to date and never recompiles them — the build reports success, exit 0, no
warnings, forever. Nothing in CMake can repair this, and test/g1freshcheck.sh cannot see it,
because the binary is newer than its sources and only its contents are stale. The ask is for
ripwire --doctor to say it in one line: each translation unit records the struct layouts it was
compiled with, and the doctor reports when they disagree.
The evidence
CLAUDE.md records three incidents from this failure mode, all from building across or editing
under a branch switch:
- A sanitizer build finishing after a checkout that changed
Symbol produced a genuine
AddressSanitizer heap-buffer-overflow report for a bug that did not exist. The overflowing region
was 1344 bytes — exactly 14 × 96, a multiple of the previous struct size.
- A header's mirrored constant kept emitting its old value through repeated successful rebuilds; a
gate failed with parserVer=41, expected 12/42 and looked like a missed mirror update.
- Ten identical uncaught
std::length_error aborts in one morning of worktree churn, thrown from a
resize( symbols.size() ), with zero repro in 38 runs on clean rebuilds of the same commit.
Facts read from the tree that the kit rests on:
- The
ripwire target is src/main.cpp plus src/ingest.cpp, src/pagerank.cpp and
src/infra/diagnostics.cpp; IngestResult carries vectors of Symbol, Reference and eight
other structs across that boundary.
Symbol is pinned by static_assert( sizeof( Symbol ) == 64 + 2 * sizeof( std::string ) ) — which
every translation unit checks against its own view of the header, so it passes in both halves of
a mixed binary. Reference has no size pin at all.
--doctor (src/verbs_doctor.h) emits one <c n= ok=> row per check and exits 1 when any fails;
test/doctorcheck.sh asserts a named row set.
--layout and --stray-content --abi model struct layout from source text ("a MODEL, NOT THE
ABI") and cannot see what a compiler actually produced.
Size
Good first issue. One recording header, one registry, one doctor row, one gate, and a short
audit of the cache-format size pins. No external corpus, no network, no API keys — a C++23
toolchain and CMake able to produce both the plain dev build and a Release build, because the
records must survive NDEBUG and LTO.
Where to start
prompts/help-wanted/struct-layout-doctor.md is a self-contained prompt for a coding agent. It
carries the hazard and its three recorded incidents, the translation units involved, why a header
static_assert cannot catch it, how to reproduce it safely in a scratch tree, the recording and
comparison design space (internal linkage, a per-unit identity, surviving Release and LTO, comparing
for agreement rather than pinning numbers), the doctor row's three states, the gate arms, the
acceptance criteria and the traps. Like every prompt in prompts/, it ends by writing a plan and
stopping — a maintainer agrees the plan before any code is written.
Build the gate first: a two-translation-unit fixture whose struct definitions differ must make the
comparator report a disagreement before the real row exists.
Comment here to claim it.
What is missing
If
src/model.hchanges while a build is running, some object files end up compiled against the oldSymbollayout and some against the new one. Their mtimes are newer than the header, somakeconcludes everything is up to date and never recompiles them — the build reports success, exit 0, no
warnings, forever. Nothing in CMake can repair this, and
test/g1freshcheck.shcannot see it,because the binary is newer than its sources and only its contents are stale. The ask is for
ripwire --doctorto say it in one line: each translation unit records the struct layouts it wascompiled with, and the doctor reports when they disagree.
The evidence
CLAUDE.mdrecords three incidents from this failure mode, all from building across or editingunder a branch switch:
Symbolproduced a genuineAddressSanitizer heap-buffer-overflow report for a bug that did not exist. The overflowing region
was 1344 bytes — exactly 14 × 96, a multiple of the previous struct size.
gate failed with
parserVer=41, expected 12/42and looked like a missed mirror update.std::length_erroraborts in one morning of worktree churn, thrown from aresize( symbols.size() ), with zero repro in 38 runs on clean rebuilds of the same commit.Facts read from the tree that the kit rests on:
ripwiretarget issrc/main.cppplussrc/ingest.cpp,src/pagerank.cppandsrc/infra/diagnostics.cpp;IngestResultcarries vectors ofSymbol,Referenceand eightother structs across that boundary.
Symbolis pinned bystatic_assert( sizeof( Symbol ) == 64 + 2 * sizeof( std::string ) )— whichevery translation unit checks against its own view of the header, so it passes in both halves of
a mixed binary.
Referencehas no size pin at all.--doctor(src/verbs_doctor.h) emits one<c n= ok=>row per check and exits 1 when any fails;test/doctorcheck.shasserts a named row set.--layoutand--stray-content --abimodel struct layout from source text ("a MODEL, NOT THEABI") and cannot see what a compiler actually produced.
Size
Good first issue. One recording header, one registry, one doctor row, one gate, and a short
audit of the cache-format size pins. No external corpus, no network, no API keys — a C++23
toolchain and CMake able to produce both the plain dev build and a Release build, because the
records must survive
NDEBUGand LTO.Where to start
prompts/help-wanted/struct-layout-doctor.mdis a self-contained prompt for a coding agent. Itcarries the hazard and its three recorded incidents, the translation units involved, why a header
static_assertcannot catch it, how to reproduce it safely in a scratch tree, the recording andcomparison design space (internal linkage, a per-unit identity, surviving Release and LTO, comparing
for agreement rather than pinning numbers), the doctor row's three states, the gate arms, the
acceptance criteria and the traps. Like every prompt in
prompts/, it ends by writing a plan andstopping — a maintainer agrees the plan before any code is written.
Build the gate first: a two-translation-unit fixture whose struct definitions differ must make the
comparator report a disagreement before the real row exists.
Comment here to claim it.