Repository navigation
fix(cli): a multi-package artifact's flat src/docs rides the package that owns its manifest - #22403
Conversation
…that owns its manifest placeCollectedDocs makes every docs placement os build performs: the per-package sets onto their bodies as before, and the stack's own flat src/docs onto the body of the packages[] entry whose id is the artifact's manifest.id when exactly one entry carries it. A stack with no packages[] keeps them on the top level, byte for byte. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…r's silence Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
… the placement 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>
…ner's docblock Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 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 3d5f3d756a3a9596ad4eb0849c9d8b373a6fa3c1 && git checkout 3d5f3d756a3a9596ad4eb0849c9d8b373a6fa3c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 11d119ab1868a658c5f43538f3026a56438b2ed6 027bc7fa48d794dbafdcff18214415f3ad4bb5cf && git checkout -B drift-repro 11d119ab1868a658c5f43538f3026a56438b2ed6 && git merge --no-ff 027bc7fa48d794dbafdcff18214415f3ad4bb5cf
node scripts/docs-audit/affected-docs.mjs --json 11d119ab1868a658c5f43538f3026a56438b2ed6
|
Fixes #22190
Clause-②: no
What this does
os buildwrote a multi-package artifact's flatsrc/docs/*.mdto the artifact's top level. Such an artifact keeps its metadata inpackages[]only (ADR-0130 D4, 2026-09-22 addendum), so these docs were the only items left at the top level. On every boot, the metadata door's residual sweep (packages/metadata/src/plugin.ts) registered them under the artifact'smanifest.idand warned that no package body declares them. The warning's remedy could not be followed in an ADR-0130 layout.The producer now conforms, as triage directed (
6054545749).placeCollectedDocs(new,packages/cli/src/utils/collect-docs.ts) is the one callcompile.tsuses to place collected docs:src/PKG/docs/set goes onto that package's body, throughattachPackageDocsas before;src/docs/set goes onto the body offlatDocsOwner(stack), after that package's own directory docs. That owner is the onepackages[]entry whose id equals the artifact'smanifest.id. If no entry, or more than one, carries that id, or the manifest has no id, the flat set stays on the top level as before;docsnever move.packages/metadatais not touched. The residual sweep stays as it is.Zone 2 hypotheses, measured
#18431comment5715694492) reads: "The flatsrc/docs/of a single-package stack keeps attaching where it does today (top level = the one package)". It says nothing about a multi-package stack. The top-level placement for multi-package stacks came from the implementation (df0c856e, two days before the 2026-09-22 emitter flip). Later seat prose (the #19246 body, the metadata attribution test's comment) restated it as a ruling, but no maintainer record says so. Clause 2 (the namespace a stack-level doc is linted against) is unchanged here: flat docs are still judged bystack.manifest.namespace.git show df0c856ecomposeStacks([service, app], { manifest: 'preserve' }), app source insrc/sales/, service insrc/service/),manifest.idisapp.example.acme, and exactly one entry (index 1) carries it. When no entry or two entries carry the id, nothing moves, and both cases are pinned. The collector has no duplicate-id refusal of its own (artifactPackagesdoes not check for one), so the two-entry case is pinned rather than assumed unreachable.flatDocsOwnerpinssrc/sales/docs/), built by base117d34deand by this branch: sha256710ab6c6acf915ac265c0fe0f05cca61e3418b85f84ace5f901e08000e000aefboth times. Build stdout is identical once timings are stripped, and the uncollected-directory warning appears in both.os dev --freshbefore and afterdocs[]git grep. The runtime reads collections generically (resolveArtifactCollectionsmerges the top level with the bodies;registerAppand the metadata door register per body), and books resolve by docpackageId. Each reader answers the same owner and count. Two answers changed, and both now match what a single-package artifact and asrc/PKG/docs/doc already answer (listed below).Boot-level reading (integration layer, run locally)
Card-shaped fixture,
objectstack dev --fresh --seed-admin --no-watch, authenticated reads. Base build is117d34de; the fix build is this branch.[MetadataPlugin] … top-level metadata item(s) that none of its 2 package bodies declareWARNmanifest,packages,docsmanifest,packagesGET /api/v1/meta/docdocs, with ownersacme_service_runbook→service,acme_faq→app,acme_guide→app,setup_overview(4)GET /api/v1/meta/book/app.example.acme/treeand the service package's bookGET /api/v1/meta/doc/acme_guidelock/editable/resettablenone/true/falsefull/false/trueGET /api/v1/packages, app packagedocs[]["acme_faq","acme_guide"]The last two rows changed. In both, the flat docs now read the way package docs already did: the service's
src/service/docsdoc readslock: fullon both builds, and a single-package artifact's flat doc readslock: full(measured on the control fixture). Before this change, docs registered by the residual sweep were missing from the owning package's installed record, so they read back as unpackaged and freely editable even though their provenance saidpackage. The changeset says so.Pins
packages/cli/src/utils/collect-docs.flat-docs-owner.test.ts(new, unit tier, so it runs on every PR): premise guard;flatDocsOwnerfor the unique, no-match, duplicate-id and no-id cases; placement (flat docs on the app body, carrying their markers); control:src/service/docsstays on the service body; the owner's own directory docs come first and are not dropped; inline top-level docs stay. The door: the realMetadataPluginbooted on the placed artifact inartifact-onlymode logs 0 warnings and serves the same docs, owners and versions. The lit control is the same artifact with the flat set put back on the top level, built from the collection rather than from the placement: it draws exactly 1 warning from the same door. The test counts warnings and never reads their text.packages/cli/test/build-package-docs-attachment.e2e.test.ts(nightly, integration): the command-level twin. It used to pin the old placement (the core body carried no docs and the top level carriedpkgdocs_index). It now pins the new placement, plus a no-owner fixture (the manifest id names no entry) that keeps the top level.packages/cli/test/validate-build-gate-parity.test.ts: the closed roster's artifact-assembly row now namesplaceCollectedDocs, the namecompile.tsnow calls. The row's reason is unchanged.Ablation, run from the committed state through
scripts/ablation-replace.mjs. The mutation isflatDocsOwner'sowners.length === 1changed to=== -22190, so no owner is ever found. On-disk proof: anchor ×1→×0, blob5db191c3f456→02e9d463bdfd. The unit file reads 5 failed | 10 passed (15): every placement and door case fails. The service control, the lit control and all no-owner and single-package controls stay green. The e2e file reads 1 failed | 3 passed (4), with only the owned-placement case failing. Restore: blob equals HEAD5db191c3f456, andgit diff HEADis empty. The subject resolves from source through a relative import, so no rebuild was needed.Verification (union run after the last commit, HEAD
027bc7fa4)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths, run at027bc7fa4) derived 65 families. The dispatch's list of 64 pluspnpm check:cli-test-child-env, which the derivation adds for thepackages/cli/testedits. All 65 exit 0.--ranreconciliation:65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN(a derived zero: every row recorded its exit code). An earlier pass readcheck:dual-build-cjs-loadsandcheck:i18n-coverageasPREREQUISITE NOT MET(exit 3: dist missing for packages outside the cli closure). Afterturbo run build --filter=!@objectstack/docs, both read OK in the union:107 published require entry point(s) across 66 package(s) loadandOK (13 config(s), 621 baselined untranslated string(s), none new).pnpm lint(the fulleslint . --no-inline-config, not narrowed): exit 0, no findings, at027bc7fa4.pnpm --filter @objectstack/cli typecheck: exit 0.tsc --noEmitpasses, andcheck:test-typecheck: OKholds 3 file(s) / 28 error(s) / 6 pinned signature(s) unchanged.unitlayer (vitest run --project unit): 271 files / 3983 tests passed. Measured at925e0742;git diff 925e0742 027bc7fa4is one docblock comment incollect-docs.ts. Re-run at027bc7fa4for the touched surface: unit (the three collect-docs files,validate-build-gate-parity,non-array-packages-readers) 5 files / 137 tests passed; integration, nightly tier (build-package-docs-attachment,build-docs-step-count,build-multi-package-artifact) 3 files / 16 tests passed. The integration layer was run locally only for these files, because the diff touches one of them. The rest of that layer is left to CI.Acceptance notes
serve.ts) is not changed, because it is outside this card's file surface. It still puts the flat set on the config's top level. Measured on this branch withos serve objectstack.config.ts --devand no compiled artifact:GET /api/v1/meta/docservesacme_service_runbookandsetup_overviewonly. The two flat docs are not served at all, and nothing warns.serve.tsis byte-identical to base, so this behaviour predates this change. It is reported for the seat to file, not fixed here.os devcompiles first and serves from the artifact, so it is not affected.packages/metadata/src/plugin-artifact-packages-attribution.test.ts(domain:engine, fenced): thedocsArtifactdocblock says that shape "is on every such artifact the compiler produces". That is now true only when no package owns the manifest. The test still passes because it builds its fixture by hand. Carrier: the next PR to touch that file.packages/cli/test/normalized-call-sites.test.ts: theserve.ts :: config.docsrow'swhysays the flat set joins the top level "ascollectAndLintDocsreturns it for the artifact's top level". For a multi-package build that is no longer whereos buildputs it. Carrier: the fix to theserve.tsmirror above.packages/cli/test/build-docs-step-count.e2e.test.ts's header quotesfinalBundle.docs = docsResult.docs. The behaviour it describes (single-package fixtures) is unchanged, but the quoted line no longer exists. Carrier: none.Generated by Claude Code