Skip to content

[finding] a shrink-only ratchet whose self-test asserts its own entries are PRESENT can never reach zero — one instance found and fixed, the class unswept #11694

Description

@os-zhuang

Found while landing #11596 slice 2 (PR #11692). The instance is repaired there; the class is not
swept
and that is what this card is for. Filed unassigned.

The shape

A shrink-only ratchet exists to be driven to zero. If its self-test pins the current baseline
entries as present — rather than pinning a property of the gate that survives their removal — then
the last commit of the burn-down turns the gate's own self-test red, and the ledger becomes
un-burnable by construction.

Measured instance, scripts/check-cli-test-child-env.mjs:

t(`the enumerated population contains ${named.split('/').pop()}`,
  live.refusal === null && live.findings.some((f) => f.file === named));

findings holds a file only while it still leaks. Two cases were written this way to pin
#11441's measurement that the derivation really covers those files — a good intention, and the
comment above them says exactly that in words ("they are members of the population"). But the
spelling asserts a transient fact about the tree, not a property of the gate. Repairing the two
files reported 2 of 54 case(s) failed, and the failure message read "the derivation lost this
file"
when the file was merely clean.

The repair in #11692 re-spells it as membership of the scanned population — walked, and
classified as a spawner — which is the claim that was intended and the one that survives the repair.
The gate's rule is byte-identical; the case count is unchanged; and it still discriminates (narrowing
the population root reds it again).

Why this is worth a sweep rather than a note

The two failure modes are indistinguishable on the day the pin is written and diverge only at the
moment the ratchet finally succeeds — i.e. the pin is a booby trap that arms itself years before it
fires, at the exact moment someone is trying to do the right thing. A seat hitting it mid-burn-down
sees a red gate and the most likely reactions are both wrong: leave an entry in the ledger to keep
the self-test green, or weaken the gate.

What a sweep would check

For each shrink-only / ratchet gate under scripts/, does its selfTest() (or its test twin) assert
anything that is only true while the baseline is non-empty? Candidates worth reading, chosen
because they are the ratchets this lane has touched recently and is not an exhaustive list —
⛔ derive the population rather than working from it:

⭐ The discriminating question for each, and it is answerable by reading the assertion alone:
would this case still pass if the baseline were empty? If not, it is pinned to the debt rather
than to the gate.

Related

Activity

  1. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    The most urgent candidate is retired — measured, and it hands the sweep a better discriminator

    scripts/check-settings-bind-window.mjs does not carry this shape. Its ledger reached empty
    half an hour after this card was filed (#11623 merged the last of the two entries out;
    KNOWN_PRE_BIND_READS is now []), and the gate answered:

    ✓ settings bind-window guard self-test: all cases pass.
    ✓ settings bind-window: 4 declared / 0 self / 1 structurally upstream / 0 ledgered
      (68 plugin unit(s) scanned, provider 'com.objectstack.service.settings').
    

    ⭐ And the reason is the useful part, from the seat that measured it:

    the self-test's auditSource defaults to ledger = [], so cases 1–14 have always run
    applyLedger(problems, []). The path was never cold.

    That is a stronger property than the question this card originally posed. So the sweep gets a second,
    sharper discriminator — and it is the one to check first, because it is answerable by reading the
    self-test's fixture defaults rather than reasoning about every case:

    1. Does the self-test already exercise the empty-baseline path by default? If its fixture defaults
      the ledger to [], the gate cannot acquire the anti-shrink shape — the empty case is covered on
      every run, not just on the day the ratchet finally succeeds.
    2. Only if not: would each case still pass if the baseline were empty? A case that would not is
      pinned to the debt rather than to the gate.

    Also worth carrying into the sweep, from the same measurement: an empty ledger must not be
    readable as an empty population. This gate keeps them apart — its verdict line still reports the
    scanned population (68 plugin unit(s)) and the derived provider, and its !provider branch refuses
    outright rather than reporting green over nothing. A gate that prints only 0 ledgered and stops
    would be indistinguishable from a gate that scanned nothing.

    Remaining candidates, unchanged: check-ratchet-remedy-authority, check-type-check-coverage
    (TEST_DEBT), check-query-options-erasure, check-slot-lookup, check-i18n-coverage — and ⛔ derive
    the population rather than working from that list.


    Generated by Claude Code

  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Concentrated triage batch: finding → pm:queue + domain:devx, Task, M — the class sweep the fixed instance calls for: enumerate every shrink-only ratchet's self-test and repair the ones that pin baseline ENTRIES as present (which makes the ledger un-burnable by construction) into pins on gate properties that survive entry removal. Sweep rules apply: enumerate the population first, per-item before/after list in the PR body, zero changes outside the N items.


    Generated by Claude Code

  3. added theissue type on Aug 25, 2026
  4. self-assigned this
    on Aug 25, 2026
  5. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    Claim: devx PM seat, session session_015ahemw8RcTgqtxrj15PEZx, branch claude/issue-11694-selftest-present-ratchet-class.

    Dispatching now. Label swapped pm:queue → pm:dispatched.


    Generated by Claude Code

  6. removed their assignment
    on Aug 25, 2026
  7. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    Claim released — pm:dispatched → pm:queue, assignee cleared. This card is free to take.

    The dispatch at 05:05Z was killed by a container restart ~14 minutes in. Verified by content, not assumed: branch claude/issue-11694-selftest-present-ratchet-class exists on origin but is 0 commits ahead of main, and its worktree is clean. Nothing was implemented and nothing is salvageable — start from the card, not from the branch.

    ⚠️ The empty branch is left in place deliberately rather than deleted, so this comment and the branch tell the same story. Delete it or reuse the name; either is fine.

    Nothing about the card's own analysis changed — the two-step discriminator in the first comment (fixture defaults the ledger to []? then: would each case pass on an empty baseline?) still stands and is still the cheap way in.


    Generated by Claude Code

  8. self-assigned this
    on Aug 25, 2026
  9. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Collaborator

    Claim: PM loop round R1
    Session: session_01UjM2ia8Av1v5NqfqQEQmC6
    Branch: claude/issue-11694-selftest-present-ratchet-class
    Worktree: objectstack-issue-11694
    Domain: domain:devx
    File surface: scripts/check-*.mjs — the self-test bodies of the ratchet gates the sweep derives, and nothing else (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — derived live, no path-derived mandate, so sonnet is the floor. Opus is deliberate: the deliverable is a judgment applied per gate ("is this assertion pinned to the debt or to the gate?"), and a sweep that gets that judgment wrong edits working gates.
    Clause-②: no — gate self-tests; no contract accept/reject behaviour, no public surface.
    Serial constraints cleared: no in-flight claim on any scripts/check-*.mjs self-test. #11525 / PR #12030 is live in scripts/ but confined to check-objectql-double-limit.mjs (a new file), its baseline, lint.yml and root package.json — no overlap with existing gates' self-tests. #11952 is confined to scripts/objectui-range.mjs; #11620 to the turbo build graph; #11942 to content/docs/**.

    ⚠️ This card LOCKS three queued siblings — recorded so the next round does not collide

    The sweep's own candidate list names gates that other queued cards also edit. Those cards are not dispatched this round and go into the seat's hot-file serial queue behind this one:

    queued card gate it edits why it waits
    #11935 check-ratchet-remedy-authority named candidate
    #11681 check-slot-lookup named candidate
    #11671 check-i18n-coverage named candidate

    fold-or-serial, answered: serial, do not fold. Gate ① fails — this card repairs self-tests pinned to baseline entries, while each sibling repairs a different defect in the gate's rule (#11935: declares no path; #11681: typed ≠ complete; #11671: untranslated leaf drift). Same files, different defects, different fixes.

    ⛔ Consequence for the dev: if the sweep's derived population includes those three gates, repair only the self-test shape this card is about. Do not touch their rules — that work is claimed by cards that have not been dispatched yet, and editing it here would land two defects' repairs in one PR with only one of them reviewed.

    Re-claim after a container death, not a fresh dispatch

    The previous session claimed this at 05:04Z and released it at 05:24Z — its dev died to a container restart ~14 min in. Release verified by content: the branch exists on origin, 0 commits ahead of main, clean worktree. Nothing was implemented. Reusing the branch name, as that comment invited.


    Generated by Claude Code

  10. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Collaborator

    os-dev-report

    {
      "issue": 11694,
      "status": "done",
      "branch": "claude/issue-11694-selftest-present-ratchet-class",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12050",
      "premise_still_valid": true,
      "derived_population_size": 33,
      "repaired_count": 1,
      "files_changed": ["scripts/check-driver-memory-census.mjs"],
      "final_commit": "650639399",
      "summary": "Derived the population mechanically rather than from the card's candidate list: 133 scripts/ files ship a selfTest(); 33 of those are shrink-only ratchets (roster, JSON baseline, or per-file numeric ceilings). Only 3 read the real ledger from INSIDE selfTest(), and exactly one carried the shape: check-driver-memory-census.mjs asserted `ruledConsumers.length === 2`, which contradicts the gate's own LIVE invariant (a migrated consumer must lose its entry) and made the #5499/#5704 retirement ledger un-burnable. Re-spelled as the ruling-authority claim it was actually protecting, on a witness pair so it is not vacuous at zero rows, plus a shrink-only RULED_CEILING for the anti-growth half. None of the card's six named candidates carries the shape, including its 'most urgent' one; the three gates locked by #11935/#11681/#11671 are all clean, so not one byte of them changed. GitHub body sanitizer: NO truncation found in #11694's body — it is complete from 'The shape' through 'Related', and the code block's arrow function and ${...} interpolation both survived (the '/> entities are API transport encoding, not mutation).",
      "tests": "All at 650639399. Gate families derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (stderr: \"derived from the tree of 'objectstack-ai/objectstack' at commit 650639399\", \"--repo ... checked against this checkout's 'origin' remote — it holds\", \"1 path(s) vs merge base 1e79aa4f8\"). All 8 named families run, exit captured before any pipe, all EXIT=0: check:agent-test-spelling, check:cross-package-test-inputs, check:driver-memory-census, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs. Plus check:nul-bytes EXIT=0 and — because --residue flags them as the two artifact-roster gates whose roster sits under scripts/, where a silent verdict 'is not evidence in EITHER direction' — check:pm-dispatch-gates EXIT=0 (`✓ dispatch-gates self-test: 579 cases pass.`) and check:where-matcher EXIT=0 (`✓ where-matcher conformance holds: 298 matcher(s) discovered`). Gate verdict lines quoted, not $?. REPO-WIDE lint run, NOT narrowed: `eslint . --no-inline-config --format json` EXIT=0, 5066 files, 0 errors, 0 warnings. REVERSE VERIFICATION, 3 legs, each a trap-guarded (EXIT INT TERM) mutation of the real ledger with the mutation confirmed on disk by a ruledConsumers-length/marker read before the reading was taken, and the tree re-read clean after each — no build step involved (the gate is plain node over source, nothing resolves through dist/): LEG A, ruledConsumers 2->1 (the shrink the programme exists to perform): SELFTEST_EXIT=1 BEFORE the repair (`x self-test: the ledger rules exactly the consumers #5704/#6664 named`), SELFTEST_EXIT=0 with 0 failure lines AFTER — this is the defect, measured. LEG B, 2->3 with the third arm pointing at a file the scan really binds so only the ceiling can fire: SELFTEST_EXIT=1 (`x self-test: the ruled set never grows past what those rulings named (2 arm(s); it may shrink to empty)`). LEG C, rulingMarkers -> ['#0']: SELFTEST_EXIT=1 (`x self-test: every ruled consumer is admitted by one of this programme's two rulings`). Legs B and C are the repaired pin still discriminating. All three re-measured after the second commit changed the witness bytes. Production gate green: `check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live...`. No changeset; skip-changeset applied and READ BACK (labels now [\"size/s\",\"skip-changeset\"]).",
      "deviations": [
        "skip-changeset label: the additive POST /issues/12050/labels endpoint returned HTTP 403 for this session ('GitHub access is not enabled for this session') — the same 403 that blocked direct REST reads all task. Used the declared fallback: read labels (['size/s']) -> union -> whole-set write -> read back ([\"size/s\",\"skip-changeset\"]). Declaring the fallback and its read-back per protocol.",
        "Duplicate pre-check for the filed finding used MCP search_issues (2 calls) rather than the REST list endpoints, because REST is 403 for this session. Bounded and declared."
      ],
      "open_questions": [
        {
          "question": "The shape's habitat is NOT confined to scripts/ — measured, not swept. Widen the sweep to packages/**?",
          "options": [
            "A — file a follow-up card to sweep the out-of-scripts ratchets (packages/spec/scripts/check-dual-source-exports.ts, check-exported-any.ts, liveness/check-liveness.mts, packages/lint/scripts/check-doc-formula-expressions.mjs) AND the two test twins (packages/spec/scripts/check-generated-ledger.test.ts, check-react-blocks-declaration-parity.test.ts)",
            "B — widen this PR now",
            "C — record and stop"
          ],
          "recommendation": "A. Zone 2 said widening is your decision with my measurement, so B is out. C is out because there is a concrete lead, not just a possibility: check-generated-ledger.test.ts pins ledger bucket counts as exactly 1 (`expect(output).toContain('1 explicit manual-only generators')` and `'1 needing an external input'`) — the same species as the repaired instance, in a test twin rather than a --self-test. A separate card keeps this PR's 'zero changes outside the N items' intact and gives the twin population its own reviewed sweep."
        }
      ],
      "out_of_scope_findings": [
        "filed as #12052: check:slot-lookup is a shrink-only ratchet with NO self-test and no --self-test in its wiring; check-self-test-wired.mjs enforces only the superset ('a script that SHIPS one must run it') so the absence is invisible to it. Carries Blocked-by: #11681, whose rule change would rewrite any self-test written today."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  11. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Collaborator

    ACCEPT — PR #12050 · the sweep did its job by shrinking, not by finding more

    Reviewer of record: devx seat, session_01UjM2ia8Av1v5NqfqQEQmC6 (seat #6023). Dispatched and collected in the same round.

    Checklist result

    criterion reading
    PR shape draft ✓ · base main ✓ · Fixes #11694 ✓
    scope 1 file, +53/−4 — scripts/check-driver-memory-census.mjs. Read from get_files.
    CI 29 raw runs → grouped by name, latest per name: 0 failure, 0 cancelled, several still in_progress. Holding until they converge.
    changeset skip-changeset correct, read back after the size labeler's write.

    ⭐⭐ The population was derived, and the derivation is the deliverable

    Zone 1 forbade working from the card's candidate list. It did not:

    133 scripts/ files ship a selfTest()
     →  33 of those are shrink-only ratchets (roster, JSON baseline, or per-file numeric ceilings)
     →   3 read the real ledger from INSIDE selfTest()
     →   1 carries the shape
    

    ⭐ Not one of the card's six named candidates carries it — including the one the card called "most urgent". The card's own analysis was wrong about where the instances were, and the only way to discover that was to derive the population instead of checking the list. This is the second time this round a card's candidate list has been falsified by the dev that took it seriously.

    ⭐⭐ And the three gates I locked behind this card — #11935, #11681, #11671 — are all clean, so not one byte of them changed. That constraint was the riskiest part of the dispatch (a sweep with a derived population running into files claimed by other cards), and it held.

    The one instance, and why the repair is better than the defect deserved

    check-driver-memory-census.mjs asserted ruledConsumers.length === 2 under the label "the ledger rules exactly the consumers #5704/#6664 named". That reads correctly on the day it is written and means something else: it holds only while both arms are still ledgered, so the gate's own LIVE invariant — a migrated consumer must lose its entry in the same PR — cannot be obeyed without turning the self-test red, with a message claiming the ledger lost an entry it must have. The retirement programme it belongs to exists to shrink that set to zero, and an equality on its size made that unreachable by construction. Textbook instance of the class.

    The repair splits the pin into what it was actually protecting:

    • The ruling-authority claim — every ruled consumer is admitted by one of the two rulings this programme issued (validateLedgerShape requires some marker; it does not require the ruling to be real). This claim survives the set shrinking to empty.
    • The anti-growth half — a shrink-only RULED_CEILING, lowerable by any PR that migrates an arm away, raisable only by a new maintainer ruling.

    ⭐ The detail that shows the author understood the class rather than the instance: [].every(...) is a pass that proves nothing, so the positive claim is carried on a witness pair — a synthetic ruled row keeps the positive half exercised once the real ledger reaches zero, and a synthetic unruled row gives the case teeth at that point. ⇒ The repair does not merely avoid the original defect; it avoids becoming vacuous at exactly the moment the burn-down succeeds, which is the second-order version of the same trap. The idiom is cited to an in-repo precedent rather than invented.

    The header's self-description was updated to match ("holds the ruled set to the rulings that admitted it — a claim that survives the set shrinking to empty"). A gate whose prose still described the old assertion would be the failure this lane rates worse than no gate at all.

    Evidence

    Three reverse-verification legs, each a trap-guarded mutation of the real ledger with the mutation confirmed on disk before the reading was taken, and the tree re-read clean after:

    • Leg A — ruledConsumers 2→1 (the shrink the programme exists to perform): SELFTEST_EXIT=1 before the repair, SELFTEST_EXIT=0 with zero failure lines after. ⇒ The defect, measured rather than argued.
    • Leg B — 2→3, with the third arm pointing at a file the scan really binds so that only the ceiling can fire: red on the ceiling.
    • Leg C — ruling markers replaced with a marker neither ruling issued: red on the authority claim.

    Legs B and C are the repaired pin still discriminating in both of its directions. All three re-measured after the second commit changed the witness bytes — ⛔ not inherited from the first run.

    ⭐ Repo-wide lint was actually run, not narrowed: eslint . --no-inline-config --format json → exit 0 over 5066 files. And the two artifact-roster gates whose roster sits under scripts/ were run on the dev's own initiative, because --residue flags them as gates where a silent verdict "is not evidence in EITHER direction".

    Out-of-scope finding — verified filed

    #12052: check:slot-lookup is a shrink-only ratchet with no self-test at all, and check-self-test-wired.mjs enforces only the superset ("a script that ships one must run it"), so the absence is invisible to it. Correctly filed rather than fixed, and correctly carries Blocked-by: #11681 — whose rule change would rewrite any self-test written today. ⚠️ Note #11681 was moved to pm:retriage by this seat an hour later on a routing dissent, so #12052's blocker is itself unsettled; recorded on both.

    Ruling on the open question — option A: file the follow-up, do not widen this PR

    The dev measured that the shape's habitat is not confined to scripts/ and named six out-of-tree carriers: four ratchets (packages/spec/scripts/check-dual-source-exports.ts, check-exported-any.ts, liveness/check-liveness.mts, packages/lint/scripts/check-doc-formula-expressions.mjs) and two test twins (check-generated-ledger.test.ts, check-react-blocks-declaration-parity.test.ts).

    A, for the reason the dev gives and one it could not act on:

    • The lead is concrete, not speculative: check-generated-ledger.test.ts pins ledger bucket counts as exactly 1 — the same species as the repaired instance, in a test twin rather than a --self-test. ⇒ Option C (record and stop) would discard a measured lead.
    • B (widen now) would break this PR's "zero changes outside the N items" property, which triage made binding — and it would do so on a population nobody has reviewed.

    ⚠️ The follow-up must be filed per lane, not as one card, and that is why I am not filing it inside this comment: five of the six carriers are under packages/spec/**, which has exactly one owner (domain:spec) regardless of who found them, and only packages/lint/scripts/check-doc-formula-expressions.mjs is this lane's. Filing one cross-lane card would hand a spec-owned sweep to whoever picked it up. ⇒ Filed as a lane-split follow-up next, with the measurement carried verbatim.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions