From b649b128baeff1faa95db5a149916c3aa003ca79 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Sep 2026 02:46:58 -0300 Subject: [PATCH 1/7] feat(doctor): detect cross-translation-unit layout drift --- README.md | 4 +- docs/ARCHITECTURE.md | 2 + docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/model.h | 17 +++ src/structlayout.h | 253 +++++++++++++++++++++++++++++++++ src/verbs_doctor.h | 64 +++++++++ test/doctorcheck.sh | 4 +- test/regression.sh | 2 +- test/structlayout_fixture.h | 15 ++ test/structlayout_probe.cpp | 51 +++++++ test/structlayout_unit.cpp | 1 + test/structlayoutcheck.sh | 126 ++++++++++++++++ 13 files changed, 540 insertions(+), 11 deletions(-) create mode 100644 src/structlayout.h create mode 100644 test/structlayout_fixture.h create mode 100644 test/structlayout_probe.cpp create mode 100644 test/structlayout_unit.cpp create mode 100755 test/structlayoutcheck.sh diff --git a/README.md b/README.md index d197b47fc..0d4b1ab83 100644 --- a/README.md +++ b/README.md @@ -1797,9 +1797,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-614 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +615 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **614 gate scripts** and is the authoritative list; +`test/regression.sh` names **615 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3199058d2..318b91c9e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -38,6 +38,8 @@ sections in call order: the lazy tags.scm prewarm (`ingest_prewarm.h`), the para (`ingest_parsepool.h`), the document post-pass (`ingest_docpass.h`), and the build-model tail — dedup, symbol assignment, span attribution, ordered emit (`ingest_model.h`). +`--doctor`'s `layout` row compares the shared model's recorded `sizeof`/`alignof` facts across translation units and fails a mixed binary instead of guessing which object is stale. + **Crawl order is deterministic, and that is load-bearing.** The walk *collects every candidate path first*, sorts them lexicographically by byte, and only then assigns node IDs and parses. Node IDs are indices into that sorted list, so they are stable across runs of the same tree; if IDs followed diff --git a/docs/EVALS.md b/docs/EVALS.md index 073cf7742..360d7698b 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 614 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 615 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5834,7 +5834,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **614 gate scripts**, all of which exist on disk. +naming **615 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6846,7 +6846,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 614. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 615. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2512732e..f45c60640 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1068,7 +1068,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["614 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["615 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1092,7 +1092,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "614", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "615", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1342,7 +1342,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["179 long flags · 33 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["614 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["615 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 70 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/model.h b/src/model.h index 3305d7eab..ac90b6096 100644 --- a/src/model.h +++ b/src/model.h @@ -10,6 +10,7 @@ #include "infra/profileScope.h" #include "smallvec.h" // rw::SmallVec — THE ONE ALIAS; the per-key span lists and per-file id buckets below +#include "structlayout.h" // cross-translation-unit sizeof/alignof tripwire for the shared model #include // std::sort — symbolsByFile below #include // std::tie — lessUnindexedExt's mixed-direction compare @@ -1016,6 +1017,22 @@ struct IngestResult std::size_t reparsedFiles = 0; }; +// These aggregates cross the ingest/main translation-unit boundary. The record is emitted once per TU by +// structlayout.h, so a header change that leaves a mixed binary can be diagnosed after linking instead of +// passing a self-consistent static_assert in each half. +RIPWIRE_LAYOUT_REGISTER_TYPES( RIPWIRE_LAYOUT_TYPE_ENTRY( std::string ), + RIPWIRE_LAYOUT_TYPE_ENTRY( Symbol ), + RIPWIRE_LAYOUT_TYPE_ENTRY( Reference ), + RIPWIRE_LAYOUT_TYPE_ENTRY( Include ), + RIPWIRE_LAYOUT_TYPE_ENTRY( ConstOpen ), + RIPWIRE_LAYOUT_TYPE_ENTRY( Binding ), + RIPWIRE_LAYOUT_TYPE_ENTRY( BindingAlias ), + RIPWIRE_LAYOUT_TYPE_ENTRY( RouteDef ), + RIPWIRE_LAYOUT_TYPE_ENTRY( RouteUse ), + RIPWIRE_LAYOUT_TYPE_ENTRY( FileHealth ), + RIPWIRE_LAYOUT_TYPE_ENTRY( SkippedOversize ), + RIPWIRE_LAYOUT_TYPE_ENTRY( IngestResult ) ); + // multi-root workspace cap: a sane bound on N crawl roots — an agent joining a // handful of checkouts is the use case; hundreds of roots is a mis-glued path list, refused loudly. inline constexpr std::size_t kMaxWorkspaceRoots = 16; diff --git a/src/structlayout.h b/src/structlayout.h new file mode 100644 index 000000000..8851913a9 --- /dev/null +++ b/src/structlayout.h @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 David Brewster + +// +// structlayout.h +// +// Cross-translation-unit layout tripwire. Every translation unit that includes +// this header registers the sizeof/alignof facts it compiled with. The registry +// is intentionally separate from src/layout.h: that file models source text for +// repositories ripwire analyzes, while this file records this binary's ABI view. +// +// The records have internal linkage. Only the registry and its registrar are +// shared, so an inline record cannot make the linker discard one TU's evidence. +// +#pragma once + +#include +#include +#include +#include +#include + +#if defined( __GNUC__ ) || defined( __clang__ ) +#define RIPWIRE_LAYOUT_USED __attribute__( ( used ) ) +#else +#define RIPWIRE_LAYOUT_USED +#endif + +// __FILE__ expands to this header at the registration site. GCC and Clang's +// __BASE_FILE__ is the primary source file passed to the compiler, which is the +// identity needed here. A build system can override it for a compiler without +// __BASE_FILE__ (and for fixture tests that compile one source twice). +#if !defined( RIPWIRE_LAYOUT_TU ) +#if defined( __BASE_FILE__ ) +#define RIPWIRE_LAYOUT_TU __BASE_FILE__ +#else +#error "RIPWIRE_LAYOUT_TU must be supplied when __BASE_FILE__ is unavailable" +#endif +#endif + +namespace rw +{ +namespace layout_registry +{ + +struct TypeLayout +{ + const char* name = nullptr; + std::size_t size = 0; + std::size_t align = 0; +}; + +struct LayoutRecord +{ + const char* unit = nullptr; + const TypeLayout* types = nullptr; + std::size_t typeCount = 0; +}; + +class Registry +{ +public: + void add( const LayoutRecord* record ) + { + records_.push_back( record ); + } + + const std::vector& records() const noexcept + { + return records_; + } + +private: + std::vector records_; +}; + +// Function-local construction keeps registration independent of the order in +// which static initializers from different translation units run. +inline Registry& registry() +{ + static Registry instance; + return instance; +} + +class Registrar +{ +public: + explicit Registrar( const LayoutRecord& record ) + { + registry().add( &record ); + } +}; + +enum class CheckState : std::uint8_t +{ + NoRecords, + NotChecked, + Agree, + Disagree, +}; + +struct LayoutMismatch +{ + const char* typeName = nullptr; + const char* unit0 = nullptr; + const char* unit1 = nullptr; + std::size_t size0 = 0; + std::size_t size1 = 0; + std::size_t align0 = 0; + std::size_t align1 = 0; + bool present0 = false; + bool present1 = false; +}; + +struct LayoutCheck +{ + CheckState state = CheckState::NoRecords; + std::size_t unitCount = 0; + std::size_t typeCount = 0; + LayoutMismatch mismatch; +}; + +inline const TypeLayout* findType( const LayoutRecord& record, const char* name ) noexcept +{ + for( std::size_t i = 0; i < record.typeCount; ++i ) + { + if( std::strcmp( record.types[ i ].name, name ) == 0 ) + { + return &record.types[ i ]; + } + } + return nullptr; +} + +inline std::vector sortedRecords() +{ + std::vector records = registry().records(); + std::sort( records.begin(), records.end(), []( const LayoutRecord* left, const LayoutRecord* right ) + { + return std::strcmp( left->unit, right->unit ) < 0; + } ); + return records; +} + +inline LayoutCheck disagreement( const LayoutRecord& first, const TypeLayout* firstType, + const LayoutRecord& second, const TypeLayout* secondType, + const char* typeName ) noexcept +{ + LayoutCheck result; + result.state = CheckState::Disagree; + result.unitCount = 2; + result.typeCount = first.typeCount; + result.mismatch = { typeName, + first.unit, + second.unit, + firstType != nullptr ? firstType->size : 0, + secondType != nullptr ? secondType->size : 0, + firstType != nullptr ? firstType->align : 0, + secondType != nullptr ? secondType->align : 0, + firstType != nullptr, + secondType != nullptr }; + return result; +} + +inline LayoutCheck compare() +{ + const std::vector records = sortedRecords(); + LayoutCheck result; + result.unitCount = records.size(); + + if( records.empty() ) + { + return result; + } + result.typeCount = records.front()->typeCount; + if( records.size() == 1 ) + { + result.state = CheckState::NotChecked; + return result; + } + + bool hasType = false; + for( const LayoutRecord* record : records ) + { + hasType = hasType || record->typeCount != 0; + } + if( !hasType ) + { + result.state = CheckState::NotChecked; + return result; + } + + const LayoutRecord& first = *records.front(); + for( std::size_t typeIndex = 0; typeIndex < first.typeCount; ++typeIndex ) + { + const TypeLayout& expected = first.types[ typeIndex ]; + for( std::size_t unitIndex = 1; unitIndex < records.size(); ++unitIndex ) + { + const LayoutRecord& actualRecord = *records[ unitIndex ]; + const TypeLayout* actual = findType( actualRecord, expected.name ); + if( actual == nullptr || actual->size != expected.size || actual->align != expected.align ) + { + LayoutCheck mismatch = disagreement( first, &expected, actualRecord, actual, expected.name ); + mismatch.unitCount = records.size(); + return mismatch; + } + } + } + + // A record with an extra type is also a disagreement. The fixed model list + // should never take this path, but it keeps the registry honest for tests + // and for future additions to the recorded aggregate set. + for( std::size_t unitIndex = 1; unitIndex < records.size(); ++unitIndex ) + { + const LayoutRecord& actualRecord = *records[ unitIndex ]; + for( std::size_t typeIndex = 0; typeIndex < actualRecord.typeCount; ++typeIndex ) + { + const TypeLayout& actual = actualRecord.types[ typeIndex ]; + if( findType( first, actual.name ) == nullptr ) + { + LayoutCheck mismatch = disagreement( first, nullptr, actualRecord, &actual, actual.name ); + mismatch.unitCount = records.size(); + return mismatch; + } + } + } + + result.state = CheckState::Agree; + return result; +} + +} // namespace layout_registry +} // namespace rw + +#define RIPWIRE_LAYOUT_TYPE_ENTRY( Type ) { #Type, sizeof( Type ), alignof( Type ) } +#define RIPWIRE_LAYOUT_DETAIL_JOIN_IMPL( left, right ) left##right +#define RIPWIRE_LAYOUT_DETAIL_JOIN( left, right ) RIPWIRE_LAYOUT_DETAIL_JOIN_IMPL( left, right ) +#define RIPWIRE_LAYOUT_DETAIL_REGISTER( id, ... ) \ + namespace \ + { \ + RIPWIRE_LAYOUT_USED static const ::rw::layout_registry::TypeLayout \ + RIPWIRE_LAYOUT_DETAIL_JOIN( kRipwireLayoutTypes, id )[] = { __VA_ARGS__ }; \ + RIPWIRE_LAYOUT_USED static const ::rw::layout_registry::LayoutRecord \ + RIPWIRE_LAYOUT_DETAIL_JOIN( kRipwireLayoutRecord, id ) = { \ + RIPWIRE_LAYOUT_TU, \ + RIPWIRE_LAYOUT_DETAIL_JOIN( kRipwireLayoutTypes, id ), \ + sizeof( RIPWIRE_LAYOUT_DETAIL_JOIN( kRipwireLayoutTypes, id ) ) \ + / sizeof( ::rw::layout_registry::TypeLayout ) }; \ + RIPWIRE_LAYOUT_USED static const ::rw::layout_registry::Registrar \ + RIPWIRE_LAYOUT_DETAIL_JOIN( kRipwireLayoutRegistrar, id )( \ + RIPWIRE_LAYOUT_DETAIL_JOIN( kRipwireLayoutRecord, id ) ); \ + } +#define RIPWIRE_LAYOUT_REGISTER_TYPES( ... ) RIPWIRE_LAYOUT_DETAIL_REGISTER( __COUNTER__, __VA_ARGS__ ) diff --git a/src/verbs_doctor.h b/src/verbs_doctor.h index 36eb9d1c9..a0d1df630 100644 --- a/src/verbs_doctor.h +++ b/src/verbs_doctor.h @@ -282,6 +282,11 @@ inline const char* doctorLegendComment() "COMMAND git would run on every read-only call, and neutralised=\"1\" says core.fsmonitor=false was " "appended to git's environment override for this run (stderr said so as git_harden=fsmonitor-hook); " "builtin, off and unset are left untouched and neutralised=\"0\". " + "layout's state=\"agree\" means the layout records match; checked=\"1\" means the comparison ran; " + "units=\"N\" counts translation units and types=\"N\" counts recorded types. On state=\"disagree\", " + "type= names the first differing type, unit0=/unit1= name the two records, and " + "present0=/present1=, size0=/size1=, and align0=/align1= disclose their values; the row gives the rebuild action; " + "state=\"not-checked\" means fewer than two records survived into this binary. " "NB no flag below is spelled with its leading dashes: an XML comment may not contain a " "double hyphen, and this legend is one comment. -->"; } @@ -644,6 +649,57 @@ inline std::string doctorGitConfigTrustAttrs( const rw::Config& cfg ) return attrs; } +struct DoctorLayoutCheck +{ + bool ok = false; + std::string attrs; +}; + +inline DoctorLayoutCheck doctorLayoutCheck( std::vector& esc ) +{ + using namespace rw::layout_registry; + const LayoutCheck check = compare(); + DoctorLayoutCheck out; + const auto escaped = [ &esc ]( const char* value ) + { + return std::string( rw::escapeXml( std::string_view( value == nullptr ? "" : value ), esc ) ); + }; + + switch( check.state ) + { + case CheckState::Agree: + { + out.ok = true; + out.attrs = "state=\"agree\" checked=\"1\" units=\"" + std::to_string( check.unitCount ) + + "\" types=\"" + std::to_string( check.typeCount ) + "\""; + break; + } + case CheckState::Disagree: + { + const LayoutMismatch& mismatch = check.mismatch; + out.attrs = "state=\"disagree\" checked=\"1\" units=\"" + std::to_string( check.unitCount ) + + "\" types=\"" + std::to_string( check.typeCount ) + "\" type=\"" + escaped( mismatch.typeName ) + + "\" unit0=\"" + escaped( mismatch.unit0 ) + "\" unit1=\"" + escaped( mismatch.unit1 ) + + "\" present0=\"" + std::string( mismatch.present0 ? "1" : "0" ) + + "\" present1=\"" + std::string( mismatch.present1 ? "1" : "0" ) + + "\" size0=\"" + std::to_string( mismatch.size0 ) + "\" size1=\"" + std::to_string( mismatch.size1 ) + + "\" align0=\"" + std::to_string( mismatch.align0 ) + "\" align1=\"" + std::to_string( mismatch.align1 ) + + "\" hint=\"mixed translation-unit layouts detected — rebuild with cmake --build build --clean-first -j\""; + break; + } + case CheckState::NoRecords: + case CheckState::NotChecked: + { + const char* const state = check.state == CheckState::NoRecords ? "no-records" : "not-checked"; + out.attrs = "state=\"" + std::string( state ) + "\" checked=\"0\" units=\"" + std::to_string( check.unitCount ) + + "\" types=\"" + std::to_string( check.typeCount ) + + "\" hint=\"not checked: layout records from at least two translation units are required\""; + break; + } + } + return out; +} + int runDoctor( const rw::Config& cfg, const char* argv0 ) { using namespace rw; @@ -858,6 +914,14 @@ int runDoctor( const rw::Config& cfg, const char* argv0 ) // check 7's lives in doctorIndexCacheRow: runDoctor is a dispatcher, and every check body it absorbs lands there. row( "git-config-trust", true, doctorGitConfigTrustAttrs( cfg ) ); + // ---- check 9: cross-translation-unit layout agreement — this is the one check that can identify a + // binary no single source tree could produce. A single record is deliberately not a pass: there is no + // second compiler view against which to compare it. + { + const DoctorLayoutCheck layout = doctorLayoutCheck( esc ); + row( "layout", layout.ok, layout.attrs ); + } + const DoctorAgentRows agentRows = doctorAgentRows( cfg, argv0 ); checks += agentRows.checks; okCount += agentRows.passed; diff --git a/test/doctorcheck.sh b/test/doctorcheck.sh index 4cff535d5..87222e5e7 100755 --- a/test/doctorcheck.sh +++ b/test/doctorcheck.sh @@ -3,7 +3,7 @@ # # --doctor is a DIAGNOSTIC verb (environment-dependent output is its whole point), so unlike every # other absorb gate this one does NOT assert byte-identical / golden output. Instead it asserts: -# (A) happy path: exit 0, all 6 rows present, xmllint-clean, checks="N" matches N emitted rows. +# (A) happy path: exit 0, every named row present, xmllint-clean, checks="N" matches N emitted rows. # (B) an unwritable cache dir (via TMPDIR) makes the cache-dir row ok="0" and the whole run exit 1. # (C) a non-repo target dir makes the git row ok="1" repo="0" (degrade, not a failure). # (D) non-vacuity without a source mutation: assert the check COUNT in checks="N" equals the number @@ -88,7 +88,7 @@ DECLARED_CHECKS="$( echo "$OUT" | grep -o ' row count" \ || no "checks=\"${DECLARED_CHECKS:-}\" disagrees with the $EMITTED_ROWS rows actually emitted" -for row in binary-path grammars cache-dir git tree-sitter tracked-binaries index-cache git-config-trust; do +for row in binary-path grammars cache-dir git tree-sitter tracked-binaries index-cache git-config-trust layout; do echo "$OUT" | grep -q "&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck declinedlistcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck structlayoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck declinedlistcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" diff --git a/test/structlayout_fixture.h b/test/structlayout_fixture.h new file mode 100644 index 000000000..7c1964b35 --- /dev/null +++ b/test/structlayout_fixture.h @@ -0,0 +1,15 @@ +#pragma once + +#include "structlayout.h" + +#include + +struct FixtureLayout +{ + std::uint32_t value = 0; +#if defined( RIPWIRE_LAYOUT_FIXTURE_WIDE ) + std::uint32_t extra = 0; +#endif +}; + +RIPWIRE_LAYOUT_REGISTER_TYPES( RIPWIRE_LAYOUT_TYPE_ENTRY( FixtureLayout ) ); diff --git a/test/structlayout_probe.cpp b/test/structlayout_probe.cpp new file mode 100644 index 000000000..aa956fa17 --- /dev/null +++ b/test/structlayout_probe.cpp @@ -0,0 +1,51 @@ +#include "structlayout.h" + +#include +#include + +namespace +{ + +const char* stateName( rw::layout_registry::CheckState state ) +{ + switch( state ) + { + case rw::layout_registry::CheckState::NoRecords: return "no-records"; + case rw::layout_registry::CheckState::NotChecked: return "not-checked"; + case rw::layout_registry::CheckState::Agree: return "agree"; + case rw::layout_registry::CheckState::Disagree: return "disagree"; + } + return "unknown"; +} + +} // namespace + +int main( int argc, char** argv ) +{ + const rw::layout_registry::LayoutCheck result = rw::layout_registry::compare(); + std::printf( "state=%s units=%zu types=%zu", stateName( result.state ), result.unitCount, result.typeCount ); + if( result.mismatch.typeName != nullptr ) + { + std::printf( " type=%s unit0=%s unit1=%s size0=%zu size1=%zu align0=%zu align1=%zu", + result.mismatch.typeName, + result.mismatch.unit0, + result.mismatch.unit1, + result.mismatch.size0, + result.mismatch.size1, + result.mismatch.align0, + result.mismatch.align1 ); + } + std::putchar( '\n' ); + + const bool single = argc > 1 && std::strcmp( argv[1], "single" ) == 0; + if( single ) + { + return result.state == rw::layout_registry::CheckState::NotChecked && result.unitCount == 1 ? 0 : 1; + } + return result.state == rw::layout_registry::CheckState::Disagree + && result.unitCount == 2 + && result.mismatch.typeName != nullptr + && result.mismatch.size0 != result.mismatch.size1 + ? 0 + : 1; +} diff --git a/test/structlayout_unit.cpp b/test/structlayout_unit.cpp new file mode 100644 index 000000000..15e31bc42 --- /dev/null +++ b/test/structlayout_unit.cpp @@ -0,0 +1 @@ +#include "structlayout_fixture.h" diff --git a/test/structlayoutcheck.sh b/test/structlayoutcheck.sh new file mode 100755 index 000000000..8b28806e8 --- /dev/null +++ b/test/structlayoutcheck.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# structlayoutcheck.sh — gate for cross-translation-unit layout records and --doctor. +# +# The fixture is deliberately deterministic: the same header is compiled into two +# translation units, but one sees an extra field through a test-only definition. +# No source header is edited and no build race is involved. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +REL="${RIPWIRE_RELEASE_BIN:-}" +[ -n "$REL" ] && [ "${REL#/}" = "$REL" ] && REL="$ROOT/$REL" +CXX="${CXX:-c++}" +TMP="$( mktemp -d )" +trap 'rm -rf "$TMP"' EXIT +fail=0 + +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +skip(){ printf ' SKIP %s\n' "$*"; } + +[ -x "$BIN" ] || { echo "structlayoutcheck: no ripwire binary at $BIN — build first"; exit 2; } +command -v "$CXX" >/dev/null 2>&1 || { echo "structlayoutcheck: no C++ compiler at $CXX"; exit 2; } + +echo "structlayoutcheck: BIN=$BIN CXX=$CXX" + +CXXFLAGS=( -std=c++23 -O2 -flto -I"$ROOT/src" ) +compile_unit(){ + local unit="$1"; shift + "$CXX" "${CXXFLAGS[@]}" "-DRIPWIRE_LAYOUT_TU=\"$unit\"" "$@" \ + -c "$ROOT/test/structlayout_unit.cpp" -o "$TMP/$unit.o" +} + +# ── A: mechanism, mixed layouts must be a hard disagreement ───────────────────────────────────── +fixture_ok=1 +if compile_unit wide-fixture -DRIPWIRE_LAYOUT_FIXTURE_WIDE=1; then + ok "mixed fixture wide translation unit compiles" +else + no "mixed fixture wide translation unit did not compile" + fixture_ok=0 +fi +if compile_unit narrow-fixture; then + ok "mixed fixture narrow translation unit compiles" +else + no "mixed fixture narrow translation unit did not compile" + fixture_ok=0 +fi +if [ "$fixture_ok" = 1 ] && "$CXX" "${CXXFLAGS[@]}" "$ROOT/test/structlayout_probe.cpp" \ + "$TMP/wide-fixture.o" "$TMP/narrow-fixture.o" -o "$TMP/mixed"; then + MIXED="$($TMP/mixed 2>&1)"; mixed_rc=$? + echo "mixed fixture output: $MIXED" + if [ "$mixed_rc" -eq 0 ]; then + ok "mixed fixture reports a disagreement instead of agreeing" + else + no "mixed fixture did not report the expected disagreement (exit=$mixed_rc)" + fi + printf '%s' "$MIXED" | grep -q 'state=disagree' \ + && ok "mixed fixture state=disagree" \ + || no "mixed fixture did not name state=disagree" + printf '%s' "$MIXED" | grep -q 'type=FixtureLayout' \ + && ok "mixed fixture names the disagreeing type" \ + || no "mixed fixture did not name FixtureLayout" + printf '%s' "$MIXED" | grep -q 'wide-fixture' && printf '%s' "$MIXED" | grep -q 'narrow-fixture' \ + && ok "mixed fixture names both translation units" \ + || no "mixed fixture did not name both translation units" +else + no "mixed fixture could not be linked" +fi + +# ── B: one record is not a comparison and must not pass vacuously ───────────────────────────────── +if compile_unit single-fixture; then + if "$CXX" "${CXXFLAGS[@]}" "$ROOT/test/structlayout_probe.cpp" "$TMP/single-fixture.o" -o "$TMP/single"; then + SINGLE="$($TMP/single single 2>&1)"; single_rc=$? + echo "single fixture output: $SINGLE" + [ "$single_rc" -eq 0 ] \ + && ok "single fixture reports not-checked rather than passing vacuously" \ + || no "single fixture did not report not-checked (exit=$single_rc)" + printf '%s' "$SINGLE" | grep -q 'state=not-checked units=1' \ + && ok "single fixture discloses state=not-checked and units=1" \ + || no "single fixture did not disclose the not-checked state" + else + no "single fixture could not be linked" + fi +else + no "single-record fixture did not compile" +fi + +check_doctor(){ + local label="$1"; local binary="$2"; local output row rc + output="$( PATH="$( dirname "$binary" ):$PATH" "$binary" "$ROOT" --doctor --no-cache 2>/dev/null )"; rc=$? + row="$( printf '%s' "$output" | tr '<' '\n' | grep '^c n="layout" ' || true )" + echo "$label doctor row: ${row:-} (exit=$rc)" + [ "$rc" -eq 0 ] \ + && ok "$label --doctor exits 0" \ + || no "$label --doctor exited $rc" + printf '%s' "$row" | grep -q 'ok="1"' \ + && ok "$label layout row agrees" \ + || no "$label layout row is not ok=\"1\"" + printf '%s' "$row" | grep -q 'checked="1"' \ + && ok "$label layout row is checked" \ + || no "$label layout row is not checked" + printf '%s' "$row" | grep -q 'units="[2-9][0-9]*"' \ + && ok "$label layout row compares at least two translation units" \ + || no "$label layout row has fewer than two translation units" + printf '%s' "$row" | grep -q 'types="[1-9][0-9]*"' \ + && ok "$label layout row compares at least one type" \ + || no "$label layout row did not report a type count" +} + +# ── C: the actual binary carries the records in the plain build ─────────────────────────────────── +check_doctor plain "$BIN" + +# ── D: Release/NDEBUG+LTO carries the same records when a reference binary is supplied ───────────── +if [ -z "$REL" ] || [ ! -x "$REL" ]; then + skip "Release binary not supplied (set RIPWIRE_RELEASE_BIN=build_rel/ripwire)" +else + check_doctor release "$REL" +fi + +if [ "$fail" -eq 0 ]; then + echo "ALL PASS" +else + echo "FAILURES ABOVE" +fi +exit "$fail" From ccb26cf9d20c8c92a290be32fca5487a046fe4e1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Sep 2026 03:05:16 -0300 Subject: [PATCH 2/7] test(showcase): refresh measured reduction caption --- test/showcase_capture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/showcase_capture.py b/test/showcase_capture.py index 924b0ecb3..97a3678e7 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -332,7 +332,7 @@ def add(section, cmd, what, **opts): S3 = "zoom the detail ladder" add(S3, f'{BIN} . --for="pagerank power iteration" --detail=2', "Importance-weighted detail: FULL bodies for top-2, signatures for the rest.") -add(S3, f"{BIN} . --pack-signatures --top-k=10", "Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 89.5% fewer bytes at top-10, 81.8% at top-50, 84.3% at top-100 (re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.") +add(S3, f"{BIN} . --pack-signatures --top-k=10", "Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 89.6% fewer bytes at top-10, 83.7% at top-50, 83.7% at top-100 (re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.") add(S3, f"{BIN} . --outline=rankGraphTeleport --top-k=0", "Control-flow skeleton of one symbol, payload-only via the new --top-k=0.") add(S3, f"{BIN} . --outline=rankGraphTeleport:1-10 --top-k=0", "CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).") add(S3, f"{BIN} . --expand=rankGraphTeleport --top-k=0", "Full body + inline callee signatures.") From 4d3e1bf62f368da26b6d05bb1b4b1385038069dd Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Sep 2026 03:28:41 -0300 Subject: [PATCH 3/7] test(argvdiff): ignore revision metadata in version vectors --- test/argvdiffcheck.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/argvdiffcheck.sh b/test/argvdiffcheck.sh index 529ac2f1b..4cc463805 100755 --- a/test/argvdiffcheck.sh +++ b/test/argvdiffcheck.sh @@ -375,7 +375,19 @@ while IFS= read -r v; do # still diffs. sed -E 's/\((main\.cpp|verbs_[a-z]+\.h):[0-9]+,/(MAINTU:LINE,/g; s/\.(cpp|h):[0-9]+,/.\1:LINE,/g' "$TMP/e.base" > "$TMP/e.base.n" sed -E 's/\((main\.cpp|verbs_[a-z]+\.h):[0-9]+,/(MAINTU:LINE,/g; s/\.(cpp|h):[0-9]+,/.\1:LINE,/g' "$TMP/e.new" > "$TMP/e.new.n" - if [ "$rcb" != "$rcn" ] || ! cmp -s "$TMP/o.base" "$TMP/o.new" || ! cmp -s "$TMP/e.base.n" "$TMP/e.new.n"; then + # --version intentionally embeds the source revision. A differential check compares behavior, not the + # revision metadata that must change between BASE and BIN; keep every other stdout byte-exact. + case "$v" in + --version|*" --version") + sed -E 's/built_from=[^)]*/built_from=REVISION/' "$TMP/o.base" > "$TMP/o.base.n" + sed -E 's/built_from=[^)]*/built_from=REVISION/' "$TMP/o.new" > "$TMP/o.new.n" + ;; + *) + cp "$TMP/o.base" "$TMP/o.base.n" + cp "$TMP/o.new" "$TMP/o.new.n" + ;; + esac + if [ "$rcb" != "$rcn" ] || ! cmp -s "$TMP/o.base.n" "$TMP/o.new.n" || ! cmp -s "$TMP/e.base.n" "$TMP/e.new.n"; then diffs=$(( diffs + 1 )) # The default 5 keeps a normal run terse. A FIX ROUND must classify EVERY diff, and capping the list # at 5 previously forced an agent to make a throwaway copy of this gate in test/ just to read its own From ab763c989690a3802d3b96c5aecdcca8caabf9a1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Sep 2026 10:03:33 -0300 Subject: [PATCH 4/7] chore(pr): address layout review scope notes --- CLAUDE.md | 4 +++- test/argvdiffcheck.sh | 14 +------------- test/showcase_capture.py | 2 +- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index eb28d6df5..df2e6bed2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,9 @@ cmake --build build --clean-first -j # and the same for asan/ if that t `test/g1freshcheck.sh` catches the ordinary stale binary (binary older than source) and is worth believing when it fires — it is not noise. It cannot catch this variant, because here the binary is *newer* than the source and only its contents are stale. Nothing in CMake can repair a source that -changed mid-compile; the discipline is the fix. +changed mid-compile; the discipline is the fix. When this variant is suspected, `--doctor`'s `layout` +row reports the cross-translation-unit `sizeof`/`alignof` evidence; treat `state="disagree"` as a +clean-rebuild requirement. ## Verify diff --git a/test/argvdiffcheck.sh b/test/argvdiffcheck.sh index 4cc463805..529ac2f1b 100755 --- a/test/argvdiffcheck.sh +++ b/test/argvdiffcheck.sh @@ -375,19 +375,7 @@ while IFS= read -r v; do # still diffs. sed -E 's/\((main\.cpp|verbs_[a-z]+\.h):[0-9]+,/(MAINTU:LINE,/g; s/\.(cpp|h):[0-9]+,/.\1:LINE,/g' "$TMP/e.base" > "$TMP/e.base.n" sed -E 's/\((main\.cpp|verbs_[a-z]+\.h):[0-9]+,/(MAINTU:LINE,/g; s/\.(cpp|h):[0-9]+,/.\1:LINE,/g' "$TMP/e.new" > "$TMP/e.new.n" - # --version intentionally embeds the source revision. A differential check compares behavior, not the - # revision metadata that must change between BASE and BIN; keep every other stdout byte-exact. - case "$v" in - --version|*" --version") - sed -E 's/built_from=[^)]*/built_from=REVISION/' "$TMP/o.base" > "$TMP/o.base.n" - sed -E 's/built_from=[^)]*/built_from=REVISION/' "$TMP/o.new" > "$TMP/o.new.n" - ;; - *) - cp "$TMP/o.base" "$TMP/o.base.n" - cp "$TMP/o.new" "$TMP/o.new.n" - ;; - esac - if [ "$rcb" != "$rcn" ] || ! cmp -s "$TMP/o.base.n" "$TMP/o.new.n" || ! cmp -s "$TMP/e.base.n" "$TMP/e.new.n"; then + if [ "$rcb" != "$rcn" ] || ! cmp -s "$TMP/o.base" "$TMP/o.new" || ! cmp -s "$TMP/e.base.n" "$TMP/e.new.n"; then diffs=$(( diffs + 1 )) # The default 5 keeps a normal run terse. A FIX ROUND must classify EVERY diff, and capping the list # at 5 previously forced an agent to make a throwaway copy of this gate in test/ just to read its own diff --git a/test/showcase_capture.py b/test/showcase_capture.py index 97a3678e7..217d4a8af 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -332,7 +332,7 @@ def add(section, cmd, what, **opts): S3 = "zoom the detail ladder" add(S3, f'{BIN} . --for="pagerank power iteration" --detail=2', "Importance-weighted detail: FULL bodies for top-2, signatures for the rest.") -add(S3, f"{BIN} . --pack-signatures --top-k=10", "Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 89.6% fewer bytes at top-10, 83.7% at top-50, 83.7% at top-100 (re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.") +add(S3, f"{BIN} . --pack-signatures --top-k=10", "Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 89.6% fewer bytes at top-10, 83.7% at top-50, 83.7% at top-100 (re-derived 2026-09-14 at the struct-layout doctor: registering shared model layouts changed the ranked corpus and body denominator — top-50 from 81.8 to 83.7. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.") add(S3, f"{BIN} . --outline=rankGraphTeleport --top-k=0", "Control-flow skeleton of one symbol, payload-only via the new --top-k=0.") add(S3, f"{BIN} . --outline=rankGraphTeleport:1-10 --top-k=0", "CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).") add(S3, f"{BIN} . --expand=rankGraphTeleport --top-k=0", "Full body + inline callee signatures.") From 022ee180c6c4daedd1a64415d2dcfd73c8056fa0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 15 Sep 2026 19:57:45 -0300 Subject: [PATCH 5/7] ci: enforce Release layout doctor arm --- .github/workflows/ci.yml | 13 +++++++++++++ test/structlayoutcheck.sh | 13 +++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 960b1305f..96d236a1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,6 +201,10 @@ jobs: # deployment target, the default's does not. An empty DEVELOPER_DIR on the Linux legs is inert. env: DEVELOPER_DIR: ${{ matrix.os == 'macos-14' && '/Applications/Xcode_16.2.app/Contents/Developer' || '' }} + # Release legs use their just-built binary as structlayoutcheck's Release reference. Plain legs + # retain the gate's explicit local-style SKIP; they must not relabel an NDEBUG-off binary as Release. + RIPWIRE_RELEASE_BIN: ${{ matrix.flavor == 'Release' && 'build/ripwire' || '' }} + RIPWIRE_RELEASE_REQUIRED: ${{ matrix.flavor == 'Release' && '1' || '0' }} steps: # L4 (Linux probe): checkout@v4 defaults to a --depth 1 clone, which leaves ONE commit of history # in the tree. The churn / co-change / ownership gates (churnjoincheck, hotspots, --owners, the @@ -261,6 +265,15 @@ jobs: - name: Build run: cmake --build build -j + # This is after Build and before the gate suite: the Release leg's own binary is the reference, + # and the required flag makes a missing/invalid path fail instead of turning the arm into a green + # SKIP. The plain leg does not run this arm because its binary is intentionally NDEBUG-off. + - name: Stage the Release binary for structlayoutcheck + if: matrix.flavor == 'Release' + run: | + test -x "$RIPWIRE_RELEASE_BIN" + "$RIPWIRE_RELEASE_BIN" --version + # The floor this matrix exists to hold, read off the BINARY rather than inferred from the compiler # version: emit= names the emitter that compiled in (src/infra/emit.h). A leg whose standard library # lacks still builds — the emitter falls back to std::format+fputs by feature test — so diff --git a/test/structlayoutcheck.sh b/test/structlayoutcheck.sh index 8b28806e8..18dfacaed 100755 --- a/test/structlayoutcheck.sh +++ b/test/structlayoutcheck.sh @@ -112,8 +112,17 @@ check_doctor(){ check_doctor plain "$BIN" # ── D: Release/NDEBUG+LTO carries the same records when a reference binary is supplied ───────────── -if [ -z "$REL" ] || [ ! -x "$REL" ]; then - skip "Release binary not supplied (set RIPWIRE_RELEASE_BIN=build_rel/ripwire)" +# The Release matrix sets RIPWIRE_RELEASE_REQUIRED so this arm cannot quietly disappear from CI. A +# local run without a second build keeps the original explicit SKIP, while an explicitly supplied but +# unusable path is always a configuration failure rather than a covered check. +if [ -z "$REL" ]; then + if [ "${RIPWIRE_RELEASE_REQUIRED:-0}" = 1 ]; then + no "Release binary is required but RIPWIRE_RELEASE_BIN is unset" + else + skip "Release binary not supplied (set RIPWIRE_RELEASE_BIN=build_rel/ripwire)" + fi +elif [ ! -x "$REL" ]; then + no "Release binary is not executable: $REL" else check_doctor release "$REL" fi From 3f235eb60d68f8d3801d40d2e77048660f45b000 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 15 Sep 2026 20:07:56 -0300 Subject: [PATCH 6/7] ci: use matrix compiler for layout gate --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96d236a1d..067fb05ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,6 +201,9 @@ jobs: # deployment target, the default's does not. An empty DEVELOPER_DIR on the Linux legs is inert. env: DEVELOPER_DIR: ${{ matrix.os == 'macos-14' && '/Applications/Xcode_16.2.app/Contents/Developer' || '' }} + # Keep CXX at job scope, not only on Configure: pargates.py launches the gates in a later step, + # and structlayoutcheck must compile its fixture with this matrix leg's front end. + CXX: ${{ matrix.cc == 'clang' && 'clang++' || matrix.cc == 'gcc' && 'g++-14' || 'c++' }} # Release legs use their just-built binary as structlayoutcheck's Release reference. Plain legs # retain the gate's explicit local-style SKIP; they must not relabel an NDEBUG-off binary as Release. RIPWIRE_RELEASE_BIN: ${{ matrix.flavor == 'Release' && 'build/ripwire' || '' }} From e9c5f28345dd9d5a0a8d864503ba2910185b03d3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 16 Sep 2026 20:44:25 -0300 Subject: [PATCH 7/7] fix(doctor): make layout gate self-discovering --- .github/workflows/ci.yml | 16 ------------- CLAUDE.md | 3 ++- docs/ARCHITECTURE.md | 2 +- src/verbs_doctor.h | 7 ++++-- test/structlayoutcheck.sh | 47 +++++++++++++++++++++++---------------- 5 files changed, 36 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 067fb05ad..960b1305f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,13 +201,6 @@ jobs: # deployment target, the default's does not. An empty DEVELOPER_DIR on the Linux legs is inert. env: DEVELOPER_DIR: ${{ matrix.os == 'macos-14' && '/Applications/Xcode_16.2.app/Contents/Developer' || '' }} - # Keep CXX at job scope, not only on Configure: pargates.py launches the gates in a later step, - # and structlayoutcheck must compile its fixture with this matrix leg's front end. - CXX: ${{ matrix.cc == 'clang' && 'clang++' || matrix.cc == 'gcc' && 'g++-14' || 'c++' }} - # Release legs use their just-built binary as structlayoutcheck's Release reference. Plain legs - # retain the gate's explicit local-style SKIP; they must not relabel an NDEBUG-off binary as Release. - RIPWIRE_RELEASE_BIN: ${{ matrix.flavor == 'Release' && 'build/ripwire' || '' }} - RIPWIRE_RELEASE_REQUIRED: ${{ matrix.flavor == 'Release' && '1' || '0' }} steps: # L4 (Linux probe): checkout@v4 defaults to a --depth 1 clone, which leaves ONE commit of history # in the tree. The churn / co-change / ownership gates (churnjoincheck, hotspots, --owners, the @@ -268,15 +261,6 @@ jobs: - name: Build run: cmake --build build -j - # This is after Build and before the gate suite: the Release leg's own binary is the reference, - # and the required flag makes a missing/invalid path fail instead of turning the arm into a green - # SKIP. The plain leg does not run this arm because its binary is intentionally NDEBUG-off. - - name: Stage the Release binary for structlayoutcheck - if: matrix.flavor == 'Release' - run: | - test -x "$RIPWIRE_RELEASE_BIN" - "$RIPWIRE_RELEASE_BIN" --version - # The floor this matrix exists to hold, read off the BINARY rather than inferred from the compiler # version: emit= names the emitter that compiled in (src/infra/emit.h). A leg whose standard library # lacks still builds — the emitter falls back to std::format+fputs by feature test — so diff --git a/CLAUDE.md b/CLAUDE.md index df2e6bed2..e66c6b9cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,8 @@ believing when it fires — it is not noise. It cannot catch this variant, becau *newer* than the source and only its contents are stale. Nothing in CMake can repair a source that changed mid-compile; the discipline is the fix. When this variant is suspected, `--doctor`'s `layout` row reports the cross-translation-unit `sizeof`/`alignof` evidence; treat `state="disagree"` as a -clean-rebuild requirement. +clean-rebuild requirement. `state="agree"` compares only the `types=` registered in `src/model.h`; a +same-size layout change or a stale constant is invisible, so `agree` does not rule out a mixed binary. ## Verify diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 318b91c9e..e7293e7e2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -38,7 +38,7 @@ sections in call order: the lazy tags.scm prewarm (`ingest_prewarm.h`), the para (`ingest_parsepool.h`), the document post-pass (`ingest_docpass.h`), and the build-model tail — dedup, symbol assignment, span attribution, ordered emit (`ingest_model.h`). -`--doctor`'s `layout` row compares the shared model's recorded `sizeof`/`alignof` facts across translation units and fails a mixed binary instead of guessing which object is stale. +`--doctor`'s `layout` row compares the shared model's recorded `sizeof`/`alignof` facts across translation units and reports evidence of a possible mixed binary instead of guessing which object is stale; agreement covers only the registered facts. **Crawl order is deterministic, and that is load-bearing.** The walk *collects every candidate path first*, sorts them lexicographically by byte, and only then assigns node IDs and parses. Node IDs are diff --git a/src/verbs_doctor.h b/src/verbs_doctor.h index a0d1df630..6ba9bccef 100644 --- a/src/verbs_doctor.h +++ b/src/verbs_doctor.h @@ -282,11 +282,14 @@ inline const char* doctorLegendComment() "COMMAND git would run on every read-only call, and neutralised=\"1\" says core.fsmonitor=false was " "appended to git's environment override for this run (stderr said so as git_harden=fsmonitor-hook); " "builtin, off and unset are left untouched and neutralised=\"0\". " - "layout's state=\"agree\" means the layout records match; checked=\"1\" means the comparison ran; " + "layout's state=\"agree\" means the layout records match; agree compares only the types= registered in src/model.h; " + "a same-size layout change or a stale constant is invisible, so agree does not rule out a mixed binary; " + "checked=\"1\" means the comparison ran; " "units=\"N\" counts translation units and types=\"N\" counts recorded types. On state=\"disagree\", " "type= names the first differing type, unit0=/unit1= name the two records, and " "present0=/present1=, size0=/size1=, and align0=/align1= disclose their values; the row gives the rebuild action; " - "state=\"not-checked\" means fewer than two records survived into this binary. " + "state=\"not-checked\" means records exist but fewer than two records with a recorded type could be compared; " + "state=\"no-records\" means no layout record was registered. " "NB no flag below is spelled with its leading dashes: an XML comment may not contain a " "double hyphen, and this legend is one comment. -->"; } diff --git a/test/structlayoutcheck.sh b/test/structlayoutcheck.sh index 18dfacaed..0c0a3647f 100755 --- a/test/structlayoutcheck.sh +++ b/test/structlayoutcheck.sh @@ -9,8 +9,10 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" -REL="${RIPWIRE_RELEASE_BIN:-}" -[ -n "$REL" ] && [ "${REL#/}" = "$REL" ] && REL="$ROOT/$REL" +BUILD_DIR="$( dirname "$BIN" )" +CACHE="$BUILD_DIR/CMakeCache.txt" +cacheVar(){ [ -f "$CACHE" ] && sed -n "s/^$1:[A-Z]*=//p" "$CACHE" | head -1; return 0; } +CXX="${CXX:-$( cacheVar CMAKE_CXX_COMPILER )}" CXX="${CXX:-c++}" TMP="$( mktemp -d )" trap 'rm -rf "$TMP"' EXIT @@ -22,10 +24,16 @@ skip(){ printf ' SKIP %s\n' "$*"; } [ -x "$BIN" ] || { echo "structlayoutcheck: no ripwire binary at $BIN — build first"; exit 2; } command -v "$CXX" >/dev/null 2>&1 || { echo "structlayoutcheck: no C++ compiler at $CXX"; exit 2; } +LAYOUT_TYPE_COUNT="$( grep -oF 'RIPWIRE_LAYOUT_TYPE_ENTRY(' "$ROOT/src/model.h" | wc -l | tr -d ' ' )" +[ "$LAYOUT_TYPE_COUNT" -gt 0 ] || { echo "structlayoutcheck: no registered layout types in $ROOT/src/model.h"; exit 2; } +. "$ROOT/scripts/cxxstd.sh" +if ! CXXSTD="$( ripwire_cxx_std_flag "$CXX" )"; then + echo "structlayoutcheck: $CXX accepts neither the C++23 standard flag nor its legacy spelling"; exit 2 +fi echo "structlayoutcheck: BIN=$BIN CXX=$CXX" -CXXFLAGS=( -std=c++23 -O2 -flto -I"$ROOT/src" ) +CXXFLAGS=( "$CXXSTD" -O2 -flto -I"$ROOT/src" ) compile_unit(){ local unit="$1"; shift "$CXX" "${CXXFLAGS[@]}" "-DRIPWIRE_LAYOUT_TU=\"$unit\"" "$@" \ @@ -103,28 +111,29 @@ check_doctor(){ printf '%s' "$row" | grep -q 'units="[2-9][0-9]*"' \ && ok "$label layout row compares at least two translation units" \ || no "$label layout row has fewer than two translation units" - printf '%s' "$row" | grep -q 'types="[1-9][0-9]*"' \ - && ok "$label layout row compares at least one type" \ - || no "$label layout row did not report a type count" + printf '%s' "$row" | grep -q "types=\"$LAYOUT_TYPE_COUNT\"" \ + && ok "$label layout row reports all $LAYOUT_TYPE_COUNT registered model types" \ + || no "$label layout row did not report types=\"$LAYOUT_TYPE_COUNT\"" } # ── C: the actual binary carries the records in the plain build ─────────────────────────────────── check_doctor plain "$BIN" -# ── D: Release/NDEBUG+LTO carries the same records when a reference binary is supplied ───────────── -# The Release matrix sets RIPWIRE_RELEASE_REQUIRED so this arm cannot quietly disappear from CI. A -# local run without a second build keeps the original explicit SKIP, while an explicitly supplied but -# unusable path is always a configuration failure rather than a covered check. -if [ -z "$REL" ]; then - if [ "${RIPWIRE_RELEASE_REQUIRED:-0}" = 1 ]; then - no "Release binary is required but RIPWIRE_RELEASE_BIN is unset" - else - skip "Release binary not supplied (set RIPWIRE_RELEASE_BIN=build_rel/ripwire)" - fi -elif [ ! -x "$REL" ]; then - no "Release binary is not executable: $REL" +# ── D: the binary's own --version selects the Release arm ───────────────────────────────────────── +# Release matrix jobs pass this same binary as $BIN, so no workflow-specific environment is needed. A +# plain dev binary keeps the local SKIP; only an exact build-type token of Release can enter this arm. +BUILD_INFO="$( "$BIN" --version 2>/dev/null )"; BUILD_INFO_RC=$? +BUILD_TYPE="$( printf '%s\n' "$BUILD_INFO" | sed -nE 's/^ripwire [^ ]+ \(([^,]+),.*$/\1/p' | head -1 )" +if [ "$BUILD_INFO_RC" -ne 0 ]; then + no "could not query $BIN --version to choose the Release arm" +elif [ "$BUILD_TYPE" = Release ]; then + check_doctor release "$BIN" +elif [ "$BUILD_TYPE" = dev ]; then + skip "Release binary not supplied ($BIN reports build type dev)" +elif [ -n "$BUILD_TYPE" ]; then + skip "Release arm not run ($BIN reports build type $BUILD_TYPE)" else - check_doctor release "$REL" + no "$BIN --version did not disclose a build type" fi if [ "$fail" -eq 0 ]; then