Scan ledger: a wrapper object that states its own ordering and authority rule - #26
Scan ledger: a wrapper object that states its own ordering and authority rule#26thedavidmeister wants to merge 3 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
|
🤖 ai:vetter |
Closes #25
Stacked on #24 (base branch
2026-08-16-issue-23) — do not merge thisbefore #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 arewritten 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
record is the one with the greatest
timestamp; ties break by arrayposition, later wins. Append order is exactly what a PR queue scrambles —
two campaigns can land in the opposite order to the order they ran.
frozen at run end, so
commitsAheadOfTagkeeps reading0however far therepo 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
commitagainst therepo today.
with no
schemaVersionpredates the wrapper: its must-haves may be absentand its
summaryis not the current shape.The live case is
rainlanguage/rain.sol.codegen. Its committedaudit/mutation-test-scans.jsonis a bare array holding one record, written atskillVersion0.30.0, whose summary shares onlyfiledwith the currenttemplate — and which says
commitsAheadOfTag: 0while sitting 35 commits and 5published 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.
timestampis authoritative for "newest", not append order.timestampis 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, thetop-level
schemaVersionis 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
schemaVersionortestsAfterCommitinto an old one would assert a measurement no run made.The one place this goes past the issue's minimum, and why
schemaVersionis written at both levels: top-level for the envelope, andper-record for that record's field set. #25 offered these as alternatives
("either a top-level object ... or a required per-record
schemaVersion"), butthe 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
schemaVersionmeans the record predates thewrapper.
skillVersioncannot do this job — it names the skill that wrote therecord and has already failed to describe its shape.
Where the enforcement lives
#24 shipped
.github/scripts/check-scan-record-schema.shand theschema hygieneworkflow, so the apparatus for pinning this schema alreadyexists 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 makesscan-record-schemago red unless the check follows. Every assertion #24made survives, retargeted at
.records[0]rather than at the top level — nonewere weakened or dropped — plus the envelope shape,
schemaVersionas amust-have,
schemaVersion != skillVersion, and one pin per new rule in eachdocument.
QA
.github/scripts/check-scan-record-schema.sh, runby Scan record: name the tree the after-campaign counts hold at #24's existing
schema hygieneworkflow and extended here from a recordcheck 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 withFAIL README.md has no fenced json block under '## Scan ledger'— the basehas 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. Againstthis branch the script prints the real envelope keys (
schemaVersion records),the real record key list, the real SHA values and nine
OKlines, exit 0 — arun that never reached the README could not have produced either transcript.
rather than from this repo's template —
rain.sol.codegen@main:audit/mutation-test-scans.jsonis a bare array of oneskillVersion0.30.0record whosesummaryshares onlyfiledwith thecurrent template, and whose frozen
commitsAheadOfTag: 0sits 35 commits and5 published tags (
sol-v0.1.7..sol-v0.1.11) behind that repo's defaultbranch. That record decided the migration rule and the "newest is not current"
rule; neither was derived from the template in isolation.
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
schemaVersiondistinct fromskillVersion. All three are here, plus the migration rule for the existingbare 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.
commitsAheadOfTagreading0forever is answered as (b)'s"newest is not current" rule rather than by changing the field, since a
historical record must not be rewritten.
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
schemaVersiondeleted; M4recordsshown empty; M5 per-recordschemaVersiondeleted; M6 recordschemaVersionaliased ontoskillVersion— 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; greatesttimestamp;"not the last array element"; the tie-break; "Newest is not current";
schemaVersiondistinct fromskillVersion; wrap-in-place; back-fillnothing). 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.
value edit inside the template (
"scope"), a pinned phrase reflowed across aline break (what
deno fmtdoes), and an unrelated sentence reworded — allexit 0.
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).
static / rs-staticred on the first push:shellcheck SC2016, three times, because pinned phrases carrying markdown
backticks (
'greatest \timestamp`'`) read to shellcheck as an expressionthat 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 --checkclean 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).
0.35.0in all four places — SKILL.md frontmatter,plugin.json,marketplace.json, and the README template'sskillVersion—satisfying both
version-hygienerules (a change underskills/bumpsplugin.json, andplugin.jsonagrees withmarketplace.json).Scope
Specification only: two markdown documents, the check that guards them, and the
version bump.
mutation-probe-rsdoes not write this ledger — the agent runninga campaign does — so there is no code change, and the flake's
srcfilesetexcludes
README.mdandskills/, leavingnix build .#mutation-probeuntouched. Nothing here changes what #24 does; its prose is preserved verbatim,
moved under its own subheading so the new subsections do not strand it.