Skip to content

feat(cli,metadata-protocol): os migrate packageless-overlays moves package-less overlay rows into .custom as declared overrides - #28689

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-f88ad8
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-f88ad8

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Requested by Leehom · project thread

Fixes #28413

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — the one taught line this touches, skills/objectstack-platform/SKILL.md's "a package-unbound Setup row still wins over the base's until D11", stays true: the ceremony moves such rows when an operator runs it, and the package-less write that makes new ones stays open until #28576, which is the change that retires that line. No file under skills/** teaches the os migrate operator ceremonies.

维护者速读

What changes

  • os migrate packageless-overlays (packages/cli/src/commands/migrate/packageless-overlays.ts), modelled on os migrate security-catalog-overlays: lists by default, moves with --apply (--yes or the prompt), --json, --force, --database-url, and serve's --preset / --dev. With --apply it boots a live PackageServicePlugin, so a pair it creates persists to sys_packages. Exit 1 only when --apply leaves a failed row (or on an error).
  • The ceremony (packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts, exposed as listPackagelessOverlays() / migratePackagelessOverlays({ actor }) on the protocol):
    • Population. Every sys_metadata row bound to no package, of the five overlay types, active or draft, over a name exactly one installed read-only package ships.
    • The move, per row:
      1. The pair is ensured through N-13's ensure door, once per base.
      2. The body gains overrides: { package, basedOn }, naming the base and its installed manifest.version.
      3. It is saved into the pair through the save door, so N-09's gate judges it, in the row's own state.
      4. The pair's row is read back and must satisfy isServedOverrideRowOf.
      5. Only then is the unbound row deleted, through the repository addressed by package_id NULL, with its history tombstone. A failed read-back keeps the unbound row and reports the row as failed.
    • Left alone, and listed. A name several packages ship (multi_shipped), a body declaring another package (foreign_declaration), and a name the pair already overrides (shadowed). Shadowed rows carry two ways out: delete the package-less row, or "Restore package original" in Studio and re-run. Names no package ships are counted as environment metadata. Permission sets and positions are not overlay types.
    • basedOn completion. A pair row declaring overrides: { package } with no basedOn gains the base's installed version, and is read back.
  • Setup keeps working on a moved item (saveMetaItemUnaudited, until metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576). A package-less authoring save whose address is a pair row declaring the override is treated as the write into that pair:
  • Census lists. The one-shot family, the confirmation-exit pin and the JSON-stdout purity roster each gain the new command. The durability gate declares the ceremony's per-row fail() report as its failure delivery. The cli vitest config resolves @objectstack/service-package to source (check:test-source-alias).

Tests

  • packages/objectql/src/protocol-packageless-overlay-migration.test.ts (10):
    • THE MOVE (active and draft, pair created, basedOn 1.2.0, five reads unchanged, a re-run moves 0).
    • LEFT ALONE (environment, multi-shipped, shadowed with shadowDeclares, an unbound draft beside an active override).
    • READ BACK FIRST.
    • basedOn completion, over every overridable type; flow and action rows missing basedOn are reported failed with the save door's ADR-0146 D9 refusal.
    • Package-less save with and without overrides landing on the pair (draft, publish, publish-mode).
    • Package-less DELETE restores the base document.
    • CONTROL: a package-less save carrying overrides over an unmoved row is still 403 NOT_OVERRIDABLE.
    • adopt-orphans skips the shipped-name row.
  • packages/cli/src/commands/migrate/packageless-overlays.integration.test.ts (3), on a real booted stack:
    • The preview writes nothing.
    • --apply moves the active and draft rows, persists the pair to sys_packages, keeps the controls, serves the same content after a fresh boot, and a re-run moves 0.
    • --apply --json without --yes gives confirmation_required and moves nothing.
  • Red first, on main. The Wanted 4 cases are red with the real refusals (undeclared_same_name without overrides, declared_outside_pair with them). The other cases are red because the ceremony does not exist.
  • Ablations. Each leg rebuilt metadata-protocol's dist and proved the marker with ablation-dist-preflight (objectql resolves it from dist). Each was red as predicted, then restored to the HEAD blob:
    • shadowed guard → LEFT ALONE red;
    • read-back check → READ BACK FIRST red;
    • adopt-orphans isArtifactBacked skip → adopt-orphans red;
    • the early pair-landing judgement (body carrying overrides) → that Wanted 4 case red;
    • the late declaration graft (body carrying none) → both Wanted 4 cases red (each case's last save carries none).

Acceptance notes

  • Wanted 3 (walled postures). No HTTP door is added: the ceremony runs only from the operator's CLI. The ensure door's walled 403 is unchanged and stays pinned by N-13's own tests.
  • Wanted 5. reassignOrphanedMetadata has skipped artifact-backed names since metadata-protocol: a republish of a view container that drops a member keeps serving the dropped member's expanded view until restart #23198. This PR pins that with a test and ablates it. Naming this ceremony in the adopt-orphans response would change ReassignOrphanedMetadataResponseSchema in packages/spec, so it is raised on the card as an open question rather than done here.
  • Seat 2's reading. A legacy overrides: { package } without basedOn gains basedOn.
  • Delta 5865a0ce (review S1, S2, N1). S1: a pair row of the item in either state shadows an unbound row; the bundle member stays in the item identity, so one translation locale does not shadow another. S2: the basedOn completion covers every overridable type, and flow and action rows are reported failed with the gate's refusal, so --apply exits 1 on a deployment holding one. N1: both types are exported type-only from the index, and the CLI imports them.
  • Seat 1's flow finding (cli + metadata-protocol: os migrate moves package-less overlay rows into <base>.custom as declared overrides — the migration ceremony only (the close is N-16b) #28413 comment 6559382296) is not covered here: flow is no overlay type (allowEnvironmentOverlay: false), a package-less row beside the pair's row is the shadowed shape this ceremony leaves alone, and a declared flow write is refused by N-09 until D9. Routed by the seat.

Verification


Generated by Claude Code

…'s .custom as declared overrides

listPackagelessOverlays / migratePackagelessOverlays classify every
package-less row of the five overlay types against the code packages that
ship its name, and move a movable one into <base>.custom: the pair ensured
through the ensure door, the body saved through the save door with
overrides { package, basedOn }, read back as the served override, and only
then the unbound row deleted by its own address. A pair declaration with
no basedOn gains it.

Until the package-less write closes, a package-less save landing on a
migrated row keeps that row's declaration, with or without overrides in
the body.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pKMJFQjFx8NCVZb62
…ves Setup overlays into .custom

Preview by default; --apply (with --yes or the prompt) moves through the
protocol's migratePackagelessOverlays, with a live package registry so
the ensured pair persists to sys_packages. --json, --force, --database-url,
--preset and --dev as the family has them. Joins the migrate family's
confirmation, one-shot boot and JSON purity censuses.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pKMJFQjFx8NCVZb62
…ss save would be refused

The lock gate stays the first store reader of a package-less save (#5706); the
landing is looked up at the declared_outside_pair refusal and at the late
undeclared_same_name re-judgement only.

Co-Authored-By: Claude <noreply@anthropic.com>
…utcome report as its failure delivery

Co-Authored-By: Claude <noreply@anthropic.com>
…emony

Co-Authored-By: Claude <noreply@anthropic.com>
…suite

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/metadata-protocol, touching 73 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/cli/vitest.config.ts, packages/metadata-protocol/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cli/vitest.config.ts, packages/metadata-protocol/src/index.ts) — pages documenting those are invisible to this run
  • 18 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8

⚠️ 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 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 → 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#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 34c1aa5d997079f8636392d81d925a80a65672c6
Local-runs: none

Inputs read: card #28413 and every comment on it (the 05:00Z split notice, the 11:56Z triage, seat 2's 12:17Z reading, the claim 6557994545, os-dev-report 6559018502, and the seat's ruling 6559037535), the PR body, its 11 files, and the net diff against main. Scope came from epic #28236 and the close half #28576. All 48 check-runs on the head are completed. Each one is success or skipped, and none is failing or still running. That includes Check Changeset, Governed Surface Queue Guard, the three Type Check consumer and source gates, the eight Test Core shards, Lint · CI, release & engine gates, and Dogfood Regression Gate. The dev's "CI NOT MEASURED" is now measured, and it is green.

① Derived judgments

Every accept-set and public-surface change in the diff, and how the PR names each one:

  1. Two new public methods on ObjectStackProtocolImplementation: listPackagelessOverlays() and migratePackagelessOverlays({ actor }). The PR body, the changeset's last line and the dev's declaration all name them. Named right. They are a TypeScript class surface only. A search of the head finds callers only in packages/cli, nothing in packages/runtime or any plugin or service routes to them, and no reflective protocol[name] dispatch exists. So they add no HTTP door, and the ensure door's walled 403 is not bypassed by any tenant path.

  2. New CLI command os migrate packageless-overlays.

    • Flags: --apply, -y/--yes, --force, --json, --database-url (the OS_DATABASE_URL env), --preset, --dev.
    • JSON outputs: the report, plus confirmation_required, database_busy and host_config_unloadable.
    • Exit codes: 1 only on a failed row under --apply, or on an error.

    The PR body and changeset name the command and its flags. Named right. The roster entries in the one-shot family, the confirmation-exit pin and json-stdout-purity follow the family's conventions.

  3. The save door now accepts two package-less saves it used to refuse (saveMetaItemUnaudited). Both apply when the save's address is a pair row that declares the override:

    • A body carrying overrides was refused 403 NOT_OVERRIDABLE (declared_outside_pair). It is now judged as a write into that pair.
    • A body carrying none was refused 403 (undeclared_same_name). It now gains the row's overrides block before the late re-judgement.

    Named right, as a widening. I checked that nothing is narrowed:

    • Both new branches are entered only when verdict === 'refuse'. No save that was accepted before reads, changes or is refused.
    • A re-judgement that still refuses goes through the same overrideWriteRefusalError and gives 403 NOT_OVERRIDABLE. Only the message can differ.
    • A verdict that becomes not_concerned falls through to the existing late re-judgement and to uninstalledNamedBaseRefusal for the inherited pair. That is what the same save gets when it names the pair directly.
    • The graft is kept only when the pair verdict admits it.

    So the dev's "nothing it admitted is refused" holds on the code.

  4. The data move. I read it against the card's Wanted 1–2:

    • The pair is ensured once per base through the protocol body behind N-13's door.
    • basedOn is the registry's installed manifest.version. A base with no version fails that row; no value is invented.
    • The row is saved through saveMetaItem with packageId set to the pair, in its own state (draft stays a draft, active becomes publish), so N-09 judges it.
    • The pair's row is read back under isServedOverrideRowOf before anything is deleted.
    • The unbound row is then deleted through SysMetadataRepository.delete with an explicit packageId: null, so only the package_id IS NULL row is matched. The delete is locked to the row's census checksum, so a row changed after the census is kept and reported failed. The delete writes a history tombstone.
    • Every failure path keeps the unbound row. Re-running is idempotent: a moved row is gone, and a row whose pair row was written but whose delete failed is classed shadowed on the next run, never moved twice.

    Sound, apart from should-fix S1.

  5. scripts/check-durability-degradation-log-level.mjs. The PR adds a FAILURE_PROPAGATION_SITES entry, scoped to protocol-packageless-overlay-migration.ts::migratePackagelessOverlays, with callee fail. This is a legitimate registration, not a weakening or exemption of the gate.

    • The map's own header defines it as a per-function vocabulary entry, not a baseline entry. catchDeliversFailure() still has to prove that every path out of the catch reaches the declared sink, and stale entries are checked.
    • The precedent is protocol.ts::migrateStoredMetadata with record, the same per-row outcome-report shape.
    • The catch catch (error) { fail(messageOf(error)); } reaches fail on its only path. fail writes to the returned report, and that report sets the exit status under --apply.
    • The baseline JSON is not touched.
  6. packages/cli/vitest.config.ts. An anchored source alias for @objectstack/service-package, required by check:test-source-alias. Test-only, and it publishes nothing.

  7. adopt-orphans (card Wanted 5). The runtime is not touched by this diff (packages.ts is not in the file list). The skip of shipped names has been there since metadata-protocol: a republish of a view container that drops a member keeps serving the dropped member's expanded view until restart #23198 and is now pinned. The claim expected a narrowing here. None exists, because nothing changed.

should-fix S1: shadowed is judged per state, but the card, the PR and the changeset all describe it per name.

  • Card Wanted 2 says: "a row whose name the pair already overrides … listed as shadowed, never merged or deleted".
  • The PR body and changeset say "a row the pair already overrides".
  • But listPackagelessOverlays keys pairRows on type|name|member|state. The module header says this too: "a row of that name in that state".

Take an unbound Setup draft beside a Studio active declared override. The draft is classed movable, saved into the pair as the pair's draft (carrying overrides), and the unbound draft is deleted. Before the move, publishing that draft was ambiguous: the address had two bindings. After it, a publish replaces the Studio customization with the Setup content. That is the merge Wanted 2 rules out, and no test covers it (the LEFT ALONE test shadows only active over active).

No data is lost. The other direction, an unbound active beside a pair that holds only a Studio draft, is a defensible move. Fix one of two ways:

  • Treat any pair row of the item that declares the override, in either state, as shadowing the unbound draft, and add the mixed-state case to LEFT ALONE.
  • Or state the per-state rule in the changeset and the PR body, and name this case and what an administrator does about it.

nit N1: the new result types are not exported from the package index. PackagelessOverlayCensus and PackagelessOverlayMigration are missing from packages/metadata-protocol/src/index.ts, while the sibling ServedOverrideEntry is exported there (line 240). As a result the CLI restates the whole shape locally (CensusRow, Migration, Outcome), and the two copies can drift. Exporting the types and importing them in the CLI would remove that copy.

② Semver level

  • Clause-② line: Clause-②: yes (widening) is in both the PR body and the changeset. Right. Items 1–3 above widen surfaces: two class methods, a new command, and a save that used to be refused and is now accepted. No narrowing was found (① item 3), and there is no !, no BREAKING banner, and no ADR-0087 disposition owed.
  • Metadata-protocol line: Metadata-protocol: added. AGENTS.md's Post-Task Checklist step 3 defines this as "the change to what a metadata author can write — a new key, accepted value … is added". A package-less save over a moved row that the save door refused is now accepted, so added is the right kind. The other kinds don't fit:
    • narrowed: nothing is refused that was accepted before.
    • semantic: no write that was accepted before changes meaning, because the graft only applies to saves that used to be refused.
  • Changeset level: '@objectstack/metadata-protocol': minor and '@objectstack/cli': minor. AGENTS.md says "the level follows the kind alone: a kind takes at least minor on a package the diff moves". The diff moves both packages and the kind is added, so minor on each is right and neither is over-levelled. The Check Changeset check-run is success.
  • Release safety: the changeset names the one operator step (preview, then --apply), as the card's Release safety section asks.

should-fix S2: the changeset says basedOn is completed on every pair row, but the code only does it for five types. The changeset says: "A row of a pair that declares overrides: { package } with no basedOn … The same command records basedOn on it". The PR body says the same, without limit. But the census reads only OVERLAY_ALLOWED_TYPES, the five overlay types. isServedOverrideRowOf counts a declaration on any overridable type, and the spec's OVERRIDE_TYPE_CLASSIFICATION lists page, app, dataset, doc, book, mapping, flow and action as well. So a legacy declaration on one of those types is still read as served, still refused on its next save, and is not completed.

The population is probably close to empty, because basedOn has been required since OverrideDeclarationSchema was born in #28467. But the sentence is published text: it becomes CHANGELOG.md and can be amended only in a dedicated docs PR after release. Fix one of two ways before merge:

  • Widen the pair-row pass to isOverridableMetadataType.
  • Or limit the changeset and PR sentence to the five types.

③ Boundary flags

  • Open question (os-dev-report 6559018502): the adopt-orphans response naming the ceremony. Answered. The seat ruled B in 6559037535 (14:06Z): the response stays as it is, the naming moves to metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576, and no spec key is added. The diff matches: no file under packages/spec or packages/runtime is touched. The ruling's "no new protocol surface for N-16a" reads as no wire or spec surface. The two class methods in ① item 1 are reachable from no route, and they are declared under Clause-② and in the changeset, so they are consistent with the ruling.
  • Seat 2's reading (12:17Z), that a legacy overrides: { package } gains basedOn. Landed for the five overlay types. incompleteDeclarations handles it, basedOn is checked again on read-back, and the test "a stored pair declaration with no basedOn gains the base's version" covers it. It does not land for the other overridable types (S2).
  • Deviation: no walled-posture test for Wanted 3. Accepted, with the dev's reason checked. The walled 403 lives at the HTTP door (packages.ts POST /:id/ensure-customization, via refuseWalledCatalogPackageWrite), and this diff leaves it unchanged. The new methods are reachable only from the operator's CLI (① item 1). A tenant administrator therefore has no path to the move. "The operator's run moves" is covered by the CLI integration test.
  • Deviation: the adopt-orphans skip was already in place (metadata-protocol: a republish of a view container that drops a member keeps serving the dropped member's expanded view until restart #23198). Accepted. It is pinned here, and the test was shown red when the skip was removed. The naming part is the open question answered above.
  • Deviation: vitest.config.ts and the durability script are outside the order's file sketch. Accepted. Each entry is the one its gate requires (① items 5 and 6).
  • Deviation: the branch was rebased onto origin/main before push. Noted. The check-runs above are on the rebased head.
  • Split boundary. The diff stays out of the close half metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576. The package-less write stays open, and the diff touches no ui/**, adds no Setup redirect, and leaves the ensure door and the write gate unchanged. protocol.org-scoped-write-refused.test.ts is not in the diff.
  • No flag is left unanswered. S1 and S2 are should-fix items for the dev. Neither needs a maintainer decision.

Implemented-by: claude/project-thread-f88ad8
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

…less row; basedOn completion over every overridable type

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 5865a0ceceed9e86ad8fc9ec8803c30ea5d84934
Local-runs: none

This is a delta record. It extends the base record for 34c1aa5d (PASS, with should-fixes S1 and S2 and nit N1) and covers the one commit 5865a0ce on top of it.

Inputs:

Everything in the base record that this commit does not touch still holds: ① items 1–7, ② and ③.

Check-runs on the new head, read at 2026-11-01T15:04Z. CI is still running. Of 45 runs:

  • 22 are in_progress: Build Core, the eight Test Core shards, the four Type Check jobs, the eight Lint jobs, and Live E2E UI.
  • 23 are completed, each with success or skipped. Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size and Dogfood Regression Gate are all success.
  • None has failed.

So this verdict judges the contract only. It is not a CI verdict, and landing still waits on every run going green. The dev's local figures (86 of 86 gates at exit 0, --ran reconciled, and the suites passing) are the dev's own account. I did not re-run any of them.

① Derived judgments

  • S1 is fixed, and I accept the fix.
    • In listPackagelessOverlays, shadowOf now looks for the pair's row in the same state first, then in the other state. So a pair row of the item in either state marks the unbound row shadowed. It is listed with both ways out and never moved.
    • The unbound draft beside an active override (the case from the base record) is now shadowed.
    • The reverse case is also shadowed now: an unbound active row beside a pair that holds only a draft. That matches the card's reading "a row whose name the pair already overrides".
    • The identity still includes the bundle member, so an override of one translation locale does not shadow another locale. This is "per name" read as "per item", the same identity the reads and the write gate use. Accepted.
    • The LEFT ALONE test gains the unbound draft beside the active override. It expects shadowed: 2, both entries with shadowDeclares: true, and every row kept. The dev reports it red on the old module and red under ablation.
    • The pairRows map still holds overlay types only, and it now feeds only the shadow lookup. That is correct, because the move's population is the five overlay types.
  • S2 is fixed by widening the scope, not narrowing the sentence. I accept it.
    • The basedOn completion now reads every type in OVERRIDABLE_METADATA_TYPES, under both spellings, so it reads a declaration wherever isServedOverrideRowOf counts one.
    • On the head, that list is the spec's overridable disposition: the five overlay types plus page, app, dataset, doc, book, mapping, flow and action.
    • object is extend_only and the security-catalog types are excluded, so neither is in the list. Unbound rows are skipped.
    • Each completing save still goes through saveMetaItem with the pair's packageId, so N-09 judges it.
    • flow and action meet the existing declared_type_not_yet_available refusal (ADR-0146 D9, protocol-packaged-base-refusal.ts:319). The row is reported failed with that sentence and is left unchanged. Nothing is forced past the gate.
    • The new page pin expects declaration_completed with basedOn 1.2.0, and the edited body kept.
    • rowsById loads the wider population only when there are incomplete declarations. The move's own row set is unchanged.
    • The two changeset sentences now match the code: "a row the pair already overrides, active or draft" and "of any type a declared override applies to".
  • N1 is fixed.
    • PackagelessOverlayCensus and PackagelessOverlayMigration are exported, type-only, from packages/metadata-protocol/src/index.ts, next to ServedOverrideEntry.
    • The CLI imports them type-only. @objectstack/metadata-protocol is already a workspace:* dependency of packages/cli. The CLI derives CensusRow and Outcome from them, so the restated copy is gone.
    • This adds two names to the package's published TypeScript surface. That widens the surface and is covered by the existing Clause-②: yes (widening) line.
  • No accept-set change. The commit does not touch the save door in protocol.ts. The save-path judgment in the base record's ① item 3 holds unchanged.

nit N2: an --apply run can exit 1 on every re-run. A deployment with a flow or action pair declaration that lacks basedOn gets a failed outcome every time, so --apply exits 1 until ADR-0146 D9 opens those types or the operator edits the row. The dev declared this as a deviation, and the refusal sentence names the fix ("Remove overrides and rename the item"). The population is close to empty, because basedOn has been required since #28467. Two optional changes:

  • List D9-gated declarations as left alone (not_yet_available) rather than failed, so a clean re-run exits 0.
  • Reword the CLI's closing line, "each package-less row that failed is kept as it was". A declaration row is a pair row, not a package-less row.

Neither blocks landing.

② Semver level

No change from the base record. Clause-②: yes (widening), Metadata-protocol: added, and minor on @objectstack/metadata-protocol and @objectstack/cli all still match the diff:

  • Widening S2 changes neither reading. It changes which rows a new operator command completes. It does not change what a metadata author can write:
    • The save door's accept set is untouched.
    • Every completing write is judged by the existing gate.
    • flow and action stay refused.
  • The census change is not an accept set. The census now lists as shadowed a row it used to list as movable. That is the ceremony's own report, and it is only one release old within this same PR, so nothing published is narrowed.
  • The N1 exports are type-only additions, a widening, and fit the existing minor.

S2 is inside the card's ruling:

  • Seat 2's reading, folded into the claim, says the ceremony "should also give a stored legacy overrides: { package } its basedOn". It names no type limit.
  • The claim's file surface is packages/metadata-protocol/src/**.
  • The seat's ruling 6559037535 forbids only a spec key and new protocol surface. The commit touches no file under packages/spec and adds no route.

Check Changeset on the new head is success.

③ Boundary flags

  • Seat 1's flow finding (6559382296): I accept the dev's reading. It needs no pin here and is out of this card's scope. I checked the dev's three measured reasons on the head:

    1. flow is registered with allowEnvironmentOverlay: false (metadata-plugin.zod.ts:978). The five types with allowEnvironmentOverlay: true are view, dashboard, report, translation and email_template. So a package-less flow row is outside the population Wanted 1 names.
    2. A package-less row beside the pair's row of the same item is exactly the shadowed shape. Wanted 2, now enforced across both states by S1, forbids the ceremony to move, merge or delete it.
    3. A declared flow write is refused by N-09 as declared_type_not_yet_available until D9.

    So the shape survives the migration by the card's own ruling. Seat 1 asked to "add a check … or say why the shape cannot survive". The dev took the second branch, and the answer is measured. The remedy, either the registry's bare-slot pick or refusing a package-less flow save beside a pair row, belongs to the card that opens flow overrides. The dev filed it as out_of_scope_findings, class c, with server seat 3 as carrier. Carrier: seat 3 routes it, and this card is not blocked on it.

  • PR body: patch it before enqueue (should-fix S3). This needs no code change and no further review. The body still reads as of 34c1aa5d:

    • "Tests … (9)" should now be 10.
    • LEFT ALONE does not mention the draft-beside-active case.
    • The basedOn completion line does not mention the widened type scope, or that flow and action are reported failed.
    • The Acceptance notes do not say how seat 1's flow finding was handled.

    The body is the landing record a reader of the merged PR sees. Its Acceptance notes are where Wanted 3 and Wanted 5 were accounted for, so leaving the flow finding out would make the account incomplete. The changeset, which is the published text, is already right. The dev's acceptance_notes_addition text is suitable to paste as written, and the seat owns that write.

  • Dev deviations in 6559681417:

    • The bundle member stays in the identity: accepted (S1 above).
    • flow and action get a failed outcome, so --apply exits 1: accepted as declared (nit N2).
    • No pin for the flow finding: accepted (above).
  • Open questions: the report has none. The base record's open question (ruling B) is unaffected.

Implemented-by: claude/project-thread-f88ad8
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28689 · 2026-11-01T15:23Z

Head-sha: 5865a0ceceed9e86ad8fc9ec8803c30ea5d84934

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28413,合并关卡正确;闭合关键词另无命中。文件 12 个,在认领 6557994545 申报面内(metadata-protocol、cli migrate 命令、objectql 测试、两份普查名单与 changeset)。
  2. 前提实测:dev 在 main 上实测 Wanted 4 的两种真实拒绝(undeclared_same_name、declared_outside_pair),其余用例因仪式不存在而红;CLI 集成测试在真实启动栈上跑通预览、--apply、重启后读取与重跑 0。每条守卫均做消融(重建 dist 并以 ablation-dist-preflight 证明),红后恢复。
  3. 门禁:本 head check-runs 37 success、15 skipped(路径未命中)、0 失败或待跑(复核时在跑的 22 条现已全部成功)。dev 本地(delta head):86 条派生门禁全部 exit 0,--ran 对账;metadata-protocol 21944、objectql 9913、cli unit 5166 条测试通过,另 184 条定向测试;typecheck 与 lint 通过。
  4. 接受的偏差:(1) S1 的身份含 bundle 成员,一个翻译语言的覆盖不遮蔽另一个语言的无包行("按名"读作"按条目",与读路径和写门同一身份);(2) S2 采用扩大:basedOn 补全覆盖全部可覆盖类型,flow/action 行因 ADR-0146 D9 拒绝被报告为 failed,含此类行的部署每次 --apply 退出 1(N2 nit,拒绝句已给处方,不挡落地)。PR 正文已由席位补齐 delta 说明与 Acceptance notes(S3)。
  5. 范围外去向:Wanted 5(adopt-orphans 响应是否点名本命令)席位裁决 B,不改响应(6559037535)。席位 1 的 flow 发现(6559382296)本卡结构上无法覆盖(flow 不可环境叠加;旁路无包行是 Wanted 2 禁搬的遮蔽形态;D9 前写门拒绝声明式 flow),已立卡 service-automation: at boot a package-less flow row can take the bare slot of a shipped flow name over its declared <base>.custom override until kernel:ready re-arms #28717(p2),席位 1 认领。Skills-docs: none(已核:skills/objectstack-platform/SKILL.md 的 D11 一句仍为真,由 metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576 退休)。
  6. 落地条款:Clause-②: yes (widening),非 spec 路径 ⇒ TIER_DEFAULT 复核,基础记录 PASS(6559155532,34c1aa5d)+ delta PASS(6559720258,Head-sha 与现 head 一致)。Metadata-protocol: added,两个包 minor 与 changeset 一致(Check Changeset 本 head success)。check-governed-merges --pr 28689:NOT governed,1949 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

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

Projects

None yet

1 participant