Skip to content

fix(metadata-protocol,cli): os migrate packageless-overlays lists a package-less view container cannot_declare, and --apply never attempts it - #30380

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-jtt4fs
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-jtt4fs

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么:os migrate packageless-overlays 的预览遇到无包的视图容器行(list / listViews / form / formViews 写在一起的那种),不再列成"可移动",而是列成 cannot_declare 并给出出路(把每个视图逐个存进配对包,再删掉容器行);--apply 不再尝试它,也不再每次报失败。
  • 为什么改:容器的 schema 没有 overrides 键,迁移第二步的声明式保存必被拒,预览却说可移动,--apply 每次失败、行原样不动,运维无从下手;另外 main 上两处关门(无包写、写名指只读基包)把容器的写也关了,403 指向一个做不到的修法,连"删掉容器行"这条出路都被拒。
  • 风险与回滚:判断取自 save 门自己的 schema(不另列类型清单),实测五种叠加类型里只有视图容器装不下 overrides;两处关门对容器恢复为关门前的行为(无包保存、删除可用),其余类型不变。回滚即撤本 PR,无数据迁移。
  • 先看:packages/metadata-protocol/src/protocol-packaged-base-refusal.ts 的 overrideDeclarationRefusedByShape 与 overlayPairFixAdmits 新增的一行;protocol-packageless-overlay-migration.ts 的 classifyPackagelessRow(新分支在 no_pair 之后、foreign_declaration 之前);packages/cli/src/commands/migrate/packageless-overlays.ts 的 reasonOf。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/protocol-packageless-overlay-migration.test.ts -t 30261(需先 build metadata-protocol),预期 5 条全绿;在 main 的字节上这 5 条全红。
  • 需要你做的:无,席位落地。

Fixes #30261

Clause-②: yes (widening) — PackagelessOverlayDisposition (reachable through the exported PackagelessOverlayCensus) gains cannot_declare; the package-less write close and the close of a write naming the read-only base no longer refuse a view container's write, whose named fix cannot be taken
Metadata-protocol: semantic — an author's package-less save or delete (and a save naming the read-only base, ?package=<base>) of a view container (list / listViews / form / formViews) over a name one read-only package ships, refused 403 NOT_OVERRIDABLE since the D11 close, succeeds again: the close names a fix (a declared save into the pair) a container cannot take. Every other shape is closed as before. @objectstack/metadata-protocol ships minor; @objectstack/cli patch (seat correction at 5cbefbb, the #30168 / PR #30322 precedent).
Skills-docs: none — skills/objectstack-platform/SKILL.md line 128 refuses the package-less write "once movable as a declared override"; a container is never movable as one, so the sentence stays true, and no skill teaches writing a container over a packaged name.

What changes

The card's Expected: "The preview and --apply agree: a row listed movable moves, or a container row is listed with the reason it cannot move." This PR takes the second arm (the order's fork: make the preview honest, leave the container schema as it is; widening ViewSchema is a packages/spec design question, not a migration fix).

One predicate, from the save door's own schema. overrideDeclarationRefusedByShape(type, body) (protocol-packaged-base-refusal.ts) parses the body with an overrides probe through resolveOverlaySchema, the resolver the save door validates with, and answers true only when the parse names overrides as an unrecognized key at the body's root. In a union it must be named by every branch still diagnosing the body (the view union mutes the branches the body does not claim to a root kind mismatch, focusClaimedBranch), so a view body that claims no member is not answered true. No list of shapes is kept: a member that gains or loses the key is answered by its schema.

Measured over the five overlay types the ceremony moves (pinned in protocol.override-declaration-shape.test.ts, which also pins that its fixture covers every allowEnvironmentOverlay type): the view container answers true in all four spellings (list, listViews, form, formViews); a single view, a list overlay, a dashboard, a report, a translation and an email template each spread OverrideDeclarationFields and answer false. So the view container is the one container-shaped member with this mismatch.

The census. classifyPackagelessRow takes the canonical type and returns cannot_declare (with base and pair) for such a body, after multi_shipped / no_pair and before foreign_declaration / shadowed, because those two dispositions' ways out end in this ceremony moving the row. migratePackagelessOverlays acts on movable rows only, so it never attempts the row: no outcome, no failure, no pair ensured for it, and the CLI exits 0.

The two closes. On main both closes refused a container's write with 403 NOT_OVERRIDABLE, naming a fix (a declared save into the pair) that the container schema refuses (measured, red leg below). Both closes are built on "close only where the fix can be taken" (#28576 ruling 2; the #28905 docblock "every write this closes has a door, the pair; every other one keeps the write it has on main"):

  • the package-less close classifies by the same classifyPackagelessRow, so a cannot_declare row's package-less save and DELETE stay open: they are the one door such a row keeps, and the way out ends with that DELETE;
  • overlayPairFixAdmits, the "fix can be taken" predicate both closes ask, now also asks the shape (for a real body; a bare identity has no shape), so the close of a write naming the read-only base leaves a container's write as it was before that close. The row such a write stores is listed cannot_declare, boundToBase, and a delete naming the base removes it.

CLI. reasonOf prints the way out under "left as they are": save each of the container's views into the pair as a single view named OBJECT.VIEW (one the base ships declaring overrides: { package, basedOn }, a new one declaring nothing), then delete the container row with a delete naming no package (naming the base, for a row bound to it). JSON rows carry disposition: "cannot_declare", outcome: "left".

Docs. Two sentences this PR made false, amended: content/docs/deployment/cli.mdx (the os migrate packageless-overlays row: "move each") and content/docs/concepts/metadata-lifecycle.mdx (the D11 callout: every package-less write onto a ✅-type packaged item is refused).

Changeset: @objectstack/metadata-protocol + @objectstack/cli patch, both declaration lines.

Tests

  • objectql protocol-packageless-overlay-migration.test.ts, new top-level describe [#30261] (5 cases, real ObjectQL engine and registry): the census lists the active and draft container rows cannot_declare with base and pair, --apply returns no outcome and no base line, ensures no pair, and a re-run lists the same; a single view beside it still moves; the close leaves the container's package-less save, DELETE and first save open; the close of a write naming the base leaves it open and the stored row is listed cannot_declare boundToBase, removable by a delete naming the base; the way out holds end to end (the default view saved into the pair declaring the override, showcase_task.mine saved as the pair's own, the container deleted naming no package, then the census lists nothing and the read serves the pair's view).
  • metadata-protocol protocol.override-declaration-shape.test.ts (new; no harness for this predicate existed): the measurement above, plus a container with an unknown key of its own still true, a body claiming no view member false, a schema-less type false.
  • cli packageless-overlays.integration.test.ts, new describe over a real SQLite database: the preview lists the container cannot_declare / left; --apply --yes exits 0 with failed: 0, moved: 0, the rows unchanged and no pair stored.

Acceptance notes

  • The order asked whether ADR-0146 D11/D2/D10 requires container rows to move through the ceremony. D11 reads "Every sys_metadata row with package_id NULL that overlays an item a code package ships becomes a declared override of that item"; D2 defines the declaration as a key "in the item's body" and never names a container. A container is not itself an item the read serves by name (it expands to OBJECT.VIEW items), and the way out moves each of those items as D11 describes. Read as not requiring the container row itself to move; reported here for the seat rather than as needs_decision.
  • basedOn is not judged and the probe's value is a fixed well-formed declaration: only the key's acceptance is read.

Verification

All readings on head 3413e7bb (the fix's four commits plus a merge of main at 0837fdc0, which carries #30322's change to the same overlayPairFixAdmits: the shape check runs first, then overrideWriteVerdict(…, fix.pendingPairScope)), except the red/green legs, taken before the merge on efbca752 / 17adf261 with main's bytes at 8b117c0e.

Red leg (main's bytes) and green leg
red: git restore --source=8b117c0e -- protocol-packaged-base-refusal.ts protocol-packageless-overlay-migration.ts
     pnpm --filter @objectstack/metadata-protocol build; ablation-dist-preflight 'cannot_declare' --absent -> marker absent from all 24 built files
     objectql protocol-packageless-overlay-migration.test.ts -t 30261 -> 4 failed (the first four cases):
       census: expected 'showcase_task@active cannot_declare', received 'showcase_task@active movable' (and @draft)
       close: package-less save of the container -> { code: 'NOT_OVERRIDABLE', status: 403 }
       way out: the package-less DELETE of the container -> { code: 'NOT_OVERRIDABLE' }
     second red leg (the named-base case, added after): -> 1 failed, { code: 'NOT_OVERRIDABLE' }
     restored with git checkout HEAD --; git hash-object equal to HEAD's blobs (4a2a3ba3, b312e979); git status clean
green: rebuild; preflight 'cannot_declare' present in 4 built files
     objectql protocol-packageless-overlay-migration.test.ts -> 33/33 (34/34 with the named-base case)
     metadata-protocol override-declaration-shape + named-base-overlay-write + view-expansion-reconcile-call-sites -> 61/61
Package suites and typechecks, on 3413e7b after the merge (under os-verify-lock.sh)
pnpm install --frozen-lockfile :: exit 0
pnpm exec turbo run build --concurrency=2 --filter=...@objectstack/cli --filter=...@objectstack/objectql :: exit 0
pnpm --filter @objectstack/metadata-protocol test :: exit 0   Test Files 313 passed | 4 skipped; Tests 22396 passed | 26 skipped
pnpm --filter @objectstack/metadata-protocol typecheck :: exit 0
pnpm --filter @objectstack/objectql test :: exit 0            Test Files 550 passed; Tests 10956 passed
pnpm --filter @objectstack/objectql typecheck :: exit 0
pnpm --filter @objectstack/cli exec vitest run --project unit :: exit 0                Test Files 384 passed; Tests 5427 passed
pnpm --filter @objectstack/cli exec vitest run --project integration src/commands/migrate/packageless-overlays.integration.test.ts :: exit 0   Tests 6 passed
pnpm --filter @objectstack/cli typecheck :: exit 0
Derived gates (dispatch-gates --commands, 95 families) and lint, on 3413e7b
94 of 95 :: exit 0 (each recorded before any pipe)
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 3 (shallow), exit 0 after git fetch origin 621a4876
pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET (needs every package's dist) -> NOT MEASURED, CI Lint & Repo Gates runs it
dispatch-gates --ran: 95 derived famil(ies) accounted for -- 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)
pnpm lint :: exit 0

Dogfood: not owed (no ! in the title, no (narrowing) arm).


Generated by Claude Code

…_declare, never attempt it

The packageless-overlays census listed a package-less view container row
movable, and every --apply failed it: the move's declared save adds
overrides, which the container schema refuses as an unrecognized key.

The census now asks the save door's own schema whether the body can carry
the override declaration (overrideDeclarationRefusedByShape) and lists a
row that cannot as cannot_declare, with the way out in the CLI; the move
never attempts it. The package-less close and the close of a write naming
the base read the same predicate, so neither closes a write whose fix
cannot be taken.

Claude-Session: https://claude.ai/code/session_01Utp6CbC5BpV41NMLrJMt8Q
Co-authored-by: Claude <noreply@anthropic.com>
…t cannot carry the override declaration

Also counts a union's refusal only when every branch still diagnosing the
body names the key, so a view body claiming no member is not answered true.

Claude-Session: https://claude.ai/code/session_01Utp6CbC5BpV41NMLrJMt8Q
Co-authored-by: Claude <noreply@anthropic.com>
…ntainer's write open

Claude-Session: https://claude.ai/code/session_01Utp6CbC5BpV41NMLrJMt8Q
Co-authored-by: Claude <noreply@anthropic.com>
… ceremony, and keeps its package-less write

Claude-Session: https://claude.ai/code/session_01Utp6CbC5BpV41NMLrJMt8Q
Co-authored-by: Claude <noreply@anthropic.com>
…-jtt4fs

# Conflicts:
#	packages/metadata-protocol/src/protocol-packaged-base-refusal.ts
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot))
  • content/docs/api/error-handling-server.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot))
  • content/docs/automation/flows.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot))
  • content/docs/concepts/metadata-lifecycle.mdx (via os migrate packageless-overlays (command, read off packages/cli/src/commands/migrate/packageless-overlays.ts))
  • content/docs/deployment/cli.mdx (via cannot_declare (literal, a string literal in PackagelessOverlayDisposition; a string literal in classifyPackagelessRow; a string literal in reasonOf), invalid_type (literal, a string literal in namesOverridesAtRoot), unrecognized_keys (literal, a string literal in namesOverridesAtRoot), os migrate packageless-overlays (command, read off packages/cli/src/commands/migrate/packageless-overlays.ts))
  • content/docs/frontend/cli.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot), invalid_union (literal, a string literal in namesOverridesAtRoot))
  • content/docs/protocol/objectql/types.mdx (via unrecognized_keys (literal, a string literal in namesOverridesAtRoot))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot))

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

  • content/docs/releases/v17/17-0.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot), invalid_union (literal, a string literal in namesOverridesAtRoot), unrecognized_keys (literal, a string literal in namesOverridesAtRoot))
  • content/docs/releases/v17/17-6.mdx (via invalid_type (literal, a string literal in namesOverridesAtRoot))
  • content/docs/releases/v18/18-3.mdx (via os migrate packageless-overlays (command, read off packages/cli/src/commands/migrate/packageless-overlays.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
  • 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 — 42 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 fdad1786e892270ac3fcc161b3fd4d57c9c47748 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 fdad1786e892270ac3fcc161b3fd4d57c9c47748 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… view-container write; metadata-protocol minor

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

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

席位核对 · ACCEPT · 2026-11-05T08:41Z · head 5cbefbb3

Reviewed against order server-seat5/dispatch-30261.md (Claim 6650721257). The fork was ruled "make the preview honest, no container schema change"; the PR does that.

Landing: ready + auto-merge.

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