fix(legend): quality-delta defines its duplication pair; exemplar def… #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # Guardrails as automated gates (CLAUDE.md "The guardrails as automated gates"): | |
| # - release job: configure (portable default, no RIPWIRE_NATIVE — CI hardware is not "this dev machine") | |
| # + build + full test/regression.sh (170+ gates incl. determinism/G4/quality) per OS. | |
| # ubuntu-24.04 here is also the FIRST real proof of L1's portability fix: real x86-64 | |
| # Linux hardware, not the local RIPWIRE_PRETEND_LINUX proxy test/portablebuildcheck.sh | |
| # uses on the (Apple Silicon) dev machines this repo is normally built on. | |
| # - asan job: configure -DRIPWIRE_ASAN=ON (G1: address,undefined,integer,float-divide-by-zero, | |
| # float-cast-overflow, fail-fast) + build + the fixture self-run + the 5 heaviest verbs | |
| # (--pack-task/--from-trace/--quality-delta/--merge-scout/--edit-check) | |
| # via their own gate scripts pointed at the ASan binary. NOT the full regression.sh under | |
| # ASan — that would run 170+ mostly-non-heavy gates at sanitizer speed for little extra | |
| # signal; the release job already proves those gates pass on a normal binary per OS. | |
| # - both jobs end with an explicit 2-run byte-diff det-gate + xmllint --noout, even though | |
| # test/regression.sh already covers both, so a CI log reader sees the two headline product | |
| # guarantees (determinism, well-formed XML / G4) called out as their own named steps. | |
| # | |
| # There is no dependency-download step and no dependency cache: tree-sitter core, all 15 grammars and | |
| # doctest are VENDORED under third_party/deps (see CMakeLists.txt), so `actions/checkout` is the whole | |
| # provisioning story. That also means CI exercises the same hermetic build a stranger's clone gets — | |
| # a cache step would have hidden a re-introduced network fetch behind a warm runner. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Never RIPWIRE_NATIVE in CI: -march=native would bake in whatever ISA the CI runner's host happens to | |
| # expose that week, defeating the entire point of testing the portable default. | |
| jobs: | |
| # ─── style: one gating clang-format check, one advisory clang-format report, one advisory clang-tidy ─── | |
| # | |
| # CLANG_VERSION is a PIN, not a floor. clang-format's output changes across major releases, so an | |
| # unpinned checker reports "unformatted" on a tree that was formatted correctly by a different release. | |
| # .clang-format was authored and verified against clang-format 22 on the dev machine; scripts/formatcheck.sh | |
| # refuses to run under any other major, so bumping this number without re-running `clang-format -i` over the | |
| # gated file set fails loudly instead of silently re-styling the repo. | |
| # | |
| # The clang-format GATE covers only the files listed in scripts/formatcheck.sh, which are the ones that | |
| # already match .clang-format byte for byte. It cannot cover the tree: the house style in CONTRIBUTING.md §3 | |
| # is hand-formatted in ways clang-format has no option to preserve (multi-statement one-liners, | |
| # `for( … ) if( … ) return i;`, packed initialiser rows and `case` labels, hand-chosen 160-200 column wrap | |
| # seams), and reformatting all 98 first-party C++ files changes 11837 lines that survive `git diff -w` — | |
| # real joins and splits — across 89 of them. A whole-tree --Werror check would therefore be red on a | |
| # correctly-styled tree, which is a gate that punishes the documented style. The --advisory step below | |
| # prints that gap on every run so it stays visible rather than forgotten. | |
| # | |
| # clang-tidy is ADVISORY and must stay that way: continue-on-error, and .clang-tidy's WarningsAsErrors is | |
| # empty. Its default catalogue argues for a different C++ than the data-oriented one G2 mandates (POD/SoA, | |
| # C arrays, 32-bit handles, VERIFY instead of exceptions), so the config is curated down to bugprone-* / | |
| # clang-analyzer-* / performance-* / misc-dangling-* and even those only report. Baseline at the commit | |
| # that added it: 312 unique sites, of which 126 are bugprone-easily-swappable-parameters and 85 are | |
| # bugprone-exception-escape. Read it as a to-triage list, not a queue of defects. | |
| style: | |
| name: style (clang-format gate + advisory clang-tidy) | |
| runs-on: ubuntu-24.04 | |
| env: | |
| CLANG_VERSION: "22" | |
| 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 | |
| # quality short-horizon-churn kind) mine `git log` for real, so on a shallow checkout they do not | |
| # error — they measure zero and fail, or worse, pass while measuring nothing. Full history it is. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # churn/co-change gates read real git history — a shallow clone reddens them | |
| - name: Install clang-format / clang-tidy (PINNED major — see the job comment) | |
| run: | | |
| wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh | |
| chmod +x /tmp/llvm.sh | |
| sudo /tmp/llvm.sh "$CLANG_VERSION" all | |
| clang-format-"$CLANG_VERSION" --version | |
| clang-tidy-"$CLANG_VERSION" --version | |
| # GATING. Runs clang-format --dry-run --Werror over scripts/formatcheck.sh's file list. | |
| # Local equivalent, proven on the dev machine before this step was written: | |
| # CLANG_FORMAT=/opt/homebrew/opt/llvm/bin/clang-format scripts/formatcheck.sh | |
| - name: clang-format — gate (scripts/formatcheck.sh) | |
| run: CLANG_FORMAT=clang-format-"$CLANG_VERSION" bash scripts/formatcheck.sh | |
| # NON-GATING by construction: --advisory always exits 0. It names every first-party C++ file | |
| # clang-format would rewrite, so the size of the ungated remainder is on the record every run. | |
| - name: clang-format — advisory report over the whole first-party set (non-gating) | |
| run: CLANG_FORMAT=clang-format-"$CLANG_VERSION" bash scripts/formatcheck.sh --advisory | |
| # embedded_queries.h and version.h are written at CONFIGURE time, not build time, so a configure | |
| # is the whole provisioning story for a compile database — no compile needed to lint. | |
| - name: Configure a compile database for clang-tidy | |
| run: cmake -S . -B build-tidy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON | |
| - name: clang-tidy — ADVISORY, never gates | |
| continue-on-error: true | |
| run: | | |
| clang-tidy-"$CLANG_VERSION" -p build-tidy --quiet \ | |
| src/main.cpp src/ingest.cpp src/pagerank.cpp src/tsprobe.cpp src/infra/diagnostics.cpp | |
| release: | |
| name: release (${{ matrix.os }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-14, ubuntu-24.04] | |
| runs-on: ${{ matrix.os }} | |
| 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 | |
| # quality short-horizon-churn kind) mine `git log` for real, so on a shallow checkout they do not | |
| # error — they measure zero and fail, or worse, pass while measuring nothing. Full history it is. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # churn/co-change gates read real git history — a shallow clone reddens them | |
| - name: Install tooling (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc | |
| - name: Install tooling (macOS) | |
| if: runner.os == 'macOS' | |
| run: brew install ripgrep | |
| - name: Configure (portable default — no RIPWIRE_NATIVE) | |
| run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release | |
| - name: Build | |
| run: cmake --build build -j | |
| - name: "test/regression.sh (170+ gates: determinism, cache transparency, G4 XML, quality, …)" | |
| run: RIPWIRE_BIN=build/ripwire bash test/regression.sh | |
| # The Release build above defines NDEBUG, which compiles DEGRADED_PATH_ALERT out — so a degrade-path | |
| # gate run against it cannot observe the alert it asserts. The e7405e7 qsnap fix was invisible to CI | |
| # before it landed, and the 2026-07-27 round added several more degrade gates (shallow-clone | |
| # v="unknown", the pr-context ref refusal, the doc-drift VERIFY demotion). Build a SECOND time with | |
| # the plain local flavour and re-run the suite against it, so both the optimizer-visible Release | |
| # behaviour and the degrade paths are covered — neither alone is enough. | |
| # | |
| # 2026-08-01: the two gates that assert an alert directly (estchargecheck #14, qualitystalecheck | |
| # arms 7/8c) no longer pass — or fail — silently on the Release leg. Each probes the flavour with | |
| # TWO independent readings (an unrelated already-gated degrade path, plus --version's build-type | |
| # token) and, only when both agree the binary is NDEBUG, prints a SKIP naming what is unobservable | |
| # and pointing at THIS second leg as where it is proven. If the alert is missing on a flavour that | |
| # should see it, they still FAIL. So the Release leg is honestly green rather than either | |
| # vacuously green (the 2026-07-27 trap) or unconditionally red (the same trap inverted). | |
| - name: Configure (plain — NDEBUG off, so DEGRADED_PATH_ALERT compiles in) | |
| run: cmake -S . -B build-debugalerts | |
| - name: Build (plain) | |
| run: cmake --build build-debugalerts -j | |
| - name: "test/regression.sh against the non-Release build (degrade paths visible)" | |
| run: RIPWIRE_BIN=build-debugalerts/ripwire bash test/regression.sh | |
| - name: det-gate — 2-run byte-identical diff | |
| run: | | |
| ./build/ripwire test/fixture --no-cache >/tmp/run_a.xml | |
| ./build/ripwire test/fixture --no-cache >/tmp/run_b.xml | |
| diff -q /tmp/run_a.xml /tmp/run_b.xml | |
| - name: G4 — xmllint --noout | |
| run: ./build/ripwire test/fixture --no-cache | xmllint --noout - | |
| asan: | |
| name: asan (${{ matrix.os }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-14, ubuntu-24.04] | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| # Darwin's arm64 runtime rejects the standalone leak sanitizer at startup (CMakeLists.txt's own | |
| # comment on this, `ripwire_asan_fixture`); Linux gets full leak detection. | |
| # matrix.os, not runner.os: the runner context does not exist at job-level env — GitHub's | |
| # parser rejects the whole workflow (found by the first real Actions run; local YAML lint | |
| # cannot see it). Step-level `if: runner.os` below is legal and stays. | |
| # log_path: a sanitizer abort inside a gate that pipes 2>/dev/null leaves NO report in the log | |
| # (CI round 3: an asan/x86-64 abort read as "pin no longer resolves"). With log_path the report | |
| # lands in a file the failure artifact below preserves. | |
| ASAN_OPTIONS: ${{ matrix.os == 'macos-14' && 'detect_leaks=0:halt_on_error=1:abort_on_error=1:log_path=/tmp/asanlog' || 'detect_leaks=1:halt_on_error=1:abort_on_error=1:log_path=/tmp/asanlog' }} | |
| UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:log_path=/tmp/asanlog | |
| LSAN_OPTIONS: suppressions=${{ github.workspace }}/lsan_suppressions.txt | |
| 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 | |
| # quality short-horizon-churn kind) mine `git log` for real, so on a shallow checkout they do not | |
| # error — they measure zero and fail, or worse, pass while measuring nothing. Full history it is. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # churn/co-change gates read real git history — a shallow clone reddens them | |
| - name: Install tooling (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc clang | |
| - name: Install tooling (macOS) | |
| if: runner.os == 'macOS' | |
| run: brew install ripgrep | |
| # CLANG, not the default cc. G1's `integer` group is Clang-only and gcc rejects the whole option | |
| # ("unrecognized argument to '-fsanitize=' option: 'integer'" — the first public CI run). CMakeLists.txt | |
| # now filters the Clang-only members out under GCC, but that is the HONEST-DEGRADE path for a | |
| # contributor, not something public CI should be running: pinning clang here keeps the COMPLETE G1 | |
| # stack on Linux, so the gates below mean the same thing on both matrix legs. | |
| # | |
| # Deliberately NOT applied to the release job: gcc there is a FEATURE. It is a second front end over | |
| # the same tree and it found three real portability bugs on its first run. | |
| - name: Configure (G1 sanitizer stack — clang, see the step comment) | |
| if: runner.os == 'Linux' | |
| env: | |
| CC: clang | |
| CXX: clang++ | |
| run: cmake -S . -B asan -DRIPWIRE_ASAN=ON | |
| - name: Configure (G1 sanitizer stack) | |
| if: runner.os == 'macOS' | |
| run: cmake -S . -B asan -DRIPWIRE_ASAN=ON | |
| - name: Build | |
| run: cmake --build asan -j | |
| - name: G1 fixture self-run (ripwire_asan_fixture) | |
| run: cmake --build asan --target ripwire_asan_fixture | |
| - name: Heavy verb — --pack-task (test/packtaskcheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire bash test/packtaskcheck.sh | |
| - name: Heavy verb — --from-trace (test/tracecheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire bash test/tracecheck.sh | |
| - name: Heavy verb — --quality-delta (test/qualitycheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire bash test/qualitycheck.sh | |
| - name: Heavy verb — --merge-scout (test/mergescoutcheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire bash test/mergescoutcheck.sh | |
| - name: Heavy verb — --edit-check (test/editcheckcheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire bash test/editcheckcheck.sh | |
| # cachefuzzcheck's two mutation tables ARE sanitizer sweeps: they drive corrupt/truncated/hostile cache | |
| # blobs and corrupt qsnap blobs through the readers and assert that no sanitizer report fires. That needs | |
| # an instrumented binary — RIPWIRE_ASAN_BIN is the whole point of the gate — and the `release` jobs, which | |
| # configure only build/, cannot supply one. Until now the gate hard-FAILED there for the missing binary | |
| # (PR #1, run 30732976779: "absorb gate (cachefuzzcheck.sh failed)" on BOTH release legs, that one line | |
| # its only failure) while NO job anywhere ran the sweeps it exists for. The gate now skips those two arms | |
| # with a named reason on a leg that has no ASan build, and this step is where they actually run: both | |
| # variables point at the instrumented binary, so the ground-truth arms and the sanitizer arms agree by | |
| # construction. Adding it here is what makes that skip reason true rather than an excuse. | |
| - name: Sanitizer sweep — corrupt cache / qsnap blobs (test/cachefuzzcheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire RIPWIRE_ASAN_BIN=asan/ripwire bash test/cachefuzzcheck.sh | |
| # NOT a heavy verb — here because it is the ONLY gate whose subject is an integer-overflow class that | |
| # a plain binary cannot observe. cppqualcheck §9 drives an unbalanced `operator>` frame through | |
| # --from-trace; before the H4 W2b fixup that aborted the G1 build (rc=134) while the plain build ran | |
| # it in silence. The suite job above runs the whole suite TWICE and both times PLAIN, so this gate was | |
| # green-by-construction in CI until it appeared here (V3-H-2). | |
| - name: Sanitizer-only class — cppqualcheck §9 operator-frame scan (test/cppqualcheck.sh) | |
| run: RIPWIRE_BIN=asan/ripwire bash test/cppqualcheck.sh | |
| - name: det-gate — 2-run byte-identical diff (ASan binary) | |
| run: | | |
| ./asan/ripwire test/fixture --no-cache >/tmp/run_a.xml | |
| ./asan/ripwire test/fixture --no-cache >/tmp/run_b.xml | |
| diff -q /tmp/run_a.xml /tmp/run_b.xml | |
| - name: G4 — xmllint --noout (ASan binary) | |
| run: ./asan/ripwire test/fixture --no-cache | xmllint --noout - | |
| # Self-dogfood, STILL NON-GATING — but for a smaller and better-measured reason than before. | |
| # | |
| # The verb now separates the historical floor from the rot: a failed anchor the AUTHOR dated is | |
| # kind="dated-record" and counts in dated=, leaving drift= for live claims. On this repo's own docs | |
| # that splits 104 failed anchors into 63 rot + 41 records, and BOTH constants this comment used to | |
| # name (`kMcpVerbCount = 22`, `kCacheVersion=7`) are now records — their authors had already written | |
| # the "as of <date>" hedge the lane reads. | |
| # | |
| # `|| true` stays, and note first that it was never suppressing an exit code: --doc-drift always | |
| # exits 0 by design (a report, not a gate), so this only guards a crash. Gating would mean asserting | |
| # drift="0", and the residual 63 will not go to zero by any rule change — they are genuine stale | |
| # file:line maps in undated DESIGN studies, plus two audits that are manifestly artifacts-of-a-date | |
| # to a human but never write that date where a machine can read it. The lane refuses to guess genre | |
| # (measured: a bare date in the opening prose mis-dates three LIVE documents here), and git history | |
| # cannot help either — 90 of 98 stale anchors were CORRECT at their own doc's last commit, records | |
| # and rot alike. So the honest options are a zero-gate that is red on day one, or a ratchet ceiling | |
| # that goes red whenever a doc is edited in a shared tree. Neither earns its noise. | |
| # | |
| # What DOES gate is test/docdriftcheck.sh, which pins every lane and every rec= kind on a labelled | |
| # fixture, including the negative controls. Re-open this decision when the DESIGN docs' anchors are | |
| # fixed and the review notes carry a self-date: at that point drift= can be gated at 0. | |
| - name: Self-report (non-gating) — --doc-drift on our own docs | |
| run: ./asan/ripwire . --doc-drift --with-history || true | |
| # Sanitizer reports written via log_path survive gates that swallow stderr; keep them when a | |
| # step failed so the log tells us WHICH abort happened, not just that one did. | |
| - name: Upload sanitizer reports on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sanitizer-reports-${{ matrix.os }} | |
| path: /tmp/asanlog* | |
| if-no-files-found: ignore |