Validate the hand-appended mutation ledger in shared CI - #385
Conversation
audit/mutation-test-scans.json is read as evidence by the audit skill's Pass-2 gate and by rain-org-health's scanner, both of which trust the commit it names and neither of which validates it. Nothing in CI parsed it, so a trailing comma, a short SHA or a rebased-away commit merged green and surfaced later as a refused or wrong-based audit. Two live examples found while building this: rain.solmem records "b3bd859", and ST0x-Technology/st0x.oracle records "05bb66bfd8d0e", which resolves to no commit in that repo at all — the run it claims names a tree that does not exist. Adds `rainix-static mutation-ledger` plus a composite wired into both static reusables. Shape: non-empty array, strict YYYY-MM-DDTHH:MM:SSZ timestamp, 40-char lowercase-hex commit, tool literal, scope, summary object. Ancestry: every recorded SHA must be an ancestor of HEAD, with "not a commit here" and "not on this history" reported differently. testsAfterCommit and skillVersion are shape-checked when written and never required: 12 of 18 committed records name no such tree and the value cannot be recovered for a finished run, so requiring it would only invite an invented SHA. A repo with no ledger passes. A shallow checkout is refused rather than answered, and the action deepens commits-only first (--filter=tree:0: 4.3s and 4.4MB -> 9.0MB on raindex). Closes #384. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
|
Warning Review limit reachedNext included review available in 44 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (9)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment: S/M/L PR Classification Guidelines:This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed. Small (S)Characteristics:
Review Effort: Would have taken 5-10 minutes Examples:
Medium (M)Characteristics:
Review Effort: Would have taken 15-30 minutes Examples:
Large (L)Characteristics:
Review Effort: Would have taken 45+ minutes Examples:
Additional Factors to ConsiderWhen deciding between sizes, also consider:
Notes:
|
Closes #384. Upstream half of rainlanguage/rain.lib.hash#62, which
rainlanguage/rain.lib.hash#98 tried to fix inside a consumer's forge suite and
was closed unmerged for it.
What was wrong
audit/mutation-test-scans.jsonis the adversarial-mutation-test skill'scommitted run record, appended by hand. Two automated consumers read it as
evidence — the audit skill's Pass-2 gate and
rain-org-health'sroh-scan—and both trust the
commitit names. Neither validates it, and no CI step inthe org parsed the file at all.
Two records in the org are already wrong, found while building this:
rainlanguage/rain.solmem"commit": "b3bd859"ST0x-Technology/st0x.oracle[1]"commit": "05bb66bfd8d0e"GET /repos/ST0x-Technology/st0x.oracle/commits/05bb66bfd8d0eanswers422 No commit found for SHAandsearch/commits hash:returns 0. That recordclaims a whole-repo run over 119 behaviours with "every non-equivalent mutant
killed … no coverage gap, no new test … zero findings filed" — against a tree
that does not exist. It has been sitting green since 2026-08-11.
What changed
rainix-static mutation-ledger, a composite action around it, and the actionwired into both
rainix-sol-static.yamlandrainix-rs-static.yaml. Rustdoes the parsing and the comparing, bash only decides whether there is a record
to look at and fetches the history ancestry needs (CLAUDE.md).
Shape. A non-empty JSON array of run objects; per record a strict
YYYY-MM-DDTHH:MM:SSZtimestamp, a 40-character lowercase-hexcommit,tool == "adversarial-mutation-test", a non-emptyscope, and asummaryobject whose inner shape is per-repo and deliberately unchecked.
Ancestry.
git merge-base --is-ancestor <sha> HEADfor every recorded SHA."not a commit in this repo" and "not an ancestor of HEAD" are reported
differently, because the fixes differ.
The decisions #384 asked for
A repo with no ledger is not failed for absence — and does not pay for the
check. Decided in two places on purpose, the same split
frozen-snapshots-append-onlyuses: the composite's bash tests for the filefirst, so a repo that has never been mutation-tested does not pay for a fetch to
be told it is fine; and the Rust treats an absent file as a pass too, so running
the binary directly gives the same verdict the step does. This validates a
record, it never requires one.
A shallow checkout is refused, not answered. The shared checkout is
fetch-depth: 1, wheremerge-base --is-ancestorcalls every commit past thegraft boundary unreachable — a silent wrong answer, the exact failure class
this gate exists to remove. So the Rust half errors rather than reporting a
false non-ancestor, and the action deepens first. The deepen is commits-only:
--filter=tree:0fetches the commit graph without trees or blobs, which is allancestry needs. Measured on
rainlanguage/raindex(186MB, the largestconsumer): 4.3s,
.git4.4MB → 9.0MB.testsAfterCommitandskillVersionare shape-checked when written, neverrequired. 12 of the 18 records committed across
rainlanguagename notestsAfterCommit, and its presence does not track the skill version either way(rain.sol.codegen wrote it at 0.30.0, rain.string did not at 0.33.0). The value
cannot be recovered for a run that is over, so requiring it would red those
repos until someone invented a SHA — the fabricated evidence this gate exists to
catch.
rain.extrospectioncarries noskillVersionfor the same treatment.Both static workflows. The ledger is a JSON record, not a Solidity artifact.
All 14 ledger repos today are sol repos, so gating only the sol job would cover
every one of them — and would leave a future rust-only repo silently ungated,
which is #341 repeating: a skip and a pass are the same green.
raindexandrain.metadatarun both jobs and do carry a ledger, so they check it twice: oneextra commits-only fetch, the price of the hole not existing.
Not
rain-org-health.roh-scanreports, it does not gate: it runs hourlyon a cron in its own repo and commits
site/health.json, blocking no mergeanywhere. It reads one file at a time over the contents API and holds no
checkout, so
merge-base --is-ancestoris not available to it; it would haveto hit the compare API per SHA per repo, and the answer would be against the
default branch rather than the branch proposing the record. And its degradation
is correct —
mutation.rsreturnsNonefor a malformed ledger, "never afabricated 'never ran'" — which is exactly why a reporter cannot be the gate.
Full reasoning in #384.
QA
Against all 14 real ledger repos in the org, each cloned
--depth 1anddeepened as the action does. 12 clean, 2 red, and rainix itself (no ledger)
skips:
Mutations, each applied to
rain.lib.hash's real ledger in its real clone andeach run. Baseline clean, 9/9 killed, exit 1 every time, one line of output
naming the record index and field:
commit→ a 40-hex SHA that exists nowhereis not a commit in this repo — a mistyped or rebased-away SHAcommit→ a real commit in the repo, off this branchis not an ancestor of HEAD … not on this history]is not valid JSON: trailing comma at line 25 column 1timestamp→"2026-09-03 09:40:00"is not YYYY-MM-DDTHH:MM:SSZ — roh-scan ranks runs by comparing these as strings[]is an empty array — … indistinguishable from a repo that has never been mutation-tested, except that it looks like evidenceis an object — the ledger is a JSON ARRAYtool→"some-other-tool"expected the string "adversarial-mutation-test"testsAfterCommit→ the same SHA uppercasedis not a 40-character lowercase-hex git commitis a shallow checkout, where ancestry is unanswerableThe non-ancestor case is a commit actually created in that clone with
git commit-tree, not a stub:git merge-base --is-ancestorreally answered 1for it and 0 for the recorded SHAs.
Unit tests: +29 in
mutation_ledger.rs, suite 203 → 232, 0 failed. Shapemessages are covered against a stub probe, and
git_probeitself against a realtemp repo built with two commits on
HEADand one on a side branch, so theancestor / non-ancestor / unknown split is exercised through git rather than
described. Separate tests cover a real shallow clone being refused and a
directory git cannot answer for erroring rather than passing.
Bats: +9. 8 on the composite's bash — a ledger reaches the check, no ledger
skips without fetching, an
audit/directory alone is not a ledger, adirectory at the ledger path is not a record, a shallow checkout is deepened
commits-only, a deep one is not refetched, the deepen precedes the check (it
must — the Rust half refuses a shallow repo, so the reverse order would red
every ledger repo), and the check runs from the composite's own checkout rather
than a pinned
RAINIX_SHA. 1 asserting each static workflow wires the action,since nothing here executes either reusable and a dropped step is invisible.
cargo fmt --check,cargo clippy -D warnings -D clippy::alland the fullpre-commit bundle are clean.
Landing consequence, stated
rainlanguage/rain.solmemandST0x-Technology/st0x.oraclego red on their nextpush until their records are corrected. That is the gate working. rain.solmem's
is mechanical —
b3bd859expands to a real object name that is an ancestor ofmain— and is being fixed alongside this. st0x.oracle's is not: the SHAresolves to nothing, so what that record should say is a human call and gets its
own issue there rather than a value invented to make CI green.
🤖 Generated with Claude Code
https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN