Skip to content

fix(spec): reject unknown keys on nested action-param literals at compile time - #12860

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-12615-action-param-excess-keys
Aug 28, 2026
Merged

fix(spec): reject unknown keys on nested action-param literals at compile time#12860
os-sales merged 1 commit into
mainfrom
claude/issue-12615-action-param-excess-keys

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes #12615

What

Triage route leg ② landed: the seam typing in packages/spec. ObjectSchema.create() infers its generic from the argument, so TypeScript's excess-property (freshness) checking never fires at any depth; the existing NoExcessObjectKeys map compensated only at the top level. A typo'd key on an actions[].params[] literal therefore passed tsc clean and was caught only by ActionParamSchema's strict parse at module load. This PR mirrors the same Record(excess-key, never) map over each element of each action's params array (NoExcessNestedActionParams in object.zod.ts), turning the typo into a located tsc error at the authoring site.

No runtime behaviour change: zero edits to any Zod schema's accept/reject, zero changes to parse timing. The strict parse at module load stays the enforcement of record.

Measurements

  • Day-of reproduction (premise valid): planted carryOverX: true on the object_permissions param literal in sys-permission-set.object.ts at origin/main (6f0fec3); pnpm --filter @objectstack/plugin-security typecheck was GREEN (VERDICT command-exit 0). Plant removed, restore proven by blob equality (e726f251).
  • RED-plant proof (seam present): same plant after rebuilding @objectstack/spec with the seam — sys-permission-set.object.ts(150,79): error TS2322: Type 'true' is not assignable to type 'never'. The location (line 150, col 79) is the carryOverX token itself; the single-line tsc form names the key by position, and the runtime parse (pinned in the new test) names it by spelling. Restore again proven by blob equality.
  • Positive control: workspace-wide pnpm typecheck (turbo, 129/129 tasks) GREEN with ZERO new errors — no legitimate call site lights up, no cast or widening added anywhere.
  • Tests: new describe in object.test.ts — the @ts-expect-error compile pin (goes unused and fails check:test-typecheck if the nested map is removed; object.test.ts is not in the test-typecheck debt ledger, so the directive is live) plus the runtime pin that the strict parse still throws naming carryOverX, and a positive control covering an action with params (canonical spellings) and one without. Full @objectstack/spec suite at HEAD eeddce0: 436 files / 11562 tests passed.
  • Gate union (derived by scripts/pm/dispatch-gates.mjs from the actual changed set, re-run post-commit at HEAD eeddce0): all 33 path-derived families plus the test-kind convention families (check:query-options-erasure, check:type-check-coverage, check:type-check-debt re-measure — "none above its recorded number", check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs) GREEN. One NOT MEASURED: scripts/pm/check-half-states.mjs exits 3 "PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential" (container has no GitHub token; board-sweep gate, no reading).

Changeset

@objectstack/spec patch: the published .d.ts shape of create()'s parameter type changes (tightens), but no working code changes meaning — every literal the new constraint refuses was already refused at import by the strict parse. Not a value-set narrowing, so patch rather than minor.

#12615 is fixed by this PR; no other issue is affected.

Generated by Claude Code


Generated by Claude Code

…pile time

ObjectSchema.create() infers its generic from the argument, so tsc's
excess-property (freshness) checking never fires at any depth; the existing
NoExcessObjectKeys map compensated only at the top level. A typo'd key on an
actions[].params[] literal passed tsc clean and was caught only by
ActionParamSchema's strict parse at module load. Mirror the same
Record<excess-key, never> map over each element of each action's params
array so the typo becomes a located tsc error at the authoring site.

Compile-layer signal only: the strict parse at module load stays the
enforcement of record; zero changes to any Zod schema's accept/reject or to
parse timing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvjTCjJQn9zSTXEhUKgT7s
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 126 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 c097f7175a3fd449164c4de295a0f544f02e1047packageMentionDocs.

Which tree this was computed on

This run read content/docs from a4d10ae6bd4861f5185984122160051a3d751ab7 — the merge of head eeddce00184b4c983e3cdd8e7e0de9c1b2d22822 into base c097f7175a3fd449164c4de295a0f544f02e1047, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a4d10ae6bd4861f5185984122160051a3d751ab7 && git checkout a4d10ae6bd4861f5185984122160051a3d751ab7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c097f7175a3fd449164c4de295a0f544f02e1047 eeddce00184b4c983e3cdd8e7e0de9c1b2d22822 && git checkout -B drift-repro c097f7175a3fd449164c4de295a0f544f02e1047 && git merge --no-ff eeddce00184b4c983e3cdd8e7e0de9c1b2d22822

node scripts/docs-audit/affected-docs.mjs --json c097f7175a3fd449164c4de295a0f544f02e1047

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] tsc does not police unknown keys on plugin action-param literals — the only enforcement is the ActionParamSchema strict parse at module load

2 participants