Skip to content

fix(scripts): read the declared type the same way in both check-doc-authoring type anchors - #13171

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-13105-satisfies-strict-options
Aug 29, 2026
Merged

fix(scripts): read the declared type the same way in both check-doc-authoring type anchors#13171
os-elon merged 2 commits into
mainfrom
claude/issue-13105-satisfies-strict-options

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#13105

inStrictOptions() read decl.type alone, so an options table written
... satisfies StrictObjectOptions climbed past the SatisfiesExpression to a
VariableDeclaration with no type and answered false. It now routes through
declaredTypeText() — the helper its sibling anchor collectTextSinkConsts()
already uses (PR #13104) — so "what type does this const declare" has exactly one
answer in this file. That agreement between the two anchors is the deliverable
the card names, and it is the property whose absence let one of them fall a
spelling behind the other, silently, with every gate green.

The card's mechanism holds. Its consequence does not — measured.

The card's walk is correct and was re-verified on today's main, not
transcribed: the climb from a guidance: property passes through a
SatisfiesExpression, which is neither a CallExpression nor a
VariableDeclaration, so the loop continues to the declaration, where p.type
is undefined and the predicate returns false.

The card's consequence — "a prescription written like this would not be
recognised as customer-facing prose" — is false on today's main, and this
PR does not pretend otherwise. collectTextSinkConsts() already registers such a
const by its declared type, and customerTextPosition() resolves the string at
via CONSTNAME without ever consulting the position test. Six shapes measured
against the detector as it stands, all already RED:

fixtureon mainwith this PR
annotated options table11
satisfies StrictObjectOptions, nested guidance11
as const satisfies StrictObjectOptions11
satisfies + guidance table hoisted into a const11
satisfies + retiredForms in a new Map const11
satisfies as argument 0 of strictObject(...)11
precision: a table satisfying some other type00

That zero-difference column is a reading rather than a dead instrument: the same
harness with collectTextSinkConsts() reverted to the annotation-only read drops
four of those six to 0, so it demonstrably can return a difference.

What the widening actually restores is the redundancy. With the sibling
anchor blinded, this change alone recovers exactly the hoisted shapes — a
guidance table lifted out of the options object and reached through the seed path
in collectTextSinkConsts() — and nothing else. So the satisfies spelling is
carried by one mechanism today, and the seed path would not have compensated if
that one were ever narrowed. After this PR it is carried by two.

No current verdict moves

Byte-identical main run before and after, measured by checking out the
pre-change file and running it:

✓ doc authoring guard: 392 files clean — no bare metadata literals.
✓ doc authoring guard: 48 published skill files clean — no internal issue-id references.
✓ doc authoring guard: 13352 customer-facing string(s) across 673 spec sources clean — no internal issue-id references (message 778 · strictObject 3325 · tombstone 751 · describe 8498).

The latency claim was re-measured on main at 901355c with its control:
satisfies StrictObjectOptions under packages/ is still zero, while the
same instrument returns packages/spec/src/ui/dashboard.zod.ts:189 for the
sibling type's as const satisfies readonly KeySetGuidance[]. The eleven text
matches for the bare token decompose to one comment, three type-only imports, two
annotated consts (both of which also match the name branch), one interface, one
interface property, two parameter annotations and one factory return type — no
options table in the satisfies spelling.

The self-test case is on the PREDICATE, deliberately

The gate's extend-the-detector rule requires a --self-test case for the new
spelling. A case written as a scan would be a phantom check here: it passes
with this change reverted, because the sibling anchor reports the same string. So
the reverse proof is asserted on inStrictOptions() itself — annotation,
satisfies, as const satisfies, a foreign satisfied type, and the
*_STRICT_OPTIONS NAME branch with no type written at all, each paired with the
matching declaredTypeText() read so the two anchors are shown to agree about
the same declaration. The battery also asserts its own case count, so a future
edit that empties it reds instead of congratulating itself.

The end-to-end fixture is still added (RED #13: an options table in the
satisfies spelling, const deliberately not named *_STRICT_OPTIONS, with a
bare internal id one object deep in guidance), and its comment states plainly
that it is a regression pin on the class rather than the reverse proof for this
widening.

Ablation, predicted before running and matched exactly. Reverting only the
one-line type read on top of the committed implementation reds 2 assertions
and no others:

✗ self-test "the position test reads `satisfies`": expected true, got false
✗ self-test "the position test reads `as const satisfies`, where the type sits OUTSIDE the `as`": expected true, got false

The annotation row, the foreign-type row, all four "both anchors agree" rows,
both NAME-branch rows, RED #13's three end-to-end rows and every pre-existing
case stayed green, and the main run's verdict line did not move. The mutation was
confirmed on disk in both directions by grep counts plus a git hash-object
comparison against the HEAD blob, restore likewise; no build or dist/ is
involved, since this gate is a script node runs directly.

Verification

Union re-run on the final commit, 9adb9bf045:

  • pnpm lint (whole repo, eslint . --no-inline-config) — clean, VERDICT command-exit 0
  • node scripts/check-doc-authoring.mjs --self-test — exit 0
  • node scripts/check-doc-authoring.mjs — exit 0, verdict quoted above
  • the 13 path-derived families from scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after syncing to origin/main because the first derivation printed STALE TREE and still exited 0 — all exit 0
  • the convention pair for editing a gate script: node scripts/pm/bare-root-worklist.mjs --self-test ("OK self-test: 50 live row(s) ... none stale, none missing, none contradicted") and pnpm check:pm-dispatch-gates ("✓ dispatch-gates self-test: 879 cases pass.")

node scripts/check-test-completeness.mjs answers a standalone run with its
usage line, not a finding — that is #13110, not a red here.

No changeset, derived rather than asserted: pnpm check:published-files
reports 69 publishable packages of 78 workspace members, every one declaring a
package-relative files whitelist; the repo-root scripts/ directory sits
outside all of them, and the root manifest is private with no files field. This
diff publishes nothing.

Two side-findings, reported and not touched here

Both are out of this card's defect class and are described in the dev report;
filing them as issues was blocked by an exhausted GitHub API quota in this
session, so they are handed to the PM seat.

  1. inStrictOptions() is blind to an options table produced by a FACTORY,
    and this one is LIVE rather than latent. navItemSurface in
    packages/spec/src/ui/app.zod.ts:203 is an arrow function whose return type
    is StrictObjectOptions; the climb hits the isArrowFunction guard and
    returns false before any type anchor is consulted, and the declaration itself
    carries no type, so collectTextSinkConsts() does not rescue it either.
    Measured on the shape: 0 violations and strictObject seen of 0 — the 21
    string literals in that factory, including the per-variant surface and the
    aliases prescriptions, are not merely unflagged but outside the population
    the per-bucket blindness floor is computed over. A const-typed control with
    the identical prose reds. This is a POSITION gap, not a type-spelling gap,
    and closing it would move live verdicts, which this card forbids.
  2. check-doc-authoring.mjs --self-test passes while registering zero cases.
    Success is failures.length === 0 with no assertion-count floor. Measured: an
    early return at the top of selfTestRule3() leaves the whole Rule 3 battery
    unrun and the script exits 0, printing its full success line claiming every
    one of those cases "holds". The success condition and the total-failure
    condition are the same output. The battery added in this PR asserts its own
    case count, but the file-wide floor is still missing.

Generated by Claude Code

…uthoring anchors
`inStrictOptions()` read `decl.type` alone, so a `StrictObjectOptions` options
table written `… satisfies StrictObjectOptions` climbed past the
`SatisfiesExpression` to a `VariableDeclaration` with no `type` and answered
false. Route it through `declaredTypeText()` — the helper its sibling anchor
`collectTextSinkConsts()` already uses — so "what type does this const declare"
has one answer in this file.
The verdict does not move, and that is measured rather than hoped: the main run
is byte-identical (392 / 48 / 13352 strings across 673 spec sources; message 778
· strictObject 3325 · tombstone 751 · describe 8498), and a satisfies-spelled
options table was already reported before this change, because
`collectTextSinkConsts()` registers the const by its declared type and
`customerTextPosition()` resolves the string at `via CONST` without consulting
the position test. What this restores is the redundancy: with the sibling anchor
reverted to the annotation-only read, this widening alone recovers the hoisted
shapes and nothing else.
Because the two anchors are indistinguishable end to end, the `--self-test` case
the extend-the-detector rule requires is asserted on the PREDICATE — annotation,
`satisfies`, `as const satisfies`, the `*_STRICT_OPTIONS` name branch and a
foreign satisfied type — plus a regression pin on the nested guidance prose of a
satisfies-spelled options table, labelled as a pin rather than as the reverse
proof it cannot be.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 29, 2026 — with Claude
@os-elon
os-elon marked this pull request as ready for review August 29, 2026 07:36
@os-elon
os-elon enabled auto-merge August 29, 2026 07:36
@os-elon
os-elon added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit 882e1ceAug 29, 2026
33 of 34 checks passed
@os-elon
os-elon deleted the claude/issue-13105-satisfies-strict-options branch August 29, 2026 09:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-elon@claude