Skip to content

fix(cli)!: docs/nav-target judges each package body's navigationContributions on a multi-package artifact - #27394

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27343-nav-target-package-bodies
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27343-nav-target-package-bodies

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #27343

Clause-②: no (narrowing) — on a multi-package composeStacks(…, { manifest: 'preserve' }) artifact, a type: 'doc' item in a package body's navigationContributions, whose doc or book names nothing the artifact carries, built clean before and is now refused by os build, os validate and os lint (docs/nav-target).
Metadata-protocol: narrowed
Skills-docs: none — skills/objectstack-ui/rules/pages.md (the type: 'doc' nav section) and content/docs/ui/apps.mdx (the docs/nav-target paragraph) already teach the refusal with no single-package limit; this change makes contributions on multi-package artifacts meet that text.

维护者速读

  • 改了什么:os build/os validate/os lint 的文档导航检查(docs/nav-target)在多包产物上改为逐包读取各包自己的 navigationContributions(只改 packages/cli/src/utils/collect-docs.ts 的 lintDocNavTargets 一处读取)。
  • 为什么改:多包 preserve 产物的顶层 manifest 只是"最后一个包"(主包)的副本,其他包贡献到别的应用里的文档导航项从没被检查过,指向不存在文档的条目会静默通过构建、到运行时才坏。
  • 风险与回滚:一次收窄(BREAKING,minor):多包产物里"非主包贡献的导航项指向不存在文档"从静默通过变为构建错误;单包应用不变,主包的贡献只判一次不重复报。回滚即 revert 本 PR。
  • 先看:packages/cli/src/utils/collect-docs.ts 中 lintDocNavTargets 的 contributions 一行;packages/cli/src/utils/collect-docs.nav-target.test.ts 新用例;.changeset/27343-doc-nav-target-package-contributions.md。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/collect-docs.nav-target.test.ts,预期 10 个用例全部通过。
  • 需要你做的:无,席位落地。

What changes

lintDocNavTargets read navigationContributions off the artifact's top-level manifest only. Measured on a real two-package preserve composition (composeStacks([svc, crm], { manifest: 'preserve' })):

Who contributes Top-level manifest.navigationContributions packages[i].manifest.navigationContributions Issues before
first package (svc, non-owner) absent packages[0] only 0
last package (crm, owner) present (the owner's copy) packages[1] 1

The contributions now come from each package body when the stack carries packages[], otherwise from the top-level manifest. The walk is the same artifactPackages(stack) the function already uses for books and apps; no second walker.

Why not "top level when present, else bodies" (the card's wording). Falsified by the table: when the owner contributes, the top level carries its copy, so that rule would still skip every other package's contributions. Reading the top level as well as the bodies would judge the owner's twice. The rule here is the one collectNavGroupInputs (packages/cli/src/utils/nav-contribution-groups.ts) already applies to the same key for the same reason.

Tests

One new case in collect-docs.nav-target.test.ts, on the real composer:

  • the top-level manifest carries no navigationContributions when only the non-owner contributes;
  • a resolving doc passes;
  • a dead doc in the non-owner's contribution is refused at apps/setup/navigation/nav_svc_help;
  • when both packages contribute a dead doc, exactly two issues come back, one per package (the owner's top-level copy is not judged a second time).

Acceptance notes

  • Narrowed population. A hand-written artifact that carries packages[] and puts contributions on its top-level manifest that no body carries is no longer judged there. The runtime registers each packages[] entry's own manifest (ADR-0130 D4), so such a top-level copy is not what gets registered; collectNavGroupInputs makes the same call.
  • Declaration lines follow the dispatch: Clause-②: no (narrowing) and Metadata-protocol: narrowed, so the changeset is minor with a BREAKING banner and the ADR-0087 not-required (no-migration-prescription) disposition. The claim comment carries the bare Clause-②: no; the narrowing arm is from triage and the dispatch.
  • No in-tree example's navigationContributions carries a type: 'doc' item (the dogfood battery below is green).

Verification

Final head 6c5250cbd (origin/main 28872e68d merged in). The unit tier, typecheck and the first gate run are at 260fe77e0, before that merge; the merge touches none of the three changed files. The dogfood battery, the gate re-run and the target test re-run are at 6c5250cbd.

Red, then green, then ablation
  • Before the source change, the new case: Tests 1 failed | 9 passed (10), AssertionError: expected [] to deeply equal [ [ 'docs/nav-target', …(1) ] ].
  • After it: Tests 10 passed (10).
  • Ablation at 91e14b22 (fix committed first), through scripts/ablation-replace.mjs: anchor const contributions = (bodies.length > 0 replaced with const contributions = (false (the old top-level-only read). Anchor x1 to x0, replacement x0 to x1, blob f64208d1cd06 to edd969ecd1bb. Run: the new case fails with the same expected [] … assertion, Tests 1 failed | 9 passed (10). Restore: blob equals HEAD (f64208d1cd06), git diff HEAD empty. The test imports ./collect-docs.js from source, so no dist/ rebuild is in the path.
Package checks
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 (under the verify lock, VERDICT command-exit 0): Test Files 346 passed (346), Tests 5033 passed (5033). The integration tier is left to CI.
  • pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck: VERDICT command-exit 0; check:test-typecheck: OK.
  • eslint with the repo config over packages/cli (eslint packages/cli --no-inline-config): exit 0.
Derived gates
  • First run at 260fe77e0: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 60 families on 3 changed paths (51 changed lines). All 60 ran, exit codes captured before any pipe; 57 exit 0. Three exit 3 on a prerequisite: check-plugin-teardown-shape --self-test (its pinned fixture commit was not in the shallow clone; exit 0, 48 cases pass after fetching that commit), check:i18n-coverage (@objectstack/connector-slack had no build; exit 0 after building it) and check:dual-build-cjs-loads (needs a full workspace build).
  • Re-run at 6c5250cbd after the dogfood closure build: the same 60 families (derivation unchanged), all 60 exit 0, check:dual-build-cjs-loads included. check-adr-0087-registration --base origin/main exit 0: the changeset reads BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed, not-required (no-migration-prescription). check:i18n-coverage: OK (14 config(s), 527 baselined untranslated string(s), none new).
  • dispatch-gates --ran at 6c5250cbd: 60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero — all 60 recorded an exit code and none of them is 3).
  • Target test re-run at 6c5250cbd: Tests 10 passed (10).
Dogfood battery (owed by the narrowing arm)

At 6c5250cbd, under the verify lock, after building the @objectstack/dogfood dependency closure, pnpm --filter @objectstack/dogfood test with no shard and no path: Test Files 348 passed | 3 skipped (351), Tests 2806 passed | 20 skipped (2826), lock VERDICT command-exit 0. No failures.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw

…ibutions on a multi-package artifact

On a composeStacks(..., { manifest: 'preserve' }) artifact a non-owner
package's navigationContributions sit only in its package body; the
top-level manifest is the owner's copy. lintDocNavTargets now reads each
body's contributions when the stack carries packages[], otherwise the
top-level manifest's, over the same artifactPackages walk.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 29, 2026 04:58
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 58 of 210 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 — 35 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 7e200b4a68c242afc9f7caf0289e985eff3fcac6 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7e200b4a68c242afc9f7caf0289e985eff3fcac6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6c5250cbdb420485586b6b642a601ad8565149a0
Local-runs: none
Scope: packages/cli/src/utils/collect-docs.ts, packages/cli/src/utils/collect-docs.nav-target.test.ts + packages/cli/src/utils/artifact-packages.ts (artifactPackages), packages/cli/src/utils/nav-contribution-groups.ts (collectNavGroupInputs), packages/spec/src/stack.zod.ts (composeStacks step 1/3a, selectManifest, preservePackageEntries, assemblePackageBody, packagesCarryEveryInputCollection, ArtifactPackageSchema), packages/spec/src/kernel/manifest.zod.ts (navigationContributions), packages/spec/src/ui/app.zod.ts (NavigationContributionSchema), packages/core/src/stack-collections.ts (resolveStackCollection), packages/metadata/src/plugin.ts (D4 read-both door, unclaimedTopLevel), packages/objectql/src/engine.ts (registerApp step 4b), skills/objectstack-ui/rules/pages.md, content/docs/ui/apps.mdx, docs/adr/0029 D7, docs/adr/0130 D4 and its 2026-09-22 addendum. Face: narrowing (the PR body and the changeset both carry the no (narrowing) declaration); the floor is every path outside the prose face, and the changeset is judged under ② only.

Inputs read: card #27343 (body, triage, claim, os-dev-report), PR #27394 (body, file list, net diff against main at the merge-base 28872e68d: 3 files, +49/-2), and the check-runs on the head. Nothing was built, run or re-run.

① Derived judgments

  1. Accept-set narrowing, the one the card asked for — right. On a stack that carries packages[], a type: 'doc' item in any package body's navigationContributions whose doc names no doc the artifact carries, or whose book names no declared book and no package id, is now refused by docs/nav-target through os build, os validate and os lint (lintDocNavTargets, collect-docs.ts lines 1199-1207 at the head). The runtime registers exactly those bodies' contributions: resolveArtifactPackageOrder answers the bodies alone when packages[] is present, and engine.ts registerApp step 4b reads navigationContributions off each registered body. The published text already promised this refusal with no single-package limit (pages.md line 356: "os build refuses a target the package lacks (docs/nav-target)"; apps.mdx lines 246-248).
  2. Accept-set widening, implied — right, and it is of a dead copy only. On a stack that carries packages[], the top-level manifest.navigationContributions are no longer judged. On every composer-produced artifact that key is selectManifest(stacks, 'last'): the last manifest-bearing input's own manifest object, which assemblePackageBody spreads into that same input's body, so every top-level contribution IS a body's contribution and is still judged (once; on main the owner's copy was the only one judged at all). The one shape that loses a judgment is a hand-written packages[] artifact whose top-level manifest carries contributions no body carries. The runtime never registers those: the residual rule (unclaimedTopLevel, packages/metadata/src/plugin.ts) walks ARTIFACT_FIELD_TO_TYPE collections only, and navigationContributions is a manifest key, not a collection. collectNavGroupInputs made the identical call for the same key with the same reasoning in its docblock. Dropping a judgment of a copy the runtime discards is the lint agreeing with the runtime.
  3. Unchanged population — right. A stack with no packages[], or with packages: [], takes the top-level read exactly as before (artifactPackages returns [] for both; collectNavGroupInputs draws the same length > 0 line).
  4. Public surface — none moved. lintDocNavTargets(stack, docNames) keeps its signature and export; no new export, no spec or schema change, no packages/spec artifact to regenerate. The docblock's "Walked:" sentence now states the new rule.
  5. Test — right and discriminating. The new case builds the artifact through the real composeStacks(…, { manifest: 'preserve' }), pins that the non-owner's contribution is absent from the top-level manifest, and pins three outcomes that differ from main: non-owner dead doc (0 issues on main, 1 at the head), resolving doc (0 both), both packages dead (1 on main, the owner's only; 2 at the head, one per package). The dev's red-first and ablation claims are not re-run here; the logic above is what the verdict rests on.

② Semver level

  • The changeset bumps @objectstack/cli minor, carries the **BREAKING** banner, a fix(cli)!: summary, the no (narrowing) declaration and the narrowed kind on their own lines, and one ADR-0087 marker reading not-required (no-migration-prescription) with its reason. The PR body carries the same two declaration lines plus Skills-docs: none — … with a reason that holds on main (both cited texts teach the refusal with no single-package limit).
  • The Clause-② value and arm are right. The declaration answers "does this card widen the accept set or enlarge the public surface" (.claude/skills/pm-dispatch/references/execution-duties.md, the claim rules): this diff widens nothing published and narrows a build-time accept set, so no; a narrowing is breaking, so the (narrowing) arm. execution-duties.md states it directly: a card narrowing a published accept set is Clause-②: no and owes this review before the queue.
  • The kind and level are right under the repo's rule. AGENTS.md Post-Task Checklist step 3 and the level table in pr-automation.yml (maintainer ruling A): "a value refused … is narrowed", and a declared kind takes at least minor on a package the diff moves. A doc/book value in an authorable navigationContributions item that built clean on a multi-package artifact is now refused at the build door — a value refused on what an author writes, in the package whose published source moves. narrowed beside no (narrowing) is the consistent pairing the level gate and the ADR-0087 gate both read; Check Changeset and Lint & Repo Gates are green on this head.
  • Precedent PR fix(cli)!: the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps #27342 (patch, yes (narrowing), none) is the one out of step, not this PR. Its diff widened nothing published (a test-only masker fix plus the same class of build-door narrowing), so yes declared a widening that did not exist, and none sat beside a refused authorable value. Under ruling A the level follows the kind, and for this class the kind is narrowed. The dispatch here chose correctly; the precedent is for the dispatching seat to note, not for this PR to mirror. No change owed to this PR.
  • The breaking changeset carries its remedy paragraph (no key is removed or renamed, so no FROM → TO mapping is owed), and the ADR-0087 disposition names a category the gate reads.

③ Boundary flags

  • os-dev-report open_questions: [], out_of_scope_findings: []. Five deviations, each answered:
    1. Declaration lines differ from the fix(cli)!: the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps #27342 precedent — answered in ②: this PR's lines are the ones the rule produces.
    2. The PM mechanism hypothesis ("top level when present, else bodies") was falsified and inverted — answered in ④ below: the implemented rule is the right one for a manifest key, and it is the rule the runtime's registration and collectNavGroupInputs already apply.
    3. eslint run scoped to packages/cli rather than the repo sweep — CI's Lint · ESLint is green on this head; nothing owed.
    4. The unit tier, typecheck and first gate run were at 260fe77e0, before the origin/main merge — the merge touched none of the three changed files, and every derived gate family and the dogfood battery were re-run at the head; the head's check-runs are the gate verdicts in any case.
    5. check:docs-transcript-drift not derived; no content/docs/** file touched — consistent with the three-file list.
  • PR author is objectstack-fleet[bot]; no relay-author note owed.
  • Observation for the dispatching seat, not a flag on this PR: the runtime's residual rule says nothing about a packages[] artifact's top-level navigationContributions that no body carries (it registers neither them nor a warning; only ARTIFACT_FIELD_TO_TYPE collections are listed). After this PR neither door speaks for that hand-written shape. Pre-existing, out of this card's scope, worth a card under the "absence must be loud" rule if the seat judges the shape reachable.

④ Design judgment

The narrowing against the published spec on main: manifest.navigationContributions is declared as what a package injects into apps it does not own, "which the runtime merges" (manifest.zod.ts lines 971-982; ADR-0029 D7), its items are the ordinary NavigationItemSchema set and so admit type: 'doc', and the published docs say the build refuses a dead doc target with no single-package limit. The change takes no declared capability away at the consumer — every contribution the runtime registers on a multi-package artifact is now judged, and nothing the runtime registers is newly refused that the single-package path would have accepted. It makes the spec's own refusal real where the lint had not reached.

"Bodies when packages[] is present, else the top level", tried shape by shape against the composer and the loaders:

  • Single package, no packages[] — top-level read, byte-identical path. Holds.
  • Preserve, non-owner contributes — the top level has no contributions (selectManifest('last') picks the owner's); the body read finds one. Holds (tested).
  • Preserve, owner also contributes — the top-level manifest is the owner's manifest object, spread into the owner's body by assemblePackageBody; the body read judges it once, the non-owner's once. Holds (tested).
  • Preserve where the last input has no manifest — selectManifest('last') falls back to the last manifest-bearing input, and preservePackageEntries emits a body for exactly the manifest-bearing inputs; a manifest-less input cannot carry a manifest key. Nothing lost. Holds.
  • Preserve yielding one body (one of two inputs manifest-less) — packages: [X], flattened half kept (packagesCarryEveryInputCollection is false below two entries); the body read finds X's contributions, which are the top-level manifest's. Holds.
  • Nested preserve — an inner artifact's packages entries are carried untouched, and its manifest is one of its own bodies' (its 'last' pick), so the outer top-level manifest is still a body's. Holds.
  • Hand-authored input carrying packages[] and a manifest that is none of its bodies — its manifest is never assembled into a body, so a contribution written there is judged on main and not at the head. The runtime never registers it either (resolveArtifactPackageOrder answers the bodies alone; the residual rule covers collections, not manifest keys), so the head agrees with the runtime where main disagreed. Not a must-fix; the silent runtime drop is the observation in ③.
  • packages[] with a body lacking manifest, or a null entry — ArtifactPackageSchema is strict with manifest required; os build/os validate hand result.data (parsed) to collectAndLintDocs, while os lint hands normalizeStackInput(config) (map-to-array normalization and the ADR-0087 conversion pass, before Zod). artifactPackages tolerates a missing manifest (?? {}: no contributions, no throw). A null entry would throw on the unparsed os lint path, but collectAndLintDocs (declaredPackageEntries) and the function's own books walk on main already call artifactPackages over the same list before this read, so the head adds no new throw surface. Holds.
  • packages: [] — bodies.length === 0, top-level read; the same line collectNavGroupInputs draws. Holds.
  • Reconciled preserve composition (flattened half kept) — the apps walk reads the top-level union (resolveStackCollection's rule, apps being a concat collection), the contributions read the bodies (a manifest key whose top level is one package's copy). No double count, no gap; each rule matches its own runtime reader. Holds.
  • Resolution set — contributed items resolve against artifactNames (every doc the artifact carries) and against books plus package ids from the top level and every body, as the apps walk does; a non-owner's contribution may name a sibling's doc and pass, which is the cross-package reading the earlier C2 ruling made ordinary. Holds. (The untouched docblock sentence "against this package's set", line 1198 on main, is stale wording from before that ruling; not this diff's, no fix owed here.)

Findings: no must-fix, no should-fix. Two notes carried in ③ (the runtime's silent drop of a hand-written top-level contribution; the #27342 declaration precedent), neither on this diff.

Check-runs on 6c5250cbdb420485586b6b642a601ad8565149a0 at this read: every completed run is success (Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, PR-body guards, Dogfood Regression Gate, Test UI, Test Core shards 3, 6, 7, 8) or skipped (the UI, docs and opt-in lanes). Still in progress: Test Core (1/8), (2/8), (4/8), (5/8). The queue reads their conclusions, not this record.

Implemented-by: claude/issue-27343-nav-target-package-bodies
Reviewed-by: session_01L6bhgXcN9HaZYSEUdJWEjw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

维护者速读(席位终稿)

  • 改了什么:os build/os validate/os lint 的文档导航检查(docs/nav-target)在多包产物上改为读取每个包自己的 navigationContributions;只改 packages/cli/src/utils/collect-docs.ts 的 lintDocNavTargets 一处读取,外加一个测试用例和一个 changeset。
  • 为什么改:多包 preserve 产物的顶层 manifest 只是主包(最后一个包)的副本,其他包贡献到别的应用里的文档导航项从没被检查过,指向不存在文档的条目会静默通过构建、到运行时才坏。
  • 风险与回滚:一次收窄(BREAKING,minor):多包产物里指向不存在文档的贡献项从静默通过变为构建错误;单包应用不变,主包的贡献只判一次。唯一不再检查的是手写多包产物顶层那份没有任何包体携带的贡献,而运行时本来就不注册它。回滚即 revert 本 PR。
  • 先看:collect-docs.ts 中 const contributions 那一处三元读取;collect-docs.nav-target.test.ts 新用例;.changeset/27343-doc-nav-target-package-contributions.md。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/collect-docs.nav-target.test.ts,预期 10 个用例全部通过。
  • 需要你做的:无,席位落地(非受管面,51 行)。
  • 席位意见:收下。契约复审 PASS,规则与运行时注册和 collectNavGroupInputs 对同一键的做法一致;声明行比 fix(cli)!: the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps #27342 先例更对。

席位核对 — ACCEPT · domain:server#1 · head 6c5250cb · 2026-10-29T05:15Z

  • Shape: 3 files, +49/−2: packages/cli/src/utils/collect-docs.ts (+12/−2, the navigationContributions read in lintDocNavTargets plus its docblock), the pin in collect-docs.nav-target.test.ts (+19), and one minor changeset (+18). check-governed-merges --pr 27394: 0 of 3 paths governed; 51 lines, under the 3,000-line bar. No packages/spec.
  • Premise, measured: the dev falsified the card's wording ("top level when present, else bodies") with a real two-package preserve composition. When the owner contributes, the top level carries the owner's copy, so the card's rule would still skip every other package. The implemented rule is "bodies when packages[] is present, else the top level". I read it in the diff. It is the same rule collectNavGroupInputs applies to the same key, over the same artifactPackages walk. The test covers non-owner only (0 → 1 issue) and both packages (exactly 2, so the owner is not judged twice). The ablation re-reds it.
  • Gates: on 6c5250cb, 34 success and 14 skipped, none other. check-expected-skips --pr 27394: OK, all 14 skips are in main's roster. mergeable_state: clean. Dogfood battery (owed by the narrowing arm) at this head: 348 files / 2806 tests passed, 0 failed.
  • Contract review: Served-tier: CONTRACT_REVIEW_TIER, PASS on this head (6488566606). No must-fix and no should-fix, so needs:contract-review is not owed.
  • Accepted deviations:
    • The inverted mechanism (above).
    • eslint was scoped to packages/cli; CI Lint · ESLint is green on the head.
    • The unit tier and typecheck ran before the origin/main merge. That merge touched none of the 3 files, and the derived gates (60 of 60) plus dogfood re-ran at the head.
  • Declaration, read against the changeset sentence by sentence:
  • Out of scope + Skills-docs:
    • Skills-docs: none holds. skills/objectstack-ui/rules/pages.md and content/docs/ui/apps.mdx already teach the refusal with no single-package limit.
    • The review's ③ observation is not filed. It concerns a hand-written packages[] artifact whose top-level navigationContributions no body carries, which the runtime drops silently. The composer never produces that shape: selectManifest('last') is always a body's own manifest. And this PR adds no new silence, because main judged that copy but the runtime never registered it.
    • The stale docblock words "against this package's set" are a nit for the next edit of that function.
  • Landing: ready → auto-merge through the queue. cli: docs/nav-target never judges a package body's navigationContributions on a multi-package preserve artifact, so a dead doc target there builds clean #27343 closes on merge (Fixes #27343).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 05:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 05:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit ebed73b Oct 29, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27343-nav-target-package-bodies branch October 29, 2026 05:32
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Landed · domain:server#1 · 2026-10-29T05:33Z

Reading Value
PR #27394 merged at 2026-10-29T05:32Z, merge commit ebed73bd, head 6c5250cb (the ACCEPTed head)
Card #27343 closed / completed by this PR's Fixes

State change: pm:dispatched removed from #27343. No other card was closed by this PR's body.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: docs/nav-target never judges a package body's navigationContributions on a multi-package preserve artifact, so a dead doc target there builds clean

2 participants