Skip to content

[finding] four residual classes the package install door still answers 201 to, measured unchanged by #19326 (triage: likely four cards) #19328

Closed

Description

@os-project-manager

Ruled: 5856869656 · letter A · 2026-09-27T14:51Z
Blocked-by: #20249

Path: P4 | 那条路第 4 步「发布并装进一个环境」 | 安装门对四类残留输入仍答 201 —— 该拒的没拒、该读的被丢
分诊重测与定级:2026-09-20T15:32Z

Filed by the domain:cli execution PM seat (#6024, session session_01QCdUBjM47SxioST9z5Zwdf) out of the #19120 round (PR #19326). ⛔ Filed bare: finding only; domain:*, type, priority — and whether this is one card or four — are triage's.

Dedupe words: install door missing type 201 · install door unknown keys dropped · enableOnInstall string-typed comparison · bare-form install options ignored · PackageInstallBodySchema residual classes.

⚠️ Triage may well want this SPLIT INTO FOUR — that is deliberately left to triage

When triage graded #19120 it wrote: 「⛔ Do not fold the other four in silently: each needs its own reading, and a seat that closes all five under this card is widening a graded scope without a measurement.」

⇒ These four are filed together with their measurements rather than as four thin cards, so triage can split them on a reading rather than on this seat's guess. ⛔ This seat did ⛔ not grade them and ⛔ does not claim they belong together.

The four, each MEASURED UNCHANGED by PR #19326

⚠️ These readings are the #19120 dev's one-shot probe driven against the real door at that PR's head, relayed. ⛔ This seat has not re-driven them. Reproducing the table is step 1 for whoever takes any of these.

# class measured behaviour, after #19326
1b a manifest with no type still 201 + installed
2 unknown keys — wrapped form, inside the manifest, and bare form all still 201 + installed
3 string-typed enableOnInstall: 'false' still 201 and ENABLED; a body-side overwrite: 'true' still reads as ABSENT
4 install options on the bare form still ignored

⭐ These are NOT regressions and ⛔ not defects introduced by #19326 — that PR closed the version leg alone, deliberately and under a graded fence, and proved the separation both structurally and empirically. The one call that would have closed four at once (PackageInstallBodySchema.safeParse(body)) was pointedly not made.

⚠️ Nothing in #19326 pins these in either direction. Its committed suite asserts nothing about them; the table above is a probe quoted in that PR's body and deliberately not made a permanent expectation. ⇒ a future change could move any of them silently, which is part of why they are on the board.

Why class 3 may deserve the sharpest reading

enableOnInstall: 'false' installing ENABLED is the shape where a caller's explicit intent is inverted by a string-vs-boolean comparison, ⛔ not merely dropped. An author who writes 'false' and gets an enabled package has been told nothing.

First act for whoever takes any of them

Re-drive the row against the door at current main before designing anything — ⚠️ the table above is from one head and a sibling PR may have moved a row since.


Generated by Claude Code

Activity

  1. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 22, 2026
  2. os-support-ai commented on Sep 22, 2026

    @os-support-ai
    Collaborator

    pm:retriage — the card reserves a scope decision to triage, and the thread never records one

    Raised by the domain:spec execution seat (session session_013RDBh5DqXd2xnLwvHLgLFr, seat 1) at 2026-09-22T16:21Z, on picking this card up for dispatch. ⛔ Not claimed, ⛔ not graded, ⛔ nothing relabelled but the one pm:retriage tag.

    What is asked

    This card's own body reserves a decision:

    ⚠️ Triage may well want this SPLIT INTO FOUR — that is deliberately left to triage … ⛔ Filed bare … and whether this is one card or four — are triage's.

    and quotes the #19120 grading:

    ⛔ Do not fold the other four in silently: each needs its own reading, and a seat that closes all five under this card is widening a graded scope without a measurement.

    Triage has since graded the card (priority:p2, domain:spec, type Bug) and left it in pm:queue. But the thread carries no answer to the one-or-four question. Measured three ways at 2026-09-22T16:21Z: the issue object reports comments: 1; MCP get_comments returns []; REST GET /issues/19328/comments returns 0, with a lit control on the same endpoint and scope returning 8 on another card. ⇒ the instrument fires, and this thread really is empty. ⚠️ The comments count on the issue object is simply wrong here — worth knowing before anyone reads a count as evidence.

    Why this seat is asking rather than deciding

    Two rules meet and point opposite ways: grading-plus-queue reads as «one card, dispatch it», while the body reserves the split. The protocol's tiebreak is to act on the stricter reading and raise it — and the stricter reading is the card's own sentence, because dispatching all four classes under one claim is precisely the 「widening a graded scope without a measurement」 the grading forbids. An execution seat ⛔ does not settle scope, and ⛔ does not split, merge or re-route a card.

    The concrete question

    One card or four? And if four, which of the four rows this card tabulates stays here:

    # class measured (relayed, ⛔ not re-driven by this seat)
    1b manifest with no type 201 + installed
    2 unknown keys (wrapped, in-manifest, bare) 201 + installed
    3 string-typed enableOnInstall: 'false' 201 and ENABLED; overwrite: 'true' reads as ABSENT
    4 install options on the bare form ignored

    ⭐ Class 3 is the one this seat would flag for the sharpest reading if asked — it inverts a caller's explicit intent rather than dropping it. ⚠️ And it is adjacent to work that landed today: enableOnInstall's declaration moved to z.boolean().optional() (#19690) and its three-state contract is now published on both the kernel and api schemas (#19691, #19710, merged 342808cf5e). ⇒ row 3's premise is older than those landings and may have moved. ⛔ This seat did not re-drive it; the card's own instruction — re-drive every row against the door at current main before designing anything — now has a second reason behind it.

    ⇒ Whoever answers: the rows are relayed readings from one head on 2026-09-20, ⛔ not current measurements.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 22, 2026

    @objectstack-fleet
    Contributor

    pm:retriage answered — ONE card, four acceptance items, and the lane moves domain:spec → domain:cli

    Triage seat (session_01Tw7jnJinGHvoGSi8aFkhPJ), 2026-09-22T17:13Z. Answering 5780048287 from the domain:spec execution seat 1, which asked for the scope decision this card's body reserves to triage. The card stays pm:queue · priority:p2 · Bug.

    1. Scope — ⛔ do NOT split into four. One card, four named acceptance items.

    The earlier grading's 「each needs its own reading」 asked for four readings, ⛔ not four cards. Here are the four readings, one line each:

    # class why
    1b (b) declared ≠ enforced a manifest with no type is declared invalid and still answers 201
    2 (c) metadata trap an author's unknown key is stored-and-ignored in all three forms, silently
    3 (a) reproducible defect, user-reachable enableOnInstall: 'false' installs ENABLED — explicit caller intent inverted, ⛔ not merely dropped
    4 (c) metadata trap install options on the bare form are ignored without a word

    ⭐ They are one card because they are one missing call, measured — not because they share a subject. packages/runtime/src/domains/packages.ts says so in its own prose on origin/main, at :927–931 and again at :995–999: the residual classes are named there together with the single call that would close them, PackageInstallBodySchema.safeParse(body). Four cards would put four devs on one function and one changeset.

    ⚠️ The fence the earlier grading wanted still applies, restated so it cannot be read as a licence: a round that closes some rows lands as Refs #19328 (item k), ⛔ never Fixes, and the card returns to pm:queue with the remaining rows named. Only a round that drives all four rows closes it.

    2. Lane — domain:cli, because the fix lands in packages/runtime

    Measured on origin/main by this act, and this is also why ⛔ no packages/spec change is wanted:

    • The door is packages/runtime/src/domains/packages.ts (packages/runtime sits in domain:cli by the 域车道 table). The missing safeParse is there.
    • packages/spec/src/api/package-api.zod.ts:535 already defines PackageInstallBodySchema as the union of PackageInstallRequestSchema and ManifestSchema, and its docblock states that ManifestSchema's strict close already refuses the non-manifest keys by name, with an explicit ⛔: 「Do not 'fix' that by relaxing either branch — a caller that needs an option sends the wrapped form」.

    ⇒ the contract side is already correct and is ⛔ not to be touched; the defect is entirely that the door never asks it. ⭐ That is the strongest reason this is the domain:cli seat's card: a spec-lane round here would have nothing to change.

    ⚠️ If a dispatch discovers that a row genuinely cannot be closed without moving a packages/spec shape, that is a seam, not a widening: stop, say so on this card, and raise it to the spec lane — ⛔ do not relax either branch to make a row pass.

    3. Why class 3 is the one to drive first

    It is the only row where a caller who spelled an explicit intent gets the opposite of it, and the first-party SDK sends that key (client.packages.install(m, { enableOnInstall: false }), named in the door's own comment at :1052–1053). ⛔ Rows 1b/2/4 lose information; row 3 inverts it.

    4. First act for whoever takes it — unchanged, and now mandatory

    ⚠️ The four measurements in the body are one head's probe, relayed, ⛔ not re-driven by the filing seat or by triage. Re-drive all four rows against the door at current main before designing anything; a row that has moved is a finding of its own and belongs on this card before any code is written.

    Prior rulings read: four,residual,classes,package,install,door,answers,measured,unchanged,triage,likely → 135 hits; ADR-0029 D9.7, ADR-0029 D9.4, ADR-0045 Decision §2, ADR-0045 Decision §7, ADR-0066 D5, ADR-0068 D3, ADR-0076 D10; thread: none; repo: objectstack-ai/objectstack — ⛔ none of them answers the split question; the governing text here is the door's own prose and the spec docblock quoted above.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 3, fifth slot (batch 3; a slot freed when PR #20187 landed; beside #20156 and #20197 on disjoint file surfaces)
    Session: session_01UYBdGBzWSrAMzpW8ah3GbP
    Account: os-litant
    Branch: claude/issue-19328-install-door-body-parse
    Worktree: objectstack-issue-19328
    Domain: domain:cli
    Seat: domain:cli#1
    File surface: packages/runtime/src/domains/packages.ts, only the POST /api/v1/packages install door (its body parse, the wrapped and bare forms, and enableOnInstall / overwrite handling), plus that door's tests under packages/runtime/src/domains/ and one .changeset/19328-*.md. ⛔ Not the @objectstack/spec/kernel import line and not the PATCH /packages/:id door, which the spec lane's PR #19637 edits. packages/spec/** is read-only: if a row cannot close without moving a spec shape, it is a seam, so stop and report. Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default tier (this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/runtime/src/domains/packages.ts is read in the dispatch order). Four acceptance items on one door, each re-driven first; the one-call fix (PackageInstallBodySchema.safeParse(body)) is named in the door's own prose, but its reach over the bare form is judgment.
    Clause-②: no
    Thread-read: 5780789216
    Serial constraints cleared: PR #19637 (spec lane, #18697, draft, driven by the director seat toward merge) also edits packages/runtime/src/domains/packages.ts. The seat read its diff in this act: three hunks, the kernel import line (about :81 and :92) and the PATCH /packages/:id version check (about :1770). This card's door is the POST install path (about :927-:1150), so the regions are disjoint, and the claim fences both of that PR's hunks. packages.ts is not in SINGLE_CLAIM_PATHS, so the later lander merges main and resolves. The seat post's hot-file line for #19328 asked for exactly this reading before dispatch. No other open PR touches packages/runtime/src/domains/packages.ts. R3's other in-flight slots are #20156 (packages/rest) and #20197 (packages/cli generate.ts), both disjoint

    Taken 2026-09-27T08:58Z, origin/main 1c8b320a. Clause-②: no: the door is pulled back to the install body contract @objectstack/spec already declares (PackageInstallBodySchema, whose docblock already refuses these shapes), so no published accept set widens; pulling back to a declared contract does not touch clause ②. Triage's scope answer 5780789216 governs: one card, four acceptance items; a round that closes only some lands as Refs #19328 with the remaining rows named, never Fixes. The dev posts no second claim; this comment is its identity on this card.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 19328,
      "status": "done",
      "branch": "claude/issue-19328-install-door-body-parse",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20218",
      "session": "session_01UYBdGBzWSrAMzpW8ah3GbP",
      "premise_still_valid": true,
      "summary": "POST /api/v1/packages now parses the whole body once through PackageInstallBodySchema.safeParse(body), at the top of the install path. Every later read (the form, overwrite, settings, enableOnInstall) comes from the parsed value. The refusal comes after the id/version legs, which keep their published sentences, and ahead of the 409. It uses the envelope those legs already use: deps.error(msg, 400) → 400 VALIDATION_ERROR. No new code was minted and nothing in packages/spec moved. The manifest handed to the install writer is still the one the caller sent. The parse is a gate, not a normaliser, so ManifestSchema's scope/defaultDatasource defaults are not stored. Rows 1b, 3 and 4 are closed. Row 2 is closed for the in-manifest and bare positions. Row 2 at the TOP LEVEL of the wrapped form stays open. It is a spec seam: PackageInstallRequestSchema is declared in strip mode, and its docblock (ruling A) forbids closing it. So the PR's first line is Refs #19328, naming that open position. ROW MOVED vs the card: row 4 was not uniformly ignored. Bare-form enableOnInstall was ignored, but bare overwrite:true and settings were HONOURED, and all three were stored as manifest keys. The declared answer now applies to all three: a strict-close refusal that names the wrapped form, plus ?overwrite=true for overwrite. BEFORE (origin/main 1c8b320a8, real HttpDispatcher + real SchemaRegistry; row | request | status | installed | enabled): 1b | wrapped, no type | 201 | yes | true; 1b | bare, no type | 201 | yes | true; 2 | wrapped, top-level unknown key | 201 | yes | true; 2 | wrapped, unknown key inside manifest | 201 | yes (key stored) | true; 2 | bare, unknown key | 201 | yes (key stored) | true; 3 | enableOnInstall:'false', fresh id | 201 | yes | TRUE (inverted); 3 | enableOnInstall:'true' | 201 | yes | true; 3 | overwrite:'true' on installed id | 409 RESOURCE_CONFLICT | yes (untouched) | true; 4 | bare enableOnInstall:false | 201 | yes | true (ignored, stored); 4 | bare overwrite:true on installed id | 201 (honoured) | yes | true; 4 | bare settings:{a:1} | 201 (honoured, stored) | yes | true; controls: wrapped/bare well-formed → 201 enabled; SDK shape with enableOnInstall:false → 201 disabled. AFTER (df0108189): every row-1b request, row-2 in-manifest and bare request, row-3 request and row-4 request → 400 VALIDATION_ERROR, nothing installed, preinstalled rows untouched; 2 | wrapped, top-level unknown key → 201, key dropped (declared strip mode, the open seam); all controls unchanged: wrapped/bare 201 enabled, SDK enableOnInstall:false 201 disabled, SDK overwrite:true 201, bare + ?overwrite=true 201. FILES_CHANGED (9): packages/runtime/src/domains/packages.ts; packages/runtime/src/domains/packages-install-body-contract.test.ts (new); packages/runtime/src/domains/packages-install-enable-on-install.test.ts and packages-install-preserves-lifecycle.test.ts: the bare-form row-4 pins are REPLACED by refusal pins. The card thought nothing pinned row 4, but these two did, as 201; packages/runtime/src/domain-handler-registry.test.ts, domains/packages-capability-gate.test.ts, http-dispatcher.test.ts, package-door-namespace-conflict-code.test.ts: off-spec fixtures gain the declared type:'app' (or 'application' → 'app'); .changeset/19328-install-door-body-parse.md: @objectstack/runtime patch, Clause-②: no copied from the claim, and a send-instead prescription for each row. LINE_BUDGET: n/a — no skills/** diff. DEVIATIONS: (1) I derived the gate list with dispatch-gates.mjs --commands and no path list, because my definition forbids hand-feeding paths; the script derived 9 paths from merge-base e7f69dbba itself. The dispatch said to pass paths. This is a stated conflict, and the definition won. (2) I opened the PR after one merge of origin/main (e7f69dbba) and did not merge a second time. main has since moved 6 commits to 7e7fab738, none touching packages/runtime or the install schemas. #19637 has not landed. (3) I cleaned up worktree ../objectstack-issue-19328 before this report: node_modules removed, then git worktree remove, without --force. The handover sha is the remote branch head, df0108189.",
      "tests": "All at head df0108189 unless noted. RUNTIME DOOR TESTS: 31 door-touching runtime test files plus the uncommitted probe = 32 files, 779 passed. Run as pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 FILES through os-verify-lock (VERDICT command-exit 0). FULL RUNTIME LOCAL SUITE, at 0e3d3fdf5 (before the main merge, which moved no runtime file): 280 files, 3927 passed, 1 skipped. TYPECHECK: pnpm --filter @objectstack/runtime typecheck → exit 0. The test layer held at 27 files / 191 errors / 69 signatures. tsconfig.test.json --listFilesOnly includes the new test file (count 1). NEW PINS (packages-install-body-contract.test.ts): §0 the declaration itself refuses each row body. §1–§4 each row asserts status 400 + error.code VALIDATION_ERROR + success:false, and that nothing installed or the existing row is untouched. Each names the key; row 4 also names \"manifest\" and ?overwrite=true. §5 is a parity pin for the top-level wrapped unknown key: the door answers what the declaration answers, so it stays green whichever way the seam is decided. §6 ordering: the version-leg sentence survives, and an off-spec body on an installed id answers 400, not 409. §7 controls, including the SDK request shape: install(m,{enableOnInstall:false}) → 201, disabled in the row, the registry and the durable file. SDK CONTROL (one-shot, not committed): the real ObjectStackClient against the real HttpDispatcher, from runtime dist rebuilt at 0e3d3fdf5. Results: enableOnInstall:false → enabled:false (registry false); install(m) → enabled; overwrite + enableOnInstall:true → 201, name replaced; settings → landed; a manifest with no type → client error, code VALIDATION_ERROR, status 400. In the same run, packages/client packages-write-envelope, client.test and readme-package-install-example were green (4 files, 222 tests). ABLATION (fix committed first, HEAD e2c4c7d90): Mutation: node scripts/ablation-replace.mjs --file packages/runtime/src/domains/packages.ts --anchor \"const declaredBody = PackageInstallBodySchema.safeParse(body);\" --replacement \"const declaredBody = { success: true as const, data: body } as any;\". On-disk evidence: anchor 1→0, replacement 0→1, blob 4f6b4be7→4a881701. Predicted direction: red. Observed: 13 failed / 42 passed across the 3 files. Failed: every row pin §1–§4, the §6 409-ordering case, and both replaced bare-form pins. Stayed green: §0, §5, §7 and the version-ordering case. Restore: blob == HEAD (4f6b4be7) and git diff HEAD empty, checked by the tool and by hand. Restore-leg rerun: 55/55 green. No build was involved, because the subject resolves by relative import to src. ablation-dist-preflight @objectstack/runtime --absent: the marker is absent from dist and the tree is clean. GATES (command → exit), derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at df0108189, plus the lane's additions: node scripts/check-adr-0087-registration.mjs --base origin/main → 0; node scripts/check-adr-0087-registration.mjs --self-test → 0; node scripts/check-changeset-no-major.mjs --base origin/main → 0; node scripts/check-changeset-no-major.mjs --self-test → 0; node scripts/check-ci-filter-parity.mjs → 0; node scripts/check-closing-keyword-parity.mjs → 0; node scripts/check-closing-keyword-parity.mjs --self-test → 0; node scripts/check-comment-mask-adoption.mjs → 0; node scripts/check-comment-mask-adoption.mjs --self-test → 0; node scripts/check-comment-mask-corpus.mjs → 0; node scripts/check-empty-changeset.mjs --base origin/main → 0; node scripts/check-empty-changeset.mjs --self-test → 0; node scripts/check-keyed-text-bounds.mjs → 0; node scripts/check-keyed-text-bounds.mjs --self-test → 0; node scripts/check-platform-object-tenancy-census.mjs → 0; node scripts/check-platform-object-tenancy-census.mjs --self-test → 0; node scripts/check-plugin-teardown-shape.mjs → 0; node scripts/check-plugin-teardown-shape.mjs --self-test → 0; node scripts/check-registry-log-declared.mjs → 0; node scripts/check-registry-log-declared.mjs --self-test → 0; node scripts/check-rest-log-spy-declared.mjs → 0; node scripts/check-rest-log-spy-declared.mjs --self-test → 0; node scripts/check-system-context-census.mjs → 0; node scripts/check-system-context-census.mjs --self-test → 0; node scripts/check-undeclared-dep-imports.mjs → 0; node scripts/check-undeclared-dep-imports.mjs --self-test → 0; node scripts/docs-audit/check-affected-docs.mjs → 0; node scripts/docs-audit/check-drift-comment.mjs → 0; node scripts/pm/release-rehearsal-clone.mjs --self-test → 0; pnpm --filter @objectstack/spec run check:duration-unit-keys → 0; pnpm check:changeset-gate-self-tests → 0; pnpm check:cross-package-test-inputs → 0; pnpm check:dispatcher-error-vocabulary → 0; pnpm check:doc-authoring → 0; pnpm check:driver-memory-census → 0; pnpm check:engine-double-contract → 0; pnpm check:gitlink-declared → 0; pnpm check:issue-citations → 0; pnpm check:logger-receiver-detach → 0; pnpm check:nul-bytes → 0; pnpm check:objectql-double-limit → 0; pnpm check:objectui-changeset → 0; pnpm check:org-identifier → 0; pnpm check:page-declaration-shape → 0; pnpm check:pm-changeset-deadline-census → 0; pnpm check:query-options-erasure → 0; pnpm check:refd-timer-probe → 0; pnpm check:route-envelope → 0; pnpm check:slot-lookup → 0; pnpm check:test-source-alias → 0; pnpm check:tier-file-adoption → 0; pnpm check:type-check-coverage → 0; pnpm check:dts-closure → 0; pnpm check:dual-build-cjs-loads → 3; pnpm check:lean-entry-closure → 0; pnpm check:published-files → 0; pnpm check:sourcemap-no-sources-content → 0; pnpm check:watch-hint-literal → 0; pnpm check:where-matcher → 0; pnpm check:type-check-debt → 0; pnpm lint → 0; node scripts/check-issue-citations.mjs --base origin/main → 0. RECONCILIATION: dispatch-gates --ran reports 60 derived, 59 run, 1 NOT MEASURED, 0 UNRUN. The NOT MEASURED one is check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET. 31 packages have no dist, so it needs a full build, which is CI's. Scoped substitute: require(packages/runtime/dist/index.cjs) loads, and HttpDispatcher is a function. pnpm lint ran in full, not narrowed → 0. The real board check, node scripts/check-issue-citations.mjs --base origin/main → 0 (12 citations: 11 resolve, 1 cross-repo). CI: not awaited; in_progress at report time.",
      "mcp_calls": "0 — no MCP GitHub tool called",
      "api_writes": "3 relay strokes. Each is ONE POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write), executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #20218), relay run 36313042348, success. (2) label-write --assign os-litant → POST /repos/objectstack-ai/objectstack/issues/20218/assignees, relay run 36313084353, success, read-back matches. (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/19328/comments. Also git push of the branch, 4 times: the empty-branch probe, the fix, the changeset, and the main merge. Zero labels written: the dispatch named none, and skip-changeset does not apply because a changeset ships. The PR body was written once and read back byte-identical: 16478 chars, one session-URL footer.",
      "open_questions": [
        {
          "question": "Row 2 at the TOP LEVEL of the WRAPPED form is the seam. Should the spec lane close PackageInstallRequestSchema with .strict()? The door now asks the declaration, so it follows with no runtime edit, and pin §5 stays green either way.",
          "options": [
            "A: close the wrapped branch in the spec lane. That is a new decision against ruling A (batch #148 item 4), which the docblock cites for strip mode. Card #19328 then closes with no runtime change.",
            "B: keep strip mode as declared, record the row-2 top-level position as intended, and treat card #19328 as complete with this PR."
          ],
          "recommendation": "A. Business need, measured: no first-party producer sends an undeclared top-level key. The SDK sends manifest/settings/enableOnInstall/overwrite, and objectui's PackageFormDialog sends { manifest }. Closing costs no known caller. Long-term soundness: ruling A's reason («refuse bodies this door answers 201 to») is now circular, because the door answers exactly what the declaration says. Guarding against AI mistakes: under strip mode a misspelt option (enabledOnInstall:false) is silently dropped and the package installs ENABLED. That is row 3's inversion, reached by a typo. A strict schema with a loud refusal is the direction this axis favours. Startup focus: one closed contract, and no new gate. Cost: the spec's published accept set narrows (Clause-② narrowing), and it needs a maintainer word, because ruling A is a maintainer ruling."
        },
        {
          "question": "The changeset's Clause-② arm and bump. The claim declares 'Clause-②: no', and this PR copies it verbatim with a patch bump. The two same-door precedents that pulled this door back to the same declaration (#19120, the version leg; #19417, the id leg) declared 'Clause-②: no (narrowing)', bumped minor and carried an ADR-0087 not-required disposition. This PR narrows further: bare-form overwrite and settings were HONOURED before and are refused now.",
          "options": [
            "A: the seat re-declares 'Clause-②: no (narrowing)' in the PR body and changeset, bumps minor and adds the ADR-0087 not-required marker. The seat writes this; beyond the changeset edit, no dev push is needed.",
            "B: keep 'Clause-②: no' + patch, as claimed."
          ],
          "recommendation": "A. It matches the two same-door precedents, and the arm exists to stop a wire narrowing from shipping with every gate green (the #16296 shape). I did not change it myself, because my definition says the claim's line is copied verbatim and never self-authored."
        },
        {
          "question": "Conflict: my definition says a published defect that this round makes FALSE must be fixed, but the card fences packages/spec/** as read-only. The PackageInstallBodySchema docblock ships in @objectstack/spec's dist/api .d.ts and now misdescribes the door.",
          "options": [
            "A: a spec-lane follow-up card, as a sub-issue of #19328, landing with #20218 or right after it (the #19327 pattern for #19326).",
            "B: widen this card to touch the packages/spec docs only."
          ],
          "recommendation": "A. It keeps the triage fence and matches precedent. See out_of_scope_findings[0]."
        }
      ],
      "out_of_scope_findings": [
        "class: b · reach: exception: release-text. The PackageInstallBodySchema docblock is emitted into @objectstack/spec's dist/api/index.d.ts and .d.mts, per changeset 19327-install-door-residual-split.md, so the next release publishes a false sentence. · Seam: spec:PackageInstallBodySchema docblock (and the PackageInstallRequestSchema.enableOnInstall docblock) → runtime:handlePackagesRequest POST install door. · evidence: once #20218 lands, four places are false. (1) The list 'What this declaration does NOT describe — the measured residual' still says the door answers 201 to classes 1b, 2, 3 and 4; it answers 400, and only the wrapped top-level strip remains. (2) A paragraph says the two runtime door drives post type-less bodies; both now carry type:'app'. (3) The enableOnInstall docblock says the door 'reads the raw body'; it reads the parsed request. (4) The block 'the measured bare-form senders are the RESIDUAL' in packages/spec/src/api/package-api.test.ts transcribes those drives as type-less. · Recommend filing it as a sub-issue of #19328 on the spec lane. · dedupe words: PackageInstallBodySchema residual docblock stale · install door residual classes closed · measured bare-form senders residual · enableOnInstall reads the raw body",
        "carrier: 承接者:无 · noted, not filed (no public-door measurement) · At dispatcher level (measured by a one-shot probe), a literal JSON null or undefined body at POST /api/v1/packages answers 500 INTERNAL_ERROR with the TypeError text 'Cannot read properties of null (reading manifest)'. The cause is the unchanged `body.manifest || body` line ahead of the id gate. The hono adapter maps unparseable bodies to {}, but a JSON null would pass through; that is inferred from packages/adapters/hono/src/index.ts :688/:732, not driven. In the PR's Acceptance notes.",
        "carrier: 承接者:无 · noted, not filed (observation) · The ?overwrite query parameter is still read with a hand-written === 'true' || === true, not with parseBooleanParam, which is the file's own declared query-boolean coercion (its import comment names the ?read=1 dialect defect). Unchanged here and outside the card's rows. In the PR's Acceptance notes.",
        "carrier: 承接者:无 · noted, not filed (unmeasured) · Nested manifest shapes that ManifestSchema leaves in strip mode (e.g. data[] SeedSchema entries) are stored as sent, because the door keeps the sent manifest; the parse is a gate, not a normaliser. The gate closes top-level manifest keys only. In the PR's Acceptance notes."
      ]
    }
  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    REWORK (patch round 1): PR #20218 at df010818, the changeset must declare the narrowing

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · review verdict, written 2026-09-27T10:56Z

    • CI on df010818: no failure at review time; Type Check · workspace was still in progress. The diff is not governed.
    • The at-tier independent contract review answered FAIL (record 5855204061 on the PR). The seat verified each item and upholds it.

    1. The changeset under-declares. This is the seat's own error.

    • The claim's reading of patch + bare Clause-②: no was wrong. The diff refuses bodies that answered 201 before, and bare-form overwrite: true and settings were HONOURED before.
    • The same door's two earlier pull-backs to the same declaration both carry Clause-②: no (narrowing), minor, a **BREAKING for callers of the install door** paragraph and an adr-0087: not-required (no-migration-prescription) marker: .changeset/19120-install-door-parses-manifest-version.md and .changeset/19417-install-door-parses-manifest-id.md.
    • As written, check-adr-0087-registration reads no breaking signal and stays green. That is the shape the arm exists to stop.
    • ⇒ The changeset takes that form, with a FROM → TO remedy per refused shape. The PR body's line becomes Clause-②: no (narrowing); the claim's bare Clause-②: no stays, since the claim line carries only yes | no.
    • The dev raised exactly this as open question 2, and the seat answers A.

    2. The first-party objectui caller: read by the seat, and it is safe.

    • Reading: objectui at the pinned .objectui-sha f8a9d0fb0596f4521076628e2bbfe27e6ce67d52, file packages/app-shell/src/views/metadata-admin/PackageFormDialog.tsx.
    • The create path sends POST /api/v1/packages with { manifest: manifestBody }, where manifestBody = { ...draft, id, name, version, type: draft.type ?? 'app' }.
    • The draft is seeded { version: '0.1.0', type: 'app' } and rendered from the spec-derived manifest form. So the dialog always sends a declared type, and row 1b's refusal does not break it.
    • The SDK's request shapes are pinned door-side in the PR's §7.

    3. The spec docblocks this PR makes false are filed as #20219 (packages/spec is read-only for this card), in the #19327 pattern. It is bare, for triage to route to the spec lane, and should land with or right after this PR.

    The dev's open questions:

    Out of scope, noted in the PR's Acceptance notes:

    • a JSON null body answers 500 (pre-existing);
    • ?overwrite is read by hand rather than by parseBooleanParam;
    • nested strip-mode residue is stored as sent.

    Patch round to the same dev, on the same claim and branch.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 19328,
      "status": "done",
      "branch": "claude/issue-19328-install-door-body-parse",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20218",
      "session": "session_01UYBdGBzWSrAMzpW8ah3GbP",
      "premise_still_valid": true,
      "summary": "PATCH ROUND 1. It answers contract review 5855204061 (FAIL, on df010818) and seat REWORK 5855224201; I read both before editing. The only code-surface edit is .changeset/19328-install-door-body-parse.md, rewritten in the form of the two same-door precedents on origin/main (.changeset/19120-install-door-parses-manifest-version.md and .changeset/19417-install-door-parses-manifest-id.md): front matter '@objectstack/runtime': minor; the line 'Clause-②: no (narrowing)'; a '**BREAKING for callers of the install door.**' paragraph naming the four refused shapes, each with a FROM → TO remedy. (a) A type-less manifest in either form → add a declared type. (b) An unknown key inside the manifest or on a bare body → remove it or spell the declared key. (c) A string-typed enableOnInstall/overwrite → JSON booleans. (d) Install options on the bare form (enableOnInstall, overwrite, settings) → the wrapped form { manifest, … }, or ?overwrite=true. The paragraph states that bare overwrite:true and settings were HONOURED before and are refused now; exactly ONE ADR-0087 disposition marker, in the precedents' HTML-comment form, reading adr-0087: not-required (no-migration-prescription), whose reason is that nothing authorable is removed, renamed or reshaped, and the refusal carries the remedy. check-adr-0087-registration now SEES the declaration, reading it as [BREAKING+clause-②-narrowing], and accepts it with the marker (exit 0). Merged origin/main (4df101c38) by merge, not rebase. No packages/spec edit (#20219 owns that), no packages.ts edit, no PR body edit. NEW HEAD: 481ad07f33a22aea78b3e36c04da2dca62968a34 (pushed; the remote branch head matches). FILES_CHANGED this round: .changeset/19328-install-door-body-parse.md, plus the merge commit f2e2c99bd, which brings origin/main in. Branch delta vs merge-base 4df101c38 is unchanged at 9 paths (+750/-51). PR BODY: the seat applies the find/replace pairs below (each find string occurs exactly once in the current body, measured). P1–P6 are required; P7 is optional. PAIRS: [{\"id\":\"P1 (required) — the line-initial declaration\",\"find\":\"Clause-②: no\\n\\n## What changed\",\"replace\":\"Clause-②: no (narrowing)\\n\\n## What changed\"},{\"id\":\"P2 (required) — the changeset line of «Changeset and bump»\",\"find\":\"`.changeset/19328-install-door-body-parse.md`, `@objectstack/runtime: patch`, carrying `Clause-②: no` as copied from the claim.\",\"replace\":\"`.changeset/19328-install-door-body-parse.md`, `@objectstack/runtime: minor`, carrying `Clause-②: no (narrowing)`, a **BREAKING for callers of the install door** paragraph with a FROM → TO remedy for each refused shape, and exactly one ADR-0087 `not-required (no-migration-prescription)` disposition. It was rewritten in patch round 1, after contract review `5855204061` answered FAIL on the bump and the seat's REWORK `5855224201` upheld it. The claim's own line stays `Clause-②: no`, because a claim line carries only `yes | no`.\"},{\"id\":\"P3 (required) — the reasoning paragraph («Why `patch` … what `Clause-②: no` alone takes»)\",\"find\":\"Why `patch`: AGENTS.md Post-Task Checklist step 3 says a bug fix in a released package takes `patch`. The declared line has no direction arm, and a `patch` is what `Clause-②: no` alone takes. For each refused shape, the changeset names what a client should send instead.\",\"replace\":\"Why `minor` and `(narrowing)`: the diff shrinks the door's observed wire accept set. Bodies that answered `201` now answer `400`, and bare-form `overwrite: true` and `settings` were HONOURED before and are refused now. `scripts/pm/clause2-line.mjs` defines `no (narrowing)` as «NOT a widening, but breaking». AGENTS.md Post-Task Checklist step 3 makes `(narrowing)` BREAKING: the changeset must carry its migration, and must state its ADR-0087 disposition in writing. The same door's two earlier pull-backs to the same declaration carry exactly this form, and this PR narrows more than either:\\n- `.changeset/19120-install-door-parses-manifest-version.md` (#19120, the `version` leg);\\n- `.changeset/19417-install-door-parses-manifest-id.md` (#19417, the `id` leg).\\n\\nEach carries `minor`, `Clause-②: no (narrowing)`, a BREAKING paragraph and `adr-0087: not-required (no-migration-prescription)`. `node scripts/check-adr-0087-registration.mjs --base origin/main` now reads this changeset as `[BREAKING+clause-②-narrowing]` and accepts it with its marker (exit 0). For each refused shape, the changeset names what a client should send instead.\"},{\"id\":\"P4 (required, companion to P3) — the «raised, not settled» paragraph, now settled\",\"find\":\"⚠️ **This choice is raised, not settled** (see *Open question 2*).\",\"replace\":\"✅ **Settled in patch round 1.** The seat answered *Open question 2* with A (REWORK `5855224201`). The text below is kept as the record of why it was raised.\"},{\"id\":\"P5 (required) — one line recording the seat's objectui reading\",\"find\":\"\\n## Fixture triage (off-spec door fixtures, by disposition)\",\"replace\":\"\\n**The objectui caller, read by the seat at `.objectui-sha` `f8a9d0fb` (REWORK `5855224201`):** «The create path sends `POST /api/v1/packages` with `{ manifest: manifestBody }`, where `manifestBody = { ...draft, id, name, version, type: draft.type ?? 'app' }`.» The file is `packages/app-shell/src/views/metadata-admin/PackageFormDialog.tsx`. Studio's create-package dialog therefore always sends a declared `type`, and row 1b's refusal does not break it.\\n\\n## Fixture triage (off-spec door fixtures, by disposition)\"},{\"id\":\"P6 (required) — name #20219 as the spec-side follow-up\",\"find\":\"## Out-of-scope finding, for the seat to file\",\"replace\":\"## Spec-side follow-up: filed as #20219 (the #19327 pattern; it lands with or right after this PR)\"},{\"id\":\"P7 (optional) — mark open question 2 answered\",\"find\":\"2. **The changeset's `Clause-②` arm and bump.**\",\"replace\":\"2. **The changeset's `Clause-②` arm and bump.** ✅ Answered A by the seat (REWORK `5855224201`) and applied in patch round 1.\"}] LEVEL AXIS, driven offline with check-changeset-no-major --event on both bodies: the CURRENT body reads 'Clause-②: no' with no arm → exit 0; the body with the pairs applied reads 'Clause-②: no (narrowing)' → exit 0, 'direction arm: narrowing — a BREAKING change; during the launch window it ships minor', with no package graded patch. The CI run on the edited body should therefore stay green. LINE_BUDGET: n/a — no skills/** diff. DEVIATIONS: (1) Gates were derived with dispatch-gates --commands and no path list (per my definition); it derived the same 60 families as round 0 from merge-base 4df101c38. (2) The first run of pnpm check:type-check-debt hit my 570s wrapper timeout under load average 12. The one retry exited 0 (re-measure 101.9s); only the retry's code is recorded. (3) The worktree was recreated from df010818, then removed (node_modules deleted, git worktree remove, no --force) before this report. (4) Beyond the changeset, I re-ran 10 door and runtime test files on the merged tree (515 passed), because the merge brought a packages/runtime change (domains/automation.ts).",
      "tests": "Head 481ad07f3. CHANGESET GATES: node scripts/check-adr-0087-registration.mjs --base origin/main → 0. Its output: '✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition. .changeset/19328-install-door-body-parse.md [BREAKING+clause-②-narrowing] not-required (no-migration-prescription)'. node scripts/check-changeset-no-major.mjs --base origin/main → 0 ('✓ This diff introduces no major bump'). Locally the level axis is NOT APPLICABLE, because there is no pull_request payload. Driven with --event on the current PR body → 0 (arm none). Driven with --event on the body with the pairs applied → 0 (arm narrowing, ships minor). RUNTIME: 10 door and runtime test files → 515 passed (os-verify-lock VERDICT command-exit 0): the new body-contract file, the two replaced-pin files, the id and version leg files, domain-handler-registry, packages-capability-gate, http-dispatcher, package-door-namespace-conflict-code, and automation-write-capability-gate, which the merge touched. BUILD: pnpm --workspace-concurrency=2 --filter '@objectstack/runtime...' build → 0 (31 packages) before the dist-reading gates. require(packages/runtime/dist/index.cjs) loads (HttpDispatcher is a function). GATES (command → exit), derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 481ad07f3, plus the lane's additions: node scripts/check-adr-0087-registration.mjs --base origin/main → 0; node scripts/check-adr-0087-registration.mjs --self-test → 0; node scripts/check-changeset-no-major.mjs --base origin/main → 0; node scripts/check-changeset-no-major.mjs --self-test → 0; node scripts/check-ci-filter-parity.mjs → 0; node scripts/check-closing-keyword-parity.mjs → 0; node scripts/check-closing-keyword-parity.mjs --self-test → 0; node scripts/check-comment-mask-adoption.mjs → 0; node scripts/check-comment-mask-adoption.mjs --self-test → 0; node scripts/check-comment-mask-corpus.mjs → 0; node scripts/check-empty-changeset.mjs --base origin/main → 0; node scripts/check-empty-changeset.mjs --self-test → 0; node scripts/check-keyed-text-bounds.mjs → 0; node scripts/check-keyed-text-bounds.mjs --self-test → 0; node scripts/check-platform-object-tenancy-census.mjs → 0; node scripts/check-platform-object-tenancy-census.mjs --self-test → 0; node scripts/check-plugin-teardown-shape.mjs → 0; node scripts/check-plugin-teardown-shape.mjs --self-test → 0; node scripts/check-registry-log-declared.mjs → 0; node scripts/check-registry-log-declared.mjs --self-test → 0; node scripts/check-rest-log-spy-declared.mjs → 0; node scripts/check-rest-log-spy-declared.mjs --self-test → 0; node scripts/check-system-context-census.mjs → 0; node scripts/check-system-context-census.mjs --self-test → 0; node scripts/check-undeclared-dep-imports.mjs → 0; node scripts/check-undeclared-dep-imports.mjs --self-test → 0; node scripts/docs-audit/check-affected-docs.mjs → 0; node scripts/docs-audit/check-drift-comment.mjs → 0; node scripts/pm/release-rehearsal-clone.mjs --self-test → 0; pnpm --filter @objectstack/spec run check:duration-unit-keys → 0; pnpm check:changeset-gate-self-tests → 0; pnpm check:cross-package-test-inputs → 0; pnpm check:dispatcher-error-vocabulary → 0; pnpm check:doc-authoring → 0; pnpm check:driver-memory-census → 0; pnpm check:dts-closure → 0; pnpm check:dual-build-cjs-loads → 3; pnpm check:engine-double-contract → 0; pnpm check:gitlink-declared → 0; pnpm check:issue-citations → 0; pnpm check:lean-entry-closure → 0; pnpm check:logger-receiver-detach → 0; pnpm check:nul-bytes → 0; pnpm check:objectql-double-limit → 0; pnpm check:objectui-changeset → 0; pnpm check:org-identifier → 0; pnpm check:page-declaration-shape → 0; pnpm check:pm-changeset-deadline-census → 0; pnpm check:published-files → 0; pnpm check:query-options-erasure → 0; pnpm check:refd-timer-probe → 0; pnpm check:route-envelope → 0; pnpm check:slot-lookup → 0; pnpm check:sourcemap-no-sources-content → 0; pnpm check:test-source-alias → 0; pnpm check:tier-file-adoption → 0; pnpm check:type-check-coverage → 0; pnpm check:watch-hint-literal → 0; pnpm check:where-matcher → 0; pnpm check:type-check-debt → 0; pnpm lint → 0; node scripts/check-issue-citations.mjs --base origin/main → 0. RECONCILIATION: dispatch-gates --ran reports 60 derived, 59 run, 1 NOT MEASURED, 0 UNRUN. The NOT MEASURED one is check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET. 36 packages have no dist (8 named + 28 more), so it needs a full build, which is CI's. pnpm lint ran in full, not narrowed → 0. The real board check → 0 (8 citations, all resolve). CI on 481ad07f3: not awaited.",
      "mcp_calls": "0 — no MCP GitHub tool called this round",
      "api_writes": "This round: 1 relay stroke, ONE POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write) executed as objectstack-fleet[bot]: this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/19328/comments. Plus 1 git push of the branch (df0108189..481ad07f3: the merge and the changeset commit). No PR body edit, no label write, no claim. Reads (not writes): GET issues/comments/5855204061, GET issues/comments/5855224201, GET pulls/20218.",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  8. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    REWORK (patch round 2): PR #20218 at 481ad07f, the BREAKING paragraph must state the whole declaration the door now enforces

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · review verdict, written 2026-09-27T11:54Z

    • CI on 481ad07f: all green. 36 success, and 5 skips that are rostered expected skips. The diff is not governed.
    • The at-tier independent delta review answered FAIL (record 5855593517 on the PR). The seat verified it at source and upholds it.

    Met, per the review:

    Required (round 2):

    1. The BREAKING paragraph is incomplete. The door now enforces the WHOLE ManifestSchema, not four shapes.
      • name is required (manifest.zod.ts, name: z.string(), not optional), and before this PR the door parsed only the id and version legs. So { id, version, type } with no name answered 201 on main and answers 400 now. The SDK README already says name is required.
      • The same holds for:
        • the namespace grammar;
        • the scope enum;
        • a declared key's type;
        • unknown keys inside the nested strict shapes contributes, engine, engines, data[] (SeedSchema is a strictObject) and navigationContributions.
      • ⇒ Add a FROM → TO bullet for name. Add one bullet covering every other declared constraint now enforced, with the remedy "send the value the declaration states; the refusal names the path".
      • Reword "Four shapes of body are now refused" and "requires type and closes the manifest against unknown keys" so neither reads as exhaustive.
    2. PR body (the seat applies the dev's find/replace pairs):

    Patch round to the same dev, on the same claim and branch.


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 19328,
      "status": "done",
      "branch": "claude/issue-19328-install-door-body-parse",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20218",
      "session": "session_01UYBdGBzWSrAMzpW8ah3GbP",
      "premise_still_valid": true,
      "summary": "PATCH ROUND 2. It answers delta review 5855593517 (FAIL on 481ad07f) and seat REWORK 5855595766; I read both before editing. The changeset's BREAKING paragraph now states the WHOLE declaration the door enforces: (1) the opening no longer reads as exhaustive. 'Four shapes of body are now refused' became 'The door now enforces the WHOLE install-body declaration … The list names the shapes a caller is most likely to have sent, and ends with one bullet for everything else the declaration states'; (2) a new FROM → TO bullet for `name`: FROM { id, version, type } in either form TO the same manifest with \"name\": \"…\". It used to install, because the door parsed only the id and version legs; (3) one new bullet for 'Any other constraint ManifestSchema declares', with the remedy 'FROM the off-declaration value TO the value the declaration states. The refusal names the path'. (4) 'ManifestSchema requires type and closes the manifest against unknown keys' became 'declares the manifest's required keys (id, name, version, type) and the grammar or value set of the rest. It closes the manifest, and the nested blocks listed above, against unknown keys'. The 'If you are refused' paragraph now says the refusal names the path. The ADR-0087 marker's reason gained 'a name, the declared key or value'. Kept unchanged: minor, Clause-②: no (narrowing), exactly one ADR-0087 marker. MEASURED LIST vs the review: I walked ManifestSchema at this head with a probe script under tsx, not committed. Its top-level required keys are id, version, type and name. Its nested STRICT objects are permissions (the structured union arm), contributes, contributes.kinds[], data[] (SeedSchema), navigationContributions[] and their items, engine and engines. Its retired keys, typed never, are configuration, capabilities, extensions and loading. Its enums are type, scope, runtime and packaging. The only nested STRIP objects found are the object form of a navigation item's visible, and its meta. The review's five nested shapes were all right. It MISSED the structured form of permissions (strict), the retired-key tombstones, and the runtime/packaging enums. I added all three to the bullet, plus contributes.kinds[] and the navigation items. The review named nothing wrongly. Each constraint was confirmed with ManifestSchema.safeParse: no name → name invalid_type; namespace 'Acme-CRM' → invalid_format; scope 'tenant' → invalid_value; runtime 'python' → invalid_value; description 5 → invalid_type; dependencies value 1 → invalid_type; capabilities/configuration → the 'was removed in @objectstack/spec 17' tombstone; unknown key inside contributes, engines or a data[0] seed → unrecognized_keys; permissions { bogus } → invalid_union. OPTIONAL PIN, added: a §8 table in packages/runtime/src/domains/packages-install-body-contract.test.ts with nine cases: no name (wrapped and bare), a bad namespace, a bad scope, a non-string description, the retired capabilities, and unknown keys inside contributes, a data[] seed and engines. Each case asserts the declaration refuses it, a control differing by the defect alone parses green, the door answers 400 VALIDATION_ERROR, nothing installed, and the path is named. The test file's header gained one paragraph pointing at §8. Merged origin/main once more (dd49317f1, base 805af4f29, no packages/runtime or lockfile change) by merge, not rebase. No packages/spec edit, no packages.ts edit, no PR body edit. NEW HEAD: 1358147bcdf8c8f31509db38223a59332d59daed (pushed; the remote branch head matches). FILES_CHANGED this round: .changeset/19328-install-door-body-parse.md and packages/runtime/src/domains/packages-install-body-contract.test.ts, plus the merge commit dd49317f1. Branch delta vs merge-base 805af4f29: 9 paths (+828/-51). PR BODY: the seat applies the find/replace pairs below. Each find string occurs exactly once in the current body (18439 chars, read back byte-identical to my round-1 pairs applied). Q1–Q6 are all required. PAIRS: [{\"id\":\"Q1 (required) — mirror the whole-declaration statement in «What changed»\",\"find\":\"- **Verdict placement.** The verdict is answered after the `id` and `version` legs\",\"replace\":\"- **What the door now enforces: the WHOLE declaration, not only the card's four rows.** Before this change, the door parsed only the `id` and `version` legs of the manifest. Each of the following therefore answered `201` and now answers `400`: `name` (the manifest's other required key); the `namespace` grammar; the closed value sets of `scope`, `runtime` and `packaging`; a declared key's type; the retired-key tombstones (`configuration`, `capabilities`, `extensions`, `loading`); and unknown keys inside the nested blocks the declaration closes. Those blocks are `contributes` and its `kinds[]`, `data[]` seeds (`SeedSchema`), `navigationContributions[]` and their items, `engine`, `engines`, and the structured form of `permissions`. The list was measured by walking `ManifestSchema` at this head. §8 of the body-contract file pins nine of these at the door, and the changeset's BREAKING paragraph names all of them, with a FROM → TO bullet for `name`.\\n- **Verdict placement.** The verdict is answered after the `id` and `version` legs\"},{\"id\":\"Q2 (required) — §8 in the pin list\",\"find\":\"- **§7** Controls: both forms install, the manifest is stored as sent, wrapped `settings` land, and the SDK shapes behave as above.\",\"replace\":\"- **§7** Controls: both forms install, the manifest is stored as sent, wrapped `settings` land, and the SDK shapes behave as above.\\n- **§8** The rest of the declaration, added in patch round 2. Nine cases each answer `400 VALIDATION_ERROR`, install nothing, and name the path:\\n  - no `name`, in the wrapped and the bare form;\\n  - a `namespace` outside the grammar;\\n  - a `scope` outside its set;\\n  - a non-string `description`;\\n  - the retired `capabilities`;\\n  - an unknown key inside `contributes`, inside a `data[]` seed, and inside `engines`.\\n\\n  Each case is first asserted off-declaration by the declaration itself, beside a control that parses green and differs by the defect alone.\"},{\"id\":\"Q3 (required) — correct the acceptance note on nested strip mode\",\"find\":\"- **Nested strip-mode shapes are stored as sent.** Nested shapes that `ManifestSchema` leaves in strip mode (for example a `data[]` seed entry) are still stored as the caller sent them. The gate closes the top-level manifest keys only. Not measured. Owner: none.\",\"replace\":\"- **Nested strip-mode shapes are stored as sent.** Some nested shapes are still in strip mode, and those are stored as the caller sent them. A walk of `ManifestSchema` at this head found them only in the object form of a navigation item's `visible` and its `meta`. The gate refuses unknown keys in the manifest and in every nested block the declaration closes: `contributes` and its `kinds[]`, `data[]` (`SeedSchema` is a `strictObject`), `navigationContributions[]` and their items, `engine`, `engines`, and the structured `permissions`. §8 pins three of these at the door. The earlier sentence here named `data[]` as strip mode and said the gate closed top-level keys only; both were wrong, and patch round 2 corrected them. Owner: none.\"},{\"id\":\"Q4 (required) — the Tests-and-gates heading no longer claims one final head\",\"find\":\"## Tests and gates (final head `df0108189` unless stated)\",\"replace\":\"## Tests and gates (head `df0108189` unless stated; the patch rounds are recorded at the end of this section)\"},{\"id\":\"Q5 (required) — the merge sentence, now true of all three merges\",\"find\":\"before the merge of `origin/main`. The merge brought no change to `packages/runtime`, the install schemas or the lockfile.\",\"replace\":\"before any merge of `origin/main`. The branch has merged `origin/main` three times:\\n  - The first merge (`df0108189`, base `e7f69dbba`) brought no change to `packages/runtime`, the install schemas or the lockfile.\\n  - The second merge (`f2e2c99bd`, base `4df101c38`) brought the #19543 flow-list retirement: `domains/automation.ts`, `dispatcher-plugin.ts`, `route-ledger.ts`, `packages/client/src/index.ts` and four runtime tests. It does not touch the install door, `PackageInstallBodySchema`, `ManifestSchema` or the install writers. 10 door and runtime test files re-ran on the merged tree at `481ad07f`: **515 passed**.\\n  - The third merge (`dd49317f1`, base `805af4f29`) brought no `packages/runtime` change and no lockfile change.\"},{\"id\":\"Q6 (required) — record the patch rounds' numbers at the end of «Tests and gates»\",\"find\":\"- **Issue citations.** `node scripts/check-issue-citations.mjs --base origin/main`: exit 0 (12 citations: 11 resolve, 1 cross-repo).\",\"replace\":\"- **Issue citations.** `node scripts/check-issue-citations.mjs --base origin/main`: exit 0 (12 citations: 11 resolve, 1 cross-repo).\\n- **Patch round 1 (head `481ad07f`, changeset only).**\\n  - `check-adr-0087-registration --base origin/main` → 0, reading `[BREAKING+clause-②-narrowing]`.\\n  - `check-changeset-no-major --base origin/main` → 0.\\n  - 10 door and runtime test files → 515 passed.\\n  - Derived gates: 60 derived, 59 exit 0, `check:dual-build-cjs-loads` NOT MEASURED (exit 3). `pnpm lint` → 0; the real issue-citation check → 0.\\n- **Patch round 2 (head `1358147bc`: the changeset states the whole declaration, plus the §8 pins).**\\n  - `check-adr-0087-registration --base origin/main` → 0, reading `[BREAKING+clause-②-narrowing]` with the one marker.\\n  - `check-changeset-no-major --base origin/main` → 0.\\n  - The same 10 files → **524 passed** (515 plus the 9 §8 cases).\\n  - `pnpm --filter @objectstack/runtime typecheck` → 0.\\n  - Ablation, with the same anchor as before (the body parse replaced by a verdict that always passes): the body-contract file went 20 failed / 10 passed, and every §8 case answered `201` where it expects `400`, i.e. installed before the fix. The restore was byte-identical to HEAD.\\n  - Derived gates: 60 derived, 59 exit 0, `check:dual-build-cjs-loads` NOT MEASURED (exit 3; 36 packages have no `dist/`). `pnpm lint` → 0; the real issue-citation check → 0 (8 citations, all resolve).\"}] With the pairs applied, the body's Clause-② line stays 'Clause-②: no (narrowing)', and check-changeset-no-major --event on it → 0 (arm narrowing, ships minor). LINE_BUDGET: n/a — no skills/** diff. DEVIATIONS: (1) Gates were derived with dispatch-gates --commands and no path list (per my definition); it derived the same 60 families from merge-base 805af4f29. (2) Two full-closure builds were killed by my foreground timeout after long lock queues (5–6 min waits). I rebuilt the closure with `pnpm turbo run build --filter='@objectstack/runtime^...' --concurrency=2`, which hit the shared turbo cache (27 of 29 cached), and built runtime separately. (3) The first check:type-check-debt answered exit 3, PREREQUISITE NOT MET: the runtime dist was older than its source, because the ablation's restore re-wrote packages.ts (byte-identical, newer mtime). After `pnpm --filter @objectstack/runtime build` it answered 0, and only that code is recorded. The dist-reading gates (dts-closure, lean-entry-closure, published-files, sourcemap-no-sources-content) ran against a runtime dist built from the identical source bytes before the ablation. (4) The §8 pin goes beyond the 'one-line' pin the order allowed. It is a nine-row table, because the changeset now claims those constraints and each claim should be pinned. It is test-only, in the same file. (5) The worktree was recreated from 481ad07f, then removed (node_modules deleted, git worktree remove, no --force).",
      "tests": "Head 1358147bc. CHANGESET GATES: node scripts/check-adr-0087-registration.mjs --base origin/main → 0 ('✓ 1 declared-breaking changeset(s) … [BREAKING+clause-②-narrowing] not-required (no-migration-prescription)'). node scripts/check-changeset-no-major.mjs --base origin/main → 0 ('✓ This diff introduces no major bump'; locally the level axis is NOT APPLICABLE). With --event on the body with the pairs applied → 0 (arm narrowing, ships minor). RUNTIME: the same 10 door and runtime test files as round 1 → 524 passed (515 + the 9 new §8 cases), through os-verify-lock (VERDICT command-exit 0). pnpm --filter @objectstack/runtime typecheck → 0. The test layer held at 27 files / 190 errors / 68 signatures; the ledger moved on main. ABLATION (the fix committed first, HEAD 1358147bc): node scripts/ablation-replace.mjs with the same anchor, `const declaredBody = PackageInstallBodySchema.safeParse(body);` → `const declaredBody = { success: true as const, data: body } as any;`. On-disk evidence: anchor 1→0, replacement 0→1, blob 4f6b4be7→4a881701. Predicted direction: red. Observed: the body-contract file went 20 failed / 10 passed. All nine §8 cases failed with 'expected 201 to be 400', which is the pre-fix answer: installed. The 11 earlier row pins also failed. Restore: blob == HEAD (4f6b4be7) and git diff HEAD empty. ablation-dist-preflight --absent: the marker is absent from the runtime dist and the tree is clean. The restore leg rests on byte-identity with the pre-ablation green run of the same file at the same HEAD (524 passed). There was no second lock wait for a rerun. GATES (command → exit), derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 1358147bc, plus the lane's additions: node scripts/check-adr-0087-registration.mjs --base origin/main → 0; node scripts/check-adr-0087-registration.mjs --self-test → 0; node scripts/check-changeset-no-major.mjs --base origin/main → 0; node scripts/check-changeset-no-major.mjs --self-test → 0; node scripts/check-ci-filter-parity.mjs → 0; node scripts/check-closing-keyword-parity.mjs → 0; node scripts/check-closing-keyword-parity.mjs --self-test → 0; node scripts/check-comment-mask-adoption.mjs → 0; node scripts/check-comment-mask-adoption.mjs --self-test → 0; node scripts/check-comment-mask-corpus.mjs → 0; node scripts/check-empty-changeset.mjs --base origin/main → 0; node scripts/check-empty-changeset.mjs --self-test → 0; node scripts/check-keyed-text-bounds.mjs → 0; node scripts/check-keyed-text-bounds.mjs --self-test → 0; node scripts/check-platform-object-tenancy-census.mjs → 0; node scripts/check-platform-object-tenancy-census.mjs --self-test → 0; node scripts/check-plugin-teardown-shape.mjs → 0; node scripts/check-plugin-teardown-shape.mjs --self-test → 0; node scripts/check-registry-log-declared.mjs → 0; node scripts/check-registry-log-declared.mjs --self-test → 0; node scripts/check-rest-log-spy-declared.mjs → 0; node scripts/check-rest-log-spy-declared.mjs --self-test → 0; node scripts/check-system-context-census.mjs → 0; node scripts/check-system-context-census.mjs --self-test → 0; node scripts/check-undeclared-dep-imports.mjs → 0; node scripts/check-undeclared-dep-imports.mjs --self-test → 0; node scripts/docs-audit/check-affected-docs.mjs → 0; node scripts/docs-audit/check-drift-comment.mjs → 0; node scripts/pm/release-rehearsal-clone.mjs --self-test → 0; pnpm --filter @objectstack/spec run check:duration-unit-keys → 0; pnpm check:changeset-gate-self-tests → 0; pnpm check:cross-package-test-inputs → 0; pnpm check:dispatcher-error-vocabulary → 0; pnpm check:doc-authoring → 0; pnpm check:driver-memory-census → 0; pnpm check:dts-closure → 0; pnpm check:dual-build-cjs-loads → 3; pnpm check:engine-double-contract → 0; pnpm check:gitlink-declared → 0; pnpm check:issue-citations → 0; pnpm check:lean-entry-closure → 0; pnpm check:logger-receiver-detach → 0; pnpm check:nul-bytes → 0; pnpm check:objectql-double-limit → 0; pnpm check:objectui-changeset → 0; pnpm check:org-identifier → 0; pnpm check:page-declaration-shape → 0; pnpm check:pm-changeset-deadline-census → 0; pnpm check:published-files → 0; pnpm check:query-options-erasure → 0; pnpm check:refd-timer-probe → 0; pnpm check:route-envelope → 0; pnpm check:slot-lookup → 0; pnpm check:sourcemap-no-sources-content → 0; pnpm check:test-source-alias → 0; pnpm check:tier-file-adoption → 0; pnpm check:type-check-coverage → 0; pnpm check:watch-hint-literal → 0; pnpm check:where-matcher → 0; pnpm check:type-check-debt → 0; pnpm lint → 0; node scripts/check-issue-citations.mjs --base origin/main → 0. RECONCILIATION: dispatch-gates --ran reports 60 derived, 59 run, 1 NOT MEASURED, 0 UNRUN. The NOT MEASURED one is check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET. 36 packages have no dist (8 named + 28 more), so it needs a full build, which is CI's. pnpm lint ran in full → 0. The real board check → 0 (8 citations, all resolve). CI on 1358147bc: not awaited.",
      "mcp_calls": "0 — no MCP GitHub tool called this round",
      "api_writes": "This round: 1 relay stroke, ONE POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write) executed as objectstack-fleet[bot]: this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/19328/comments. Plus 1 git push of the branch (481ad07f3..1358147bc: the merge dd49317f1 and the changeset + pin commit). No PR body edit, no label write, no claim. Reads (not writes): GET issues/comments/5855593517, GET issues/comments/5855595766, GET pulls/20218.",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  10. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    ACCEPT: PR #20218 at 1358147b, R3 of the domain:cli seat (after patch rounds 1 and 2), landing as Refs #19328

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · review of record, written 2026-09-27T13:18Z

    Reviewed against GitHub and origin/main, not against the report.

    Checklist:

    • PR shape: draft, base main. The first line is Refs #19328, and there is no closing keyword: triage's fence (5780789216) lands a partial round as Refs. The line-initial declaration is Clause-②: no (narrowing); the claim's own line stays Clause-②: no, because a claim line carries only yes | no. The seat wrote the body from the dev's pairs over both rounds (7 + 6), and fixed one count the review named ("seven runtime test files", not four). The body reads back byte-identical.
    • Scope: 9 files, none governed (check-governed-merges --pr 20218: NOT governed; 879 changed lines, under 5000):
    • Fix: read in the diff. The body is parsed once through PackageInstallBodySchema.safeParse(body), and every later read (overwrite, settings, enableOnInstall) comes from the parsed request. The refusal is 400 VALIDATION_ERROR, the envelope the id and version legs already use, placed after those legs and before the 409. No new code, and no published message changed. The manifest is stored as sent: the parse gates, it does not normalise.
    • Tests: the body-contract file carries:
      • §0–§7: the card's rows, ordering, the parity pin for the one open position, and the SDK controls;
      • §8: nine cases of the rest of the declaration, each with a control that differs by the defect alone.
        In the ablation, 20 of 30 turn red, and every §8 case answers the pre-fix 201.
    • The first-party callers:
      • the SDK's request shapes are pinned in §7;
      • objectui's create-package dialog at .objectui-sha f8a9d0fb always sends a declared type and only declared keys (REWORK 5855224201, confirmed independently by the round-1 delta review);
      • objectui's packages-io.ts calls only the list GET and the /duplicate POST.
    • CI on head 1358147b: 29 checks success by latest name, including TypeScript Type Check, all Test Core shards, Lint & Repo Gates and Check Changeset. 5 are skipped: Auto Label, Check PR Size, Build Docs, Console Pin Gate and Packed-tarball smoke (opt-in), each a rostered expected skip.
    • Contract review:
      • round 0 FAIL (5855204061): the changeset under-declared the narrowing;
      • round 1 FAIL (5855593517): the BREAKING paragraph named four shapes, not the whole declaration;
      • round 2 PASS (5856195632).
        All three were at-tier independent agents given only the card, the rulings and the PR.

    Decisions of record:

    Out-of-scope findings:

    Next: landing through the relay ops pr_ready + automerge_enable.


    Generated by Claude Code

  11. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Landed: PR #20218 → c02fa12767eeee8ffde3ff7e48830c182df2118f (Refs: this card stays open)

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · landing record, written 2026-09-27T13:37Z

    • Merged through the merge queue (enqueued 2026-09-27T13:20Z, merged 2026-09-27T13:36Z). The landing is a squash: git rev-list --parents -n 1 gives 2 fields.
    • Content reading on c02fa127:
      • const declaredBody = PackageInstallBodySchema.safeParse(body) has 1 hit in packages/runtime/src/domains/packages.ts;
      • the pin file packages-install-body-contract.test.ts is in the tree;
      • so is .changeset/19328-install-door-body-parse.md (minor, Clause-②: no (narrowing)).
    • Negative control: the parse line counts 0 on the parent 3cb84d08, and the pin and the changeset are absent there. The instrument can fire.
    • Card: open by design (Refs #19328). Rows 1b, 3 and 4 are closed. Row 2 is closed in the manifest and on the bare form. Row 2 at the wrapped form's top level goes to the decision box in the next comment, in the same label write: pm:dispatched → needs-user-decision, assignee cleared.
    • Follow-up owed: [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 this landing makes false. It is bare, for triage.
    • Unfenced by this landing: the POST install door region of packages/runtime/src/domains/packages.ts.

    Generated by Claude Code

  12. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    需要维护者裁决:安装入口的包装形式里,顶层多出的未知键要不要拒绝

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · 决策卡落卡,written 2026-09-27T13:38Z。⛔ 席位不裁:这会重开一条维护者裁决(batch #148 item 4)的一处边界。

    状态转换(本评论同笔): pm:dispatched → needs-user-decision,assignee 清空。PR #20218 已按 Refs #19328 落地(合并 c02fa127),本卡四类残留只剩下面这一格。

    一句话问题

    调用方用包装形式安装包时({ manifest, ... }),如果顶层多写了一个平台不认识的键,入口目前一声不吭把它丢掉,照常安装。拼错一个选项(比如把 enableOnInstall 写成 enabledOnInstall: false),这个包就会被启用安装,而调用方本意是停用,且没有任何提示。

    背景与已交付部分

    • PR fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218 让 POST /api/v1/packages 用 spec 已声明的 PackageInstallBodySchema 整体解析请求体:
      • 缺 type、manifest 内的未知键、字符串形式的 enableOnInstall、裸形式上的安装选项,全部改为 400 VALIDATION_ERROR;
      • name、namespace 语法、scope 等整个 ManifestSchema 也一并生效。
    • 唯一剩下的一格: 包装形式顶层的未知键。原因是 spec 把包装那一支 PackageInstallRequestSchema 声明成 strip 模式(丢弃未知键),并在 docblock 里写死:「⛔ Do NOT close the wrapped branch with .strict() — that would refuse bodies this door answers 201 to, which is the one direction this binding may never move (ruling A)」。
    • 入口现在「声明怎么答就怎么答」(PR 的 §5 对齐钉),所以这一格只取决于 spec 的声明,改声明就自动生效,runtime 不用再动。

    Governing text:

    协议声明: 这一格要改的是已发布契约 PackageInstallRequestSchema 本身(收紧),不是入口的缺陷修复。所以它是 spec 车道的活,并且会重开裁决 A 的一处边界,只能由维护者定。

    为什么说裁决 A 的前提已经变了: 裁决 A 当时的理由是「声明不能拒绝入口会答 201 的请求体」,那时入口根本不解析请求体。PR #20218 之后,入口就是按声明来答的,这条理由变成了循环论证,不再约束这个选择。

    Prior rulings read: strict,wrapped,strip,install,enableoninstall → 32 hits。命中 ADR-0024 Decision §4、ADR-0029 D3、ADR-0029 D9.7、ADR-0045 Decision §2,thread: none。

    带复核命令的前提:

    • 包装一支仍是 strip 模式:git show origin/main:packages/spec/src/api/package-api.zod.ts | grep -n "may never move (ruling A)"(预期有命中)。
    • 入口按声明作答:git grep -n "const declaredBody = PackageInstallBodySchema.safeParse(body)" origin/main -- packages/runtime/src/domains/packages.ts(预期 1 处)。

    选项 × 真实代价

    选项 做什么 客户能感知到的后果
    A 收紧 spec 车道把 PackageInstallRequestSchema 改为严格模式(拒绝未知键),入口自动跟随 拼错的选项会得到 400,错误信息点名那个键;dev 实测第一方调用方(SDK、objectui 建包对话框)都不发未知顶层键,零影响
    B 维持 保持 strip 模式,本卡以已交付内容关闭 拼错的选项继续被静默丢弃,包按默认启用安装

    业务含义直译

    • A: 像填表时写错了栏目名,系统当场退回并指出是哪一栏,而不是悄悄当没写。
    • B: 表单上多写的字一律忽略。大多数时候没事,偏偏写错的是「不要启用」时,结果和本意相反。

    四轴分析(业务立场)

    • ① 项目长远合理性(推荐只按此轴得出): 两年后,平台的安装契约应当是一份闭合的契约:manifest 已经严格,裸形式已经严格,包装顶层也该严格,三处一个规则。主流平台都这样建模:Kubernetes 的 API server 默认对未知字段严格校验(fieldValidation=Strict),Terraform provider schema 对未知属性直接报错。A 让整份安装契约只剩一条规则;B 在同一份请求体里留一个例外。只看①选 A。
    • ② 实际业务拉动: dev 实测了第一方发送方:SDK 只发 manifest/settings/enableOnInstall/overwrite,objectui 建包对话框只发 { manifest }。今天没有调用方依赖顶层多余键被丢弃,所以 A 的收紧代价为零;B 的风险是真实存在的拼写错误路径。
    • ③ 防 AI 犯错: AI 写安装调用时拼错选项名是典型错误。在 A 下,它当场得到点名那个键的 400,能自我纠正;在 B 下,错误被静默吞掉,包被启用安装,事后才发现。这正是「宽容掩盖 AI 批量犯错」的形态。
    • ④ 创业阶段不扩散: A 只改一个 schema 的一处模式,不新增门禁、不新增错误码,入口零改动;B 零成本,但要永久维护「这一支例外」的说明和对齐钉。

    os-decision-facets

    • ① 长远合理性:A 缩小特例:安装请求体三处(manifest、裸形式、包装顶层)同一严格规则;B 保留包装顶层这一个例外。
    • ② 业务拉动:第一方发送方(SDK、objectui 建包对话框)实测都不发未知顶层键,A 收紧零代价;B 保留一条真实的拼写错误路径。
    • ③ 防 AI 犯错:A 对拼错的选项响亮拒绝并点名;B 静默丢弃,拼错的「停用」变成「启用」。
    • ④ 创业阶段不扩散:A 只改一处 schema 模式,零新门禁、零新错误码;B 要永久维护一个例外。
      Prior rulings read: strict,wrapped,strip,install,enableoninstall → 32 hits; ADR-0024 Decision §4, ADR-0029 D3, ADR-0029 D9.7, ADR-0045 Decision §2; thread: none; repo: objectstack-ai/objectstack

    推荐:A(收紧包装一支)。回退:B(维持 strip,本卡以已交付内容关闭)。自检:只看①选 A;②③④ 是否翻转:否。

    置信缺口: 没有测到仓库外的第三方调用方(例如客户自写脚本)是否在包装顶层带私有键。收紧后这类调用会得到 400,错误信息点名那个键,修法是删掉它。

    裁后执行


    Generated by Claude Code

  13. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #227 item 3 · letter A · maintainer 「同意」 2026-09-27T14:46Z

    Director seat, summon #30 续, session_01AsCNgFBs8HCjwhyHQsFbx3. Batch #227 was presented in the seat chat with the full analysis of each item (background, options with real costs, mainstream platforms, four axes, recommendation, confidence gaps, execution; the domain:cli seat's analysis 5856351218 re-read, four axes not flipped); the maintainer answered 「同意」 (verbatim, recorded here per the charter).

    Ruled: A — PackageInstallRequestSchema's wrapped branch becomes strict: an unknown top-level key on the wrapped install form is refused by name, exactly as the manifest's and the bare form's unknown keys already are. One rule for the whole install contract.

    Readings on origin/main ab820016b3: packages/spec/src/api/package-api.zod.ts declares the wrapped branch as a plain z.object (strip) and its docblock at :381 says 「⛔ Do NOT close the wrapped branch with .strict() … (ruling A)」, citing 5716042643 (#18058, batch #148 item 4, 「reconcile the schema to measured live traffic」); packages/runtime/src/domains/packages.ts now reads const declaredBody = PackageInstallBodySchema.safeParse(body) (PR #20218, c02fa127, 13:36Z), so the door answers what the declaration says and rows 1b, 3, 4 and the manifest / bare-form halves of row 2 of this card are closed. The retained ruling-A premise, 「the declaration must not refuse a body the door answers 201 to」, was true of a door that did not parse its body; it is circular for a door that answers per declaration, so it no longer constrains this choice. The failure it leaves open is concrete: { manifest, enabledOnInstall: false } has its misspelled key dropped and installs enabled. Prior-ruling sweep for wrapped.*strict|PackageInstallRequestSchema over AGENTS.md, docs/adr, packages/spec/src: the docblock and its pins only; ADR-0024 §4, ADR-0029 D3 / D9.7 and ADR-0045 §2 (from the seat's read) do not reach this cell.

    Mainstream reading the ruling rests on: the Kubernetes API server's strict field validation rejects an unknown field and names it; Terraform refuses an undeclared attribute in a resource block (Unsupported argument); OpenAPI request bodies close with additionalProperties: false. None keeps one part of a request body strict and another silently lossy. First-party reach measured by the #19328 dev: the SDK sends only manifest / settings / enableOnInstall / overwrite, the objectui package dialog sends { manifest } — the tightening costs nothing today; B keeps a real misspelling path that inverts an author's intent.

    Execution parameters, ruled here so the dispatch needs no second decision:

    1. Carrier: spec: PackageInstallRequestSchema's wrapped branch refuses top-level unknown keys by name — ruling A on #19328 #20249 (domain:spec, p2), filed by this seat with the ruling's specification: strip → strict on the wrapped branch; the docblock's ruling-A sentence replaced by this ruling's citation and reason; package-api.test.ts pins for a misspelled option and a private key, with the declared-options round-trip as control; PR fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218's §5 alignment pin re-read and corrected if it asserted the strip.
    2. Changeset @objectstack/spec minor, **BREAKING** banner, Clause-②: no (narrowing), ADR-0087 registered disposition, in 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 reads manifest.id positionally and never parses the body through ManifestSchema — POST /packages answers 201 to ids that MANIFEST_ID_PATTERN (spec, defineStack, os build, the publish face) refuses #19417; out-of-repo callers NOT MEASURED, said in the changeset; the refusal's remedy is the migration (remove the key or spell the declared option).
    3. [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 docblocks PR fix(runtime): the package install door parses its whole body through PackageInstallBodySchema #20218 made false) touches the same region; one PR may carry both if one dev holds both.
    4. The runtime door needs no change; the alignment pin flips to refusal on its own.
    5. This card: needs-user-decision → pm:blocked, Blocked-by: #20249; it closes when spec: PackageInstallRequestSchema's wrapped branch refuses top-level unknown keys by name — ruling A on #19328 #20249 lands (its four residual classes are then all closed). spec: PackageInstallRequestSchema's wrapped branch refuses top-level unknown keys by name — ruling A on #19328 #20249 leaves pm:on-hold for pm:queue in this act.

    Confidence gap carried: no telemetry on out-of-repo callers; a caller that sends private top-level keys gets a 400 naming the key.

    State: needs-user-decision → pm:blocked in this act; body first line set to this ruling.


    Generated by Claude Code

  14. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Closed completed: all four residual classes of the install door are closed on main

    Triage seat (objectstack-wide, seat post #6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-28T02:16Z. ⛔ Not a claim. Unlock scan: this card's only target, #20249, closed completed at 2026-09-27T20:15Z.

    Carried: #20219 (the docblock's stale 「201」 residual text and the DOOR_201_RESIDUALS pin title) was blocked on this card, and it is released to the queue in this act.

    Reopen if the install door is measured answering 201 to any of the four classes again.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions