Skip to content

fix(runtime)!: package commit revert and rollback act only on a commit of the package their path names - #29090

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ae077b
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ae077b

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28987

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no page under skills/** or content/docs says which package a commit revert or rollback acts on; content/docs/api/metadata-api.mdx only lists the door names.

维护者速读

  • 改了什么:POST /packages/:id/commits/:commitId/revert 和 POST /packages/:id/rollback 现在只处理路径里那个包自己的提交。提交属于别的包 → 404 COMMIT_NOT_FOUND("No commit 'X' in package 'B'."),什么都不写;路径里的包不存在(含已卸载) → 404 Package 'X' not found,与兄弟门一致。
  • 为什么改:原来两个门都不读 :id。按包回滚的管理员一旦把提交号串错包,就会悄悄回滚另一个包的元数据(创建的被软删、编辑的被还原),回 200,还往那个包的历史里写一条 revert 提交,即使那个包已卸载。
  • 风险与回滚:同包的提交照旧回滚,响应体不变;鉴权(manage_metadata)、围墙目录、查询参数三道拒绝的顺序不变。只读包(代码包/安装包)不加只读守卫:实测它们唯一的提交是环境覆盖(ADR-0131 D6)发布,撤销覆盖仍可用,已钉为对照。回滚即还原本 PR。
  • 先看:packages/metadata-protocol/src/protocol.ts 的 readRevertableCommit(新增的包比对);packages/runtime/src/domains/packages.ts 两个门把 packageId 交给协议、未知包 404;packages/qa/dogfood/test/package-commit-revert-path-package.dogfood.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/package-commit-revert-path-package.dogfood.test.ts(需先建闭包),预期 7/7 通过;在 main 上其中 3 例为红。
  • 需要你做的:无,席位落地。

What changes

  • @objectstack/metadata-protocol. revertCommit and rollbackToPackageCommit take an optional packageId. readRevertableCommit, the one commit read both use, refuses a commit whose package_id is not that package with the existing COMMIT_NOT_FOUND (404), worded No commit 'ID' in package 'PKG'., before the legacy-organization check and before anything is written. A caller that sends no packageId reads any commit, as before. This optional member is a widening of two published method signatures, beside the door narrowing: hence yes (narrowing).
  • @objectstack/runtime. Both doors read :id and hand it to the protocol as packageId. An unknown :id (packageIsUnknown, the predicate the draft doors and GET /:id/commits use) answers 404 Package 'ID' not found without calling the protocol. The check sits inside each door's capability probe, so a partial protocol host still answers 501, and the rollback's 400 Body { commitId } is required still comes first. manage_metadata, the walled-catalog refusal and the closed query set keep their order ahead of it.
  • No read-only guard (seat's ruling, option A). The order asked for requireWritablePackage on both doors. Measured on main: a read-only package carries commits. An environment overlay draft on showcase_task.default bound to com.example.showcase (a booted code package), published with publish-drafts, records an apply commit in the showcase's history, and reverting it through the showcase's own path answers 200 and removes the overlay. Those are the only commits a read-only package can carry (POST /packages/:id/publish already refuses read-only packages). The sibling POST /packages/:id/revert answers a read-only package's stored rows before its own guard, so the guard there never refuses them. A guard on these doors would refuse the undo of an ADR-0131 D6 customization, a narrowing beyond the card. The seat chose not to add it; the overlay revert is pinned as a control.

BREAKING (patch, ruling A #25105: a non-protocol narrowing ships patch):

  • FROM: POST /packages/B/commits/COMMIT_OF_A/revert answered 200, reverted A's items and recorded a revert commit in A's history. TO: 404 COMMIT_NOT_FOUND, nothing written.
  • FROM: POST /packages/B/rollback { commitId: COMMIT_OF_A } answered 200 and reverted A's newer commits. TO: 404 COMMIT_NOT_FOUND, nothing written.
  • FROM: either door with an unknown or uninstalled :id acted on the commit. TO: 404 Package 'ID' not found.

Tests

  • Dogfood, new package-commit-revert-path-package.dogfood.test.ts (7 cases, showcase boot, REST): cross-package revert 404 and A's history and items unchanged; cross-package rollback 404, both histories and A's items unchanged; unknown package 404 on both doors; member without manage_metadata 403 on both; controls: own-path revert (200, revertedCount: 1, revert commit recorded, item removed) and own-path rollback (200, the newer commit reverted); a read-only package's overlay commit: 404 through another package's path with the showcase history unchanged, 200 through its own path removing the overlay.
  • Runtime unit, new packages-commit-door-path-package.test.ts (6 cases): each door hands the protocol { commitId, packageId }, relays the protocol's COMMIT_NOT_FOUND as 404, and answers an unknown package 404 without calling the protocol.
  • Protocol unit, packages/objectql/src/protocol-commit-history.test.ts (+2 cases): revertCommit and rollbackToPackageCommit with another package's id reject COMMIT_NOT_FOUND/404 with the message, with no delete, no restore and no new commit row; the same call with the commit's own package reverts.
Red on main, green on head, ablation
  • Red on main ab085a04 (dogfood file, dist/ built from main): 3 failed | 4 passed (7). Cross-package revert: 200 {"revertedCount":1,"reverted":[{"type":"object","name":"cmtpa_second","action":"removed"}],"revertCommitId":"cmt_…"}. Cross-package rollback: 200 {"revertedCommits":["cmt_…"],"failed":[]}. Unknown package: 200 (revertedCount: 1). The controls passed on main.
  • Read-only probe on main (same boot): overlay draft save 200 (Saved customization overlay (env-wide, package=com.example.showcase, state=draft)), publish-drafts 200 outcome: published with a commitId, GET /packages/com.example.showcase/commits lists that apply commit, its revert 200 reverted: [{ type: view, name: showcase_task.default, action: removed }].
  • Green on head a4647aa1: dogfood file 7 passed (7); @objectstack/runtime pnpm test: 514 passed files, 8273 passed | 19 skipped tests; objectql protocol-commit-history.test.ts 32 passed (32).
  • Ablation (scripts/ablation-replace.mjs on readRevertableCommit: the condition becomes packageId === '__ablated_pkg_check__' && …, so it never matches; @objectstack/metadata-protocol rebuilt; ablation-dist-preflight found the marker in 2 built files): dogfood 3 failed | 4 passed (7), exactly the cross-package revert, the cross-package rollback and the overlay case at its cross-package step (expected 404, received 200); the unknown-package case (runtime check) and every own-path control stayed green. objectql 2 failed | 30 passed (32), the two new mismatch cases only. Restored: blob == HEAD, git diff HEAD empty, rebuilt, preflight --absent clean. A first attempt used if (false && …) as the mutation; the bundler folded it away, the dist preflight answered "marker ABSENT", and that run's reading was discarded.

Acceptance notes

Verification

Every local reading above is on head a4647aa1. pnpm typecheck for @objectstack/metadata-protocol, @objectstack/runtime (with check:test-typecheck), @objectstack/objectql (with check:test-typecheck) and @objectstack/dogfood: exit 0. Derived gate families (node scripts/pm/dispatch-gates.mjs --commands): 67 derived; at the time of writing 63 had run, 61 with exit 0; check-plugin-teardown-shape --self-test answered exit 3 on the shallow clone and exit 0 (48 cases) after git fetch --depth=1 of its fixture commit; check:dual-build-cjs-loads exit 3, NOT MEASURED (its prerequisite, every package's dist/, is not met in this checkout); check:type-check-debt, check:undeclared-dep-imports, check:watch-hint-literal and check:where-matcher were still running when the maintainer's pause arrived and are NOT MEASURED here. check-adr-0087-registration --base origin/main: exit 0, not-required (no-migration-prescription). Full dogfood battery (owed by the (narrowing) arm): started on this head under the verify lock, still running at the pause; NOT MEASURED here, the reading goes to the seat. The repo-wide lint, test:repo and the full farm are CI's.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5nWnKw7n4za79sxnfc

… of the path's package

POST /packages/:id/commits/:commitId/revert and POST /packages/:id/rollback
never read :id, so a commit of package A reverted through package B's path
answered 200 and wrote a revert commit into A's history. Both doors now pass
the path's package to the protocol, which refuses a commit of another package
with COMMIT_NOT_FOUND (404) before writing, and answer an unknown package id
with the sibling doors' 404.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nWnKw7n4za79sxnfc
…mmit of the path's package

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nWnKw7n4za79sxnfc
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime, touching 9 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/permissions/system-context.mdx (via handlePackagesRequest (symbol, a top-level function))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v18/18-1.mdx (via revertCommit (symbol, a method of class ObjectStackProtocolImplementation))

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
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 33 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 d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bcc66ff1e5a3822f76a984c1b2203d37efdb7a9e — the merge of head a4647aa192a5f76356d05441074559b0a324387e into base d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e, 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 bcc66ff1e5a3822f76a984c1b2203d37efdb7a9e && git checkout bcc66ff1e5a3822f76a984c1b2203d37efdb7a9e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e a4647aa192a5f76356d05441074559b0a324387e && git checkout -B drift-repro d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e && git merge --no-ff a4647aa192a5f76356d05441074559b0a324387e

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

⚠️ 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 d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e → 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:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

席位核对

ACCEPT (landing waits on a same-head contract review PASS) · domain:server seat 2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T08:20Z · head a4647aa1 · claimed by seat 2 on #28987 (comment 6572330372, taking over the released claim 6570512674)

The maintainer's words for this seat, verbatim: 「接手所有开发,包括跟进之前未完成的开发落地。」

  1. Shape: a draft against main, Fixes #28987, ! title, Clause-②: yes (narrowing) and Metadata-protocol: none at line start in the body and the changeset, BREAKING banner with FROM→TO and "what to do". check-governed-merges --pr 29090: NOT governed (0 of 6 paths), +381 / -5.
  2. Diff, read by the seat: both doors (POST /packages/:id/commits/:commitId/revert, POST /packages/:id/rollback) now hand the path id to the protocol as packageId and answer an unknown :id with the sibling doors' 404 Package 'ID' not found, after the existing manage_metadata, walled-catalog, query and (rollback) body refusals, whose order is unchanged. readRevertableCommit refuses a commit whose package_id differs with the existing COMMIT_NOT_FOUND 404 before any write; no new error code. The protocol request types gain an optional packageId; an in-process caller that sends none behaves as before. The previous seat's ruling holds: no requireWritablePackage on these doors (a read-only package's commits are its overlay publishes; pinned as a control).
  3. Evidence (os-dev-report 6571600286; the PR body's Verification section predates the pause and is superseded by this record): red on main 3 failed / 4 passed in the new dogfood file; green on this head 7/7; runtime 8273 passed; objectql commit-history 32/32; ablation on readRevertableCommit reds only the new mismatch cases (dogfood 3, objectql 2) and restores to HEAD (a first ablation folded away by the bundler was detected by the preflight marker and discarded). Full dogfood battery on a4647aa1 under the verify lock: 384 files / 3170 tests passed, 0 failed. Local gates: 67 derived, 66 run exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, prerequisite; CI runs it).
  4. Gates: check-runs on a4647aa1: 36 success, 13 skipped, 0 red, none pending.
  5. Owed before landing: a CONTRACT_REVIEW_TIER review on this head (the narrowing arm, plus the optional protocol member: the reviewer judges whether the declared yes is right). needs:contract-review is put on the PR in the same act.
  6. Out of scope: the dev's note (an uninstalled package's commit trail is still readable, only new reverts through its path are refused) is the intended behaviour, stated in the changeset; nothing filed. Serial: none. Landing after PASS: pr_ready + automerge_enable through the relay.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a4647aa192a5f76356d05441074559b0a324387e
Local-runs: none
Scope: packages/metadata-protocol/src/protocol.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/domains/packages-commit-door-path-package.test.ts, packages/objectql/src/protocol-commit-history.test.ts, packages/qa/dogfood/test/package-commit-revert-path-package.dogfood.test.ts + packages/runtime/src/http-dispatcher.ts (errorFromThrown), packages/objectql/src/registry.ts (getPackage, uninstallPackage), packages/spec/src/api/package-lifecycle.zod.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/client/src/index.ts (revertCommit, rollback), ui/packages/app-shell/src/preview/commitHistory.ts, docs/adr/0067-commit-history-and-rollback-for-ai-authoring.md

Inputs: card #28987 (body and all 6 comments, the claim 6570512674 ruling and the os-dev-report 6571600286 included), PR #29090 body and file list, git diff origin/main...a4647aa1, the check-runs on the head (54, all completed: 36 success, 18 skipped, 0 failed, none pending; Check Changeset, Governed Surface Queue Guard, Dogfood Regression Gate, Lint · PM dispatch-gates all success). Face: no published-schema or governed path in the committed tree; the changeset's Clause-②: yes (narrowing) arm decides, so the narrowing face at CONTRACT_REVIEW_TIER with every non-prose path of the diff as the floor.

① Derived judgments

  1. REST accept set narrowed (declared, BREAKING): POST /packages/:id/commits/:commitId/revert and POST /packages/:id/rollback now answer 404 COMMIT_NOT_FOUND for a commit whose sys_metadata_commit.package_id is not :id. Read in the tree: readRevertableCommit(commitId, packageId?) throws COMMIT_NOT_FOUND/404 with No commit 'ID' in package 'PKG'. right after the existing not-found throw and BEFORE the legacy-organization check, parseCommitItems, any repo.delete/restoreVersion and recordPackageCommit; the rollback reads the target through the same function before listCommits. Right: nothing is written on the refused path.
  2. REST accept set narrowed (declared): an unknown :id answers 404 Package 'ID' not found on both doors via packageIsUnknown(registry, id), placed inside the capability probe (501 still wins for a partial host) and after manage_metadata, walled-catalog and query refusals (rollback: after the 400 Body { commitId } is required check). Same predicate and same text as publish-drafts (metadata: the save door and the package publish door accept a package id that is not installed (object, dashboard, page …), storing and even publishing items into a phantom package; Studio renders a writable builder for it #26708) and GET /:id/commits. Right. An uninstalled package is unknown by this predicate: SchemaRegistry.uninstallPackage deletes the package collection entry, so getPackage answers undefined; the changeset's "which includes a package that has been uninstalled" holds by reading (not pinned by a test, see ③).
  3. Published protocol surface widened (declared as the yes arm): ObjectStackProtocolImplementation is exported from @objectstack/metadata-protocol's entry (src/index.ts line 3, exports["."] → dist/index.d.ts), and the request types of revertCommit and rollbackToPackageCommit gain an OPTIONAL packageId?: string. Additive and optional: every existing call compiles and behaves as before (packageId === undefined skips the comparison). Right, and yes (narrowing) is the one spelling scripts/pm/clause2-line.mjs reserves for "widens one surface and narrows another".
  4. No new error code: the mismatch reuses COMMIT_NOT_FOUND, which is in error-code-ledger.zod.ts (line 657), so HttpDispatcher.errorFromThrown relays status 404 and the code undemoted (the new runtime unit test asserts the code and message on the wire). The unknown-package 404 is the sibling doors' code-less deps.error(..., 404). Right: no error-code ledger change owed.
  5. Response bodies unchanged: RevertPackageCommitResponseSchema and RollbackToPackageCommitResponseSchema in packages/spec/src/api/package-lifecycle.zod.ts are untouched, and the success limb's return shape is untouched in both methods. Right.
  6. Own-package path unchanged: with the path's package, the comparison passes and the code after it is the pre-PR code line for line. Right.
  7. In-process callers: git grep finds no non-REST caller of either method besides the rollback's own inner revertCommit({ commitId: c.id }) steps, which iterate listCommits({ packageId: target.package_id }) and so cannot cross packages; packages/client (revertCommit(id, commitId), rollback(id, commitId)) and ui/.../commitHistory.ts (revertCommit(packageId, commitId)) already send the package id in the path, and the Console's timeline sends the package whose GET /:id/commits it rendered. Right: no consumer in the repo loses a working call.
  8. Metadata-protocol: none: nothing a metadata author can write changes (no key, value, type or default). Right.
  9. Skills-docs: none: no page under skills/** or content/docs/** states which package a commit revert acts on (the dev's reason is checked against the drift-check output: only symbol-level anchors in release-owned and concept pages). Prose face, outside this record's judgment; noted as consistent.

② Semver level

.changeset/package-commit-revert-path-package.md: @objectstack/runtime: patch, @objectstack/metadata-protocol: patch, both line-initial declarations present (Clause-②: yes (narrowing), Metadata-protocol: none), a BREAKING banner with three FROM → TO pairs and a "What to do" line, and exactly one adr-0087: not-required (no-migration-prescription) marker. Under ruling A (#25105, AGENTS.md Post-Task Checklist step 3: "the level follows the kind alone ... none takes patch; a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition"), patch on both packages is the ruled level for a none kind, the optional protocol member notwithstanding; check-changeset-no-major (the Check Changeset run on this head) is green. The ADR-0087 disposition is right: no authorable key, export or error code is removed or renamed, so no migration is prescribed. The declaration lines and the level agree with the diff.

③ Boundary flags

  • Dev flag: no requireWritablePackage on the two doors (seat's option A, against the claim's first wording "a read-only package is refused as the package-level revert refuses it"). Answered: holds. The sibling POST /packages/:id/revert asks the protocol for the stored rows FIRST and reaches requireWritablePackage only when stored: false, so it never refuses a read-only package's stored overlay rows either; a guard on the commit doors would refuse the undo of an ADR-0131 D6 overlay publish, which the dogfood control pins as working (200, view showcase_task.default removed) through the package's own path and 404 through another's. No must-fix.
  • Dev out-of-scope note: an uninstalled package's commit is now refused through its own path too (404 Package 'ID' not found), while GET /:id/commits still serves the trail. Answered: intended and declared in the changeset's third FROM → TO pair; consistent with the claim's ruling ("otherwise it answers 404 and writes nothing") and with the draft doors. Should-fix (not blocking): no test pins the uninstalled case; the dogfood "unknown package" case uses a never-installed id. The claim holds by reading uninstallPackage in registry.ts, but a one-case pin on the dogfood file would turn the changeset sentence from a reading into a measurement.
  • Ordering on rollback: the 400 Body { commitId } is required check precedes the unknown-package 404, where publish-drafts refuses the unknown id before validating its body. Declared in the PR body; harmless; noted, no fix owed.
  • Check-order of the mismatch vs the legacy-organization refusal: a commit of another package that is also legacy-organization-scoped now answers 404 instead of the 403 NOT_OVERRIDABLE; the path's package could never see that commit, so the 404 is the right answer and leaks nothing. No fix owed.
  • open_questions: [] in the os-dev-report; nothing to escalate. PR author is objectstack-fleet[bot]; no extra author note owed.

④ Design judgment

  • packages/metadata-protocol/src/protocol.ts (readRevertableCommit, revertCommit, rollbackToPackageCommit): the narrowing against ADR-0067 D3 and the spec on main: D3 declares revertCommit(commitId) and rollbackToPackageCommit(commitId) with commits "scoped per environment" and a package-scoped HEAD, and the REST ledger names both doors under /packages/:id/...; the spec never grants a consumer the ability to act on another package's commit through a package path. So the change makes the door do what its path names, and takes no declared capability away at the consumer. Tried to break it: a commit with a null package_id would become unreachable through REST, but every writer of sys_metadata_commit in the tree (publishPackageDrafts → recordPackageCommit({ packageId: request.packageId }), and the revert commit packageId: row.package_id) records a string package id, and listCommits filters by package_id, so such a commit would be listed under no package anyway; spelling: the stored package_id is the same decodeURIComponent(parts[0]) string the publish door received, so the strict string comparison cannot drift on encoding. The comparison precedes ensureOverlayIndex-dependent work? No: revertCommit calls ensureOverlayIndex() before the read, a cache build with no write, so "nothing written" holds. Holds.
  • packages/runtime/src/domains/packages.ts (both doors): the unknown-package 404 is defence in depth, not the only guard; if a host's registry lacks getPackage, packageIsUnknown answers false and the protocol comparison still refuses the mismatch. Refusal order read in the tree matches the PR's account (403 → walled → query → [400 on rollback] → 501 probe → 404 unknown → protocol). The walled-catalog message for rollback is the same text, now from the hoisted id. Holds.
  • packages/runtime/src/domains/packages-commit-door-path-package.test.ts: drives the real HttpDispatcher over a real SchemaRegistry with one installed package and a recording protocol; asserts { commitId, packageId } is handed over, the 404 relay with code and message, and that the unknown id never reaches the protocol. The scenario it pins is the door's own contribution; the mismatch itself is pinned one layer down. Holds.
  • packages/objectql/src/protocol-commit-history.test.ts (+2): the mismatch refusal with the exact message, no delete/restoreVersion, no new commit row, and the own-package control for both methods. Holds.
  • packages/qa/dogfood/test/package-commit-revert-path-package.dogfood.test.ts: the card's repro (cross-package revert and rollback: 404, A's history and served objects unchanged), unknown package on both doors, the 403 control, own-path revert and rollback controls with the unchanged response bodies, and the read-only overlay control. Covers every scenario the narrowing is accepted against except the uninstalled-package case (should-fix in ③). Holds.

Implemented-by: claude/project-thread-ae077b
Reviewed-by: session_01ADHkmQxwoGb5fSnrFkpxmt

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 08:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 08:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit f1c1ca5 Nov 2, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ae077b branch November 2, 2026 09:58
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Landing record · PR #29090

domain:server seat 2 · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T09:58Z · card #28987

Reading Probe Result
Merged PR #29090 state merged, merge commit f1c1ca57e2
On main git merge-base --is-ancestor f1c1ca57e2 origin/main yes
Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29090 0
Content git grep -n "readRevertableCommit(commitId: string, packageId" origin/main -- packages/metadata-protocol/src/protocol.ts present

Release: ships patch for runtime and metadata-protocol at the next release, with the BREAKING banner its changeset carries.


Generated by Claude Code

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

Projects

None yet

1 participant