Uh oh!
There was an error while loading. Please reload this page.
feat(spec): refuse unknown top-level stack keys — ObjectStackDefinitionSchema goes strict (#8687) - #9186
Merged
Merged
Conversation
…onSchema goes strict (#8687) The outermost authoring door was the last strip-mode surface of the #4001 campaign: an unknown top-level stack key parsed green and was silently dropped, adding zero warnings to 'os validate' (exit 0 even under --strict). Maintainer-ruled Shape B (2026-08-16): top-level strictObject, keeping the near-miss resolver — the did-you-mean now rides the refusal itself, and lintUnknownStackKeys goes quiet on the strict surface by its own posture rule. onEnable is now declared rather than undeclared-but-honoured (declared = honoured; #4095 grafting unchanged), with a 'single' composition disposition. Curated prescriptions answer storage / approvals / approvalProcesses / workflows / portals / onDisable. Protocol-18 semantic migration entry stack-top-level-unknown-keys-refused; registry regenerated the scripted way. Closes the root of hotcrm#1141. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
…ject declaration (#8687) The gate slices the stack-collection set out of ObjectStackDefinitionSchema's source text, anchored on the literal declaration line. #8687 rewrote that declaration from z.object({shape}) to strictObject({options}, {shape}), which emptied the anchor — and the gate failed loudly by design instead of reconciling against nothing. The extractor now slices the options body first, then the shape body that follows it; the self-test synthetic source moved to the new shape and gained a missing-shape-argument case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code — list omitted above 15 rows. Re-derive: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.
|
os-zhuang
marked this pull request as ready for review
August 17, 2026 00:40
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#8687
Maintainer-ruled Shape B (2026-08-16, recorded 13:03Z on the card): top-level
.strict()onObjectStackDefinitionSchema, keeping the already-written near-miss resolver. B subsumes Shape A — a strict parse failure failsos validateoutright, so no warning-accounting change is made. This also closes the root cause behind objectstack-ai/hotcrm#1141 (plain reference — that card is in a different repo and is not closed by this PR).What changed
packages/spec/src/stack.zod.ts— the top-level stack door is now the sharedstrictObjecttemplate (the #4001 campaign idiom, same asIndexSchema's #9045 close),.superRefine(applyApiEndpointGates)retained:unrecognized_keys, path[]), naming the surface and the key. The card's own measured control inverts:flow,approvalProcessesand a stray key now fail parse instead of adding zero warnings with exit 0.strictObject's edit-distance suggester answersobjectzwithDid you mean `objectz` → `objects`?(andflow→flows).lintUnknownStackKeysgoes quiet on the strict surface by its own posture rule — one voice, not two; the generic lint machinery is unchanged and re-pinned against a synthetic strip-mode schema.storage(deployment config,OS_STORAGE_*/ Setup → Settings → Storage),approvals/approvalProcesses(Approval-node flows, ADR-0019),workflows(state_machinevalidation rules, ADR-0020),portals(removed, [P2] PortalSchema disposition: prune the dead schema or build a portal runtime #3464),onDisable(never invoked, Four of the five declared plugin lifecycle hooks are never invoked — and the metadata-type registry docs tell plugins to use one of them #4212). Thehistorysentence names where the legal keys are enumerated.onEnableis now DECLARED (z.function().optional()). It was undeclared-but-honoured (STACK_RUNTIME_MEMBERS): the parse stripped it whileAppPluginexecuted it off the authored bundle — a strict close of an undeclaredonEnablewould have refused the patternexamples/app-todoandapp-showcaseship. Declared = honoured, in both directions; Config-booted apps lose theironEnablehook — every script action handler goes unregistered and 404s at dispatch (examples/app-todo: all 8) #4095 artifact-boot grafting is unchanged (GRAFTABLE_RUNTIME_MEMBERSstill derives fromSTACK_RUNTIME_MEMBERS).composeStacksgives it a'single'disposition: same value passes, disagreement refuses naming both stacks.Contract plumbing (campaign shipping shape):
packages/spec/src/migrations/entries/semantic/18.stack-top-level-unknown-keys-refused.ts;registry.tsregenerated viagen:migration-registry(never hand-edited).@objectstack/specminor with BREAKING annotation, FROM → TO guidance,adr-0087: registered stack-top-level-unknown-keys-refusedmarker — matching the feat(spec): close IndexSchema against unknown keys — #4001 批 20 site 14, after objectui#4772 #9045-family changesets.scripts/check-stack-collection-maps.mjs: re-anchored on the new declaration shape (the gate failed loudly by design when thez.objectanchor vanished; its own error text prescribes fixing the anchor). Self-test updated to the new shape + a missing-shape-argument case (11 → 12 assertions).Tests
packages/spec/src/stack-top-level-strict.test.ts(new): rejection pins assert issuecode+path+keys(statusis the publish door's uniform ADR-0112 wrap — stated in the docblock as the family convention, since this layer is pre-HTTP); near-miss pin; curated-prescription pins; accept-side pins (all 44 declared keys still accepted; representative stack keeps its top-level key set exactly — item-level defaults are pre-existing ADR-0122 behaviour);onEnableaccepted AND retained in parsed output; lint-quiescence pin;defineStackthrow-path pin.packages/cli/test/validate-top-level-strict.e2e.test.ts(new): real-CLI exit-status pin (bin/run-dev.js+ tsx, themigrate-exit-codepattern) — a stack withflow:exits non-zero without--strict, naming the key and the near-miss; the identical stack spelledflows:exits 0.metadata-authoring-lint.test.ts: the stack section graduated — real-schema tests now pin silence (the refusal side moved to the new file); the lint's own machinery re-pinned against injected strip schemas; the runtime-member rot pin now asserts everySTACK_RUNTIME_MEMBERSmember is declared.Fixture/consumer sweep
Consumers swept (downstream of
@objectstack/spec,pnpm --filter '<pkg>...'closure built first): spec, objectql, metadata, metadata-protocol, lint, runtime, cli test suites + examples build (app-todo / app-crm / app-showcase / embed-objectql). 13 repo-internal fixtures fixed — every one carried authoring that was never spec-valid and had been silently stripped:packages/metadata(3): artifacts with manifest fields flattened at top level (incl.scope: 'app', not even a manifest scope) → nested undermanifest:.packages/cli(7): five schema-migrate/stored-flow integration artifacts +migrate-meta.e2epre-17 config (top-levelname/label) +emit-json-pipe(stray top-levelnamewould have added a 901st error to an exact-count assertion).packages/lint(2):...MANIFESTspread flat into the stack →manifest: MANIFEST; the org-axis test that PINNED the strip behaviour rewritten to pin the refusal.packages/spec(1): connector reachability probe carried a stray top-levelname.composeStacks' #5005 completeness pin forced theonEnabledisposition (the type-level half of that gate working as designed).Verification (union run at final head
9b89f58da)@objectstack/metadatadeclares notypecheckscript (ledgered) — verified as a zero-match rather than read as green; its edits are test-only and runtime-verified.dispatch-gates.mjs, run at9b89f58da): changeset-gate-self-tests, cross-package-test-inputs, doc-formula-expressions, durability-log-level, merge-driver, objectui-changeset, spec-parsed-alias, stack-collection-maps, type-source-resolution, adr-0087-registration, changeset-no-major, empty-changeset, dev-prereqs, query-options-erasure, engine-double-contract, where-matcher, nul-bytes, type-check-coverage, type-check-debt (--re-measure, full built closure), migration-registry, check:generated, check:authorable-surface, strictness-ledger — all green.git checkoutof the pre-changestack.zod.ts, spec tests run on source so no dist rebuild involved): exactly the new pins went red — 10 tests (all rejection/near-miss/curated/onEnable-survival/lint-quiescence pins) — while the 30 accept-side controls stayed green; restore is byte-identical to the commit and the two files are 40/40 green again.Behaviour notes for review
defineStack(config, { strict: false })skips the parse entirely (unchanged); such a stack now gets NO top-level unknown-key warning (the lint yields to a strict schema it cannot know was bypassed). The key is no longer dropped on that path either (no parse ⇒ no strip) — the warning it loses was always slightly wrong there ("dropped at load" while nothing dropped). Called out rather than hidden._parseAndRegisterArtifact) — the population the ruling accepts as "breaking exactly for those already silently broken".Generated by Claude Code
Generated by Claude Code