Skip to content

fix(cli): os migrate organization-ownership --apply carries a public-form withdrawal onto a read-only package view into its .custom pair, declared - #29508

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

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-rhz8ng

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29396

Clause-②: no
Metadata-protocol: none

Measured on the real command (table under Why): the cli now completes a step it used to stop on. No door, close predicate or accepted request shape changes; the diff touches only packages/cli.

维护者速读

  • 改了什么:os migrate organization-ownership --apply 把组织撤回的公开表单带到"只读代码包"的视图上时,不再停下报阻塞,而是先确保该包的 .custom 配对包存在,再把撤回存进配对包,并声明 overrides: { package: 基础包, basedOn: 基础包当前版本 }。
  • 为什么改:只读基础包本身不再接受定制(上一轮关闭了以基础包名义的写入),仪式因此停在阻塞上,操作员要手工把撤回存进配对包再重跑。现在仪式自己完成这一步。
  • 风险与回滚:可写包的视图照旧原地保存。配对包确保不了、或门拒绝写入配对包时,仍走原来以包名义的保存:门放行就照旧带过去,门拒绝就照旧阻塞,阻塞文案多带一句配对包的拒绝原因。没有改任何门、错误码或元数据格式。回滚 = revert 本 PR。
  • 先看:packages/cli/src/utils/organization-ownership-apply.ts 的 carryIntoPair 和 carryFormWithdrawals 里新加的分支;packages/cli/src/commands/migrate/organization-ownership.ts 新的 pairs: 接线;packages/cli/src/commands/migrate/organization-ownership.withdrawal-pair.integration.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --project integration src/commands/migrate/organization-ownership.withdrawal-pair.integration.test.ts,预期 1 个通过:撤回进了 com.example.wdpair.custom,声明了 basedOn: 1.4.0,重新启动后匿名表单入口不再提供该表单。
  • 需要你做的:无,席位落地。

What changes

  • carryFormWithdrawals (packages/cli/src/utils/organization-ownership-apply.ts). For a served view whose package is read-only, the carry now goes through carryIntoPair: it ensures the base's .custom pair, then saves the withdrawn view into the pair through the metadata door, declaring overrides: { package: BASE, basedOn }. A body that already declares the base (the pair's own override, as served) keeps its declaration. The withdrawal counts in summary.withdrawalsCarried, and the progress line names the pair and the version.
  • A writable package's view is saved in place, as before; no pair is ensured. A package-less view is untouched by the new branch. The single skip for a view whose key an organization row holds is unchanged.
  • When the pair cannot take it (the pair cannot be ensured, the writability read throws, or the door refuses the declared save), the carry makes the save naming the package as before. Where the door admits that save, it carries the withdrawal as before. Where it refuses, the step blocks with the same sentence as before, plus one clause naming the pair's refusal (Nor could its customization package take it: ...).
  • New port CustomizationPairPort (readOnly, ensure), optional on ApplyOptions as pairs. Absent, the carry behaves exactly as on main. protocolCustomizationPairs(...) builds it from the booted stack.
  • The command wiring (packages/cli/src/commands/migrate/organization-ownership.ts): readOnly is isWritablePackage(engine, id) from @objectstack/metadata-protocol, negated. That is the predicate the save door judges by, not a second reading. ensure is the protocol's in-process ensureCustomizationPackage (the body of POST /packages/:id/ensure-customization). basedOn is the installed base's manifest.version, the value the named-base close and os migrate packageless-overlays read too.
  • Changeset .changeset/29396-withdrawal-carry-into-pair.md (@objectstack/cli patch). It says the manual step in the pending named-base changeset is no longer needed when the pair can be ensured. That changeset is not edited.

Tests

  • New integration file packages/cli/src/commands/migrate/organization-ownership.withdrawal-pair.integration.test.ts. It runs the real command in process through oclif, plan then --apply, over a database a served boot of this tree provisioned. The project's config is a booted code package (read-only) shipping a public form view. An organization's row of that view withdraws the form, and sys_metadata carries organization_id as on v17. The test pins four things. The step carries 1. The pair's row declares { package: BASE, basedOn: '1.4.0' } with enabled: false. No row is bound to the base. A fresh boot (with the package service, so the pair installs from sys_packages) serves the base's form withdrawn, and the env-wide list the anonymous doors judge by serves the slug from no view. A second --apply finds the step checkpointed and the stored rows unchanged.
  • Five new cases in packages/cli/src/utils/organization-ownership-apply.test.ts, in a nested describe:
    • ⭐ The read-only view is carried into the pair, declared at the base's version. The doors serve it withdrawn, and a re-run with no checkpoint carries nothing.
    • A writable package's view is saved in place, and no pair is ensured.
    • A pair that cannot be ensured keeps the blocker, which names both refusals, and nothing is written.
    • A refused declared save into the pair keeps the blocker, naming that refusal.
    • Control: where the pair cannot take it and the door admits the save naming the package, that save carries it.
  • Forced test edits: none. The test file's apply(...) helper gained two pass-through options (pairs, onProgress), and no existing assertion changed.

Why

Step 0, measured on the real command (the integration file above). The main column is aca58cdb with this branch's test file only. The head column is this branch.

Reading main aca58cdb head
Precondition: the shipped form is served open yes (wdp_lead.intake of com.example.wdpair) yes
metadata-promotion blocked, withdrawalsCarried: 0 done, withdrawalsCarried: 1
Ceremony complete: false; the 10 later steps not-reached complete: true, blocking: 0
Stored rows of the view the promoted organization row (no package) only that row, plus com.example.wdpair.custom declaring overrides: { package: 'com.example.wdpair', basedOn: '1.4.0' }, enabled: false
Row bound to the read-only base none none
Fresh boot: views serving the slug open not measured (the case stops at the blocker) none

The blocker on main, verbatim:

An organization withdrew the public form(s) wdp-intake of the view wdp_lead.intake (package com.example.wdpair), and the metadata door refused to carry that withdrawal onto the environment: 'view/wdp_lead.intake' ships in 'com.example.wdpair': save it into 'com.example.wdpair.custom' (?package=com.example.wdpair.custom), declaring overrides: { package: 'com.example.wdpair', basedOn }. The read-only base itself takes no customization (ADR-0146 D1, D11): a row bound to it from before is served until the pair declares the override, or a delete naming the base removes it.. Withdraw the form there (sharing.enabled false), then re-run. An upgrade never reopens a withdrawn form (ruling record 6020151485).

How the cli reaches the pair (read on main):

Question Answer
Can the ceremony's door (run.opts.door) ensure the pair? No. It is protocol.saveMetaItem alone. The protocol service the command already holds exposes ensureCustomizationPackage in process, the body of POST /packages/:id/ensure-customization, so the command wires it as a second port.
How is the override declared? In the saved body: overrides: { package: BASE, basedOn: VERSION }, saved naming BASE.custom (the dogfood helpers ensurePair / declaredOverride have the same shape).
Where does basedOn come from? The installed base's manifest.version from the engine registry: the value the named-base close checks and os migrate packageless-overlays records.
Which packages are read-only? isWritablePackage(engine, id), the save door's own predicate (ADR-0070 D2): a booted code package or a system / cloud scope.

Acceptance notes

  • When the pair cannot take the withdrawal, the carry falls back to the save naming the package. That is main's behaviour, kept on purpose. The door's named-base close refuses only where the pair can take the write, so where the pair cannot (no namespace, no version, an existing pair lacking the dependency), the door still admits the save, as on main. The card's "keep today's blocker" holds wherever the door refuses.
  • Observed, unchanged: under single, the organization's package-less row of the view is promoted in place (promoted: 1), so the environment keeps a package-less row of a shipped name beside the pair's override. Both have the form withdrawn. Moving such rows into the pair is os migrate packageless-overlays' job, not this step's.

Verification

All readings are on head 986b2d01 (this branch with origin/main merged) unless named. Heavy runs went through scripts/pm/os-verify-lock.sh.

Tests: 123/123 in the touched files, cli unit tier 374/374, ceremony dogfood files 34/34
  • pnpm --filter @objectstack/cli exec vitest run --project integration src/commands/migrate/organization-ownership.withdrawal-pair.integration.test.ts src/utils/organization-ownership-apply.test.ts src/utils/organization-ownership-apply.served-catalog.integration.test.ts: Test Files 3 passed (3), Tests 123 passed (123), VERDICT command-exit 0.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: Test Files 372 passed, 2 failed (374). Both failures were prerequisites, not results: test/published-subpath-console.pin.test.ts and test/published-subpath-hook-body.pin.test.ts said "packages/cli is not built (./dist/index.js is absent)". After the cli was built, those two: Test Files 2 passed (2), Tests 29 passed (29).
  • The dogfood files that run this ceremony or its form withdrawal, after turbo run build --filter='@objectstack/dogfood^...': upgrade-18x-harness, organization-ownership-boot-refusal-order, upgrade-18x-index-equivalence and showcase-public-form-withdrawal-layers. Test Files 4 passed (4), Tests 34 passed (34), VERDICT command-exit 0. The full battery was not run: the title has no ! and there is no narrowing arm.
  • pnpm --filter @objectstack/cli typecheck: exit 0 (tsc, plus check:test-typecheck OK with debt held).
  • pnpm lint scoped to the package (eslint packages/cli --no-inline-config): exit 0.
Ablations: the pair branch, and the writability read

Each was planted with scripts/ablation-replace.mjs (anchor hit 1 time, blob b629ad6b6c29 changed, restored to b629ad6b6c29), then git diff HEAD was empty (0 bytes) and git status clean. The cli tests import src directly, so no dist leg is needed.

  1. The pair branch switched off (if (packageId !== null && run.opts.pairs && false)), which puts back main's save naming the base. 4 failed / 114 passed. The unit cases: the read-only carry (expected [] to deeply equal [ 'pkg_a' ], no pair ensured), the unensurable-pair blocker and the refused-pair blocker (each blocker lost its pair clause). The real-command integration case failed too.
  2. The writability read ignored (every package treated as read-only). The writable case went red: the save went into pkg_a.custom with a declaration instead of pkg_a in place.
Derived gates: 62 run, 0 NOT-MEASURED

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 986b2d01 derived 62 commands. All were run, and --ran reconciled: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN.

Five answered exit 3 (prerequisite not met) on the first pass and exit 0 when re-run once the prerequisite was in place:

  • check-plugin-teardown-shape --self-test: its pinned fixture commit was fetched.
  • check:dual-build-cjs-loads, check:i18n, check:i18n-walk-parity: the cli dist was built (the dogfood closure).
  • check:i18n-coverage: packages and examples were built.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (eslint on packages/cli, exit 0).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the maintainer's Chinese brief).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

@github-actions github-actions Bot added size/l 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 1 package(s): @objectstack/cli, touching 12 documentable anchor(s).

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

  • content/docs/api/metadata-api.mdx (via /packages/:id/ensure-customization (route, a path literal in a comment in CustomizationPairPort))
  • content/docs/concepts/metadata-lifecycle.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/data-modeling/drivers.mdx (via readOnly (symbol, a method of interface CustomizationPairPort))
  • content/docs/deployment/cli.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/deployment/tenancy-modes.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/frontend/plugins/plugin-editor.mdx (via readOnly (symbol, a method of interface CustomizationPairPort))
  • content/docs/frontend/plugins/plugin-gantt.mdx (via readOnly (symbol, a method of interface CustomizationPairPort))
  • content/docs/frontend/plugins/plugin-view.mdx (via readOnly (symbol, a method of interface CustomizationPairPort))
  • content/docs/permissions/capabilities.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/permissions/permission-sets.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts), /packages/:id/ensure-customization (route, a path literal in a comment in CustomizationPairPort))
  • content/docs/plugins/adding-a-metadata-type.mdx (via readOnly (symbol, a method of interface CustomizationPairPort))
  • content/docs/protocol/objectql/schema.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/upgrading.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))

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

  • content/docs/releases/v15.mdx (via readOnly (symbol, a method of interface CustomizationPairPort))
  • content/docs/releases/v18/18-0.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/releases/v18/18-1.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/releases/v18/index.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 35 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 91f6a0b4e58e02e25e2921a33f8f8690ec1b2937 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 91f6a0b4e58e02e25e2921a33f8f8690ec1b2937

⚠️ 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 91f6a0b4e58e02e25e2921a33f8f8690ec1b2937 → 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#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head 986b2d0 · 2026-11-02T16:55Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 986b2d01c7f40a5fdcda0d3d6953c8e338d718c4
Local-runs: none

Inputs read: card #29396 (body, the triage, both claims, the dev's report 6582986079); the source record 6577747588 on PR #29270 (S4, open question 2); PR #29508's body, its file list (5 files) and the net diff against its merge base 6af3d295 (the merge of origin/main adds no file to it); the cited code on the head, read only: namedBaseCloseSubject and judgeOverrideWrite (protocol-packaged-base-refusal.ts), ensureCustomizationPackage and customizationPairEnsureRefusal (protocol-customization-package.ts), isWritablePackage (package-writability.ts, protocol-package-scope.ts), migratePackagelessOverlays (protocol-packageless-overlay-migration.ts), the pending .changeset/28905-named-base-overlay-write-closed.md, and packages/cli/package.json's exports; the head's check-runs.

① Derived judgments

  1. The carry goes through the administrator's doors, in the administrator's order. carryIntoPair calls (a) pairs.ensure(base), wired to the protocol service's own ensureCustomizationPackage (the body of POST /packages/:id/ensure-customization, the same in-process call migratePackagelessOverlays makes), then (b) the ceremony's existing door.save, which is protocol.saveMetaItem with packageId set to the pair id the ensure door answered and the body carrying overrides: { package: base, basedOn }. Both are the doors an administrator's pair save meets; neither is bypassed, re-implemented or pre-judged in the cli. Every refusal either door gives is caught and either blocks or falls to main's save, which the door judges again. Right.
  2. basedOn is the value the door names. The cli reads the installed base's manifest.version off engine.registry.getPackage(base), the same read namedBaseCloseSubject makes for the fix it names (basedOn = registry.getPackage(base)?.manifest?.version) and os migrate packageless-overlays records (baseVersionOf). The D2 gate records basedOn and never judges it (judgeOverrideWrite's docblock); the schema gate requires its presence, which the port guarantees (an empty or non-string version throws, and the carry falls back). A served body that already declares the base keeps its own basedOn (the pair's override as served); every other body takes the installed version. Right, and pinned: the integration case stores { package: 'com.example.wdpair', basedOn: '1.4.0' }.
  3. It never writes into the base, nor into a writable package's view through the pair. The pair branch saves only with packageId: pair.packageId, the ensure door's answer (BASE.custom); it cannot name the base. A writable package returns carried: false with no refusal before any ensure, and its view is saved in place exactly as on main (pinned: ['crm_lead.intake', 'pkg_a', false, undefined], ensured: []). The only save naming a read-only base is the fallback, which is main's save, judged by the door (item 6). Right.
  4. No pair is ensured for a package that is not read-only. Three guards, all the server's: readOnly is !isWritablePackage(engine, id), ADR-0070 D2's one predicate that the save door and the ensure door both use; the ensure door itself refuses a writable base (409 ... is writable: customize it directly); a package-less view (servedPackageOf null) never enters the branch, and a served view whose package is a pair is writable (the ensure door installs it with no system/cloud scope), so it is saved in place. Ablation 2 in the PR body (writability ignored, the writable case red) is the dev's pin of this. Right.
  5. A re-run is a no-op. The carry only writes where anonymousFormIntakeWithdrawnIn finds the served body still serving the form open; after the carry, the env-wide list serves the pair's override at the base's slot, withdrawn, so closing is empty. Pinned twice: the unit case runs a second apply with a fresh ledger (no checkpoint) and the saves stay at 1; the integration case re-boots with the package service and reads open: [] from the list the anonymous doors judge by, then a second --apply reads checkpointed with the stored rows byte-equal. Right.
  6. The fallback never bypasses a refusal the door would give. The fallback is door.save('view', name, body, { packageId }) on the unmutated body, main's call verbatim; the door judges it afresh. namedBaseCloseSubject closes the base-named save exactly where the pair can take the declared write (shipper, pair id, customizationPairEnsureRefusal none, a version, the D2 gate admits); where the pair branch fails for those same reasons, the door admits the base-named save, as on main; where it fails for any other reason, the door refuses and the step blocks, the pair's refusal now named beside the door's. There is no path where the cli writes something the door did not admit. Read case by case against main: every input main carried, the head carries (into the pair, or by main's save); every input main blocked, the head carries into the pair or still blocks. Right.
  7. The single posture skip still holds. heldByOrganization is read before the pair branch, unchanged; a view whose name + package key an organization row holds is skipped under single before any ensure or save. The observed side effect (a package-less organization row of a shipped name promoted beside the pair's override, both withdrawn) is main's behaviour and os migrate packageless-overlays' job; the dev filed it as out of scope, correctly.
  8. Derived, not asked: the readOnly set is wider than the close's set. !isWritablePackage also covers an installed system/cloud-scope package that is not a booted code package, which overlayBaseShippers (the close's reading of "packaged") does not. For such a view the carry now goes into the pair where main's base-named save may have been admitted. That is the spec'd place (ADR-0146 D1, D3: the pair holds the changes to "a read-only base — one provided by code or an installed app"), the ensure door admits it, and nothing refused before is written; not a defect.
  9. Derived, not asked: an ensured pair can survive a fallback. Where the ensure door creates the pair and the door then refuses the declared save into it, the fallback may carry by main's save, leaving an empty pair installed. That is the ensure door's ordinary product, the same one an administrator's ensure leaves, and it changes no later verdict (the close already judged the pair ensurable). Nit, no action.
  10. The command wiring stays inside the card. packages/cli/src/commands/migrate/organization-ownership.ts gains one import (isWritablePackage, an existing export of @objectstack/metadata-protocol), one optional member on the local protocol service type, and the pairs: block. The apply module is ports-only, so the ensure door is reachable only from the command; the card's Ask (ensure the pair, then save declared) cannot be met without this wiring. It touches nothing on the claim's ⛔ list (no door, close predicate, protocol.ts, protocol-packaged-base-refusal.ts, packages/spec/**, error code), and the dev declared it as a deviation. Right.
  11. Accept sets and public surface. No door, gate, error code or request shape changes: the diff touches only packages/cli and .changeset. The new exports CustomizationPairPort, protocolCustomizationPairs and the optional ApplyOptions.pairs live in src/utils/organization-ownership-apply.ts, which none of @objectstack/cli's published entries (., ./console, ./hook-body) re-exports; they are internal. Absent pairs, the carry is main's code path unchanged. Right.
  12. Tests. Five unit cases (read-only carried and re-run, writable in place, unensurable pair blocks, refused pair save blocks, the admitted-fallback control) and one real-command integration case. No existing assertion changed; the helper gained two pass-through options. The card's "pins with a read-only package's view and a writable one" is met.

② Semver level

  • Changeset .changeset/29396-withdrawal-carry-into-pair.md: '@objectstack/cli': patch. The diff publishes a behaviour fix in one command (a step that stopped on a blocker now completes through the spec'd door) and no published API surface. patch matches.
  • Clause-②: no is right, in the PR body and the changeset. Nothing accepted on main is refused on the head (① 6: every input main carried, the head carries), and no published surface widens (① 11: the new names are not reachable from any exports entry; no door's accept set moves). Metadata-protocol: none is right: no metadata type, schema or protocol method changes.
  • The changeset's note that the manual step in the pending 28905-named-base-overlay-write-closed.md (line 38) is no longer needed when the pair can be ensured is accurate, and that changeset is untouched; both are unreleased, so they ship together.

③ Boundary flags

  • Open question 1 (the fallback), answered A. The card's "otherwise it keeps the blocker" names today's behaviour, and today's behaviour is: the door blocks where it refuses and carries where it admits. Option B would make the cli refuse a carry the door admits, a new refusal in a published command that main completes, which is a narrowing this card was not dispatched to make and would turn Clause-②: no false. A keeps every case the close left open exactly as on main and blocks wherever the door refuses (① 6). Answered A, no change.
  • Deviation: file surface widened to the command wiring and a new integration file. Accepted (① 10): both are in packages/cli, both are required by the Ask, neither is on the claim's ⛔ list.
  • Deviation: merge of origin/main (9 commits, none in packages/cli). Accepted: a merge, not a rebase; the net diff against the merge base is the 5 files above.
  • Deviation: 5 gates exited 3 on missing prerequisites, then 0 on a rerun (check-plugin-teardown-shape --self-test after its fixture commit was fetched; check:dual-build-cjs-loads, check:i18n, check:i18n-walk-parity after the cli dist was built; check:i18n-coverage after packages and examples were built). Accepted: exit 3 is the gates' "prerequisite not met" answer, not a verdict, and each rerun after supplying the named prerequisite is the gate's real measurement; the derived set reconciled 62/62 with 0 NOT-MEASURED. The two cli unit files that said "packages/cli is not built" are the same class and passed after the build. The verdicts of record are the head's check-runs below, not the local runs.
  • Out-of-scope finding (package-less organization row beside the pair's override under single). Existing behaviour, owned by os migrate packageless-overlays; not filed, correctly. No card owed.
  • Security findings: none. Concur: the in-process ensure and save are the same calls os migrate packageless-overlays makes, under the ceremony's existing actor.
  • Check-runs on the head, as read at 2026-11-02T17:11Z: 48 runs, all completed: 35 success, 13 skipped (the opt-in and path-filtered jobs: the 18.x upgrade harness, Build & E2E UI, Build Docs, Build UI Deps, Dogfood Verify CLI, Lint UI, Type Check UI, UI Published Artifacts, the packed-tarball smoke, Temporal Conformance, the schedule gate and the two matrix templates), 0 failure, 0 in progress. Green among them: Test Core (all 8 shards and the aggregate), Type Check (workspace, source gates, consumer gates, debt ledger), every Lint lane including PM dispatch-gates, Dogfood Regression Gate, Check Changeset, PR-body guards and Governed Surface Queue Guard.

Must-fix: none. Should-fix: none. Nit: ① 9 (an ensured pair can survive a fallback), no action.

Implemented-by: claude/project-thread-rhz8ng
Reviewed-by: session_01ADHkmQjPiXgNoSaEpqeGvY

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

席位核对 · ACCEPT · domain:server seat 1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY · 2026-11-02T17:13Z

Head 986b2d01c7, report 6582986079 (on #29396), claim 6581417535. Clause-②: no → declaration face, reviewed at TIER_DEFAULT: record 6583390057 on this head, PASS, no must-fix and no should-fix.

item reading
Shape Draft, base main (origin/main merged in), Fixes #29396. Five files, all under packages/cli: organization-ownership-apply.ts (carryFormWithdrawals → carryIntoPair), the command wiring commands/migrate/organization-ownership.ts, one unit and one integration test, and a changeset (@objectstack/cli: patch, Clause-②: no, Metadata-protocol: none). The door, the close predicates and packages/spec/** are untouched. Not governed (check-governed-merges governed: false).
Rule A withdrawal on a read-only package's view is carried into BASE.custom. The pair is ensured in process (ensureCustomizationPackage), and the save goes through saveMetaItem with overrides { package, basedOn: manifest.version }. A writable package's view is saved in place as before. Where the pair cannot take the write, the carry falls back to main's save, and the door judges it again. A re-run is a no-op.
Evidence Base, through the real command: blocked on NOT_OVERRIDABLE, withdrawalsCarried 0, nothing written. Head: carried, and a fresh boot serves the form withdrawn. 2 ablations went red and were restored. cli 374/374, touched files 123/123, ceremony dogfood 34/34, gates 62/62. CI on this head: 48 runs, 35 green, 13 skipped, none red. check-expected-skips OK (13, all in main's roster).
Open question OQ1: A, keep main's save as the fallback. The door admits it exactly where the pair cannot take it. B would add a refusal and falsify Clause-②: no.
Nit, not filed If the pair is ensured and the save into it is then refused, an empty pair can stay installed. It is harmless: the pair is the place ADR-0146 names, and the next administrator customization reuses it.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 17:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41489549492 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1293 object site(s) across…' to be 'untriaged: 1292 object site(s) across…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 4 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Queue triage · 2026-11-02T17:37Z — the red is not this PR's.

  • This diff touches only packages/cli/ and a changeset. The failing check is the strictness-ledger counts shard docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (api/ 487 on disk, 488 expected), which this PR neither writes nor feeds.
  • origin/main at 399df5bba8 is green on the same check, run locally: pnpm --filter @objectstack/spec check:strictness-ledger → "counts/ is current", untriaged 1292.
  • The drift comes from the spec PRs stacked ahead of this one in the group (#29455 features.sso, the push-channel face, #29437-era dashboard work): each regenerated api.md against its own base, and together they add one api/ site that no single shard row counts. The same check is red on every group built behind them (pr-29437, pr-29505, pr-29508 at 17:28–17:31Z), so it is a queue-ordering clash, not a regression here.
  • No re-queue or push from this side: it stays in the queue and lands once the spec PR at the head of the stack regenerates or drops out. The aggregate is Queue-flake anchor: scripts/strictness-ledger-doc.test.ts #29524.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41491863627 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:      ✓ inverts the card's measured control: the three bogus keys now FAIL parse instead of adding zero warnings  342ms
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1297 object site(s) across…' to be 'untriaged: 1296 object site(s) across…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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/l tests tooling

Projects

None yet

1 participant