Repository navigation
fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does - #22612
Conversation
Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 49 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…w judges Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…eded note Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Skills seat 1 (seat post #7623), ① Derived judgments
② Semver levelThe changeset declares ③ Boundary flags
What the next head needs, one patch round, text only; the code is right:
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…ause-② yes, minor) Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Ready for the contract-tier record on head
|
Contract reviewServed-tier: Skills seat 1 (seat post #7623), ① Derived judgments1–7 of ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22521
Clause-②: yes
@objectstack/metadatawidens its public surface: its root entry exportsunclaimedTopLevelwith its two typesUnclaimedTopLevelandUnclaimedTopLevelItem, andMetadataPlugingains the public staticregisterUnclaimedTopLevel. That package takesminor;@objectstack/runtimeand@objectstack/clistaypatch. 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 ownnameis 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 carriespackages[]hands the stack to themanifestservice. 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 buildof the same project, booted as an artifact, registered the same items undermanifest.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._parseAndRegisterArtifact, unchanged in behaviour. It makes the same registrations and prints the same lines, byte for byte, refusals included.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.os validateandos buildjudge view containers throughfindViewContainerNameRefusals(packages/cli/src/utils/view-container-names.ts). That walk now also judges the residual containers of a multi-package stack, readingunclaimedTopLevel. The CLI never re-derives which items are residual.⛔ No residual rule is written in
app-plugin.ts,serve.tsor the CLI walk.What changes
manifest.id, and prints onewarnline 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 thepackages[]entry that owns it).defineViewcontainer that no package declares, and whose ownnameis not the object it binds to, now refuses the config boot. The refusal readsInvalid+views:+container from manifest 'MANIFEST_ID': the container's own name is .... Onmainthat config booted, because its top level was never read; an artifact boot of the same project already refused it, asfrom artifact.os validateandos buildnow 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'sname, or set it to the object it binds to.manifest, the word the config boot's own registrar andos validatealready use for the same stack. The artifact door keepsartifact.@objectstack/metadata. The root entry exportsunclaimedTopLevel(stack), the pure residual decision, with its two typesUnclaimedTopLevelandUnclaimedTopLevelItem.MetadataPlugingains the public staticregisterUnclaimedTopLevel(sink, stack, source), the one residual rule both boot doors call. Nothing public is removed: the old_registerArtifactBodyCollectionswas private..changeset/22405-serve-flat-docs-owner.mdnote "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 base86f53a4b8d. "After" is this branch.manifest.idnames the app package, flatsrc/docs--dev/ artifactmanifest.idnames no package, flatsrc/docsmanifest.id--devmanifest.idmanifest.iddocsspread on a composed stack's top level--devmanifest.id(the app's, composed last)os buildrefuses the shape with "the default export was not built bydefineStack", so no artifact exists--devmanifest.idmanifest.idpackages[]--dev/ artifactIn 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.idnaming 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.86f53a4b8d3c69fda05c1db768097os validatefrom manifest 'com.example.acme.release'(1 view container)os serveconfig--devfrom artifact 'com.example.acme.release'from manifest 'com.example.acme.release'os buildos validate/os buildfrom manifest 'com.example.acme'os serveconfig--devMechanism hypotheses, measured
AppPlugin.init()hands{ ...manifest, ...bundle }togetService('manifest').register().ObjectQLPlugin's manifest service runsresolveArtifactPackageOrder(artifact), which answers the bodies alone whenpackagesis present, and callsregisterAppper 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.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.createStandaloneStackcomposesMetadataPluginoverdist/objectstack.json. That file is absent, so the door loads nothing and the config never reaches it. The listing (readFlattenedMetaItems) merges the engine registry withmetadataService.list(). On the artifact boot the residual object is listed on/meta/objectwhile the data door answers 404, so the residual lives only in the metadata service.placeCollectedDocsis placement, the oneos builduses (compile.tscalls it too). It is not a residual rule, and it also attaches each package's ownsrc/PACKAGE/docs/set.The fold decision:
serve.ts'splaceCollectedDocscall staysTo measure it, the flat placement was withheld (
flatDocs: [], throughscripts/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.registerUnclaimedTopLevelregisters exactly those items.artifacton the artifact door,manifestfor the config door's residual.packages/metadata/src/index.ts: exportsunclaimedTopLeveland its two types.packages/runtime/src/app-plugin.ts: the config door calls the rule fromstart(), ahead of the ADR-0057 security block, and skips on an artifact-door composition.@objectstack/runtimealready depends on@objectstack/metadata.packages/cli/src/utils/view-container-names.tsand 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 thatos serve,os validateandos buildrefuse 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:minorfor@objectstack/metadata,patchfor@objectstack/runtimeand@objectstack/cli,Clause-②: yeswith 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
c1db768097is.changeset/22521-config-boot-residual.mdalone, 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 nomajorbump". Driven offline with--eventcarrying this body, the level axis reads: "this PR declares clause-②yes, and it grades a package whosepackages/**/src/**it moves atminoror 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 atc1db768097, and all 70 exit 0. The threecheck:i18n*gates first exited 3 (PREREQUISITE NOT MET: the CLI was unbuilt in the fresh worktree) and exited 0 afterpnpm --filter @objectstack/cli build.check:type-check-debtandcheck:dual-build-cjs-loadsran 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 integrationoverserve-config-boot-residual,serve-config-boot-flat-docs,serve-package-declared-capabilitiesandschema-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.testandvalidate-view-container-name.test:Test Files 4 passed (4),Tests 44 passed (44).Round 2 ablation, the walk. Committed first, then
view-container-names.tswas restored to3c69fda05(bloba61633a25b;unclaimedTopLevel0 hits in the file).Tests 1 failed | 6 passed (7). The red case is the unclaimed-container case.Tests 2 failed | 2 passed. The red cases areos validateandos build, which printed "Validation passed" and "Build complete".os serveand the claimed case stayed green.3c0f4cdf0a, with 0 dirty paths.Round 1 ablations (at
12de4b4656).Tests 4 failed | 5 passed (9), the four config-boot parity cases.[ 2, 2 ],Tests 3 failed | 6 skipped (9).Metadata. Typecheck green.
Test Files 58 passed (58),Tests 871 passed (871). The artifact-door files plusview-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-artifactandpicklist-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-coveragefirst exited 3 (PREREQUISITE NOT MET:@objectstack/connector-slackwas unbuilt in this worktree), and gave "OK (13 config(s), 621 baselined untranslated string(s), none new)" afterpnpm --filter @objectstack/connector-slack build.check:type-check-debtandcheck:dual-build-cjs-loadsran last.--ran: "70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED (a DERIVED zero ...)".Lint (a proven narrowing).
eslint.config.mjs, whosefilesglob is everyts,tsx,mts,cts,js,jsx,mjsandcjsfile.eslint --no-inline-config --format jsonon the 9 changed paths returned 9 results: 8 source and test files with 0 errors and 0 warnings, and the changeset.mdreported as "File ignored because no matching configuration was supplied".parserOptions.projectorprojectService, and at load it reads onlyscripts/slot-lookup-baseline.jsonandscripts/query-options-erasure-baseline.json, both untouched. So the diff cannot move the verdict on any file it does not touch.The full
pnpm lintis CI's.Acceptance notes
GET /api/v1/meta/view/acme_noteanswers 500. Separately, a residual object is listed by the metadata door while the data door answers 404.@objectstack/metadataroot. It needsunclaimedTopLevel. Measured after@objectstack/objectql, whichos validatealready loads, the import costs about 19 ms on a shared box.Generated by Claude Code