Skip to content

Scan ledger: a wrapper object that states its own ordering and authority rule - #26

Open
thedavidmeister wants to merge 3 commits into
2026-08-16-issue-23from
2026-08-17-issue-25
Open

Scan ledger: a wrapper object that states its own ordering and authority rule#26
thedavidmeister wants to merge 3 commits into
2026-08-16-issue-23from
2026-08-17-issue-25

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes #25

Stacked on #24 (base branch 2026-08-16-issue-23) — do not merge this
before #24.

The ledger was a bare JSON array. A bare array cannot carry a schema version or
an ordering invariant, so every rule for reading the file lived in prose that
does not ship with the file — which is the defect, not a fix for it. It is now
{"schemaVersion": 1, "records": [...]}, and the rules a reader needs are
written where the schema is defined and pinned by the check that already guards
that schema.

What a reader of an existing ledger must now do differently

  1. Stop taking the last array element as the newest record. The newest
    record is the one with the greatest timestamp; ties break by array
    position, later wins. Append order is exactly what a PR queue scrambles —
    two campaigns can land in the opposite order to the order they ran.
  2. Stop reading a record as a statement about the repo now. Every value is
    frozen at run end, so commitsAheadOfTag keeps reading 0 however far the
    repo moves on — a stale record does not decay into looking stale. The ledger
    answers "what was audited, and when", never "is that still true"; that
    question is answered by comparing the newest record's commit against the
    repo today.
  3. Stop assuming the newest record's shape holds across the file. A record
    with no schemaVersion predates the wrapper: its must-haves may be absent
    and its summary is not the current shape.

The live case is rainlanguage/rain.sol.codegen. Its committed
audit/mutation-test-scans.json is a bare array holding one record, written at
skillVersion 0.30.0, whose summary shares only filed with the current
template — and which says commitsAheadOfTag: 0 while sitting 35 commits and 5
published tags behind the default branch. That record is the whole issue in one
file, and it is why the migration rule is not optional: without it, the next
campaign in that repo has no defined behaviour and the spec would be violated on
contact by its only real consumer. Its repo-side half is
rainlanguage/rain.sol.codegen#89.

The three calls #25 left open

1. Wrapper object, not a bare array. {"schemaVersion": 1, "records": [...]}.
The complaint is that the file cannot state its own read rule, and a bare array
structurally cannot hold one.

2. timestamp is authoritative for "newest", not append order. timestamp
is the only field measuring when the run actually happened. Tie-break stated:
equal timestamps resolve by array position, later wins — array position is the
only other total order the file carries, so the alternative is leaving ties
undefined, which is the same defect at smaller scale.

3. Migration is stated, in both documents. A campaign that opens a bare
array wraps it in place on that run's append: the array becomes records, the
top-level schemaVersion is added, and existing records are preserved exactly —
no field back-filled, no value corrected, nothing reordered. A record is a
historical fact about a tree, so back-filling schemaVersion or
testsAfterCommit into an old one would assert a measurement no run made.

The one place this goes past the issue's minimum, and why

schemaVersion is written at both levels: top-level for the envelope, and
per-record for that record's field set. #25 offered these as alternatives
("either a top-level object ... or a required per-record schemaVersion"), but
the migration rule makes them complementary rather than exclusive. Migration
preserves old records untouched, so the file provably holds records of more
than one shape; a single top-level number therefore cannot honestly describe
both the envelope and everything inside it.

Picking only one leaves a symptom #25 explicitly names unfixed — "two records in
one array can carry mutually incompatible summaries with ... no stated rule for
how a reader is meant to handle that". The rule needs somewhere to attach, and
the per-record field is what makes it checkable rather than a request to read
defensively. Absence is now the signal, and it is a signal precisely because
migration never back-fills it: no schemaVersion means the record predates the
wrapper. skillVersion cannot do this job — it names the skill that wrote the
record and has already failed to describe its shape.

Where the enforcement lives

#24 shipped .github/scripts/check-scan-record-schema.sh and the
schema hygiene workflow, so the apparatus for pinning this schema already
exists in this repo and no new one is invented here. Updating that script was
not optional: it reads the README's fenced template with
jq keys_unsorted / .commit, so wrapping the template in an object makes
scan-record-schema go red unless the check follows. Every assertion #24
made survives, retargeted at .records[0] rather than at the top level — none
were weakened or dropped — plus the envelope shape, schemaVersion as a
must-have, schemaVersion != skillVersion, and one pin per new rule in each
document.

QA

  • Discriminating tests: .github/scripts/check-scan-record-schema.sh, run
    by Scan record: name the tree the after-campaign counts hold at #24's existing schema hygiene workflow and extended here from a record
    check into a ledger check. Each fails on base: the committed script copied
    unchanged into a worktree of this PR's base (Scan record: name the tree the after-campaign counts hold at #24 head afe795d) exits 1 with
    FAIL README.md has no fenced json block under '## Scan ledger' — the base
    has no ledger section at all, only a record template, which is the defect.
    That first anchor is what the base trips on, so the substantive rejection is
    demonstrated by mutant M2 instead, which puts a bare array under the correct
    heading and gets
    FAIL the ledger template must be a wrapper object, not a bare array. Against
    this branch the script prints the real envelope keys (schemaVersion records),
    the real record key list, the real SHA values and nine OK lines, exit 0 — a
    run that never reached the README could not have produced either transcript.
  • Oracle: the committed ledger of the only real consumer, read from GitHub
    rather than from this repo's template —
    rain.sol.codegen@main:audit/mutation-test-scans.json is a bare array of one
    skillVersion 0.30.0 record whose summary shares only filed with the
    current template, and whose frozen commitsAheadOfTag: 0 sits 35 commits and
    5 published tags (sol-v0.1.7..sol-v0.1.11) behind that repo's default
    branch. That record decided the migration rule and the "newest is not current"
    rule; neither was derived from the template in isolation.
  • Category check: The scan ledger is a bare array with no stated ordering or authority rule, so a stale record is indistinguishable from a current one #25's proposed fix asks for three things — (a) the README
    shows the array rather than only one element, with the ordering invariant
    beside it and one field named authoritative for "newest"; (b) SKILL.md's "the
    newest entry" replaced by that same rule so writer and reader agree by
    construction; (c) the file versioned, with schemaVersion distinct from
    skillVersion. All three are here, plus the migration rule for the existing
    bare array, which the issue implies through its live-consumer evidence and
    which the ledger's only real consumer needs in order not to violate the spec
    on contact. commitsAheadOfTag reading 0 forever is answered as (b)'s
    "newest is not current" rule rather than by changing the field, since a
    historical record must not be rewritten.
  • Mutations applied — 23 killed, 0 survived: each a single break of exactly
    what this PR changes, applied to the committed docs and scored by the
    committed script, restored from git between runs (baseline committed first).
    Envelope: M1 wrapper removed so the template is the bare record again; M2
    template reverted to a bare array (what every committed ledger still is); M3
    top-level schemaVersion deleted; M4 records shown empty; M5 per-record
    schemaVersion deleted; M6 record schemaVersion aliased onto skillVersion
    — the exact collapse The scan ledger is a bare array with no stated ordering or authority rule, so a stale record is indistinguishable from a current one #25 warns against. README rules R1–R9, one per pinned
    rule (append-only; never rewritten/reordered/removed; greatest timestamp;
    "not the last array element"; the tie-break; "Newest is not current";
    schemaVersion distinct from skillVersion; wrap-in-place; back-fill
    nothing). R10 deletes the entire ordering + migration prose while every field
    name survives elsewhere. SKILL.md S1–S6 mirror the rules, and S7 deletes the
    whole ledger paragraph from the document a closing run actually reads while
    the README still states every rule — S1–S6 vs R1–R9 prove both documents are
    checked, since each mutant leaves the other document correct and is still
    caught. All exit 1, each naming which rule went missing.
  • Three controls (the check must not merely react to any diff): an unrelated
    value edit inside the template ("scope"), a pinned phrase reflowed across a
    line break (what deno fmt does), and an unrelated sentence reworded — all
    exit 0.
  • Full static bundle green locally, exactly as CI runs it:
    nix develop github:rainlanguage/rainix/53e96a7d#rust-shell -c pre-commit run --all-files
    — all 11 hooks pass (deadnix, denofmt, nil, nixfmt, no-consumer-prettier,
    prettier-rainix, rustfmt, shellcheck, statix, taplo, yamlfmt).
  • Third commit answers a real static / rs-static red on the first push:
    shellcheck SC2016, three times, because pinned phrases carrying markdown
    backticks ('greatest \timestamp`'`) read to shellcheck as an expression
    that will not expand in single quotes. Fixed at the source rather than
    suppressed — the two rules are now stated in prose that needs no backticks
    ("whose timestamp is greatest", "Neither of these is skillVersion") and the
    pins follow the prose. The mutation pass was re-run in full against the
    reworded docs, still 23/0, with R3/R7/S3 and control C2 re-aimed at the new
    phrasing.
  • deno fmt --check clean on both documents (it caught one reflow in SKILL.md,
    fixed in the second commit — the same failure that red-ed Scan record: name the tree the after-campaign counts hold at #24's first push).
  • Version bumped to 0.35.0 in all four places — SKILL.md frontmatter,
    plugin.json, marketplace.json, and the README template's skillVersion
    satisfying both version-hygiene rules (a change under skills/ bumps
    plugin.json, and plugin.json agrees with marketplace.json).

Scope

Specification only: two markdown documents, the check that guards them, and the
version bump. mutation-probe-rs does not write this ledger — the agent running
a campaign does — so there is no code change, and the flake's src fileset
excludes README.md and skills/, leaving nix build .#mutation-probe
untouched. Nothing here changes what #24 does; its prose is preserved verbatim,
moved under its own subheading so the new subsections do not strand it.

@thedavidmeister thedavidmeister self-assigned this Aug 17, 2026
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eefb47c5-bbbb-4452-8925-e152ab8582d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister thedavidmeister added the ai:needs-work Needs rework — the producer's inbox (vetter verdict or human ruling) label Aug 17, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
vet-protocol 4
lens source@027249c6c9d5dc679792326250b8b6a89a3a67dd + audit skill invoked at pr:26
Reviewed 027249c: needs-work — re #25 (Closes unlinked while stacked on #24) — two defects in the diff's own content: (1) README's "All five are must-haves" sentence contradicts the PR's own script, which enforces SIX record must-haves including per-record schemaVersion (check-scan-record-schema.sh:66) — a writer following the prose omits schemaVersion and their record then misreads as predating the wrapper; (2) the migration section instructs wrapping consumer ledgers into {"schemaVersion":1,"records":[...]}, but the org audit skill's mutation-ledger gate reads a top-level ARRAY and would refuse a wrapped ledger as unledgered — state the consumer-migration order (reader change lands first) in the migration rule, or sequence the wrap behind it. Also pin the pre-wrapper reading rule (README-only, unpinned) while in there.
cost 348 — ledger envelope spec + consumer sequencing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:needs-work Needs rework — the producer's inbox (vetter verdict or human ruling)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants