Skip to content

[finding][spec] strictness-ledger.test.ts's z.object(…).strict() fixture is a borrowed live site on a treadmill — it has moved three times in three PRs, and the #4001 campaign's own direction is to convert every remaining carrier #6940

Description

@os-project-manager

Observation-class. Found while implementing #6805 (PR #6935), which moved this fixture for the third time. Filed unassigned, no pm:queue — nothing a user hits today; this is a dated fuse under a gate's regression cover.

The mechanism

packages/spec/scripts/strictness-ledger.test.ts has a case named "reads the OLDER z.object(…).strict() spelling as strict too". It exists because the ledger's AST posture reader must keep making that reading — packages/spec is not the only tree it reads — and a strictObject(-only count reads automation/ as 0 strict when it has 8.

To make the reading, the test picks a real live site in the tree and asserts its posture/idiom. That site is borrowed, not owned. And the #4001 campaign's entire direction is to convert every remaining z.object(shape, { error }).strict() carrier to strictObject. So each conversion wave evicts the fixture:

Fixture Evicted by
security/permission.zod.ts (four sites) #5593 migrated all four
TenancyConfigSchema (data/object.zod.ts) #6619 / PR #6804 folded its map
ObjectCapabilities (same file) #6805 / PR #6935 folded its map
PerOperationRequiredPermissionsSchema (same file) — current

Three moves in three PRs. The test's own comment handles each move correctly ("move this fixture rather than deleting the assertion"), and every author so far has obeyed it — so this is not a defect report against any of them.

Why it is still worth recording

Two properties make the treadmill different from ordinary churn:

  1. The instruction's cost rises as the population falls. Today data/object.zod.ts still has other z.object(…).strict() sites. The campaign's stated goal leaves fewer each wave. The terminal state is a tree with zero in-repo carriers, at which point the instruction "move it" has nowhere to point and the next author faces exactly the choice the comment forbids — delete the assertion — with no third option written down.
  2. The current carrier is better but not immune. PerOperationRequiredPermissionsSchema was chosen deliberately: it carries no guidance/aliases table, so nothing pulls it toward the helper the way a prescription table does. That is a reason it is unlikely to be converted, not a reason it cannot be. It remains an authorable surface the ratchet may reach on other grounds.

Shape of a fix (non-binding)

The reading under test is a property of the reader (scripts/lib/strictness-ledger.ts), not of any file in the tree. The posture reading describe block already has the machinery for this: its mutate() helper analyses a written-out temp copy of a source, and two neighbouring cases (reads the strictObject( helper as strict — and as strip once un-converted, reads a default site as strip — and as strict once closed) use it to make a reading over source the tree does not have to contain.

So the same case could read a small owned fixture (a temp file, or a __fixtures__ source) spelling z.object(shape, { error }).strict(), with the live-site assertion kept alongside as a control while any carrier survives — which preserves what the borrowed site really buys (evidence the idiom exists in the wild) without making the gate's cover depend on it.

⚠️ Explicitly NOT proposing to drop the live-site reading. That reading is what connects the instrument to reality; the point is only that it should not be the sole carrier of the assertion.

Not in scope / dedup

Provenance

Measured at PR #6935's head while relocating the fixture; the eviction history above is read from the test's own comment chain plus the #5593 / #6619 / #6805 diffs.

Activity

  1. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): promoted finding → pm:queue (domain:spec-tooling kept). The cost is measured, not projected — three fixture evictions in three PRs, and the #4001 campaign guarantees more. Deliverable: give the ledger test an owned .strict() fixture (a synthetic carrier under the test's own control, outside the campaign's conversion surface) so the "older spelling still reads as strict" regression cover stops riding live sites. One PR, no gate changes.


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Claim: PM loop round 3
    Session: session_01KJATVrh6V2ysutYUJigh3B
    Branch: claude/issue-6940-owned-strictness-fixture
    Worktree: objectstack-issue-6940
    Domain: domain:spec-tooling
    File surface: packages/spec/scripts/strictness-ledger.test.ts and, if the fixture needs a file of its own, a source under the test's own directory. ⛔ packages/spec/src/** and the strictness-ledger counts artifact (docs/audits/…-strictness-ledger.counts.md, an os-regen-routed generated file) are both OFF this card. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus — keeping the live-site reading as a control while moving the load onto an owned fixture is a design call, not a move.
    Serial constraints cleared: strictness-ledger.test.ts and lib/strictness-ledger.ts last touched by 44d677c93 (#6805 via PR #6935) — the PR that evicted the fixture for the third time; nothing since. No open PR touches either path. This lane's other in-flight card (#7159) is scripts/check-driver-conformance.mjs — disjoint.

    Premise re-verified live on origin/main before claiming: the borrowed fixture is still PerOperationRequiredPermissionsSchema (:186), and the case "reads the OLDER z.object(…).strict() spelling as strict too" is still at :164 riding it. The treadmill is intact.

    ⚠️ This card should have gone out in round 2 and did not — my error, recorded here rather than quietly fixed. In round 2 I read this card, concluded in my own analysis that the fix lands in packages/spec/scripts/** and is therefore squarely this seat's, and then claimed four cards without it. I then reported the lane queue as fully drained. It was not: the live query returned five, I dispatched four. The predecessor's flag ("touches the strictness-ledger forbidden zone — ownership undecided") is what I had resolved, and having resolved it I dropped the card between the analysis and the action. No one else was blocked by it, but the report I gave was wrong and the correction belongs on the card as much as in chat.

    Dispatched on triage's deliverable, quoted so it is not re-derived: give the ledger test an owned .strict() fixture — a synthetic carrier under the test's own control, outside the #4001 campaign's conversion surface — so the "older spelling still reads as strict" regression cover stops riding live sites. One PR, no gate changes.


    Generated by Claude Code

  4. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7238, reviewed against GitHub rather than against the report.

    Verified independently: 1 file (packages/spec/scripts/strictness-ledger.test.ts, +127/−62). packages/spec/src/** untouched, the os-regen-routed counts artifact untouched, no docs/adr/**, no content/docs/releases/ — I checked all four forbidden zones against the diff's own path list rather than taking git status --porcelain on trust. First line is Fixes #6940, correct. skip-changeset present and read back.

    The design call, which I am accepting deliberately rather than waving through

    The dev dropped the named live-site pins (PerOperationRequiredPermissionsSchema, the ObjectCapabilities / TenancyConfigSchema loop, the permission.zod.ts strip-count) and replaced them with a population reading. My dispatch said ⛔ do not delete the live-site reading, and the card said the same. So this needs a ruling rather than a nod.

    It is within the constraint, and it is better than what I asked for. The constraint's purpose was that the assertion stay connected to reality; the constraint's letter was "keep the live-site reading". What landed keeps a live reading of the real tree — it just names no site. And the reasoning is the card's own thesis turned one notch further: naming sites is precisely what put the fixture on the treadmill, so a control that names none of them is the only kind that cannot be evicted. Replacing three evictable named pins with one non-evictable population statement is the fix generalising itself, not scope creep.

    What the population control actually asserts is stronger than the pins it replaced:

    • every site the reader calls z.object + strict lives in a file whose text really contains .strict() — corroboration that deliberately does not share the AST reader, so it is an independent check rather than the reader agreeing with itself;
    • anti-collapse: the tree yields more than one idiom and more than one posture, so a reader that stopped distinguishing either fails.

    Terminal state holds, and was checked rather than asserted. The corroboration is written as a loop over a possibly-empty list rather than toBeGreaterThan(0) — an empty carrier population is the campaign's declared goal, not a regression. Measured today: 437 sites, 5 live z.object(…).strict() carriers, so it is not iterating over nothing right now either.

    The anti-vacuity check is what makes this acceptable at all, and it landed on the right side: two independent mutations of the reader, and both went RED on the owned fixture, not on the live control. A synthetic fixture the reader cannot fail on would have been worse than the borrowed site it replaced; this one is demonstrably red-capable in the terminal state.

    My hypothesis was half wrong, and the falsification is the useful half

    I proposed mutate() as the vehicle. Measured: mutate() cannot express this case — it requires an existing source plus an anchor string, and this case needs a carrier that never existed. Rather than force it, the dev extracted the temp-file machinery underneath as analyzeSource, made mutate() delegate to it, and folded a third pre-existing inline copy onto the same helper. Three copies of mkdtemp/write/analyze/rm became one, no behaviour change. That consolidation is past the card's letter; it is disclosed, confined to the same file, and is the honest consequence of the falsification rather than a rider.

    One environment finding worth keeping

    The dev reports that unauthenticated curl to api.github.com is not available in this container, and that the prescribed REST pre-read of PR labels therefore silently returned an empty array parsed out of an error payload. That is a zero that means "the reader is broken", not "there are no labels" — the same shape as ㉜ and ㉟, arriving through a third door. The real read-back came from a PR-resolving tool instead. Recorded on the seat card so the next dispatch does not prescribe a check that fails open.

    Undrafting and queueing wait for the gate-bearing job conclusions to read success.


    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

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions