Skip to content

fix(scripts): label an any index signature index signature, not return type - #9510

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-7653-index-signature-any-label
Sep 14, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-7653-index-signature-any-label

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #7653

Port of objectstack#14910 (landed there as objectstack#17545). The judgement was already settled upstream and recorded on this card at comment 5626147408; this PR implements it and does not re-derive it.

The defect

ts.isFunctionLike is true for every SignatureDeclaration kind, and an IndexSignatureDeclaration is one. So describeAnyPosition fell through to the return type arm and reported a bare [k: string]: any at a position it does not occupy. Flagging it is right -- an any index signature erases checking on every keyed access -- so the fix is a label, and the new arm has to sit before the function-like fallback.

The label is half of a finding's baseline row key, so a wrong one is a finding that cannot be declared the day such a fence is marked.

The one behavioural line

if (ts.isIndexSignatureDeclaration(parent) && parent.type === node) return 'index signature';

The boundary that is NOT narrowed

A return any on a function type standing inside a type argument stays flagged as return type. The boundary this guard draws is the DIRECT parent -- the predicate parent.type === node -- never ancestry:

shape parent.type === node verdict
Record<string, any> false not a finding
any[] false not a finding
Promise<any> false not a finding
Array<() => any> true finding, return type

In the last row the any's parent is the FunctionTypeNode whose return slot it fills, not the TypeReference above it. Reading "nested" as ancestry would hand an author going red the one-token evasion the rule already refuses for parameters: wrap the offending function type in a type argument and the gate goes green over an unchanged defect. That in/out decision is now stated in the file's SCOPE header, naming the predicate rather than a count.

My own re-measure (⛔ the card's 2026-09-03 numbers were NOT inherited)

Re-measured on this branch's base af4ff1d1e0. Population, in words: every candidate ts fence under skills/, marked or not (--measure mode); and separately the marked-only population the gate actually gates.

  • marked-only population: 14 selected ts fences, 0 bare-any findings, 0 stale baseline rows.
  • --measure population: 121 selected ts fences (out of 191 candidate fences of all kinds), 4 bare-any findings.

⚠️ The card recorded "4 findings, all correctly labelled". That second half was wrong. One of the four is a real shape-(1) site:

skills/objectui/guides/plugin-development.md:236   BEFORE: return type   AFTER: index signature

That fence opens at document line 236 and the offending line is [key: string]: any;. So this repo did have a live mislabelled finding, not a purely prophylactic port.

No baseline row changes, and the gate's verdict is unchanged (check:skill-examples exits 0 before and after). The site sits in an unmarked fence, so it is visible only under --measure and carries no KNOWN_BARE_ANY_EXAMPLES row; the marked population has zero bare-any findings. What the fix buys is that the row key is now correct on the day that fence is marked.

Every zero above was taken with a control in the same command: the same reader reports a non-empty selected corpus (14 / 121 fences), and the label probe returns two non-empty readings beside the empty one.

Both pin legs proven able to FAIL

Shape (2) was already flagged before this change, so a pin asserting only "it is a finding" passes on the broken code and pins nothing. Both legs assert the LABEL STRING. Two ablations, each mutation proven on disk by anchored counts before it was believed, each restore proven by a blob hash equal to the HEAD blob plus an empty git diff HEAD:

Ablation A — delete the new index-signature arm. Anchor count 1 to 0 on disk.

  • self-test exit=1: got ["2:return type","4:return type"], want ["2:index signature","4:return type"]
  • vitest exit=1: × labels an 'any' index signature 'index signature', not 'return type' -- 1 failed | 112 passed

Ablation B — narrow the function-like arm to an ancestry reading (the forbidden change). Marker count 0 to 1 on disk.

  • self-test exit=1: got ["2:index signature"], want ["2:index signature","4:return type"] -- the finding disappeared entirely
  • vitest exit=1: × keeps a return 'any' on a function type inside a type argument as 'return type' -- 1 failed | 112 passed

Ablation B is the one that matters: it demonstrates that the narrowing silently drops a real finding, and that the boundary pin now reds instead of going quiet.

⚠️ First attempt at ablation A was a no-op and was correctly declared VOID by its own guard: the anchor was matched with grep -cE and the unescaped parentheses in the predicate made it an ERE that matched nothing (1 -> 0 expected, 0 -> 0 observed). Re-run with grep -cF. Recording it because a mutation script that silently does nothing is exactly the failure this discipline exists to catch.

Verification

command exit
node scripts/check-skill-examples.mjs --self-test (60 cases) 0
node scripts/check-skill-examples.mjs (the real gate) 0
pnpm exec vitest run scripts/__tests__/check-skill-examples.test.ts (113 tests) 0
pnpm exec vitest run scripts/__tests__/ (158 files, 4639 tests) 0
pnpm run lint:root (0 errors, 32 pre-existing warnings) 0
pnpm run type-check:scripts 0
check:skill-examples check:skill-eval-tokens check:control-bytes check:comment-mask-corpus check:upstream-port-parity check:new-line-citations check:test-path-roots check:shell-escape-residue check:lint-rule-coverage all 0

The scoped build the self-test's type-check leg requires ran under the shared verify lock (turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2, 29/29 tasks successful). Before that build the self-test exited 2 = PRECONDITION NOT MET, which is the gate's documented "could not run" state and is read here as NOT MEASURED, never as a pass or a failure.

Controls on the greens, so none of them is vacuous:

  • type-check:scripts -- tsc --listFiles confirms scripts/__tests__/check-skill-examples.test.ts is 1 (inside the project), so the green is about my file.
  • vitest -- both new pins confirmed by name in --reporter=verbose output, against a control pin name known present.
  • lint:root is the scan that owns scripts/; it ran over its full population (17s), so no narrowing argument is needed. Type-aware linting is not enabled (no project key anywhere in eslint.config.js), which is also why this diff cannot move the verdict on any untouched file.
  • ⚠️ check:upstream-port-parity passes but does not track this file (0 occurrences of check-skill-examples in its output; it tracks 11 ported files, none of them this one). Its green therefore says nothing about this change, and is reported here as coverage-absent rather than as evidence.

gates_weakened: NONE. This change makes the gate strictly more precise, not more permissive: the same set of any positions is flagged, one of them under a correct label. Nothing that was flagged before is unflagged now -- ablation B exists precisely to keep it that way.

No changeset is owed: node scripts/check-changeset-presence.mjs exits 0 -- "2 file(s) changed, 0 of them published source of a package the release covers". node scripts/check-governed-queue-guard.mjs --test on both paths reports NOT GOVERNED.

Acceptance notes

Out of scope, noted and deliberately not filed:

  • scripts/check-skill-examples.mjs is an acknowledged port of objectstack's packages/spec/scripts/check-skill-examples.ts (its own SCOPE paragraph says "ported from that file rather than re-derived"), yet it is not in check:upstream-port-parity's tracked set, so upstream drift in it is not mechanically detected -- this port is the evidence. Carrier: the next agent porting a change into this file. Not filed because it is a coverage observation, not a reproducible defect, a declared-contract violation, or a metadata trap -- and adding a file to that ledger is a decision about the ledger's scope, not about this card.
  • The three other --measure findings (data-integration.md:331 parameter appConfig, plugin-development.md:175 and :438 property schema) are correctly labelled and sit in unmarked fences. They are pre-existing guide debt for the per-fence marking sweep, untouched here. Carrier: whoever marks those fences.

Generated by Claude Code

…return type`

`ts.isFunctionLike` is true for every SignatureDeclaration kind, and an
IndexSignatureDeclaration is one — so `describeAnyPosition` fell through to the
`return type` arm and reported a bare `[k: string]: any` at a position it does
not occupy. Flagging it is right (an `any` index signature erases checking on
every keyed access), so the fix is a LABEL, and the new arm has to sit BEFORE
the function-like fallback.

The label is half of a finding's baseline row key, so a wrong one is a finding
that cannot be declared the day such a fence is marked. This repo has a live
site: the `typescript` fence at `skills/objectui/guides/plugin-development.md`
line 236 carries `[key: string]: any` and was reported as `return type`.

A return `any` on a function type inside a type argument STAYS flagged as
`return type`. The boundary this guard draws is the DIRECT parent — the
predicate `parent.type === node` — never ancestry: the three shapes the header
calls "nested" all have it false, while `Array<() => any>` has it true because
the `any`'s parent is the FunctionTypeNode whose return slot it fills. Reading
"nested" as ancestry would hand an author the one-token evasion the rule
already refuses for parameters.

Both shapes were already flagged before this change, so a pin asserting only
"it is a finding" passes on the broken code. Both legs assert the LABEL STRING
instead, in the self-test and in the vitest suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VGeMu3p6qEFWR6K6GGLaW
@github-actions github-actions Bot added the tests label Sep 14, 2026
@os-try-charles
os-try-charles marked this pull request as ready for review September 14, 2026 13:39
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 2e1d0f0 Sep 14, 2026
35 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-7653-index-signature-any-label branch September 14, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants