Skip to content

claims.yaml has a duplicate reason: key: a +622-line waiver is recorded with a +10-line justification, and claim_check passes #1087

Description

@avrabe

The ledger stopped recording why the North Star's headline metric moved, and the gate stayed green

claims.yaml on main carries a duplicate reason: key inside a single waiver mapping of the selector_lines_code ratchet:

waivers:
  - to: 18910
    reason: >-
      RQ-60-VFPPRESSURE increment 1 (#1069, #869): +622 lines — the AEABI
      builtin route ... Growth, not substitution, by design ...
    reason: >-                                   # <-- SECOND `reason:`, same mapping
      RQ-59-I64SHIFT (#1048): +10 lines in select_default ...

YAML keeps the last value on a duplicate key, silently. So the surviving justification for a +622 line growth of the instruction selector is RQ-59-I64SHIFT's reason for a +10 line growth — a different lane, a different release, a different change.

Measured from the ledger's own history (git log -L on the value: line):

16b135ba  RQ-59-SUBTRACT          18278
2785ffe9  RQ-59-I64SHIFT          18288   +10   <- this reason
e6a3b27a  RQ-60-VFPPRESSURE inc1  18910  +622   <- this growth
0ec9dc7a  RQ-60-VFPPRESSURE inc2  19199  +289

The lane did its job — its commit message and the discarded reason both state the +622 honestly. The file edit turned to: 18288 into to: 18910 and appended a second reason: instead of adding a second waiver entry, and nothing in the toolchain could see it.

Why every gate passed

scripts/claim_check.py loads the ledger with yaml.safe_load (permissive), and check_ratchet asks only:

  1. does a waiver exist with to == value? — yes
  2. is its reason non-empty? — yes, it is a perfectly good string

Neither question can distinguish "the right reason" from "someone else's reason". And the waiver's whole purpose is stated in the ratchet documentation: "the waiver is bound to that value, so a second regression needs a second waiver — permission is per-growth, never standing." A duplicate key converts a per-growth permission into a standing one and deletes the record of the growth it was granted for, leaving the gate green.

Red-first, against the real artifact rather than a fixture — the same claims.yaml from main, under the shipped checker and under a duplicate-key-strict one:

$ python3 <shipped>/claim_check.py claims_from_main.yaml
51/51 claims hold.                                                    EXIT=0

$ python3 <fixed>/claim_check.py claims_from_main.yaml
claim_check: duplicate key 'reason': declared at line 1311 and AGAIN at
line 1330 — YAML keeps the LAST one silently, so the first value is
discarded with no diagnostic. Split the mapping.                      EXIT=1

The class, and why it landed here specifically

This is #1059's duplicate-key defect, in a different file. scripts/status_evidence_check.py already parses duplicate-key-strict — but only artifacts/release-*.yaml. The strict loader was never pointed at claims.yaml, which is the one file every other claim in the repo is checked against.

Swept for completeness: claims.yaml has exactly 1 duplicate key (this one); artifacts/**/*.yaml and rivet.yaml have 0 — the existing strict loader is doing its job on the surface it covers.

Fix

  1. Split the mapping — restore RQ-60-VFPPRESSURE increment 1's reason on to: 18910 and give RQ-59-I64SHIFT's back its own to: 18288 entry. The record is the deliverable; the numbers were always right.
  2. Parse claims.yaml duplicate-key-strict in claim_check.py (both load sites), with a diagnostic that names the discarded value's line — because the failure mode is silence, not error.
  3. Pin it in scripts/test_claim_check.py: the exact shipped shape refused, the fixed two-waiver split accepted, and the repo's own ledger asserted clean (non-vacuity against the real artifact, not only fixtures). Mutation-verified — neutering the check fails 2 tests and lets the real defective ledger pass again.

Found while auditing v0.60's ratchet numbers for the release notes: selector_lines_code reads +1,302 against a baseline it must fall below, with sel_dsl_rules flat at 80 — worth stating plainly in the notes either way, and the reason each step was permitted is exactly what the ledger is for.

Activity

  1. added a commit that references this issue on Aug 27, 2026
  2. avrabe commented on Aug 27, 2026

    @avrabe
    ContributorAuthor

    Fixed on main as #1088 (67a4e0ef). Staying open until v0.60 is tagged, per the close-on-the-release rule.

    Verified live against main's own checker:

    $ python3 <main>/scripts/claim_check.py <the pre-fix claims.yaml from 64aa5bd2>
    claim_check: duplicate key 'reason': declared at line 1311 and AGAIN at line 1330
      — YAML keeps the LAST one silently, so the first value is discarded with no
      diagnostic. Split the mapping (#1087).                                  EXIT=1
    
    $ strict duplicate-key load of main's current claims.yaml
      strict-clean
    

    So the exact ledger state that shipped this defect is now refused by the gate that let it through, and the record it destroyed — RQ-60-VFPPRESSURE increment 1's justification for a +622-line growth — is restored on the waiver it belongs to.

  3. reopened this on Aug 27, 2026
  4. avrabe commented on Aug 27, 2026

    @avrabe
    ContributorAuthor

    Reopened — closed by accident, by me.

    PR #1088's body opened with Closes #1087, so merging auto-closed this. The repo's rule is to close on the tagged release, not the merge: "merged ≠ released — close on the tagged-release ref, never a merge commit alone; the tag is the evidence an outsider can check." I wrote the closing keyword anyway, one message after saying this issue should stay open until v0.60 is tagged.

    Nothing about the fix changes — it is on main as 67a4e0ef and verified live. This issue closes when v0.60 is tagged, citing the tag.

    Noting the mechanism rather than just the slip: Closes #N in a PR body is evaluated by GitHub at merge time, which is structurally the wrong moment for a repo whose closure rule is release-based. Refs #N is the correct keyword here in every case, and the same trap false-completes umbrella issues when a PR fixes one item of several.

  5. avrabe commented on Aug 27, 2026

    @avrabe
    ContributorAuthor

    Closed by v0.60.0 (tag 79e2daa5), fixed as PR #1088.

    Verified live against main's own checker, not the PR's:

    $ <main>/scripts/claim_check.py <the pre-fix claims.yaml from 64aa5bd2>
    claim_check: duplicate key 'reason': declared at line 1311 and AGAIN at line 1330
      — YAML keeps the LAST one silently, so the first value is discarded with no
      diagnostic. Split the mapping (#1087).                                EXIT=1
    
    $ strict duplicate-key load of main's current claims.yaml
      strict-clean
    

    The ledger state that shipped this defect is now refused by the gate that let it through, and the record it destroyed — RQ-60-VFPPRESSURE increment 1's justification for a +622-line growth of the instruction selector — sits on the waiver it belongs to.

    One number to read carefully in the v0.60 notes: selector_lines_code's waiver count moved 9 -> 10 with the value unchanged at 19,199. That is a destroyed record being restored, not a further growth being permitted. The two are opposite events and look identical in a summary table.

    The class is #1059's duplicate key in a different file: the strict loader already existed in status_evidence_check.py and had never been pointed at the ledger every other claim is checked against. Swept for completeness — claims.yaml had exactly one, artifacts/**/*.yaml and rivet.yaml have zero.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions