Skip to content

fix(legend): quality-delta defines its duplication pair; exemplar def… #20

fix(legend): quality-delta defines its duplication pair; exemplar def…

fix(legend): quality-delta defines its duplication pair; exemplar def… #20

Workflow file for this run

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