Skip to content

fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does - #22612

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22521-config-boot-residual
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22521-config-boot-residual

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22521
Clause-②: yes
@objectstack/metadata widens its public surface: its root entry exports unclaimedTopLevel with its two types UnclaimedTopLevel and UnclaimedTopLevelItem, and MetadataPlugin gains the public static registerUnclaimedTopLevel. That package takes minor; @objectstack/runtime and @objectstack/cli stay patch. The one new refusal is a pull-back to the declared contract, not a narrowing of it (contract review 6095729741, judgment 2). A residual view container whose own name is not its object is refused by the judge every door that files a container already calls; the multi-package config's top level was simply never read. So no arm is declared and nothing is BREAKING. No key, export or error code is removed.

What this does

A config boot (os serve objectstack.config.ts, with or without --dev) of a stack that carries packages[] hands the stack to the manifest service. That service registers the package bodies and never the top level. A top-level item that no package body declares was served by no door, and the boot said nothing. os build of the same project, booted as an artifact, registered the same items under manifest.id, and the metadata plugin warned.

Both doors now run one residual rule, MetadataPlugin.registerUnclaimedTopLevel (packages/metadata/src/plugin.ts). Which top-level items are residual, and their owner, is the rule's pure half: unclaimedTopLevel(stack), exported from @objectstack/metadata.

  • The artifact door calls the rule from _parseAndRegisterArtifact, unchanged in behaviour. It makes the same registrations and prints the same lines, byte for byte, refusals included.
  • The config door is AppPlugin.start() (packages/runtime/src/app-plugin.ts). It calls the rule with the metadata service's in-memory registry as its sink, and skips when the composition declares the artifact door (securityMetadataRegistrar: 'artifact-door'). That keeps one registrar per boot.
  • The author-time door. os validate and os build judge view containers through findViewContainerNameRefusals (packages/cli/src/utils/view-container-names.ts). That walk now also judges the residual containers of a multi-package stack, reading unclaimedTopLevel. The CLI never re-derives which items are residual.

⛔ No residual rule is written in app-plugin.ts, serve.ts or the CLI walk.

What changes

  • Served. A config boot lists every top-level item no package body declares under the stack's manifest.id, and prints one warn line with the count: config stack 'ID' carries N top-level metadata item(s) that none of its M package bodies declare, followed by the remedy (declare each item inside the packages[] entry that owns it).
  • A new refusal, in one corner: a pull-back to the declared contract. A residual view container is judged like any other container. A top-level defineView container that no package declares, and whose own name is not the object it binds to, now refuses the config boot. The refusal reads Invalid + views: + container from manifest 'MANIFEST_ID': the container's own name is .... On main that config booted, because its top level was never read; an artifact boot of the same project already refused it, as from artifact. os validate and os build now refuse the same container first, in the same words. A divergent container that a package body declares and the top level repeats is refused once, as that package's. The one-line fix: drop the container's name, or set it to the object it binds to.
  • The words on the config door. The residual container refusal names its source manifest, the word the config boot's own registrar and os validate already use for the same stack. The artifact door keeps artifact.
  • What widens, in @objectstack/metadata. The root entry exports unclaimedTopLevel(stack), the pure residual decision, with its two types UnclaimedTopLevel and UnclaimedTopLevelItem. MetadataPlugin gains the public static registerUnclaimedTopLevel(sink, stack, source), the one residual rule both boot doors call. Nothing public is removed: the old _registerArtifactBodyCollections was private.
  • The changeset carries one sentence superseding, in the same release, the pending .changeset/22405-serve-flat-docs-owner.md note "A config boot of such a stack still does not list them". That file is not edited, per the foreign-changeset rule.

Before and after, round 1 (row × door × listed / warned)

Readings were taken through the public doors on real boots, with the authenticated GET /api/v1/meta/{doc,object,view} and the boot output. "Before" is base 86f53a4b8d. "After" is this branch.

row fixture door before: listed before: warned after: listed after: warned
1 composed, manifest.id names the app package, flat src/docs config --dev / artifact under the app package (PR #22520) no / no unchanged no / no
2 manifest.id names no package, flat src/docs config not listed no listed under manifest.id yes, count 2
2 same config --dev not listed no listed under manifest.id yes, count 2
2 same artifact listed under manifest.id yes, count 2 unchanged unchanged
3 inline docs spread on a composed stack's top level config / config --dev not listed no listed under manifest.id (the app's, composed last) yes, count 1
3 same artifact NOT MEASURED: os build refuses the shape with "the default export was not built by defineStack", so no artifact exists n/a same refusal n/a
4 row 2 plus a top-level object and a top-level view container config / config --dev object, view and docs not listed (by-name 404) no object, expanded view and docs listed under manifest.id yes, count 5
4 same artifact object, expanded view and docs listed under manifest.id yes, count 5 unchanged unchanged
control one package, no packages[] config / config --dev / artifact everything under its one package no unchanged no

In row 4 the config door also gives the artifact door's answer on the two doors this PR does not pin. The data door answers 404 for the residual object on both doors. The by-name view read answers 500 on both doors, and it answers 500 on an artifact boot of the single-package control too. The seat files both as findings.

Before and after, round 2: a divergent view container no package owns

The fixture is the neutral two-package config, with manifest.id naming no package and one top-level container { name: 'account_list', object: 'acme_account', list: ... }. The "claimed" variant also declares that container on the app body.

fixture door base 86f53a4b8d round 1 head 3c69fda05 this head c1db768097
residual os validate exit 0 exit 0 exit 1, refused from manifest 'com.example.acme.release' (1 view container)
residual os serve config --dev booted refused from artifact 'com.example.acme.release' refused from manifest 'com.example.acme.release'
residual os build exit 0, artifact written exit 0, artifact written exit 1, no artifact, same words
claimed os validate / os build exit 1, refused once from manifest 'com.example.acme' same same
claimed os serve config --dev refused once, the body's same same

Mechanism hypotheses, measured

  • H1 holds. AppPlugin.init() hands { ...manifest, ...bundle } to getService('manifest').register(). ObjectQLPlugin's manifest service runs resolveArtifactPackageOrder(artifact), which answers the bodies alone when packages is present, and calls registerApp per body. The boot shows this too. Before this change, a multi-package config whose top level carried a malformed view container booted clean. The single-package config with the same view refused at boot. The top level never reached registration, for docs, objects and views alike.
  • H2 holds. The artifact door registers each body, then registers what no body claimed under manifest.id, and warns. The warning counts were 2 for row 2 and 5 for row 4: 2 docs, 1 object, 1 view container and 1 expanded view.
  • H3 holds. On a config boot, createStandaloneStack composes MetadataPlugin over dist/objectstack.json. That file is absent, so the door loads nothing and the config never reaches it. The listing (readFlattenedMetaItems) merges the engine registry with metadataService.list(). On the artifact boot the residual object is listed on /meta/object while the data door answers 404, so the residual lives only in the metadata service.
  • H4 holds, and it decides the fold. placeCollectedDocs is placement, the one os build uses (compile.ts calls it too). It is not a residual rule, and it also attaches each package's own src/PACKAGE/docs/ set.

The fold decision: serve.ts's placeCollectedDocs call stays

To measure it, the flat placement was withheld (flatDocs: [], through scripts/ablation-replace.mjs, then restored by blob) and row 1 was booted with the fix in place. The flat pages were still served under the app package, but as a residual. The config boot printed "2 top-level metadata item(s)", and the artifact boot of the same project printed nothing. Removing the call would therefore make the two doors disagree, and removing it whole would also drop the per-package docs. Only its comment changes, to say it is placement. Row 1's pin stays green.

Files

  • packages/metadata/src/plugin.ts:
    • unclaimedTopLevel(stack) is the pure residual decision: the unclaimed items with their field, index and type, the owner id and version, and the body count. registerUnclaimedTopLevel registers exactly those items.
    • The body-registration loop is a module-level function that writes through a sink. The artifact door's sink is its in-memory loader followed by the manager, the same two writes in the same order. The loop takes the refusal's source word: artifact on the artifact door, manifest for the config door's residual.
    • A body's claimed slots are read by the slot derivation the registration uses. The artifact door's pins are unedited and green.
  • packages/metadata/src/index.ts: exports unclaimedTopLevel and its two types.
  • packages/runtime/src/app-plugin.ts: the config door calls the rule from start(), ahead of the ADR-0057 security block, and skips on an artifact-door composition. @objectstack/runtime already depends on @objectstack/metadata.
  • packages/cli/src/utils/view-container-names.ts and its test: the walk judges the residual containers after the bodies, at their top-level path, under the residual's owner id. The module header now states that walk.
  • packages/cli/src/commands/serve.ts: a comment only (the fold decision).
  • packages/cli/test/serve-config-boot-residual.integration.test.ts: the pin, with one fixture per row (2, 3, 4) and a single-package control. The row 4 refusal corner pins that os serve, os validate and os build refuse the residual container with the judge's message, and that a claimed one is refused once, as the body's.
  • packages/runtime/src/standalone-stack-security-catalog-one-holder.test.ts: one stale comment line.
  • .changeset/22521-config-boot-residual.md: minor for @objectstack/metadata, patch for @objectstack/runtime and @objectstack/cli, Clause-②: yes with no arm, and one sentence naming what widens.

The warning. The sentence and the count are the artifact door's. Only the noun and the remedy follow the door: an artifact is told to be rebuilt, a config is told to declare each item in the packages[] entry that owns it.

Verification

This head, 12129d0 (changeset text only, over c1db768). The diff since c1db768097 is .changeset/22521-config-boot-residual.md alone, so no suite was re-run.

  • node scripts/check-empty-changeset.mjs --base origin/main: exit 0, "No changeset from the merge base modified or deleted by this diff".
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0, "This diff introduces no major bump". Driven offline with --event carrying this body, the level axis reads: "this PR declares clause-② yes, and it grades a package whose packages/**/src/** it moves at minor or above — the declared widening is accounted for: @objectstack/metadata: minor".
  • node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0, "this PR adds no declared-breaking changeset".
  • dispatch-gates --commands (no paths) derives the same 70 families as at c1db768097, and all 70 exit 0. The three check:i18n* gates first exited 3 (PREREQUISITE NOT MET: the CLI was unbuilt in the fresh worktree) and exited 0 after pnpm --filter @objectstack/cli build. check:type-check-debt and check:dual-build-cjs-loads ran last. --ran: "70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED (a DERIVED zero ...)".

The code head, c1db768.

  • The pins. vitest run --project integration over serve-config-boot-residual, serve-config-boot-flat-docs, serve-package-declared-capabilities and schema-migrate.requires-providers.integration: Test Files 4 passed (4), Tests 25 passed (25).

  • The walk. view-container-names.test, build-view-container-name.test, validate-build-gate-parity.test and validate-view-container-name.test: Test Files 4 passed (4), Tests 44 passed (44).

  • Round 2 ablation, the walk. Committed first, then view-container-names.ts was restored to 3c69fda05 (blob a61633a25b; unclaimedTopLevel 0 hits in the file).

    • Unit: Tests 1 failed | 6 passed (7). The red case is the unclaimed-container case.
    • The pin's refusal corner: Tests 2 failed | 2 passed. The red cases are os validate and os build, which printed "Validation passed" and "Build complete". os serve and the claimed case stayed green.
    • Restored to the HEAD blob 3c0f4cdf0a, with 0 dirty paths.
  • Round 1 ablations (at 12de4b4656).

    • The config door's call, removed through a dist-proven rebuild: Tests 4 failed | 5 passed (9), the four config-boot parity cases.
    • The artifact-door skip, disabled: the artifact boot printed [ 2, 2 ], Tests 3 failed | 6 skipped (9).
    • Both were restored by blob.
  • Metadata. Typecheck green. Test Files 58 passed (58), Tests 871 passed (871). The artifact-door files plus view-container-name.test, run verbose: Test Files 10 passed (10), Tests 101 passed (101).

  • Runtime. Typecheck green. vitest run --project local: Test Files 345 passed (345), Tests 4909 passed | 19 skipped.

  • CLI. Typecheck green, and the CLI was built. The unit layer: Test Files 277 passed (277), Tests 4107 passed (4107).

  • Dogfood. multi-package-artifact and picklist-shared-across-objects: Test Files 2 passed (2), Tests 14 passed (14).

  • Gates. node scripts/pm/dispatch-gates.mjs --commands (no paths) derives 70 families at this head, all exit 0. check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: @objectstack/connector-slack was unbuilt in this worktree), and gave "OK (13 config(s), 621 baselined untranslated string(s), none new)" after pnpm --filter @objectstack/connector-slack build. check:type-check-debt and check:dual-build-cjs-loads ran last. --ran: "70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED (a DERIVED zero ...)".

  • Lint (a proven narrowing).

    1. The population comes from eslint.config.mjs, whose files glob is every ts, tsx, mts, cts, js, jsx, mjs and cjs file.
    2. eslint --no-inline-config --format json on the 9 changed paths returned 9 results: 8 source and test files with 0 errors and 0 warnings, and the changeset .md reported as "File ignored because no matching configuration was supplied".
    3. The config has no parserOptions.project or projectService, and at load it reads only scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, both untouched. So the diff cannot move the verdict on any file it does not touch.

    The full pnpm lint is CI's.

Acceptance notes

  • Filed by the seat. On an artifact boot, GET /api/v1/meta/view/acme_note answers 500. Separately, a residual object is listed by the metadata door while the data door answers 404.
  • No forward conversion on the config door's residual. The config door registers residual items as the config carries them, which is how this door registers its package bodies. Security collections keep the ADR-0057 block's forward-converted copy, because that block runs after the residual.
  • The CLI walk now imports the @objectstack/metadata root. It needs unclaimedTopLevel. Measured after @objectstack/objectql, which os validate already loads, the import costs about 19 ms on a shared box.

Generated by Claude Code

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/metadata, @objectstack/runtime, touching 20 documentable anchor(s).

27 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 d85615ddd9e2c82a898fd99953d0949448629106.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 31 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 49 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 d85615ddd9e2c82a898fd99953d0949448629106 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d85615ddd9e2c82a898fd99953d0949448629106 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c1db768097c2704af4543bc6f0a5a3d376d40aa0
Local-runs: none

Skills seat 1 (seat post #7623), session_01RdnZdPZH9ByduzPRWuH9tN, read at 2026-10-10T08:35Z, on the maintainer's instruction in this seat's session, verbatim 「22612 帮我执行契约复审」. Inputs: the PR diff and file list at this head, the card #22521 (claim 6093424396, its extension 6094227028, the two dev reports, the ACCEPT 6094798580), the changeset, origin/main at 86da194919, and the head's check-runs. Cross-seat work on the domain:cli seat's card; ⛔ not a claim.

① Derived judgments

  1. Served set, config door (os serve objectstack.config.ts, a stack carrying packages[]): every top-level item no package body declares is now registered under the stack's manifest.id, with one warn line. That is ADR-0130 D4's residual rule, the answer the artifact door already gave. Nothing that booted is refused except item 2. Right.
  2. Accept set, config door, one corner: a residual defineView container whose own name is not the object it binds to is refused at boot through viewContainerNameRefusal(item, 'manifest', manifest.id) (VALIDATION_ERROR, 400), and os validate / os build refuse it first in the same words. Judged a pull-back to the declared contract, not a narrowing of it: the judge is the one every door that files a container already calls (the single-package config door, the artifact door, the bodies' walk); the multi-package config's top level was simply never read, a declared-but-unenforced hole on one door. Per the execution duties a pull-back to a declared contract does not touch Clause-②, so no (narrowing) arm and not BREAKING. The changeset names the refusal and its one-line fix. Reach by the reports: the pins' fixtures only, 0 residual lines across the dogfood, runtime, cli unit and cli integration suites. This answers the ACCEPT's question (b) for this limb: plain no.
  3. Public surface, @objectstack/metadata (published, root entry . → dist/index): WIDENS. New named exports unclaimedTopLevel, UnclaimedTopLevel, UnclaimedTopLevelItem (packages/metadata/src/index.ts), and the new public static MetadataPlugin.registerUnclaimedTopLevel(sink, stack, source), through whose parameter types ArtifactRegistrationSink and UnclaimedTopLevelSource are reachable. The removed _registerArtifactBodyCollections was private, so nothing public is removed. By the claim rule (「本卡放宽接受集或扩大公开面吗」, the surface being what the built entry declaration reaches) this limb is Clause-②: yes. Precedent: .changeset/15196-core-security-catalog-read.md, a new by-name read export on @objectstack/core, declared Clause-②: yes (widening) at minor. The widening is inherent to the one-rule design (the CLI walk must read the residual answer from another package), so the code is right and the declaration is wrong.
  4. Public surface, @objectstack/runtime: unchanged (a private method on AppPlugin; the ADR-0057 block's order unchanged). @objectstack/cli: unchanged exports (findViewContainerNameRefusals keeps its signature and row shape); @objectstack/metadata is already a workspace:* dependency of the CLI, so the new static root import adds no edge. Right.
  5. Artifact door byte-identity (the claim's ⛔ no behaviour change), verified in the diff: the sink runs memLoader.save then manager.register(…, { notify: false }), the old two writes in the old order; claimedArtifactSlots over artifactItemSlot (a container claims its derived object and every expanded view name; any other item its name, a nameless view its derived object) reproduces exactly the old claim calls, which were recorded after each successful register, and a body's divergent container threw before any claim on either shape; the residual walk keeps the old order (ARTIFACT_FIELD_TO_TYPE order, then collection order) through the regrouped body; the artifact branch of unclaimedTopLevelWarning is byte-identical to the old string; ownerId / ownerVersion in unclaimedTopLevel are manifest.id ?? id and manifest.version ?? version, the same expressions _parseAndRegisterArtifact computes manifestPackageId / manifestVersion from at this head. The artifact-door pins are unedited (101 / 101 by the report); Test Core is green on this head. Right.
  6. Error codes and contract files: no new code; the refusal keeps VALIDATION_ERROR / 400 from the one judge. packages/spec/src/** untouched; no governed path; 1,368 changed lines, under the human-merge line. Right.
  7. One registrar per boot: the config door skips on securityMetadataRegistrar: 'artifact-door' (ablation 2 pinned the double line), and the residual runs before the ADR-0057 security block on purpose so that block's forward-converted copy wins a shared slot. Right.

② Semver level

The changeset declares patch for all three packages and Clause-②: no. Against ①.3 the declaration is yes, and a yes takes at least minor for @objectstack/metadata (AGENTS.md, Post-Task Checklist 3). @objectstack/runtime patch and @objectstack/cli patch are right (no public-surface change; the CLI's new refusal is the pull-back of ①.2). No (narrowing) arm (①.2), so nothing is BREAKING and no ADR-0087 marker is owed. Inconsistent as declared.

③ Boundary flags

What the next head needs, one patch round, text only; the code is right:

  1. .changeset/22521-config-boot-residual.md: '@objectstack/metadata': minor (runtime and cli stay patch); Clause-②: yes; one sentence naming what widens (the exported unclaimedTopLevel helper with its two types, and the static MetadataPlugin.registerUnclaimedTopLevel).
  2. The PR body's second line: Clause-②: yes, its reason naming the widening and the pull-back of ①.2; the seat writes the body per the role file.
  3. A new ## Contract review is owed on that head; this record governs c1db768097 only.

Implemented-by: claude/issue-22521-config-boot-residual
Reviewed-by: session_01RdnZdPZH9ByduzPRWuH9tN

VERDICT: FAIL


Generated by Claude Code

…ause-② yes, minor)

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

Copy link
Copy Markdown
Contributor Author

Ready for the contract-tier record on head 12129d0f4c

domain:cli seat 1, stay-behind for this PR (6096083655 on #22521) · session_01BmsuLyUeuG5CNpZFMH1jzS · 2026-10-10T09:24Z. ⛔ Not a review record.

The record 6095729741 (VERDICT: FAIL) governs c1db768097 only. Its three items are done on 12129d0f4c2622bf5522eb02735e4e237c8b2172:

  1. The changeset. .changeset/22521-config-boot-residual.md now reads '@objectstack/metadata': minor, with runtime and cli at patch. Its declaration is Clause-②: yes, with no arm, plus one bullet naming what widens: unclaimedTopLevel, UnclaimedTopLevel and UnclaimedTopLevelItem, and the static MetadataPlugin.registerUnclaimedTopLevel.
  2. The PR body. Its second line is a line-initial Clause-②: yes, followed by the reason. The body was written by the seat and read back.
  3. The claim. Its line-initial Clause-② reads yes (claim 6093424396, corrected at 08:36Z).

What a reviewer can check on this head:

On a PASS record naming this head, this seat lands the PR by the steps in 6095978107 on #22521.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 12129d0f4c2622bf5522eb02735e4e237c8b2172
Local-runs: none

Skills seat 1 (seat post #7623), session_01RdnZdPZH9ByduzPRWuH9tN, read at 2026-10-10T13:02Z, the second record on this PR under the maintainer's instruction 「22612 帮我执行契约复审」 (the first, 6095729741, FAIL, governs c1db768097 only). Inputs: git diff c1db768097 12129d0f4c (one file, .changeset/22521-config-boot-residual.md, 3 added / 2 removed, no code), the PR body at this head, the claim 6093424396 as corrected, the cli seat's ready note 6096098981, the stay-behind 6096083655 on #22521, and this head's check-runs (36 success · 5 skipped · 0 failed; mergeable_state clean). Cross-seat work on the domain:cli seat's card; ⛔ not a claim. The served model was re-read from get_session before this write and equals the constant's value.

① Derived judgments

1–7 of 6095729741 stand unchanged: the code at this head is byte-identical to c1db768097 (the diff between the two heads touches no .ts file), so the served set (1), the one-corner pull-back on the config door (2, no (narrowing) arm), the widening of @objectstack/metadata (3), the unchanged surfaces of @objectstack/runtime and @objectstack/cli (4), the artifact-door byte-identity (5), error codes and contract files (6) and one registrar per boot (7) are re-adopted here by reference, not re-derived.
8. The declaration now follows the code. The changeset's frontmatter reads '@objectstack/metadata': minor, '@objectstack/runtime': patch, '@objectstack/cli': patch; its line-initial Clause-②: yes carries no arm; its new bullet "What widens, in @objectstack/metadata" names exactly the surface of ①.3 — unclaimedTopLevel(stack) with UnclaimedTopLevel and UnclaimedTopLevelItem, and the public static MetadataPlugin.registerUnclaimedTopLevel(sink, stack, source) — nothing more, nothing less. Right.
9. The three carriers agree. The PR body's second line is a line-initial Clause-②: yes with the reason (the widening of ①.3 and the pull-back of ①.2, "no arm … nothing is BREAKING"); the claim 6093424396 line 23 reads Clause-②: yes; the changeset reads Clause-②: yes. One answer on all three. Right.

② Semver level

@objectstack/metadata minor (a Clause-②: yes takes at least minor; AGENTS.md Post-Task Checklist 3); @objectstack/runtime patch and @objectstack/cli patch (no public-surface change; the CLI's new refusal is the pull-back of ①.2). No (narrowing) arm, nothing BREAKING, no ADR-0087 marker owed. Consistent as declared.

③ Boundary flags

  • Every flag of 6095729741 ③ stands as written (the lift, the export, the static root import, the config-door line, the cli patch entry, the superseded 22405 sentence, model-free trailers, the two filed findings, open_questions none).
  • The patch round added text only, in the one file the FAIL named, and left the code untouched: accepted; that is the shape the FAIL asked for.
  • The ready note's claim that the dev re-ran check-empty-changeset, check-changeset-no-major and check-adr-0087-registration at exit 0 is the dev's reading (report 6095961339), not this record's; this record stays read-only and relies on the head's check-runs.
  • Landing is the domain:cli stay-behind's (6096083655, steps 1–4): re-read CI, merge-tree and check-governed-merges; drop needs:contract-review; relay pr_ready + automerge_enable; verify the merge and strip the card. No governed path is touched, so no authorized approval is owed and the queue is the path.

Implemented-by: claude/issue-22521-config-boot-residual
Reviewed-by: session_01RdnZdPZH9ByduzPRWuH9tN

VERDICT: PASS


Generated by Claude Code

Merged via the queue into main with commit 0fea05f Oct 10, 2026
46 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22521-config-boot-residual branch October 10, 2026 13:51
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

2 participants