Skip to content

fix(spec): label an any index signature as its own position, and state the direct-parent boundary - #17545

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-14910-any-position-labels
Sep 10, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-14910-any-position-labels

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14910

Clause-②: no

A note on spelling in this body. GitHub's body sanitizer deletes tag-shaped angle
fragments — inside fenced code and inline code too — so every generic below is written with
< / > entities in prose, and code blocks avoid angle brackets by spelling the shape
in words. The source file itself carries the ordinary spellings.

Re-derived from what this PR ships, not inherited from the claim. The diff is one file,
packages/spec/scripts/check-skill-examples.ts — CI tooling. Measured on this branch: the file
carries 0 ^export lines, so no new exported symbol; and no published payload gains a key
— grepping @objectstack/spec's files[] roster (dist, json-schema, liveness, prompts,
llms.txt, README.md, api-surface, spec-changes.json) for a symbol from the changed file
returns 0 files, with the positive control (HookContext) lighting 34. The baseline this
card worried about turns out not to exist as an artifact at all (see "Premise re-measured"), so
there is no governed accept set that moves either.

What was wrong

describeAnyPosition keys a bare any's position on its DIRECT parent node kind. The last arm
was ts.isFunctionLike(parent) && parent.type === node returning 'return type', and
ts.isFunctionLike is true for every SignatureDeclaration kind — IndexSignatureDeclaration
included. Measured on this tree's TypeScript (6.0.3):

probe parent kind isFunctionLike parent.type === node today's label
interface Bag { [k: string]: any } IndexSignature true true return type ← wrong position
const fns: Array<() => any> = [] FunctionType true true return type ← correct
function f(): any FunctionDeclaration true true return type
const b: Record<string, any> = {} TypeReference false false not flagged
const r: any[] = [] ArrayType false false not flagged
const t: [any, string] = [1, 'x'] TupleType false false not flagged
type D<T = any> = T TypeParameter false false not flagged

So an any index signature was reported 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,
not an exclusion: an IndexSignatureDeclaration arm placed BEFORE the function-like fallback,
returning index signature.

The judgement triage placed on this card, and how it was settled

Shape (2) — a function type's return any inside a type argument — is IN SCOPE. It stays
flagged, labelled return type, and the arm is unchanged. The header is what was wrong.

Settled from the file's own declared rule, not by preference. The describeAnyPosition docblock
defines the boundary operationally in the same sentence that names the exclusions:

The check is parent.type === node (or the assertion's own type slot), so an any nested in
a larger type — Record<string, any>, any[], Promise<any> — has a
TypeReference/ArrayType parent
and is not a finding.

"Nested" is therefore a claim about the any itself — that it is a component of a composite
type, filling no annotation slot of its own — and not a claim about its ancestry. All three
named exclusions share exactly that property, and the table above confirms it: each has
parent.type === node false. In Array<() => any> the any fills the return slot
of a FunctionTypeNode (parent.type === node true), which the top-of-file SCOPE list already
names in so many words: "a variable/property/return annotation". The header and the arm never
actually conflicted; the shorthand "nested any is deliberately not flagged" simply reads as
ancestry when quoted without its operational clause. So there was nothing to stop and escalate on
— the file does give a way to tell which reading is authoritative — and this PR writes that
reading into the header, the docblock and the gate's own failure text.

The four axes agree, and none was the deciding one on its own:

⚠️ For the objectui port (objectstack-ai/objectui#7653, which carries Blocked-by: on this
card):
the decision is label shape (1), keep shape (2). Add the IndexSignatureDeclaration
arm before the function-like fallback with the label index signature; do not narrow the
function-like arm; state the direct-parent boundary in the header. The port should not re-derive
this.

Premise re-measured — one premise falsified

There is no baseline artifact for this gate. The card and the dispatch both plan around
"find the baseline, measure which rows move, regenerate it with the repo's own tooling". That
artifact does not exist. The file says so itself:

Measured at the time of writing, the whole 208-block corpus contained three bare any
annotations, all in one block, so the wider scope cost nothing to adopt and there is no
ratchet file: the baseline is zero and stays zero.

Measured rather than taken on the file's word, with a lit control: packages/spec carries
nine ratchet/baseline artifacts — authorable-surface.base.json,
docs-import-surface.baseline.json, dual-source-exports.baseline.json,
entry-nameability.baseline.json, react-declaration-parity.baseline.json,
scripts/liveness/key-mention.baseline.json,
scripts/liveness/undrilled-containers.baseline.json, test-typecheck-debt.json,
unemitted-schemas.baseline.json — and 0 of them name this gate. The gate reads no accept
set at all: its only JSON reads are two package.json reads. It calls fail() on the first hit.
So the label is half a row key only in the prospective sense the card meant, and nothing to
regenerate exists today
.

The corpus re-measure the card asked for (objectstack's own marked population, never measured
when the card was filed): zero sites, of either shape or any other.

   258 marked example(s) across 106 file(s), 3 surface(s):
     - skills + docs (@objectstack/spec): 225 block(s)
     - spec source TSDoc (@objectstack/spec): 10 block(s)
     - client SDK (@objectstack/client-react, @objectstack/client): 23 block(s)

OK 258 prose examples type-check across 3 surface(s) - every marked block parsed, so tsc ran the SEMANTIC pass on all of them
os-verify-lock: VERDICT command-exit 0

⇒ no verdict changes, no message changes, no row moves. The grade stays p3.

Verification

Ablation — two mutations, both predicted before running, both proven landed on disk, both
restored by blob equality under an EXIT/INT/TERM trap.
No dist is involved: the gate
runs from source via tsx, so the mutation's path to the code under test is the file itself.

leg mutation proof it landed self-test failing legs
control none — exit 0 0
A delete the IndexSignatureDeclaration arm removed-anchor count 1 to 0, injected 0 to 1, 1 insertion(+), 1 deletion(-), blob 1b8f63a8… ≠ HEAD b7cf53d6… exit 1 — RED, as predicted 1, the new leg only
B narrow the function-like arm to the ancestry reading (bail if any ancestor is a TypeReference) removed-anchor count 1 to 0, injected 0 to 1, 9 insertions(+), 1 deletion(-), blob 0f328d26… ≠ HEAD exit 1 — RED, as predicted 1, the new leg only
A: positions fixture: got ["6:return type","8:return type"], expected ["6:index signature","8:return type"]
B: positions fixture: got ["6:index signature"],             expected ["6:index signature","8:return type"]

restore: disk=b7cf53d6ce997f078c5739ab837e1a215fc449e7 head=b7cf53d6ce997f078c5739ab837e1a215fc449e7  git-diff-HEAD-empty=yes

Ablation B exists because shape (2) was already flagged before this PR, so a leg asserting
only "line 8 is a finding" would pass on the broken code and pin nothing. B proves the leg can
fail, which is what makes it a pin on the boundary rather than a restatement of today's output.
Both legs assert the label string, for the same reason: the bug is the label, so "it is
flagged" is not an assertion about it.

Gates. Families derived mechanically, never hand-listed —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, change set
derived by the tool itself (1 path). Reconciled with --ran, exit codes recorded per command:

dispatch-gates --ran: 54 derived famil(ies) accounted for - 53 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
  NOT-MEASURED - DERIVED (1): pnpm check:dual-build-cjs-loads [recorded exit 3 - PREREQUISITE NOT MET]

check:dual-build-cjs-loads exits 3, its own "PREREQUISITE NOT MET — this is NOT a pass:
nothing was measured" code: it reads built output and 33 packages have no dist/ in this
worktree. Declared to CI, which builds first; not reported as a failing check. All other 53
exited 0.

Tests and type-checking. Every heavy verdict read from os-verify-lock's own
VERDICT command-exit line, never through a pipe:

  • pnpm --filter @objectstack/spec run check:skill-examples — exit 0 (self-test plus the
    real 258-block corpus; re-run green on the final commit).
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 — exit 0,
    Test Files 500 passed (500), Tests 13781 passed (13781).
  • pnpm --filter @objectstack/spec typecheck — exit 0, and proven to actually reach the
    changed file rather than assumed. tsconfig.json and tsconfig.test.json both use
    include: ["src/**/*"] and list it 0 times; the package's typecheck chains
    check:scripts-typecheck (tsc --noEmit -p tsconfig.scripts.json), whose --listFiles names
    scripts/check-skill-examples.ts 1 time in a 909-file program. A direct
    tsc --ignoreConfig --strict over the file reports 0 diagnostics, with a lit control (an
    injected const CONTROL: number = 'not a number') reddening it at TS2322 and then restored,
    proven by blob equality against HEAD.
  • eslint . --no-inline-config --format json — exit 0, 6586 files, 0 errors, 0 warnings,
    the changed file present in the run, measured on the final commit c23aa7a4. The repo-wide run
    fit in budget, so no narrowing is claimed and none is needed.
  • Control characters: pnpm check:nul-bytes green, plus a direct grep -naP over the changed
    file for the C0/DEL range — no hits.

No changeset — skip-changeset

Nothing published moves, measured above (0 symbol hits across the files[] roster, positive
control lit at 34). packages/spec/scripts/** ships in no package. This is the case
.github/workflows/lint.yml names in its own words: "'this PR edits a CI-internal script' is
the textbook skip-changeset case — such a PR releases nothing, so by the workflow's own
prescription it takes the label."

验收备注

  • The dispatch order asked for a changeset; the measurement says skip-changeset. Flagged
    rather than resolved silently. A changeset here would version-bump @objectstack/spec for a
    change that ships in nothing.
  • noted, not filed: the function-like fallback also claims MethodSignature, CallSignature,
    ConstructSignature, FunctionType, ConstructorType and the accessors. All of those
    genuinely have a return slot, so return type is correct for each — measured, not assumed. No
    further arm is owed. 承接者: whoever ports this to objectui#7653.
  • noted, not filed: a first reading of packages/spec's tsconfigs suggested scripts/** sat
    outside every tsc program. That reading was wrong and is corrected above —
    tsconfig.scripts.json covers it. Recorded because the wrong reading is one a future author is
    likely to reach independently. 承接者: none.

Reviewer's one-minute read

The behavioural delta is one label string, on one shape that does not occur in the corpus
today
. Everything else in the diff is the header, the docblock, the gate's failure text and the
self-test legs — the parts that make the boundary say out loud what the code has always done, so
the next reader of "nested any is deliberately not flagged" does not narrow a good arm.

⛔ Do not flip this out of draft, enqueue it, arm auto-merge or submit an approving review — the
at-tier verdict is the seat's.


🤖 Generated with Claude Code

https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH


Generated by Claude Code

os-bill and others added 2 commits September 10, 2026 20:01
`ts.isFunctionLike` is true for every SignatureDeclaration kind, so a bare
`[k: string]: any` in a marked block fell into the `return type` arm and was
reported at a position it does not occupy. Flagging it is right; the label was
not. Adds an `IndexSignatureDeclaration` arm before the function-like fallback,
and states the direct-parent boundary in the header so a function type's return
`any` inside a type argument reads as the finding it already is.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
Comment reflow only, no behaviour change.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 46cf7051627cb8b42de0fd55c757a2057e414ac4 → packageMentionDocs.

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Seat at-tier review — PASS, zero must-fix. ⏳ Not enqueued: ④ still converging.

Head c23aa7a44558273db9bab3afe9c51af86f9f6842. Readings 2026-09-10T21:01–21:04Z against origin/main 0aa88eb6b0.

# check reading
① at-tier verdict PASS (this comment); clause ② no, tooling that ships in no package
② --pair 17545 exit 0
③ check-governed-merges --test, final 1 path exit 0; lit control AGENTS.md ⇒ exit 3
④ checks on this head ⏳ 31 distinct, 16 pending, 0 failing

⚠️ My dispatch order was wrong in two places. The round was right in both.

1. It told you to find and regenerate a baseline that does not exist. My order said "the label is half the baseline row key… find the baseline, measure which rows move, and regenerate it with the repo's own tooling". Measured:

check-skill-examples.ts:109  "…there is no ratchet file: the baseline is zero and stays zero."
JSON reads in the gate       : 2   (both of them package.json reads)
LIT CONTROL — baseline/ratchet artifacts that DO exist in packages/spec : 8, none naming this gate

The gate fail()s on the first hit. There was nothing to regenerate, and planning around a phantom artifact is how a round burns time on a non-task.

2. It demanded a changeset; skip-changeset is correct. packages/spec's files[] is ['dist','json-schema','liveness','prompts','llms.txt','README.md','src/**/*.zod.ts','CHANGELOG.md','api-surface','spec-changes.json'] — ⛔ scripts is not among them, so this file ships in no package, and lint.yml names this exact case in its own words.

⭐ This is the second consecutive round where my order demanded a changeset the measurement refuses (#14850 was the first). Two in a row makes it a defect in my template's boilerplate rather than two judgement calls, and it is recorded as such at #17520 rather than quietly corrected in the next order.

The judgement triage placed here: settled, with an argument rather than a preference

I asked the round to settle shape (2) — a function type's return any nested in a type argument — from the file's own declared rule, and to STOP if the header and the code genuinely conflicted. They do not conflict, and the round showed why:

the docblock defines the check as parent.type === node on the DIRECT parent (:885-887), in the same sentence that names the three exclusions. All three named exclusions (Record<string, any>, any[], Promise<any>) have that predicate false; a function type's return any has it true.

⇒ shape (2) is in scope and stays flagged as return type. The supporting argument is the one that makes this more than bookkeeping:

the alternative reading would hand an author going red the one-token evasion this gate already refuses for parameters — wrap the offending function type in a type argument and the gate goes green over an unchanged defect.

That is reasoning from the gate's own purpose, ⛔ not from which answer was easier.

The fix

One behavioural line, placed before the isFunctionLike fallback in the same predicate form as every sibling arm:

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

⭐ Ablation leg B is the one worth keeping: shape (2) was already flagged before this PR, so a pin asserting only "it is a finding" would pass on the broken code. Leg B narrows the function-like arm to the ancestry reading and proves the boundary pin can actually fail. Both legs assert the label string, since the label is the bug.

For the objectui twin

The decision is stated in the PR body so objectui#7653 does not re-derive it: label shape (1) index signature; keep shape (2) as a return type finding; ⛔ do not narrow the function-like arm. That repo was correctly not touched.

Recorded, not acted on

The round reports that create_pull_request appended a second attribution footer to the PR body. ⚠️ I measure the stored body at 1 footer now, so either that was transient or a later write settled it — noting the disagreement rather than resolving it from one reading. The attribution grid is #15275's subject.

Enqueue when ④ reports green. ⛔ Do not "fix" the missing changeset — skip-changeset is the measured answer.


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 10, 2026 21:31
@os-bill
os-bill added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit ad715ac Sep 10, 2026
40 checks passed
@os-bill
os-bill deleted the claude/issue-14910-any-position-labels branch September 10, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

1 participant