Repository navigation
spec: PackageInstallRequestSchema's wrapped branch refuses top-level unknown keys by name — ruling A on #19328 #20249
Description
Filed by the director seat (summon #30 续, session_01AsCNgFBs8HCjwhyHQsFbx3) from the maintainer's ruling on #19328 (batch #227 item 3, letter A, maintainer 「开始总监决裁」). Reader who acts: the domain:spec execution seat. ⛔ Not a claim. Grading beyond what is written here is triage's.
What the ruling settles
POST /api/v1/packages answers exactly what PackageInstallBodySchema declares since PR #20218 (packages/runtime/src/domains/packages.ts: const declaredBody = PackageInstallBodySchema.safeParse(body), one hit on origin/main ab820016b3). Three of the four residual classes on #19328 are closed by that landing. The last one is the wrapped form's top level: PackageInstallRequestSchema's wrapped branch (packages/spec/src/api/package-api.zod.ts, docblock at :381 「⛔ Do NOT close the wrapped branch with .strict() … (ruling A)」) is declared strip-mode, so a caller who writes { manifest, enabledOnInstall: false } (a misspelling of enableOnInstall) has the key dropped silently and the package installs enabled.
The docblock's ruling A (5716042643, #18058, batch #148 item 4) rested on 「the declaration must not refuse a body the door answers 201 to」 at a time when the door did not parse the body at all. Since PR #20218 the door answers what the declaration says, so that premise is circular and no longer constrains the choice. The manifest and the bare form are already strict; the maintainer ruled the wrapped top level joins them: one rule for the whole install contract (the Kubernetes fieldValidation=Strict / Terraform provider-schema idiom).
Work
PackageInstallRequestSchema, wrapped branch: strip → strict. An unknown top-level key is refused by name, with the same envelope the manifest's and the bare form's unknown keys already get (400 VALIDATION_ERROR, the key named, the remedy: remove it or spell the declared option). ⛔ No alias, no grace window.- The docblock at
package-api.zod.ts:381: replace the ruling-A sentence with this ruling's citation ([finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328 batch 🔗 Broken links detected in documentation #227 item 3, letter A) and the reason above; do not leave the retired reasoning in place. - Pins:
package-api.test.ts's wrapped-branch rows assert the refusal (a misspelled option, a private key) and the controls (manifestalone;manifest+ the four declared options round-trip). PR fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218's §5 alignment pin (「the door answers what the declaration says」) flips from strip to refusal on its own: re-read its expectation and correct it in the same PR if it asserted the strip. - Changeset:
@objectstack/specminor,**BREAKING**banner,Clause-②: no (narrowing), ADR-0087registereddisposition (the wording of [finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120 / [finding] the HTTP install door readsmanifest.idpositionally and never parses the body throughManifestSchema— POST /packages answers 201 to ids thatMANIFEST_ID_PATTERN(spec,defineStack,os build, the publish face) refuses #19417). Reach measured first-party by the [finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328 dev: the SDK sends onlymanifest/settings/enableOnInstall/overwrite; the objectui package dialog sends{ manifest }. Out-of-repo callers are NOT MEASURED; say so in the changeset. - [finding] once PR #20218 lands,
PackageInstallBodySchema's published docblock still says the install door answers 201 to residual classes 1b, 2, 3 and 4, which it then refuses 400 #20219 (the spec docblocks PR fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218 made false) touches the same docblock region; the two may land as one PR if the same dev holds both. Neither is a claim on the other.
Acceptance
- A wrapped body with an unknown top-level key is refused
400 VALIDATION_ERRORnaming the key at the spec door and, through fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218's alignment, atPOST /api/v1/packages; a well-formed wrapped body installs exactly as before. Check Changeset/ ADR-0087 registration green;Clause-②: no (narrowing)line-leading in the changeset.- On landing, [finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328 (
pm:blockedon this card,Blocked-by:in its body) closes; its four residual classes are then all closed.
Clause-②: no (narrowing)
Activity
objectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 1
Session:
session_01CiCTczDo7tGhafXjf61dUJAccount:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20249-install-request-wrapped-strictWorktree:
objectstack-issue-20249Domain:
domain:specSeat:
domain:spec#4(seat post #18917)File surface:
packages/spec/src/api/package-api.zod.ts:PackageInstallRequestSchema's wrapped branch (strip → strict), and its docblock at about :379, which carries the retired ruling-A reasoning. Plus any sentence in that file which this change itself makes false.packages/spec/src/api/package-api.test.ts: the wrapped-branch rows.packages/runtime/src/domains/packages-install-body-contract.test.tsand its two siblings, test files only.registeredentry plus the regeneratedmigrations/registry.tsregion, the generated artefacts that follow (regenerated, never hand-edited), and.changeset/.packages/runtime/src/domains/packages.ts(source).PackageInstallBodySchemaresidual section's pre-existing stale text (about :383–:410, the 「answers 201」 classes). That is [finding] once PR #20218 lands,PackageInstallBodySchema's published docblock still says the install door answers 201 to residual classes 1b, 2, 3 and 4, which it then refuses 400 #20219's,pm:blockedbehind [finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328.(stop on breach; explain in the report)
Container & model:
S/M,mode:subagent,model: default judgment tier(dispatch-gates.mjs --tieronpackage-api.zod.ts: 「no path-derived mandate」). The diff narrows a published accept set underpackages/spec/src/**, so the at-tier review is owed before enqueue.Clause-②: no
Thread-read: none
Serial constraints cleared: read at 2026-09-27T15:25Z. Open-PR census: 10 open PRs besides the release PR, 160 file rows (lit control: 125
packages/rows); none touchespackage-api.zod.ts, its test or the runtime install pins. In-flight claims:domain:cli[finding] the runtime dispatcher's /meta item reads apply NO per-caller read gate: through a catch-all host, GET /meta/doc/:name serves a permission-set-gated doc body to a non-holder, and /meta/app/:name serves requiredPermissions-gated entries #20193 namesruntime/src/domains/meta.tsand fences outpackages.ts; seat 2'spackages: nullon a release artifact: the schema andcomposeStacksrefuse it, while every reader reads it as absent #19926 names thepackages: nullreaders and fences outpackages.ts. Neither namespackage-api.zod.ts. The same-region sibling [finding] once PR #20218 lands,PackageInstallBodySchema's published docblock still says the install door answers 201 to residual classes 1b, 2, 3 and 4, which it then refuses 400 #20219 ispm:blocked(Blocked-by: #19328) and is ⛔ not folded into this claim. PR fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218 ([finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328), which made the door parse its whole body, landed asc02fa1276.The narrowing grade (
Clause-②: no (narrowing),minor+**BREAKING**, and an ADR-0087registereddisposition) is the card's own execution text (item 4). This claim line carries the closed spelling only.Ruling carried: #19328 batch #227 item 3, letter A (maintainer 「开始总监决裁」), as this card's body records it. The wrapped top level joins the manifest and the bare form: strict, with an unknown key refused by name, ⛔ no alias and no grace window.
objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{ "issue": 20249, "status": "done", "branch": "claude/issue-20249-install-request-wrapped-strict", "pr": "https://github.com/objectstack-ai/objectstack/pull/20277", "session": "session_01CiCTczDo7tGhafXjf61dUJ", "premise_still_valid": true, "summary": "The wrapped branch of PackageInstallBodySchema, PackageInstallRequestSchema, moved from z.object (strip) to strictObject (surface 'this package install request'). An unknown top-level key is now refused by name, in the envelope the manifest's and the bare form's unknown keys already get. A near-miss is offered the declared key (enabledOnInstall → enableOnInstall), and the history sentence carries the remedy: remove the key or spell the declared option. There is no alias and no grace window. The docblock's retired no-strict sentence and asymmetry paragraph were replaced by ruling record 5856869656 (batch #227 item 3, letter A) and its reason: the old premise is circular since c02fa1276 made the door parse its whole body. POST /api/v1/packages now answers 400 VALIDATION_ERROR naming the key and installs nothing, with no runtime source edit. Also added: the ADR-0087 D3 entry package-install-request-unknown-keys-refused, the regenerated registry.ts region, and the changeset (@objectstack/spec minor, BREAKING banner, Clause-②: no (narrowing) line-leading, registered disposition, FROM → TO migration, out-of-repo callers NOT MEASURED). PR #20277 is draft; its body line 1 is Fixes #20249 and line 2 is Clause-②: no (narrowing); the body read back byte-identical after the write. origin/main a9fb83ef0 was merged in before the PR (AGENTS.md §10). The merge was clean, registry.ts was regenerated, the closure was rebuilt, and everything was re-run at head 5941e9aa5. The worktree was removed after the PR opened.", "tests": "All at final head 5941e9aa5, under os-verify-lock, VERDICT command-exit 0 (build → tests → typechecks in one held run). BUILD: turbo run build --concurrency=2 --filter='@objectstack/client^...' --filter=@objectstack/client → exit 0, 33 of 33 tasks successful. TESTS, direction stated (the schemas' consumers found by git grep: spec, runtime, client): '@objectstack/spec exec vitest run --project local' exit 0, 550 files, 16244 passed, 1 todo; '@objectstack/runtime exec vitest run --project local' exit 0, 281 files, 4019 passed, 1 skipped; '@objectstack/client exec vitest run' exit 0, 50 files, 636 passed (readme-package-install-example.test.ts imports both schemas). TYPECHECK: spec, runtime and client 'run typecheck' (tsc --noEmit plus check:test-typecheck) all exit 0. The test:repo projects and the runtime integration layers were left to CI. ABLATION at 469f5e3a7 via scripts/ablation-replace.mjs in wrap mode, plus the script's own EXIT/INT/TERM trap with an absolute path. The merge touched neither ablated file nor either pin file, and resolution runs through src in both suites (spec imports relatively; runtime aliases @objectstack/spec to src), so no dist leg is needed. Mutation: anchor \"}).describe('Install package request'));\" became \"}).strip().describe('Install package request'));\"; anchor count 1 → 0, injected text 0 → 1, blob c4f6b506b477 → 70f87496fc7b. Result: spec package-api.test.ts had 3 failed and 81 passed, the three refusal pins failing with 'the bound union refuses it: expected true to be false' while the controls stayed green. Runtime packages-install-body-contract.test.ts had 3 failed and 29 passed: §5's misspelled and _source pins failed with 'expected 201 to be 400', meaning the door installed under strip, and §0's premise failed too; the §5 control stayed green. Restore: blob == HEAD blob c4f6b506b477 and git diff HEAD is empty. GATES: dispatch-gates --commands --repo objectstack-ai/objectstack at 5941e9aa5 derived 88; 86 exit 0 and 2 exit 3 (PREREQUISITE NOT MET: whole-workspace build), which is NOT MEASURED. The --ran reconciliation exit 0 reads '88 derived, 86 run, 2 NOT-MEASURED, 0 UNRUN'. LINT, narrowed on purpose: eslint --no-inline-config --format json over the 5 changed .ts files gave 5 files, 0 errors, 0 warnings. --print-config resolves all five; the .md changeset matches no config. eslint.config.mjs has no parserOptions.project or projectService (not type-aware), so no untouched file's verdict can move. Repo-wide pnpm lint is left to CI.", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "3 REST writes, each a fleet-write relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls, draft forced (run 36338761159, PR #20277); (2) label-write --issue 20277 --assign os-sales → POST /repos/objectstack-ai/objectstack/issues/20277/assignees (run 36338800164), read back as assignees [os-sales], labels untouched; (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20249/comments. Not REST: git pushes of claude/issue-20249-install-request-wrapped-strict (the empty-branch probe, 5 commit pushes, 1 merge push). Reads used REST GET single-card/list endpoints only.", "open_questions": [ { "question": "This change makes one comment false in a file the claim fences out. packages/runtime/src/domains/packages.ts, about lines 1003-1011, the install branch comment: 「⚠️ What the union does NOT refuse, deliberately: an unknown key at the TOP LEVEL of the wrapped form. PackageInstallRequestSchema is a plain z.object (strip mode) and its docblock forbids closing it …」. The rule 本轮改动令其变假…必修 and the claim's fence (⛔ Not packages.ts source) point opposite ways, so I left the comment untouched. Should the fix ride this PR or go elsewhere?", "options": [ "A: the seat widens this claim's surface to that one comment block, and a single comment-only commit lands on PR #20277 (a patch round). It changes no behaviour and no gate family.", "B: route it to a separate comment-only follow-up, or to #20219's lane if the seat folds it there." ], "recommendation": "A. The comment is this change's own falsity. Keeping the sentence and the close in one landing means main never carries a runtime comment that contradicts the declaration. It is one comment, no code moves, and no new verification surface is needed." } ], "out_of_scope_findings": [ "carrier: this PR's patch round if the seat picks A above, else 承接者:无 · packages/runtime/src/domains/packages.ts:1003-1011 comment says the wrapped top level is strip mode and forbidden to close; false at 5941e9aa5 · recorded in PR #20277 Acceptance notes · dedupe words: wrapped top level strip comment packages.ts, PackageInstallRequestSchema plain z.object comment", "carrier: #20219's dev · packages/spec/src/api/package-api.test.ts test 'the door answers 201 to all of them anyway — so this declaration is a SUBSET of the door' (DOOR_201_RESIDUALS) has been false since c02fa1276, where the door answers 400. Same stale-201 class as the residual docblock; this PR neither touched it nor changed its truth · noted, not filed · dedupe words: DOOR_201_RESIDUALS stale 201, install residual 201 pin" ], "gates_head": "5941e9aa5", "gates": [ "exit 0 @ 5941e9aa5 · node scripts/check-adr-0087-registration.mjs --base origin/main", "exit 0 @ 5941e9aa5 · node scripts/check-adr-0087-registration.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-changeset-no-major.mjs --base origin/main", "exit 0 @ 5941e9aa5 · node scripts/check-changeset-no-major.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-ci-filter-parity.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-closing-keyword-parity.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-closing-keyword-parity.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-comment-mask-adoption.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-comment-mask-adoption.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-comment-mask-corpus.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-dev-prereqs.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-empty-changeset.mjs --base origin/main", "exit 0 @ 5941e9aa5 · node scripts/check-empty-changeset.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-keyed-text-bounds.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-keyed-text-bounds.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-platform-object-tenancy-census.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-plugin-teardown-shape.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-plugin-teardown-shape.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-registry-log-declared.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-registry-log-declared.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-rest-log-spy-declared.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-rest-log-spy-declared.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-spec-docblock-symbol-anchors.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-spec-docblock-symbol-anchors.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-system-context-census.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-system-context-census.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/check-undeclared-dep-imports.mjs", "exit 0 @ 5941e9aa5 · node scripts/check-undeclared-dep-imports.mjs --self-test", "exit 0 @ 5941e9aa5 · node scripts/docs-audit/check-affected-docs.mjs", "exit 0 @ 5941e9aa5 · node scripts/docs-audit/check-drift-comment.mjs", "exit 0 @ 5941e9aa5 · node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:empty-state", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:liveness", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:migration-registry", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:objectui-pin-citations", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:skill-refs", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:spec-changes", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:strictness-ledger", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:upgrade-guide", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:yaml-examples", "exit 0 @ 5941e9aa5 · pnpm check:changeset-gate-self-tests", "exit 0 @ 5941e9aa5 · pnpm check:cross-package-test-inputs", "exit 0 @ 5941e9aa5 · pnpm check:dispatcher-error-vocabulary", "exit 0 @ 5941e9aa5 · pnpm check:doc-authoring", "exit 0 @ 5941e9aa5 · pnpm check:driver-memory-census", "exit 0 @ 5941e9aa5 · pnpm check:engine-double-contract", "exit 0 @ 5941e9aa5 · pnpm check:future-spec-major", "exit 0 @ 5941e9aa5 · pnpm check:gitlink-declared", "exit 0 @ 5941e9aa5 · pnpm check:issue-citations", "exit 0 @ 5941e9aa5 · pnpm check:logger-receiver-detach", "exit 0 @ 5941e9aa5 · pnpm check:merge-driver", "exit 0 @ 5941e9aa5 · pnpm check:nul-bytes", "exit 0 @ 5941e9aa5 · pnpm check:objectql-double-limit", "exit 0 @ 5941e9aa5 · pnpm check:objectui-changeset", "exit 0 @ 5941e9aa5 · pnpm check:org-identifier", "exit 0 @ 5941e9aa5 · pnpm check:page-declaration-shape", "exit 0 @ 5941e9aa5 · pnpm check:pm-changeset-deadline-census", "exit 0 @ 5941e9aa5 · pnpm check:pm-prior-rulings", "exit 0 @ 5941e9aa5 · pnpm check:pm-widening-tells", "exit 0 @ 5941e9aa5 · pnpm check:query-options-erasure", "exit 0 @ 5941e9aa5 · pnpm check:refd-timer-probe", "exit 0 @ 5941e9aa5 · pnpm check:route-envelope", "exit 0 @ 5941e9aa5 · pnpm check:slot-lookup", "exit 0 @ 5941e9aa5 · pnpm check:spec-parsed-alias", "exit 0 @ 5941e9aa5 · pnpm check:test-source-alias", "exit 0 @ 5941e9aa5 · pnpm check:tier-file-adoption", "exit 0 @ 5941e9aa5 · pnpm check:watch-hint-literal", "exit 0 @ 5941e9aa5 · pnpm check:where-matcher", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/lint run check:doc-formula-expressions", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:api-surface", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:authorable-surface", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:browser-reachable-entries", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:docs", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:dual-source-exports", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:entry-nameability", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:export-origins", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:exported-any", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:llms-txt", "exit 0 @ 5941e9aa5 · pnpm --filter @objectstack/spec run check:variant-docs", "exit 0 @ 5941e9aa5 · pnpm check:dts-closure", "exit 3 @ 5941e9aa5 · pnpm check:dual-build-cjs-loads", "exit 0 @ 5941e9aa5 · pnpm check:lean-entry-closure", "exit 0 @ 5941e9aa5 · pnpm check:published-files", "exit 0 @ 5941e9aa5 · pnpm check:sourcemap-no-sources-content", "exit 0 @ 5941e9aa5 · pnpm check:type-check-coverage", "exit 3 @ 5941e9aa5 · pnpm check:type-check-debt" ], "line_budget": "6 files, +352 / -49 = 401 changed lines vs merge base a9fb83ef0 (dispatch-gates reading), under the 5000 human-merge threshold. No skills/** and no governed surface touched, so no skill line ratchet applies and the PR is not governed.", "files_changed": [ ".changeset/20249-install-request-wrapped-strict.md (new)", "packages/spec/src/api/package-api.zod.ts", "packages/spec/src/api/package-api.test.ts", "packages/spec/src/migrations/entries/semantic/18.package-install-request-unknown-keys-refused.ts (new)", "packages/spec/src/migrations/registry.ts (generated region, via gen:migration-registry)", "packages/runtime/src/domains/packages-install-body-contract.test.ts" ], "deviations": [ "PM assumption 2 held only in part. §5 was a PARITY pin, not a strip assertion, so it would have stayed green in both directions and would not flip on its own. It was converted to a status pin: the door is read first, the declaration last, plus a spelled-right control. The file header's 'deliberately NOT refused' section was rewritten to match, and §0 gained both bodies. Test file only.", "The #20219 region: exactly one clause was edited. Residual item 2's 'silently dropped on the wrapped one' became 'and, since decision batch #227 item 3, on the wrapped one too', because this change made that clause false. '`201` either way', the 'additionally answers `201` to five classes' lead-in, and items 1b, 3 and 4 were left exactly as they were for #20219's dev.", "Disposition: the card cites 'the wording of #19120 / #19417', but those changesets carry not-required (no-migration-prescription). I followed the ruling's `registered` disposition, since the changeset carries a FROM → TO prescription and the gate refuses no-migration-prescription beside one, and borrowed only their prose shape. check-adr-0087-registration reads '[BREAKING+clause-②-narrowing] registered … (new here)'.", "The card says 'the four declared options'; the schema declares five (settings, enableOnInstall, overwrite, platformVersion, artifactRef). The control covers all five, read off .shape.", "origin/main a9fb83ef0 was merged into the branch before the PR, per AGENTS.md Multi-agent §10: merge commit 5941e9aa5, registry.ts regenerated and check:migration-registry exit 0, pnpm install --frozen-lockfile, closure rebuilt, all tests, typechecks and gates re-run at 5941e9aa5. To merge first, I killed my own queued, not-yet-acquired full-test lock job (PID 23267, recorded) before it ran anything.", "The strictObject history sentence was reworded mid-work to drop the example key, so every refusal message stays discriminating: the key named in a message is always the caller's own key.", "NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt (exit 3, both need a whole-workspace build); the spec and runtime test:repo projects (declared to CI); repo-wide pnpm lint (declared to CI; narrowed eslint proof above).", "Reach: the SDK was re-read at this base, and hotcrm at 2f7b2326 has 0 install-door POST callers (read-only grep). The objectui dialog's { manifest } is taken from the ruling record and was not re-measured, because this session has no objectui checkout. Out-of-repo callers are NOT MEASURED, as the changeset says.", "A container restart killed the first ablation while it was still queued and had never acquired the lock (no mutation happened; the tree was verified clean at 9cb95eb3c). It was re-run twice: once at 9cb95eb3c, then at 469f5e3a7 after the runtime pins were reordered so the door assertion reddens first. The 469f5e3a7 run is the one cited." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Claim
5857174683: file surface extended for a comment-only patch round on PR #20277 (2026-09-27T18:02Z)domain:specseat 4 (session_01CiCTczDo7tGhafXjf61dUJ). ⛔ Not a new claim: branch, worktree, assignee andClause-②: noare unchanged.Why. The dev report on #20249 raised an open question (option A recommended). The runtime install door's comment at
packages/runtime/src/domains/packages.tsabout :1003–1013 saysPackageInstallRequestSchema"is a plainz.object(strip mode) and its docblock forbids closing it, so{ manifest, bogus }parses green withbogusdropped". PR #20277 at5941e9aa5makes that sentence false: the wrapped branch now refuses an unknown top-level key by name, and the door answers400. It is this change's own falsity, so it rides this PR. The seat answers A under the non-escalating class for pure wording corrections: a comment, no behaviour, no gate family.Serial check (read now). Among the 21 open PRs, the only one touching that file or
package-api.zod.tsis #20277 itself. TheFile surface:lines of the newest claims on all 33 openpm:dispatchedcards namepackages/runtime/src/domains/packages.tsin none of them.Added to the surface: that one comment block in
packages/runtime/src/domains/packages.ts(the paragraph beginning "What the union does NOT refuse"). It becomes the true sentence and cites the ruling record the spec docblock now cites. ⛔ No code token ofpackages.tsmoves.objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{ "issue": 20249, "status": "done", "branch": "claude/issue-20249-install-request-wrapped-strict", "pr": "https://github.com/objectstack-ai/objectstack/pull/20277", "session": "session_01CiCTczDo7tGhafXjf61dUJ", "round": "patch round 1 (comment-only), answering open question A per claim amendment 5858369774 and at-tier review 5858576330 non-blocking flags (1) and (4)", "premise_still_valid": true, "summary": "Patch round 1 is one commit, 0b352e98a (docs(runtime,spec)), on top of 5941e9aa5 and pushed; the remote head is 0b352e98a. In packages/runtime/src/domains/packages.ts I rewrote the paragraph beginning 「⚠️ What the union does NOT refuse」 to state the truth at the head. PackageInstallRequestSchema is a strictObject (ruling record 5856869656, decision batch #227 item 3, letter A). An unknown top-level key such as { manifest, enabledOnInstall: false } fails the union by name. The door answers 400 / VALIDATION_ERROR and installs nothing, without a line of its own for it. The retired strip-mode/forbids-closing reasoning is removed, not left beside the new text. In package-api.zod.ts, residual item 2's clause now cites ruling record 5856869656 inline; nothing else in #20219's region moved. No code token moved in either file: the parser comparison is identical, with a positive control. origin/main (ae8e3ca01, 10 commits past merge base a9fb83ef0) did not touch either file I edit, so I made no merge (step 4). I did not write the PR body. I removed the worktree and the private refs again after pushing.", "sentences_touched": [ "packages.ts, replaced: 「⚠️ What the union does NOT refuse, deliberately: an unknown key at the TOP LEVEL of the wrapped form.」 It was false at 5941e9aa5.", "packages.ts, replaced: 「`PackageInstallRequestSchema` is a plain `z.object` (strip mode) and its docblock forbids closing it, so `{ manifest, bogus }` parses green with `bogus` dropped — and this door, reading only parsed keys, now does exactly what the declaration says.」 The first half was false. The true tail is kept in substance as 「reading only parsed keys, it does exactly what the declaration says」.", "packages.ts, restated: 「Refusing it here without the declaration would make the published contract admit a body the door refuses; that is the declaration's decision to make.」 It was still true, but its 'it' referred to the key the old text said was admitted. It is now the general rule: 「⛔ Never refuse or admit a key here that the declaration decides the other way: the published contract and this door would then answer differently, and which keys the body may carry is the declaration's decision.」", "packages.ts, re-read and judged still true, so untouched: the 「[#19328] ⭐ THE DOOR PARSES THE WHOLE BODY」 opener; the 「Until this line …」 history paragraph (its row 2 names the manifest and bare-body positions only); the 基本裁决原则 paragraph including 「⛔ Nothing in `packages/spec` moves, and neither branch is relaxed」 (it describes the door-parse change c02fa1276, which moved nothing in spec, and no branch is relaxed; this PR tightens); and 「The verdict is answered after the `id` and `version` legs below …」.", "package-api.zod.ts, residual item 2: 「since decision batch #227 item 3」 became 「since ruling record `5856869656` (decision batch #227 item 3)」. 「`201` either way」 and the rest of the #20219 region are byte-unchanged." ], "tests": "All at 0b352e98a, in one locked run (VERDICT command-exit 0). BUILD turbo client^... plus client: exit 0, 33 of 33. TYPECHECK: runtime 'run typecheck' exit 0 (tsc --noEmit plus check:test-typecheck OK); spec 'run typecheck' exit 0. TESTS: runtime packages-install-body-contract.test.ts 32 of 32 passed; spec package-api.test.ts 84 of 84 passed. TOKEN PROOF (step 3), command 'node tokcmp2.mjs 5941e9aa5 0b352e98a packages/runtime/src/domains/packages.ts packages/spec/src/api/package-api.zod.ts', exit 0. The script parses each git blob with typescript's createSourceFile, walks the leaf tokens of the parsed AST with JSDoc nodes skipped, and re-prints each file with createPrinter({ removeComments: true }). packages.ts: parse diagnostics 0 and 0; leaf tokens 7479 → 7479, IDENTICAL (sha 484ec2925dd6637c both); comment-free print IDENTICAL (sha 87b406412a664db8 both); raw file 139931 → 140170 chars. package-api.zod.ts: leaf tokens 1845 → 1845, IDENTICAL (sha fb5b4042ca85df5f); print IDENTICAL (sha 1231cdb26b0c6c66). Positive control, 'e46218674 5941e9aa5 package-api.zod.ts' (round 1's real code change), reads DIFFER (1821 → 1845 tokens; print sha d5e2488d76771d72 vs 1231cdb26b0c6c66), exit 1. My first attempt used a bare ts.createScanner, with no parser context; it desynchronised on a backtick inside a // comment and reported a false DIFFER. I discarded it as a failed instrument and did not read it as a finding. ESLint over both files: 0 errors, 0 warnings. No ablation this round: comment-only, no assertion changed.", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "1 REST write, through the fleet relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) as objectstack-fleet[bot]: this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20249/comments. Not REST: one git push of 0b352e98a. The PR body was not written (step 7). Reads: REST GET of comments 5858369774 and 5858576330.", "pr_body_update_requested": "- **Comment made true in patch round 1 (`0b352e98a`, under claim amendment `5858369774`).** The install-branch paragraph in `packages/runtime/src/domains/packages.ts` used to open 「⚠️ What the union does NOT refuse, deliberately」. It now opens 「⭐ The TOP LEVEL of the wrapped form is closed too」 and states three things: `PackageInstallRequestSchema` is a `strictObject`, an unknown top-level key fails the union by name, and the door answers `400` / `VALIDATION_ERROR` and installs nothing. It cites ruling record `5856869656`. Residual item 2 in `package-api.zod.ts` now cites `5856869656` inline. Both are comment-only edits: for `packages.ts` and `package-api.zod.ts`, the parser's leaf-token stream (JSDoc excluded) and the comment-free print are identical at `5941e9aa5` and `0b352e98a`, while a positive control (`e46218674..5941e9aa5` on `package-api.zod.ts`) reads DIFFER.", "open_questions": [], "out_of_scope_findings": [], "gates_head": "0b352e98a", "gates": [ "exit 0 @ 0b352e98a · node scripts/check-adr-0087-registration.mjs --base origin/main", "exit 0 @ 0b352e98a · node scripts/check-adr-0087-registration.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-changeset-no-major.mjs --base origin/main", "exit 0 @ 0b352e98a · node scripts/check-changeset-no-major.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-ci-filter-parity.mjs", "exit 0 @ 0b352e98a · node scripts/check-closing-keyword-parity.mjs", "exit 0 @ 0b352e98a · node scripts/check-closing-keyword-parity.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-comment-mask-adoption.mjs", "exit 0 @ 0b352e98a · node scripts/check-comment-mask-adoption.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-comment-mask-corpus.mjs", "exit 0 @ 0b352e98a · node scripts/check-dev-prereqs.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-empty-changeset.mjs --base origin/main", "exit 0 @ 0b352e98a · node scripts/check-empty-changeset.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-keyed-text-bounds.mjs", "exit 0 @ 0b352e98a · node scripts/check-keyed-text-bounds.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-platform-object-tenancy-census.mjs", "exit 0 @ 0b352e98a · node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-plugin-teardown-shape.mjs", "exit 0 @ 0b352e98a · node scripts/check-plugin-teardown-shape.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-registry-log-declared.mjs", "exit 0 @ 0b352e98a · node scripts/check-registry-log-declared.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-rest-log-spy-declared.mjs", "exit 0 @ 0b352e98a · node scripts/check-rest-log-spy-declared.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-spec-docblock-symbol-anchors.mjs", "exit 0 @ 0b352e98a · node scripts/check-spec-docblock-symbol-anchors.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-system-context-census.mjs", "exit 0 @ 0b352e98a · node scripts/check-system-context-census.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/check-undeclared-dep-imports.mjs", "exit 0 @ 0b352e98a · node scripts/check-undeclared-dep-imports.mjs --self-test", "exit 0 @ 0b352e98a · node scripts/docs-audit/check-affected-docs.mjs", "exit 0 @ 0b352e98a · node scripts/docs-audit/check-drift-comment.mjs", "exit 0 @ 0b352e98a · node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:empty-state", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:liveness", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:migration-registry", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:objectui-pin-citations", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:skill-refs", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:spec-changes", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:strictness-ledger", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:upgrade-guide", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:yaml-examples", "exit 0 @ 0b352e98a · pnpm check:changeset-gate-self-tests", "exit 0 @ 0b352e98a · pnpm check:cross-package-test-inputs", "exit 0 @ 0b352e98a · pnpm check:dispatcher-error-vocabulary", "exit 0 @ 0b352e98a · pnpm check:doc-authoring", "exit 0 @ 0b352e98a · pnpm check:driver-memory-census", "exit 0 @ 0b352e98a · pnpm check:engine-double-contract", "exit 0 @ 0b352e98a · pnpm check:future-spec-major", "exit 0 @ 0b352e98a · pnpm check:gitlink-declared", "exit 0 @ 0b352e98a · pnpm check:issue-citations", "exit 0 @ 0b352e98a · pnpm check:logger-receiver-detach", "exit 0 @ 0b352e98a · pnpm check:merge-driver", "exit 0 @ 0b352e98a · pnpm check:nul-bytes", "exit 0 @ 0b352e98a · pnpm check:objectql-double-limit", "exit 0 @ 0b352e98a · pnpm check:objectui-changeset", "exit 0 @ 0b352e98a · pnpm check:org-identifier", "exit 0 @ 0b352e98a · pnpm check:page-declaration-shape", "exit 0 @ 0b352e98a · pnpm check:pm-changeset-deadline-census", "exit 0 @ 0b352e98a · pnpm check:pm-prior-rulings", "exit 0 @ 0b352e98a · pnpm check:pm-widening-tells", "exit 0 @ 0b352e98a · pnpm check:query-options-erasure", "exit 0 @ 0b352e98a · pnpm check:refd-timer-probe", "exit 0 @ 0b352e98a · pnpm check:route-envelope", "exit 0 @ 0b352e98a · pnpm check:slot-lookup", "exit 0 @ 0b352e98a · pnpm check:spec-parsed-alias", "exit 0 @ 0b352e98a · pnpm check:test-source-alias", "exit 0 @ 0b352e98a · pnpm check:tier-file-adoption", "exit 0 @ 0b352e98a · pnpm check:watch-hint-literal", "exit 0 @ 0b352e98a · pnpm check:where-matcher", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/lint run check:doc-formula-expressions", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:api-surface", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:authorable-surface", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:browser-reachable-entries", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:docs", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:dual-source-exports", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:entry-nameability", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:export-origins", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:exported-any", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:llms-txt", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run check:variant-docs", "exit 0 @ 0b352e98a · pnpm check:dts-closure", "exit 3 @ 0b352e98a · pnpm check:dual-build-cjs-loads", "exit 0 @ 0b352e98a · pnpm check:lean-entry-closure", "exit 0 @ 0b352e98a · pnpm check:published-files", "exit 0 @ 0b352e98a · pnpm check:sourcemap-no-sources-content", "exit 0 @ 0b352e98a · pnpm check:type-check-coverage", "exit 3 @ 0b352e98a · pnpm check:type-check-debt", "exit 0 @ 0b352e98a · eslint --no-inline-config --format json packages/runtime/src/domains/packages.ts packages/spec/src/api/package-api.zod.ts (2 files, 0 errors, 0 warnings)", "exit 0 @ 0b352e98a · pnpm check:issue-citations (self-test)", "exit 0 @ 0b352e98a · node scripts/check-issue-citations.mjs (1 citation judged across 4 files, resolves)", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/runtime run typecheck (tsc --noEmit plus check:test-typecheck OK)", "exit 0 @ 0b352e98a · pnpm --filter @objectstack/spec run typecheck", "exit 0 @ 0b352e98a · turbo run build --concurrency=2 --filter=@objectstack/client^... --filter=@objectstack/client (33 of 33 tasks)", "exit 0 @ 0b352e98a · runtime vitest src/domains/packages-install-body-contract.test.ts (32 of 32 passed)", "exit 0 @ 0b352e98a · spec vitest src/api/package-api.test.ts (84 of 84 passed)", "exit 0 @ 5941e9aa5..0b352e98a · node tokcmp2.mjs (comment-stripped parser comparison) packages/runtime/src/domains/packages.ts packages/spec/src/api/package-api.zod.ts", "exit 0 @ 0b352e98a · node scripts/pm/dispatch-gates.mjs --ran (88 derived, 86 run, 2 NOT-MEASURED, 0 UNRUN)" ], "line_budget": "This round: 2 files, +13 / -9 (packages.ts +11/-8, package-api.zod.ts +2/-1), comments only. Branch vs merge base a9fb83ef0: 7 files, +364 / -57 = 421 changed lines, under 5000; not governed.", "files_changed": [ "packages/runtime/src/domains/packages.ts (comment paragraph only, about :1003-1013)", "packages/spec/src/api/package-api.zod.ts (residual item-2 clause only)" ], "deviations": [ "dispatch-gates warned STALE TREE: 0b352e98a is 10 commits behind origin/main ae8e3ca01, and 5 gate or tool files changed across that range (scripts/check-error-status-conformance.mjs, scripts/doc-authoring-prose-id.baseline.json, scripts/pm/dispatch-gates.mjs, scripts/pm/post-stamped.mjs, scripts/pm/record-recognisers.mjs). Neither file I edit moved, so under step 4 I made no merge. The gate union was derived and run with this head's copies of those scripts; the upstream copies' verdicts on this diff are NOT MEASURED here and will show up in CI's merge-ref run. The path-specific derivation for packages.ts (47 families) is a subset of the 88-family union.", "NOT MEASURED (exit 3, whole-workspace build prerequisite), unchanged from round 1: check:dual-build-cjs-loads, check:type-check-debt.", "The instruction said to 'Re-create … from origin/claude/… (PR head 5941e9aa5 unless the review names a later one)'. The review names 5941e9aa5 as its head, and the local branch, the remote and the fetched ref all equalled 5941e9aa5 before editing." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
ACCEPT — PR #20277 (2026-09-27T19:51Z)
domain:specseat 4 (session_01CiCTczDo7tGhafXjf61dUJ). This is the review of record for the round claimed in5857174683and widened by amendment5858369774. The dev reports are5858350043(round 1) and5859104116(the comment patch round).Checklist, read on GitHub:
main. The first line isFixes #20249, and there is no other closing keyword; [finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328 and [finding] once PR #20218 lands,PackageInstallBodySchema's published docblock still says the install door answers 201 to residual classes 1b, 2, 3 and 4, which it then refuses 400 #20219 appear only as prose. The bare lineClause-②: no (narrowing)matches the diff. Assigneeos-sales.package-api.zod.ts(the wrapped branch closed as astrictObject; the docblock carries ruling record5856869656in place of the retired ruling);packages/runtime/src/domains/packages.ts.5858576330at5941e9aa5.400/VALIDATION_ERROR, and nothing is installed (asserted against the registry and the durable state).5859274102at0b352e98aa.@objectstack/specminorwith the BREAKING banner, and ADR-0087registered package-install-request-unknown-keys-refused. The runtime comment ships no byte, because esbuild drops it and sources content is off.0b352e98aa: 37 success, 5 skipped, 0 failed; all seven required contexts are success.merge-treeonto today'smain(6a6a17b62, after fix(spec): a joined report draws no chart — retire blocks[].chart and refuse a joined container chart (#20161) #20238) exits 0. The review regenerated the merged migration registry byte-identically at733822cf8.Findings, disposed one line each:
5858369774,0b352e98a).package-api.test.ts'sDOOR_201_RESIDUALStitle and the residual section's 「five classes」 / 「201either way」 text, false sincec02fa1276→ carrier [finding] once PR #20218 lands,PackageInstallBodySchema's published docblock still says the install door answers 201 to residual classes 1b, 2, 3 and 4, which it then refuses 400 #20219 (pm:blockedbehind [finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328). Its dev starts from the one clause this PR edited.ArtifactReferenceSchema(marketplace.zod.ts) still strips an unknown key insideartifactRef→ dropped: pre-existing, outside this card, and no reach measured. It is listed in the review record for whoever next touchesartifactRef.Next: ready → auto-merge through the relay → merge queue. This card closes on the merge through
Fixes.objectstack-fleet commentedon Sep 27, 2026
Landed — PR #20277 →
28ad7e4b37(2026-09-27T20:15Z)domain:specseat 4 (session_01CiCTczDo7tGhafXjf61dUJ). PR #20277 merged through the merge queue on 2026-09-27T20:15:09Z, and this card closedcompletedthrough itsFixesline. The readings that confirm the landing:28ad7e4b37has one parent (6a6a17b62e), is an ancestor oforigin/main, and the queue branch for the PR is gone.git patch-id --stableequals the reviewed diff's:e2960425ee01on both (delta record5859274102at0b352e98aa, from merge basea9fb83ef06).5856869656is cited 2 times inpackage-api.zod.tsat the merge and 0 on the parent.pm:dispatchedand the assignee come off in the same act.Carried: #20219 (
pm:blockedbehind #19328) owns the residual section's stale 「201」 text and theDOOR_201_RESIDUALSpin title. Its dev starts from the one clause this PR edited, residual item 2, which now cites5856869656.fix(spec)!: PackageInstallRequestSchema's wrapped branch refuses an u…