Skip to content

docs(cli): state the package-docs search as the any-depth rule it is - #21013

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19525-cli-docs-any-depth
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19525-cli-docs-any-depth

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19525
Clause-②: no

What changed

Docs only: content/docs/deployment/cli.mdx, the ADR-0130 paragraph under os compile (one paragraph; no code, no content/docs/releases/**, no governed surface).

The paragraph described per-package docs collection as src/PKG/docs/, one level under src/. Since PR #19492 (0e671d20) the collector finds each package's directory from the packages the artifact registers, at any depth. No sentence on the page was false; it was narrower than the rule. The rewrite:

  • keeps verbatim the two sentences that still hold: the id / last-dot-segment matching, and the display name not being a directory key;
  • states the depth rule (src/PKG/docs/ and src/packages/PKG/docs/ are one case);
  • states where the search stops: it descends through directories that name no declared package (never into node_modules) and stops at the first one that names any of them. Everything under that root is the package's own source, so a deeper src/PKG/components/docs/ is neither read nor reported;
  • states the no-packages[] case: nothing to search for, so docs are read from src/docs/ alone and a one-level src/DIR/docs/ is reported, not read;
  • restates what the warning now means, as three cases: a directory naming none of the packages (an intermediate one such as src/packages/ included), a directory naming more than one, and the new refusal of one package that answers to more than one docs directory (neither is read; each warning names the others).

Each behaviour against the source, not the card

Read at origin/main a5bce40888 (packages/cli/src/utils/collect-docs.ts), then measured.

Page sentence Source Measured by
any depth; two-level layout collected packageDirectoryCandidates docblock: "derived from the packages the artifact REGISTERS rather than from a fixed depth under src/"; walk recurses only into a directory with no owners, and only when the artifact declares packages test "collects the ADR-0130 D4 two-level layout…"; probe P2 (src/a/b/orders/docs collected)
never into node_modules entry.name !== 'node_modules' on the descent condition probe P3 (src/node_modules/orders/docs: nothing collected, no issue)
stops at the first directory naming any package; deeper docs/ neither read nor reported descent requires owners.length === 0 test "stops at the package root…" (issues is []); probe P4 (an ambiguous directory stops the search too)
no packages[] ⇒ one level, reported not read no refs ⇒ no recursion; the refs.length === 0 branch emits uncollectedDocsMessage test "a stack with no packages[] gets the original sentence"; test "single-package regression…"; probe P6
id and its last dot segment, not name docsPackageRefs tests "resolves a directory named by the id TAIL…", "the package name is not a resolution spelling"
none ⇒ warning listing the declared packages, intermediate directories included owners.length === 0 message: "Declared packages: …" test "a directory naming NO package keeps its own answer at the new depth"; probe P1 (src/packages/docs reported while src/packages/orders/docs is collected)
more than one ⇒ warning naming those packages message for a directory with two or more owners test "an AMBIGUOUS directory name keeps its own answer at the new depth"
one package, more than one docs directory ⇒ neither read, each names the others locationsOf refusal in sweepPackageDocsDirectories test "ONE package answering to TWO docs directories is refused…"; probe P5 (three directories, each warning lists the other two); control P7 (a package directory without Markdown is not a second location)

The probe was a one-off tsx script over collectDocsFromSrc against temp trees, run from the built dependency closure. It is not committed.

Verification, at head 32d0a0178c

  • pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/collect-docs.package-docs.test.ts: Tests 40 passed (40) (code unchanged; this confirms the behaviours the page now states are the ones the landed tests pin).
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) derived 41 commands from the 1-path change set. All 41 were run, and --ran answered 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED. They include pnpm check:doc-authoring (exit 0), pnpm check:docs-transcript-drift (exit 0: "4 declared transcript value(s) across 408 page(s)… equal what the registry derives today"), pnpm check:nul-bytes (exit 0) and pnpm --filter @objectstack/spec run check:docs (exit 0).
  • check:skill-examples first exited 3 (PREREQUISITE NOT MET: no client-react declarations). After building @objectstack/client-react... and @objectstack/client... under the verify lock it exited 0 ("259 prose examples type-check across 3 surface(s)").
  • MDX compile of the page with @mdx-js/mdx 3.1.1: compiles at HEAD and at base.
  • NOT MEASURED locally: CI's Build Docs job (ci.yml), which this path schedules. It is left to CI.

Changeset: none. content/docs/** ships in no package's files[] (apps/docs is private), so the PR takes the skip-changeset label.

Acceptance notes

  • Two rows of the card's table, corrected by measurement. Row 3 says "the warning is what happens when the search finds nothing". In fact every directory the search passes through is a candidate in its own right: src/packages/docs/*.md is reported even while src/packages/orders/docs/ is collected beneath it (probe P1). Row 4 merges two cases. A directory name matching more than one package (named in the warning by package) is a different case from the new refusal of one package answering to more than one directory (named in the warning by directory). The page states both separately.
  • Silent by design, now stated on the page. Markdown under a resolved package's subtree, under node_modules, or under an ambiguous directory's subtree is neither collected nor reported. With no packages[], a docs/ two levels down is not reported either. This is the ruled design (packageDirectoryCandidates property 1) and is pinned by tests. The module header's "Absence" bullet ("Markdown under a DIR/docs/ that no registered package claims is … REPORTED") reads wider than that; the function docblocks are exact. Comment precision only, not filed. Carrier: none.
  • doc-pages.mdx, skills/objectstack-ui/rules/pages.md and ADR-0046 are untouched: the card records them as a separate, older gap.

Generated by Claude Code

claude added 2 commits October 1, 2026 01:22
The ADR-0130 paragraph in deployment/cli.mdx described per-package docs
collection as one level under src/. The collector now finds each package's
directory from the packages the artifact registers, at any depth: restate the
depth rule, where the search stops, the no-packages case, and the three cases
that end in the uncollected-directory warning (none, more than one, and one
package answering to more than one docs directory).

Claude-Session: https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9
Co-authored-by: Claude <noreply@anthropic.com>
"stops at the first directory that names one" read as one package only; a
directory naming two declared packages stops the search as well, so say
"any of them". Reflow the paragraph.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 32d0a0178cc8836da7dabaa25a3194a7d8d37bc4
Local-runs: none

PR #21013 (card #19525), head 32d0a0178cc8836da7dabaa25a3194a7d8d37bc4, two commits over merge-base a5bce40888. Net diff: one file, content/docs/deployment/cli.mdx (+24 / -5), one hunk (@@ -627,17 +627,36 @@), the ADR-0130 docs-collection paragraph under os compile. Every behavioural sentence below is judged against packages/cli/src/utils/collect-docs.ts, artifact-packages.ts, stack-collections.ts and the two collector test files as they stand on origin/main 9b0de7de73; those paths and the page are byte-identical between the merge-base and origin/main (the diff-stat over them is empty), so the base the dev read is the base this record reads. Nothing was built, run or re-run; the check-runs on the head are the gate verdicts.

① Derived judgments

Accept set and public surface: no code, schema, export or CLI behaviour moves. The published surface that moves is the page's description of what os compile / os build collects, so each sentence is held to the shipped collector, not to the PR body or the card:

  1. A PKG/docs/ directory whose PKG names a declared packages[] entry is read into that package's own body — RIGHT. sweepPackageDocsDirectories collects the single-owner branch into packageDocs keyed by package index; attachPackageDocs writes packages[i].manifest.docs and never the top level (pins: "attributes a directory named by the last segment of the package id, with its pedigree", where top-level docs equals []).
  2. The step line 12 collected (4 from 2 package directories) (kept, re-verified) — RIGHT. compile.ts:884-887 prints N collected and appends (M from K package director(y|ies)) only when M is above zero; K is packageDocs.length, one set per directory read. Pinned by packages/cli/test/build-package-docs-attachment.e2e.test.ts:182.
  3. Found from the packages the artifact registers, at any depth under src/; src/PKG/docs/ and src/packages/PKG/docs/ are one case — RIGHT. packageDirectoryCandidates walks from src with no depth bound (walk(srcDir, 'src')), and docsPackageRefs is the only source of names (pins: "collects the ADR-0130 D4 two-level layout and attributes it to the right package"; "lit control: the flat layout is collected exactly as before, beside a two-level one").
  4. The search descends through every directory whose name names none of the declared packages, never into node_modules — RIGHT. The descent condition at collect-docs.ts:467 is exactly three conjuncts: owners.length === 0, refs.length above zero, and entry.name !== 'node_modules'. One unstated exclusion, noted and not faulted: a directory named docs is dropped from children at :457, so it is neither a candidate nor descended into. Under the contract the paragraph states, a docs/ directory is the flat leaf being looked for, not a place packages live, so the omission cannot mislead a reader.
  5. Stops at the first directory that names any of them; everything under it is that package's own source; a deeper src/PKG/components/docs/ is neither read nor reported — RIGHT. Descent requires owners.length === 0, so one owner and several owners both stop it; a package root's subtree is never walked (pin: "stops at the package root — a docs/ deeper inside a resolved package is not a second one", issues equal []). "Names any of them" correctly covers an ambiguous directory, which stops the walk under the same condition.
  6. A stack with no packages[] reads from src/docs/ alone; a src/DIR/docs/ one level down is reported, not read — RIGHT. With refs.length === 0 there is no descent, so candidates are src/'s direct children; the refs.length === 0 branch emits uncollectedDocsMessage, byte-identical to the pre-cli: read package docs from each package directory under an ADR-0130 layout — the widening half of #18170, blocked on two contract questions #18431 sentence (pins: "a stack with no packages[] gets the original sentence, unchanged"; "single-package regression: a stack with no packages[] is walked ONE level and reports nothing deeper", issues equal []).
  7. PKG is matched against the package id and the last dot-separated segment of that id; the display name is not a directory key (kept verbatim) — RIGHT. docsPackageRefs adds exactly those two spellings, and only when manifest.id is a non-empty string; neither name nor namespace is a spelling (pins: "derives exactly two directory spellings per package"; "the package name is not a resolution spelling"; "namespace is not a resolution spelling").
  8. Docs the search finds but cannot attribute are still not read, and each such docs/ directory is reported in a warning naming the files it skipped, never a silent 0 collected — RIGHT. All three with-packages messages and the no-packages message end in Found: plus the file list; each is severity: 'warning', so the build stays green and loud.
  9. Case 1: a directory the search passes through, src/packages/ itself included, naming none of the declared packages; the warning lists the declared packages — RIGHT. The zero-owner message carries Declared packages: plus the ids. Every walked directory is pushed as a candidate before the descent decision (:463), and candidates are filtered only on holding at least one .md file in their docs/, so src/packages/docs/x.md is reported while src/packages/orders/docs/ is collected beneath it. That intermediate case is not pinned on origin/main; it is read straight off the walk and the sweep and agrees with the dev's probe P1.
  10. Case 2: a directory naming more than one declared package; the warning names those packages — RIGHT. The multi-owner message lists the owner ids (pins: "an AMBIGUOUS directory is reported and never guessed"; "an AMBIGUOUS directory name keeps its own answer at the new depth").
  11. Case 3: one package answering to more than one directory with Markdown in its docs/; neither is read, nothing is merged, each warning names the others — RIGHT. locationsOf is built over candidates after the Markdown filter and only for single-owner candidates; more than one location takes the refusal branch (continue before compileDocsDirectory) and prints (also …) with every other location (pin: "ONE package answering to TWO docs directories is refused — never merged, never silently dropped"). "With Markdown in its docs/" is the exact operand: an owned directory with an empty or absent docs/ is not a location.

Removed or narrowed text — nothing true is lost. The old closing sentence (none / more than one is still not read; the warning names the files it skipped and the spellings it tried; never a silent 0 collected) is carried forward whole: "still not read", "names the files it skipped" and "never a silent 0 collected" survive verbatim; "none" and "more than one" became bullets 1 and 2; "the spellings it tried" became "lists the declared packages" and "names those packages", which is what the two messages print (the zero-owner message also restates the id / last-segment rule, and the page states that rule in the sentence immediately before). src/PKG/docs/ in the opening sentence became PKG/docs/, bounded two lines later by "at any depth under src/" — a generalisation, not a loss, and the bound is right because the walk starts at src/ and nowhere else.

Over-claims — none found. Every sentence of the new paragraph names behaviour the collector performs. The page says nothing about os validate / os lint / os dev, which reach the same collectAndLintDocs seam, so nothing there is contradicted.

The card's table had two rows the collector does not support (row 3: the warning is not only "when the search finds nothing" — an intermediate directory's own docs/ is reported beside a collected package; row 4 merged the ambiguous-directory case with the one-package-several-directories refusal). The page follows the collector, not the card, and states the two cases separately. Correct.

Page gates: the placeholders stay inside inline code throughout; the new bullet list is set off by a blank line. On the head, Build Docs is success, Check Documentation Links is success, and Lint & Repo Gates (which carries check:doc-authoring, check:docs-transcript-drift and check:nul-bytes) is success. No page was added, so no meta.json edit is owed.

② Semver level

skip-changeset is the correct declaration and Clause-②: no is the correct line. The diff publishes nothing from any released package: content/docs/** appears in no package.json files[] on origin/main, and its only consumer, apps/docs (@objectstack/docs), is "private": true. No .changeset/*.md is in the diff and none is owed (AGENTS.md Post-Task step 3: the label is for a diff that publishes nothing from any released package). No accept set moves — the collector is untouched — so Clause ② is no with neither arm; the PR body carries Clause-②: no on its second line, matching the claim comment. Check Changeset is success on the head.

Check-runs on the head: 36 — 26 success, 10 skipped, 0 failure. The skipped runs (Build Core, Temporal Conformance (live PG + MySQL), Console Pin Gate, Dogfood Verify CLI, the Dogfood Regression Gate matrix shards, both opt-in packed-tarball smokes, and one duplicate run each of Check PR Size, Check Changeset and Auto Label) are path-filtered by the filter job (success) for a docs-only diff. Of the seven queue contexts, Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate and Governed Surface Queue Guard are success; Build Core and Temporal Conformance are skipped by that filter. No red of any signature, so the merge-base exception is not needed.

Governed surfaces: none in the file list. content/docs/** is a review face, which is why this record exists; it is not a landing-tier surface.

③ Boundary flags

deviations: none declared. open_questions: none declared.

out_of_scope_findings, one entry, carrier none — answered, not escalated. The collect-docs.ts module header's "Absence" bullet (:25-28: Markdown under a DIR/docs/ that no registered package claims "is REPORTED rather than passed over") reads wider than the code. Confirmed on origin/main: four shapes are passed over silently — a resolved package's subtree (pin "stops at the package root", issues equal []); everything below one level when there is no packages[] (pin "single-package regression", issues equal []); the node_modules subtree (:467); and an ambiguous directory's subtree (same condition). The function docblocks (packageDirectoryCandidates, properties 1 and 2) state the exact rule, and the behaviour is the ruled design (batch #204 item 5, letter B), so this is a header comment that over-describes — not a reproducible defect, not a contract violation, not an authoring trap. AGENTS.md Prime Directive 10 puts it in acceptance notes, where the PR carries it, and PR #19492's own acceptance notes already record the deeper-docs case as noted and not filed. No card is owed. The header lives in packages/cli/**, outside this PR's dispatched file surface, so it is correctly left alone here; the next PR that edits that header should tighten the bullet to the two properties.

Dispatch surface check: the claim (5922744522) bounds the diff to the ADR-0130 paragraph of content/docs/deployment/cli.mdx; the single hunk sits entirely inside that paragraph. Held. doc-pages.mdx, skills/objectstack-ui/rules/pages.md and ADR-0046 are untouched, as the card records them as a separate, older gap.

The dev report's tests line names a local vitest run and a one-off probe over temp trees; neither is a gate the head's check-runs left unanswered (Test Core is success), and nothing in this record rests on the report's prose.

Implemented-by: claude/issue-19525-cli-docs-any-depth
Reviewed-by: session_01JAhu8u8QfBvRjVZDox7CP9

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 02:05
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 02:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 173b68e Oct 1, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19525-cli-docs-any-depth branch October 1, 2026 03:23
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 skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants