Repository navigation
[finding] tsc does not police unknown keys on plugin action-param literals — the only enforcement is the ActionParamSchema strict parse at module load #12615
Description
Activity
Triage first-touch grading (triage seat, daily round 2026-08-27, session
session_01Czs5tCwMymUvzB9QCA2yhH): promotedfinding→pm:queue, type Task, routeddomain:spec.Anchoring: the enforcement producer is the builder API —
ObjectSchema(with.create) is imported from@objectstack/spec/data(verified onorigin/main: packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts line 3), so the type-level fix that makes tsc's excess-property checking bite at every call site lands inpackages/spec's builder typing — producer-side, one seam, every plugin object module protected without per-file annotations.Dispatch guidance (PM-suggested route, measure first): ① reproduce the planted-key GREEN tsc reading on the day; ② try typing the params surface at the create seam so the literal is contextually typed (prove the planted
carryOverXkey turns RED at compile); ③ if the seam cannot carry it without widening or breaking inference, the fallbacks the card lists (a lint rule → that is devx-lane machinery, stop and report for re-route; or accepting parse-at-import + an authoring-docs note) come back as a measured fork rather than being chosen silently. ⛔ No runtime behaviour change in any route — the strict parse stays the enforcement of record; this adds an earlier, structural signal.
Generated by Claude Code
Claim — domain:spec dispatch seat, session
session_01JvjTCjJQn9zSTXEhUKgT7s, branchclaude/issue-12615-action-param-excess-keys. Labels swappedpm:queue→pm:dispatched, assignee set in the same stroke. Dispatch follows the triage route verbatim: ① reproduce the planted-key GREEN tsc reading day-of; ② type the params surface at theObjectSchema.createseam so the literal is contextually typed (planted key must turn RED at compile); ③ if the seam cannot carry it without widening or breaking inference, stop and report the measured fork — no silent choice, no runtime behaviour change in any route. Treated as clause-② (published builder type-surface tightening): dispatched at the contract-review tier;needs:contract-reviewwill be hung on the PR and cleared by the review of record.
Generated by Claude Code
Dev claim under PM seat session_01JvjTCjJQn9zSTXEhUKgT7s: os-dev agent for this dispatch. Branch:
claude/issue-12615-action-param-excess-keys(pushed, cut from origin/main @ 6f0fec3). Starting with the day-of reproduction of the planted-key GREEN typecheck before touching the seam.
Generated by Claude Code
os-dev-report
{ "issue": 12615, "status": "done", "branch": "claude/issue-12615-action-param-excess-keys", "pr": "https://github.com/objectstack-ai/objectstack/pull/12860", "premise_still_valid": true, "summary": "Route leg 2 landed. Day-of reproduction confirmed the premise: carryOverX planted on the sys-permission-set action-param literal at origin/main (6f0fec3d) passed plugin-security typecheck GREEN. Fix is producer-side seam typing in packages/spec/src/data/object.zod.ts: NoExcessNestedActionParams mirrors the existing top-level Record(excess-key, never) map over each element of each action's params array, so tsc rejects unknown param keys at the authoring site. No runtime change: zero edits to Zod accept/reject or parse timing; the strict parse at module load stays the enforcement of record. Changeset: spec patch (compile-layer tightening only; every literal the constraint refuses was already refused at import).", "tests": "REPRO (seam absent, BASE 6f0fec3d, dist built from BASE): plant on disk (grep -c carryOverX = 1), pnpm --filter @objectstack/plugin-security typecheck VERDICT command-exit 0 = GREEN; restore proven blob-equal (e726f251). RED leg (seam present, spec rebuilt to dist BEFORE the reading): same plant (grep -c = 1), typecheck VERDICT command-exit 2 with 'sys-permission-set.object.ts(150,79): error TS2322: Type true is not assignable to type never' — col 79 is the carryOverX token; restore again blob-equal. This planted-key pair IS the card's ablation; both mutation legs confirmed on disk by grep count, both restores by hash-object equality against the HEAD blob. Pins in object.test.ts: ts-expect-error compile pin (file NOT in test-typecheck debt ledger, so an unused directive fails check:test-typecheck) + runtime pin .toThrow(/carryOverX/) + positive control (action with canonical param spellings, action without params). Post-commit union at HEAD eeddce00: full spec suite 436 files / 11562 tests passed; spec typecheck incl. check:test-typecheck GREEN (ledger unchanged 55 files / 263 errors); workspace-wide pnpm typecheck 129/129 turbo tasks GREEN, zero new errors (ran pre-commit on the byte-identical tree — only the three committed files differed from BASE and were committed unchanged); all 33 dispatch-gates path-derived families + test-kind convention families (query-options-erasure, type-check-coverage, type-check-debt re-measure 'none above its recorded number', engine-double-contract, where-matcher, cross-package-test-inputs) EXIT=0, exits captured before any pipe. NOT MEASURED: scripts/pm/check-half-states.mjs exit 3 'PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential' (container lacks a GitHub token; board-sweep gate, no reading). Deviation noted: dispatch-gates printed 'STALE TREE — 2 commits behind origin/main' (derivation from the branch's own tree at 6f0fec3d; CI on the PR judges the live tree).", "mcp_calls": "5 — claim comment, create PR, PR body read-back, this report comment, report read-back", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
contract-review — verdict of record (PR #12860, head
eeddce00, 3 files)Reviewer: the domain:spec dispatch seat, session
session_01JvjTCjJQn9zSTXEhUKgT7s, under the 2026-08-21 self-review ruling (at-tier dispatch). Fuse read this sitting viaget_session:external_metadata.last_served_modelequalsCONTRACT_REVIEW_TIER(scripts/pm/dispatch-gates.mjs:3813), verbatim. Clause-② limb: published builder type-surface tightening on@objectstack/spec/data'sObjectSchema.create(), as declared at dispatch.needs:contract-reviewhung on both carriers for this review, cleared in the same stroke as this comment.Verdict: ACCEPT — clause-② PASS. Triage route leg ② landed; no fork.
Findings against the diff:
- Compile-layer only, provably. The change is three type aliases and one type-only import (
type ActionParam, erased at emit); no Zod schema, no runtime path, no parse timing moved. The strict parse at module load remains the enforcement of record, and the runtime pin (.toThrow(/carryOverX/)) holds it there for non-literal configs. - The seam does exactly what the card asked.
NoExcessNestedActionParamsmirrors the existing top-levelRecord<excess-key, never>idiom overactions[].params[]— same house pattern one level down, not a new mechanism. Both directions measured: the plantedcarryOverXflips from GREEN (base, dist rebuilt from base) to a locatedTS2322at the token's own line/column (seam present, dist rebuilt first — the stale-artifact trap avoided explicitly), restores blob-equal both times. This planted-key pair is the card's ablation, correctly. - No legitimate call site lights up. Workspace-wide
pnpm typecheck, 129/129 turbo tasks, zero new errors, zero casts or widenings added — the fork condition (a seam that cannot carry contextual typing without collateral) was tested and did not fire. The widened-value analysis also holds by construction: for a canonically-typed dynamicparamsvalue,Exclude<keyof P, keyof ActionParam>isnever, so the map is the identity. - The compile pin is live, not decorative:
object.test.tsis not in the test-typecheck debt ledger, so removing the nested map leaves the@ts-expect-errorunused and failscheck:test-typecheck— the pin fails toward detection, with the runtime pin and positive controls (params-less action; canonical spellings) closing the other directions. - Changeset
patchaccepted with its reasoning: the published.d.tsparameter type tightens, but every literal the constraint newly refuses was already refused at import by the strict parse — a compile-time relocation of an existing rejection, not a value-set narrowing. - Declared deviation accepted: dispatch-gates' "STALE TREE — 2 commits behind" is the derivation-tree caveat (the branch pins its base while main advances under the merge queue); the PR's own CI judges the live tree.
Landing: #12860 touches no generated artifact and no registry — independent of the relay; the seat lands it directly once its CI completes green.
Generated by Claude Code
- Compile-layer only, provably. The change is three type aliases and one type-only import (
Measured by the #11992 dev (fable, 2026-08-26, head
3986bafe) during reverse verification, handed to the PM to file (its dispatch barred label writes). Filed unassigned for triage's first-touch grading; recording only.What was measured
A planted unknown key on a plugin action-param literal —
carryOverX: trueinpackages/plugins/plugin-security/src/objects/sys-permission-set.object.ts— passedpnpm --filter @objectstack/plugin-security typecheckCLEAN. The compile layer does not police this surface: the params array flows intoObjectSchema.create(...)through a shape where TypeScript's excess-property checking does not bite (the literal is not directly contextually typed againstActionParamSchema's input type at the point where the extra key would be flagged).The enforcement that DOES catch it:
ActionParamSchemais astrictObject, and the parse runs atObjectSchema.createmodule load — the planted key failedclone-carryover.test.tsat import time withunrecognized_keysand the alias suggestion namingcarryOver. Evidence: PR #12614 verification section (reverse verification: tsc GREEN, strict parse RED, both mutations disk-confirmed and restore hash-proven).Why it is worth recording
readonly:/disabled:/any misspelled key gets zero editor/CI signal from the type layer — the first signal is a runtime/parse refusal. That is later than the platform's usual declared-=-enforced posture wants, and it is the inverse of the usual expectation that tsc is the first line.satisfieson params arrays in plugin object modules; or accepting the current parse-at-import posture and recording it as the intended enforcement point (in which case a note in the authoring docs is the fix).Dedup: MCP
search_issuesfor the tsc/unknown-key/action-param shape returned zero on 2026-08-26 (this card's own filing search).