Skip to content

fix(spec): PackageInstallResponseSchema declares the bare installed row the install door serves - #30593

Queued
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/issue-30464-install-response-bare-row
Queued

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/issue-30464-install-response-bare-row

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
Fixes #30464

Clause-②: yes (narrowing) — PackageInstallResponseSchema now accepts the bare installed row the door serves, which it refused, and refuses the { package } wrapper it accepted; PackageInstallResponse['data'] loses package, dependencyResolution, namespaceConflicts and message. No export is added, removed or renamed.
Metadata-protocol: none — the shape of an API answer, not anything a metadata author writes.
Skills-docs: none — git grep -nE 'PackageInstallResponse|data\.package\b' -- skills/ content/docs/ ':!content/docs/references' has 0 hits; the data.package reads in packages/ and ui/ source are other answers (override declarations, the permission-set clone, the marketplace detail), none the install answer.

维护者速读

What changes

The published contract follows the wire; the door is not changed. Triage audit 6658640900: "The published contract is wrong, not the door … The change corrects the published schema to match the wire".

  • packages/spec/src/api/package-api.zod.ts: PackageInstallResponseSchema.data is InstalledPackageSchema, reused rather than restated: the InstalledPackage row SchemaRegistry.installPackage recorded, which the door answers as success(pkg) with status 201 on both install arms. The docblock says why, names the authoring stage of the row (this door computes no writable / disableable), and names the pin. The type names PackageInstallResponse / PackageInstallResponseParsed and the PackageApiContracts.installPackage binding are unchanged.
  • packages/spec/src/api/package-api.test.ts: the two cases that pinned the wrapper are replaced by three: the bare row parses; the { package } wrapper is refused at data.manifest and data.source; PackageApiContracts.installPackage.output is this schema.
  • packages/client/src/packages-write-envelope.test.ts: a contract-vs-wire block. A real SchemaRegistry and the real HttpDispatcher.handlePackages('', 'POST', …) produce the 201 body, which goes untouched to PackageApiContracts.installPackage.output.safeParse: it parses green, data equals the registry's row and the parse strips no key the door sent; the wrapper built from the door's own row is refused. Nothing in the block writes the expected body by hand.
  • packages/spec/dropped-refinements.baseline.json: the three api/PackageInstallResponse sites move from data.package.manifest.… to data.manifest.…: the same three refinements, their path moved with the shape (the spec build gates this ledger).
  • Regenerated: content/docs/references/api/package-api.mdx; docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (api/ 501 → 499 sites: the wrapper object and the conflict-entry object are gone).
  • Changeset (@objectstack/spec: patch) with both declaration lines, the FROM → TO mapping, the consumer fix and the ADR-0087 disposition not-required (no-migration-prescription).

Tests

Head 3789cc39e (this branch merged with origin/main at c23eafa88).

Package suites, typecheck, eslint
  • pnpm --filter @objectstack/spec test: Test Files 815 passed (815), Tests 25644 passed, 1 todo.
  • pnpm --filter @objectstack/client test: Test Files 54 passed (54), Tests 699 passed. The pin file alone: 6 passed (6).
  • pnpm --filter @objectstack/runtime test: Test Files 543 passed (543), Tests 8655 passed, 19 skipped. The suites that drive the install door (handlePackages('', 'POST', …)): the eight packages/runtime/src/domains/packages-install-*.test.ts, packages-ensure-customization.test.ts, http-dispatcher.test.ts.
  • pnpm --filter @objectstack/spec run typecheck: exit 0 (test layer: 52 files / 240 errors / 131 signatures held in the existing debt ledger, unchanged). pnpm --filter @objectstack/client run typecheck: exit 0 (test layer 0 / 0 / 0).
  • eslint (--no-inline-config) over the three changed TypeScript files: exit 0, no findings.
Dogfood battery (owed by the narrowing arm)

pnpm --filter @objectstack/dogfood test under the verify lock, after a full workspace build (turbo run build, 119/119 tasks): VERDICT command-exit 0; Test Files 420 passed, 3 skipped (423); Tests 3496 passed, 36 skipped (3532); no failure.

Ablation: main's wrapper put back on `data`
  • Mutation: scripts/ablation-replace.mjs replaced the data: line with main's { package, dependencyResolution?, namespaceConflicts?, message? } object (anchor 1 → 0, marker ABLATION_30464) and moved the three ledger sites back to data.package.manifest.… (anchor 3 → 0): a two-file mutation, because the spec build gates dropped-refinements.baseline.json. Spec rebuilt; ablation-dist-preflight read the marker in 2 built files.
  • Under the mutation: client pin 2 failed, 4 passed (6), exactly the two #30464 cases; spec pin 2 failed, 96 passed (98), the bare-row and wrapper-refusal cases.
  • Restore: both paths back to their HEAD blobs, git diff HEAD empty; spec rebuilt; preflight --absent (marker absent from all 592 built files, tree clean). Client pin 6 passed (6); spec pin 98 passed (98).
  • The first mutation attempt is void: it moved only the schema, the spec build refused on the ledger, the preflight read the marker ABSENT from dist/, and its client reading was discarded.

Acceptance notes

  • Package install: POST /api/v1/packages installs a package with no protocol range unchecked and says so only in the server log (the install-local door now answers it) #30412 depends on this shape. That card (domain:server#6) has the install door answer an optional protocolCheckSkipped notice, as the install-local door does. With this PR the declared data is the bare InstalledPackage row, so where that notice sits (on the envelope beside data, or elsewhere) is decided on that card against this shape; there is no wrapper left to add a member to. packages/runtime/** is untouched in this PR.
  • Declarations changed from the claim. The claim carries Clause-②: yes and Metadata-protocol: semantic. Measured and read against the rules, this PR declares yes (narrowing) and none, at patch: semantic is "the meaning or default of an authorable key", and this schema is an API answer; the narrowing half (the wrapper refused, four type members gone) is real on the published @objectstack/spec/api surface. Both pairs pass check-changeset-no-major and check:adr-0087-registration, which do not judge the act; none with minor exits 1 (none-minor). The seat updates the claim lines.
  • ADR-0087 vocabulary gap, noted. type-surface-only is closed to a narrowing whose schema lives in packages/spec (predicate 2, no-spec-diff), and runtime-interface-only to a *.zod.ts, so a spec API-answer narrowing whose affected party is a TypeScript consumer has no exact category. It carries no-migration-prescription, which the gate admits because its detector reads no arrow or framed table in the consumer line; the marker says so and cites D8.
  • InstallPackageResult (packages/spec/src/contracts/package-service.ts) still declares { package, dependencyResolution?, namespaceConflicts?, message? }. It is the in-process IPackageService result, not the HTTP answer, and this PR does not touch it.

Verification

Head 3789cc39e.

Derived gate families: 102 run, reconciled
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 102 commands for 7 paths vs merge base c23eafa88. Each was run with its exit code captured before any pipe; --ran answers "102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero: all 102 carry an exit code).
  • First pass: 100 exit 0, 2 exit 3 (PREREQUISITE NOT MET, nothing measured). check-plugin-teardown-shape --self-test needed a positive-control commit this shallow clone lacked: fetched at depth 1, rerun exit 0 (48 cases). check:dual-build-cjs-loads needed 32 more packages' dist/: full build, rerun exit 0 (116 entries across 71 packages load).
  • check:skill-examples: the first run exits 3, PREREQUISITE NOT MET, because packages/client-react/dist held no .d.ts. After pnpm --filter @objectstack/client-react build: exit 0, "283 prose examples type-check across 3 surface(s)".
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date.
  • Local-only families (spec build touched): check:dev-prereqs exit 0 after the full build (107 artifacts present); check:skill-frame-freshness exit 0.
Declaration gates
  • check-changeset-no-major.mjs --base origin/main --event with a pull_request payload carrying this body's lines: exit 0, "LEVEL AXIS: … a non-protocol change at patch".
  • check-adr-0087-registration.mjs --base origin/main: exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [clause-②-narrowing] not-required (no-migration-prescription)".
  • Control: the claim's pair (yes / semantic / minor) also exits 0 on both gates, and none beside the old minor exits 1 (none-minor). The gates check the declarations against the levels; which pair is true is read from the diff (Acceptance notes).
Outside the derived total
  • Four CI-only families take a value from the workflow ($RUNNER_TEMP, the shard matrix) and have no local invocation: check-issue-citations.mjs --census, check-shard-attestation.mjs (dogfood, dogfood-verify) and check-test-completeness.mjs. NOT MEASURED locally; read on this PR's check-runs.
  • origin/main moved 15 commits past the merge base (to 48b3ae6eb) after the merge. None touches this diff's files or the install answer: the two inside its packages are the seed-apply catch in packages/runtime/src/domains/packages.ts and the seed landed describe in packages/spec/src/api/protocol.zod.ts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P

…ow the install door serves

POST /api/v1/packages answers success(pkg): the InstalledPackage row as
data. The published contract declared data: { package, dependencyResolution?,
namespaceConflicts?, message? }, a wrapper no surface sends. The contract now
follows the wire; a contract-vs-wire pin in the client harness drives the real
dispatcher and parses its answer with PackageApiContracts.installPackage.output.

Claude-Session: https://claude.ai/code/session_01WGufzgC3iduZ3nT4eSzSkT
Co-authored-by: Claude <noreply@anthropic.com>
…, add the changeset

Claude-Session: https://claude.ai/code/session_01WGufzgC3iduZ3nT4eSzSkT
Co-authored-by: Claude <noreply@anthropic.com>
…ocol narrowing at patch

The schema now refuses the { package } wrapper it accepted and the
PackageInstallResponse type loses four members, so the Clause-2 line
carries the narrowing arm. An API answer shape is nothing a metadata
author writes, so the kind is none and the level patch. The ADR-0087
disposition is not-required (no-migration-prescription), with D8 named.

Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/environment-routing.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/automation/connectors.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/deployment/app-dependencies.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/deployment/cli.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/deployment/install-modes.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/deployment/marketplace-handbook.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/frontend/plugins/plugin-chatbot.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/getting-started/examples.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/kernel/contracts/metadata-service.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/permissions/permission-sets.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/ui/apps.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/releases/v17/17-5.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/releases/v17/17-7.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/releases/v18/18-0.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/releases/v18/18-1.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))
  • content/docs/releases/v18/18-2.mdx (via /api/v1/packages (route, a path literal in a comment on a changed line; bridged from symbol PackageInstallResponseSchema — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bdbaf80cdb01ececd5af069757fb1f35c653bb8a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 74d921b447c3630b7115cbeb7b912fb16be3bdb0 — the merge of head 89a687971f528d1578b45dd35c9ae3f669bc46ca into base bdbaf80cdb01ececd5af069757fb1f35c653bb8a, which is what actions/checkout gives a pull_request run. Not the PR head.

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

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

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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bdbaf80cdb01ececd5af069757fb1f35c653bb8a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3789cc39e295168beb002b55ef182667dee0d067
Local-runs: none
Scope: packages/spec/src/api/package-api.zod.ts + packages/spec/src/api/package-api-assembled.zod.ts, packages/spec/src/kernel/package-registry.zod.ts, packages/spec/src/api/contract.zod.ts, packages/spec/src/contracts/package-service.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/http-dispatcher.ts, packages/objectql/src/registry.ts, packages/client/src/packages-write-envelope.test.ts, packages/spec/src/api/package-api.test.ts, .changeset/30464-install-response-bare-row.md, .changeset/30326-install-answer-protocol-check-skipped.md, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, AGENTS.md, .github/workflows/pr-automation.yml, docs/adr/0087-metadata-protocol-upgrade-contract.md

Reviewed read-only against card #30464 (body and every comment, 6658640900 through 6664176068), PR #30593's body and file list, the net diff c23eafa88..3789cc39e, and the check-runs on the head. Independent of the dispatch order and of the dispatching seat's conclusions.

① Derived judgments

  • Accept-set, widened: PackageInstallResponseSchema.data now parses the bare InstalledPackage row. Right. The door answers deps.success(pkg) with res.status = 201 (packages/runtime/src/domains/packages.ts L1900-1902) inside the dispatcher envelope { success: true, data, meta } (packages/runtime/src/http-dispatcher.ts L1122-1125). On both install arms pkg is the registry's own row: the protocol arm unwraps its { package } (pkg = out?.package ?? out, L1740), the fallback arm takes registry.installPackage(manifest, 'authored', settings) directly (L1761); the enableOnInstall arms reassign it only to the row enablePackage / disablePackage return. Nothing between the arms and the 201 wraps it.
  • Accept-set, narrowed: the { package, dependencyResolution?, namespaceConflicts?, message? } wrapper is refused at data.manifest and data.source. Right. No surface sends it: git grep of data.package over packages/ and ui/ at the head finds reads of other answers only (override declarations, the packaged-base refusal, the permission-set clone, the marketplace detail page), none of this route; no file outside packages/spec/src/api/ names PackageInstallResponse.
  • Row stage and keys: declared as InstalledPackageSchema (authoring stage, no writable / disableable). Right. The install door never calls withWritableVerdict / withDisableableVerdict (those sit on GET /packages/:id, L2862), and the row SchemaRegistry.installPackage builds carries exactly manifest, source, status, enabled, statusChangedAt?, installedAt, updatedAt, settings (packages/objectql/src/registry.ts), every one a key InstalledPackageSchema declares, so the non-strict z.object parse strips nothing the door sends. manifest goes through toRecordManifest(withDeclaredFunctionEntries(...)), a key-by-key projection that adds no top-level key, so ManifestSchema's strict close is not tripped by the projection; the client pin is the witness and the head's Test Core shards are the gate.
  • Public surface: export names PackageInstallResponseSchema, PackageInstallResponse, PackageInstallResponseParsed unchanged (no api-surface/ row moves in the diff); the binding PackageApiContracts.installPackage.output unchanged (package-api-assembled.zod.ts L285-289), pinned by the new spec test. The type PackageInstallResponse['data'] loses four members and gains the row's. Right, and correctly declared as the breaking half.
  • Entry graph: InstalledPackageSchema was already imported into /api for the old wrapper, so the /api vs /api-assembled split (api-entry-graph.pin.test.ts) is untouched in kind; DependencyResolutionResultSchema stays in use at L655. Right.
  • Ledgers: the three api/PackageInstallResponse dropped-refinement sites move data.package.manifest.… to data.manifest.… (same refinements, moved path); the unknown-key count api/ 501 to 499 is the wrapper object and the conflict-entry object gone. Right by count; check:generated is the gate.
  • content/docs/references/api/package-api.mdx: generated output, read only as the rendering of the new data description; not reviewed.

② Semver level

  • @objectstack/spec: patch, Clause-②: yes (narrowing), Metadata-protocol: none in both the PR body and the changeset. Matches what the diff publishes. pr-automation.yml "WHICH LEVEL" puts REST answers under none (patch); AGENTS.md step 3: "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". Not skip-changeset: the spec publishes.
  • The changeset carries the FROM to TO mapping, the one-line consumer fix, and exactly one ADR-0087 marker, not-required (no-migration-prescription). The Check Changeset run on the head is green.
  • Should-fix (S1), .changeset/30464-install-response-bare-row.md: no **BREAKING** banner. The rule above says a non-protocol breaking change keeps the banner, and 21 of the 22 stock yes (narrowing) changesets carry one. Should-fix rather than must-fix because the ADR-0087 gate reads the (narrowing) arm as the breaking declaration (signal 4 in breakingDeclaration, check-adr-0087-registration.mjs), so the disposition was asked for and given and no carrier is missing mechanically; add one **BREAKING** line to the changeset body before landing so the published CHANGELOG entry greps like its siblings.

③ Boundary flags

  • Declaration lines (open question in os-dev-report 6664176068): the diff supports Clause-②: yes (narrowing) / Metadata-protocol: none / patch, as the PR and changeset now carry. semantic is defined in AGENTS.md step 3 and the workflow text as "the meaning or default of an authorable key"; data of POST /api/v1/packages is a REST answer no metadata author writes, which the workflow lists under none. The narrowing arm is real: the schema refuses where it accepted, and PackageInstallResponse['data'] drops four members. check-changeset-no-major grades the level against the kind only and none plus patch is consistent; check-adr-0087-registration reads the arm and finds the disposition. The stock precedent .changeset/30343-timeline-one-action-one-entry.md declares a read-answer narrowing the same way. So the claim comment 6662275642 (Clause-②: yes, Metadata-protocol: semantic) is what moves, and that is the seat's own act. Answered, not escalated.
  • ADR-0087 category (out_of_scope 1): type-surface-only is closed by its own predicates 2 and 3 (no-spec-diff, no *.zod.ts), runtime-interface-only by the zod file, so no-migration-prescription is the one category the gate admits here; the marker's prose cites D8 for why the compiler is the channel. Consistent with the gate and the ADR as they stand; a sharper category is a vocabulary card, not this PR. Answered.
  • Gate prose vs level (out_of_scope 2): the "ships minor during the launch window" sentence in check-changeset-no-major.mjs's header predates ruling A; the LEVEL AXIS verdict is what exits. Observation, no action owed here.
  • InstallPackageResult (out_of_scope 3): packages/spec/src/contracts/package-service.ts L47-56 is the in-process IPackageService.install result, and the door unwraps it (out?.package ?? out). Not the HTTP contract; correctly left alone.
  • Package install: POST /api/v1/packages installs a package with no protocol range unchecked and says so only in the server log (the install-local door now answers it) #30412 dependency (Acceptance note): with data declared as the bare row, a data.protocolCheckSkipped member added the way the install-local door did it (.changeset/30326-install-answer-protocol-check-skipped.md) would be silently stripped by PackageApiContracts.installPackage.output.parse and stay undeclared, because InstalledPackageSchema is a non-strict z.object. So Package install: POST /api/v1/packages installs a package with no protocol range unchecked and says so only in the server log (the install-local door now answers it) #30412 (domain:server#6, runtime only) cannot land that notice on this route without a spec change to PackageInstallResponseSchema (an extend of the row, or a declared place on the envelope). Not a defect of this PR; escalated as the content of the Notify: landed the claim already owes Package install: POST /api/v1/packages installs a package with no protocol range unchecked and says so only in the server log (the install-local door now answers it) #30412.
  • Skills-docs: none verified at the head: git grep of PackageInstallResponse|data.package|dependencyResolution|namespaceConflicts over content/docs/ (minus references/, releases/) and skills/ has 0 hits; the Docs Drift Check's 14 pages are listed via the route literal in a comment line and none restates this answer's shape. No doc edit owed.
  • File surface: all seven paths inside the claim's surface; packages/runtime/**, ui/**, skills/**, docs/adr/**, .claude/**, AGENTS.md untouched. Holds.
  • Check-runs on 3789cc39e: 65 check-runs, all completed: 57 success, 8 skipped (opt-in and matrix placeholders), 0 failures. These conclusions are the gate verdicts for every derived family, the declaration gates (Check Changeset, PR-body guards, Governed Surface Queue Guard), Type Check UI and the Test Core / Test UI shards; nothing was re-run locally.

④ Design judgment

packages/spec/src/api/package-api.zod.ts (the one hits file), judged against the triage ruling it implements (6658640900: "the published contract is wrong, not the door") and its first consumer PackageApiContracts.installPackage.output, with the real install door as the wire witness.

  • The declaration is the row the door serves, by reference to the one schema the registry row is typed against, not a restatement: a later key on InstalledPackageSchema reaches this answer without a second edit. Holds.
  • Every claim the new docblock makes was read in the live tree and holds: both arms (L1738-1761), 201 (L1900-1901), the client following the wire since client.packages declares envelopes two mounted surfaces do not emit: get diverges between them, and install/enable/disable declare { package } where the only serving surface sends the bare row #12034 (packages-write-envelope.test.ts L159-176), no per-read verdicts on this door (L2862 is the GET).
  • Break attempts: (a) a non-row answer on some install path: none, every reachable pkg is a registry row; refusals (400/409/422 and the dependency-missing answer) leave through deps.error, which BaseResponseSchema.error already declares. (b) A key the door sends that the parse strips: none, see ①. (c) A key the schema requires that the door omits: manifest and source are the only required keys and the registry stamps both. (d) The wrapper kept alive by some consumer: none in packages/ or ui/ (Post-Task Checklist step 4 satisfied; Type Check UI on the head is the gate). (e) Pinning: the spec test pins the schema and the binding; the client test parses the real dispatcher's body untouched and refuses the wrapper built from the door's own row, so the contract and the wire cannot both read true again.
  • No must-fix. No should-fix in the hits file. The one should-fix (S1) is in the changeset, named in ②.

Implemented-by: claude/issue-30464-install-response-bare-row
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

…rowing

A non-protocol breaking change ships patch and keeps its banner; the
line follows the spelling the other narrowing changesets use.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Regen-provenance: 6664812981 · 3789cc39e295168beb002b55ef182667dee0d067 → 89a687971f528d1578b45dd35c9ae3f669bc46ca · git diff --name-only 3789cc39e295168beb002b55ef182667dee0d067 89a687971f528d1578b45dd35c9ae3f669bc46ca -- . ':!.changeset/' → (empty)

维护者速读

  • 改了什么:已发布的 API 契约里「安装包」接口(POST /api/v1/packages)的应答形状改成服务器实际返回的样子:data 就是安装后的包记录本身,不再声明一个从未发送过的 { package, … } 外层包装。服务器和官方客户端的行为都不变。
  • 为什么改:照着契约写集成代码的人会读 data.package,实际拿到 undefined;官方客户端早已按真实返回改过,只剩已发布契约是错的。依赖它的 Package install: POST /api/v1/packages installs a package with no protocol range unchecked and says so only in the server log (the install-local door now answers it) #30412 要在这个形状上决定安装门是否加提示。
  • 风险与回滚:只改 spec 里一个应答 schema 和由它生成的文档,服务器代码不动。按旧契约读 data.package 等四个成员的 TypeScript 代码升级后会编译报错(这些代码运行时本来就读到 undefined),changeset 标了 **BREAKING** 并写明改法。回滚即撤本 PR。
  • 先看:packages/spec/src/api/package-api.zod.ts 的 PackageInstallResponseSchema(data 那一行);packages/client/src/packages-write-envelope.test.ts 的契约对线上应答块(真实安装门的应答原样交给契约解析);.changeset/30464-install-response-bare-row.md。
  • 两分钟验证:pnpm --filter @objectstack/client exec vitest run src/packages-write-envelope.test.ts,预期 6 条全绿;把 data 换回 main 上的包装(连同 dropped-refinements.baseline.json 里那三行)并重建 spec 后,该块的 2 条转红。
  • 需要你做的:无,席位落地。
  • 席位意见:接受。契约面达档复核 PASS(comment 6664812981,Head-sha 3789cc39),复核之后只补了 changeset 一行,用上面的 Regen-provenance: 续管。声明行按 diff 改为 yes (narrowing) / none / patch,认领已修订(6664850682)。

席位核对

ACCEPT · head 89a687971f528d1578b45dd35c9ae3f669bc46ca · 2026-11-05T17:39Z

  1. 形态:draft,base main,正文第 5 行 Fixes #30464,全文只有这一个闭合关键词。7 个文件全在认领文件面内;packages/runtime/**、ui/**、skills/** 都没碰,也没有 content/docs/releases/。正文节序是 What changes / Tests / Acceptance notes / Verification,多了一节 Tests,接受。
  2. 前提实测:在 head 上读到,安装门答 deps.success(pkg),并在 packages/runtime/src/domains/packages.ts L1900-1901 设 res.status = 201。协议臂先把 { package } 拆开(L1740 pkg = out?.package ?? out),所以 data 是裸行,旧契约的外层包装从未发送。前提成立。
  3. 门禁 check-runs:
    • 旧 head 3789cc39 上 57 success、8 skipped、0 failure,全部跑完。
    • 新 head 89a68797 只多一行 changeset,新一轮 CI 在跑,全绿后才 pr_ready + automerge_enable。
    • 契约复核 6664812981 在 Served-tier: CONTRACT_REVIEW_TIER 下 PASS,Scope 覆盖 hits(package-api.zod.ts)和所引代码。
    • dev 本地 102 条派生门禁 --ran 0 NOT-MEASURED,只证本地子集。
    • (narrowing) 要求的 dogfood_battery 在 3789cc39 上全量 command-exit 0(420 个文件通过、3 个跳过)。到 89a68797 只改了 .changeset/,运行时代码不变,这个读数沿用。
    • 补丁轮在新 head 上重跑 check:adr-0087-registration 和 check-changeset-no-major(带 PR 事件),都 exit 0。
  4. 席位自读(TIER_DEFAULT,hits 之外的部分):
    • package-api.test.ts 用三条用例代替了钉旧包装的两条:裸行能解析,包装在 data.manifest 和 data.source 被拒,绑定的 output 就是这个 schema。
    • 客户端 pin 用真 SchemaRegistry 加真 HttpDispatcher.handlePackages 产出 201 应答,原样交给 safeParse,没有手写期望应答。
    • dropped-refinements.baseline.json 那三处位点只是随形状从 data.package.manifest.… 挪到 data.manifest.…。docs/audits/…/api.md 从 501 到 499 个位点,少的是包装对象和冲突条目对象。
    • changeset 逐句核过:
      • FROM → TO 与 diff 一致。
      • “The server's answer is unchanged”与 diff 不碰 packages/runtime/** 一致。
      • “Replace response.data.package with response.data”就是给消费者的一行改法。
      • **BREAKING** 行列的四个成员,与旧 schema 的四个键一致。
      • ADR-0087 处置 not-required (no-migration-prescription) 过闸,理由写在注释里(API 答案没有存储行可迁移)。
  5. 接受的偏差:
    • 声明从认领的 yes / semantic / minor 改为 yes (narrowing) / none / patch。依据是 diff:API 答案不是元数据作者能写的东西。认领已修订(6664850682)。
    • 复核 should-fix S1(缺 **BREAKING** 标记)已在 89a68797 补上。
    • main 在 dev 合并后又前进了 15 个提交,都没碰本 PR 的文件,mergeable_state 不是 dirty,所以不再合并。
  6. 范围外去向和 Skills-docs::
  7. 落地条款:check-governed-merges.mjs --pr 30593 在新 head 上重读。215 行 ≤ 3000,没碰 governed 路径,属于 Tier S。新 head 全绿后经 relay pr_ready + automerge_enable,跟到 MERGED。同一动作里摘 pm:dispatched,向 server#6 信箱 pm:mailbox — domain:server#6 · os-bill #30458 发 Notify: landed · #30464,带上上面的升级点。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 18:12
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 18:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

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

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PackageApiContracts.installPackage declares data.package for POST /api/v1/packages, but the door answers the bare installed row

1 participant