Skip to content

fix(cli)!: the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps - #27342

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-cgmcnw
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-cgmcnw

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Requested by Bill · project thread

Fixes #27210

Clause-②: yes (narrowing) — on a multi-package composeStacks(…, { manifest: 'preserve' }) artifact, a type: 'doc' navigation item inside an app a package body carries, 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: none
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 multi-package artifacts meet that text.

维护者速读

  • 改了什么:修好 cli 普查测试 normalized-call-sites 的代码遮罩 codeOnly:嵌套模板字符串和正则字面量不再让它"走丢位置";修好后普查新看见一处读取 stack.apps(os build/os validate/os lint 的文档导航检查),顺手把它改为在多包产物上读各包自己的 apps,并补一条普查分类行。
  • 为什么改:旧遮罩遇到嵌套模板或含引号的正则就把后面的真代码当字符串抹掉,普查因此对看不见的读取答"绿"。多包 preserve 产物顶层没有 apps,所以包里 app 的文档导航项(指向不存在的 doc/book)从没被检查过。
  • 风险与回滚:测试改动零运行时风险。源码改动是一次收窄(BREAKING,patch 级,同 fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204 先例):多包产物里"包内 app 指向不存在文档"的导航项从静默通过变为 docs/nav-target 构建错误(单包应用不变;仓内示例零命中;dogfood 全绿)。回滚即 revert 本 PR。
  • 先看:packages/cli/test/normalized-call-sites.test.ts 的 codeOnly;packages/cli/src/utils/collect-docs.ts 中 lintDocNavTargets 的 apps 一行;.changeset/27210-doc-nav-target-package-apps.md。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/normalized-call-sites.test.ts src/utils/collect-docs.nav-target.test.ts,预期全部通过。
  • 需要你做的:无,席位落地。

What changes

The census masker (codeOnly, test only). It scanned quotes linearly, so two shapes made it lose its place and blank real code after them:

  • a template literal nested inside another's interpolation (the did-you-mean suffix of the doc refusals);
  • a quote or backtick inside a regex literal (for example the quote-stripping regex near the top of collect-docs.ts).

The masker now tracks template interpolations with a per-hole brace count, so the code inside an interpolation is code at any depth. It also recognises a regex literal by the code character before its / and masks the regex body. Template text, string bodies, comments and regex bodies are blanked; offsets, quotes and newlines are kept, so the bracket-read path still reads keys back from the source. Both header paragraphs that stated the old bound ("a site inside a template literal's interpolation is blanked too") now state the new one.

What the fixed masker sees on the real tree (origin/main ebfadb02). Exactly one new read, utils/collect-docs.ts :: stack.apps in lintDocNavTargets. A nested template literal hid it. The card's four reads are classified as follows:

Read On origin/main ebfadb02 Classification
stack.objects, stack.flows, stack.permissions Gone. #27204 replaced them with collection(key) over the doc owner plus its declared dependencies in lintMetadataEmbeds. Not a top-level member read any more. A row would fail the census's own "no row is stale" test.
stack.books Live, seen by both maskers. Existing top-level row (added by #27204). Its why now names the measured cause.
stack.apps (new) Live. A nested template hid it. Source fix plus a top-level row, see below.

The one source change (collect-docs.ts, declared). lintDocNavTargets walked only the top level's apps. A multi-package composeStacks(…, { manifest: 'preserve' }) artifact keeps its apps in the package bodies, so a type: 'doc' nav item in a package's app was never judged. It now walks the top level's apps when the stack carries the key, otherwise each package body's (artifactPackages(stack), the same walk the function already uses for books). That is resolveStackCollection's rule. The core helper itself is not used because its whole-entry parse throws on a duplicate-id artifact, and collect-docs.flat-docs-owner.test.ts pins that collectAndLintDocs does not throw on that shape. The census row for stack.apps is top-level, with that reason. Single-package stacks are unchanged. No in-tree example carries a type: 'doc' nav item.

Tests

  • normalized-call-sites.test.ts: two new cases. The first covers a nested template followed by a package-owned read, reads inside interpolations (with braces, strings and a nested template), a regex literal holding quotes and a backtick, a division that must not read as a regex, and a quoted ${. The second checks the exact masked output of a nested template and a regex.
  • collect-docs.nav-target.test.ts: one new case on a real two-package preserve composition. It asserts that the top level carries no apps, that a resolving doc passes, and that a dead doc target in the package body's app is refused at its path.

Acceptance notes

  • The card's mechanism, measured. On the pre-fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204 collect-docs.ts (39fd5fbf), a masker that fixes nested templates alone still hides all four reads. The regex literal is what flipped the parity there. Templates plus regex reveals all four, and also stack.apps. On origin/main, the nested template alone hides stack.apps.
  • Exactness, measured once. The new masker's output was compared with a TypeScript-scanner reference over all 179 scanned packages/cli/src files: 0 files differ. The origin/main masker differs on 140 files, mostly the interpolations it blanked by design. A templates-only fix still differs on 12 files, all at regex literals. This was a one-time scratch comparison; no permanent test file was added.
  • Declaration lines. The seat ruled the newly reached refusal a narrowing, following fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204: the changeset carries the fix(cli)! title, the Clause-②: yes (narrowing) line, the BREAKING banner and the ADR-0087 not-required (no-migration-prescription) disposition, at patch. check-adr-0087-registration reads it as BREAKING+bang+clause-②-narrowing.
  • Sibling, not fixed here. lintDocNavTargets reads navigationContributions off the top-level manifest only. On a preserve artifact, a package that does not own the artifact manifest carries its contributions in its body, and a dead doc target there is not judged. Measured: a single-package stack returns one apps/setup/navigation/nav_help issue; the same contribution on the non-owner package of a two-package preserve composition returns none. The census cannot see this read (a member chain off an envelope key). Reported to the seat.

Verification

Test readings are at head 47f1a12a; the dogfood battery and the gate re-run are at head 1139c31f (the changeset declaration only, on top of 47f1a12a), both on origin/main ebfadb02.

Red, then green: the masker

Old masker with the new cases, at ebfadb02 plus the uncommitted cases: Tests 2 failed | 11 passed (13).

  • keeps its place past a nested template literal and a regex literal: expected [] to deeply equal [ 'stack.objects' ]
  • masks a nested template exactly: the received output shows the interpolation blanked.

The census's own "no read is unclassified" stayed green over the old masker: that is the false green.

New masker: the masker cases pass, and the census goes red with exactly one unclassified read, utils/collect-docs.ts :: stack.apps.

Red, then green: docs/nav-target

Before the source change, the new case gave Tests 1 failed | 8 passed (9), with expected [] to deeply equal [ [ 'docs/nav-target', … ] ].

After it: normalized-call-sites.test.ts plus src/utils/collect-docs* gave Test Files 5 passed (5) · Tests 126 passed (126).

Package and repo checks
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 342 passed, Tests 4961 passed | 29 skipped. Two pin files failed only because packages/cli was not built (./dist/index.js is absent). After pnpm --filter @objectstack/cli build, both pass: Test Files 2 passed (2) · Tests 29 passed (29). The integration tier is left to CI.
  • pnpm --filter @objectstack/cli typecheck: exit 0, and check:test-typecheck OK.
  • pnpm lint: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 61 families derived. All were run. 60 exit 0. pnpm check:dual-build-cjs-loads exits 3 with PREREQUISITE NOT MET (it needs a full workspace build), so it is NOT MEASURED locally and left to CI. check:i18n, check:i18n-coverage and check:i18n-walk-parity exit 0 after the cli build. check:registry-log-declared exits 0 on a rerun; its first run hit an ENOENT on a temp file the concurrent cli unit run was writing.
  • dispatch-gates --ran: 61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED.
  • Re-run at 1139c31f: 61 derived, 60 exit 0, check:dual-build-cjs-loads exit 3 (NOT MEASURED, left to CI); --ran: 60 run, 1 NOT-MEASURED, 0 UNRUN. check-adr-0087-registration --base origin/main: exit 0, one declared-breaking changeset carrying its disposition. Census plus src/utils/collect-docs*: Test Files 5 passed (5) · Tests 126 passed (126).
Dogfood battery (owed by the narrowing arm)

At 1139c31f, 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

…d regex literals

codeOnly scanned quotes linearly, so a template literal nested in another's
interpolation, or a quote inside a regex literal, flipped its parity and it
blanked the code after it. A template's interpolations are now code at any
depth and regex bodies are masked; fixtures pin both, red on the old masker.

Claude-Session: https://claude.ai/code/session_01Utp6CXc5hequf76uJfzrdK
Co-authored-by: Claude <noreply@anthropic.com>
The fixed census masker saw `stack.apps` in lintDocNavTargets, hidden until
now behind a nested template literal. On a multi-package preserve artifact
the top level carries no apps, so a dead doc or book target in a package's
app was never judged. Walk the top level's apps when present, otherwise each
package body's, and classify the read with a top-level census row.

Claude-Session: https://claude.ai/code/session_01Utp6CXc5hequf76uJfzrdK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

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 56 of 208 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 8eeb3b3b9255686046a361cf9f9145d6b8e6bfec → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8eeb3b3b9255686046a361cf9f9145d6b8e6bfec

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

…a narrowing

The seat ruled the newly reached refusal a Clause-② narrowing, following
the docs/metadata-embed-ref precedent: breaking title, the shape that built
before and is now refused, the ADR-0087 not-required disposition, patch.

Claude-Session: https://claude.ai/code/session_01Utp6CXc5hequf76uJfzrdK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(cli): the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps fix(cli)!: the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

席位核对 — ACCEPT · domain:server#3 · session_01Utp6CXc5hequf76uJfzrdK · head 1139c31f · 2026-10-28T22:17Z

  • Shape: 4 files, +139/−10: the census test's codeOnly masker (packages/cli/test/normalized-call-sites.test.ts), packages/cli/src/utils/collect-docs.ts (+9/−2, lintDocNavTargets apps walk), its pin collect-docs.nav-target.test.ts, one patch changeset. No governed surface, no packages/spec, under the 3,000-line threshold. The src edit is the order's declared conditional (a hidden read proved a real unclassified read).
  • Ruling, held in the diff: triage 6467195085 (⛔ no allowlist). No allowlist row was added; the census now sees past nested templates and regex literals (the measured cause on the pre-fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204 file was a regex literal holding a quote), and the one live hidden read (stack.apps) is fixed in source with a top-level row whose why names the rule. stack.objects/flows/permissions were already rewritten by fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204 on main; stack.books already had a row (its why now names the measured cause).
  • Tests read: the masker fixture covers a nested did-you-mean template, braces and strings inside ${…}, a nested template in a hole, a regex with quotes/backtick, a division that is not a regex, and ${ inside a quoted string; red 2 on the old masker. Exactness compared once against a TypeScript-scanner reference over all 179 scanned cli files (0 differ; old masker 140). The nav-target pin is red on base, green after.
  • Declaration (seat call B, fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204 precedent 91a4036a): fix(cli)!, Clause-②: yes (narrowing) — a dead doc/book nav target in a package body's app on a multi-package preserve artifact built clean and is now refused; Metadata-protocol: none; patch; ADR-0087 disposition present (check-adr-0087-registration exit 0). Skills-docs: none — skills/objectstack-ui/rules/pages.md and content/docs/ui/apps.mdx already teach the refusal with no single-package limit.
  • Dogfood battery (owed by the narrowing arm): @objectstack/dogfood at 1139c31f, 348 files / 2806 tests passed, 0 failed.
  • Gates: on 1139c31f 36 success, 16 skipped, none other; mergeable_state: clean.
  • Out of scope: package-body navigationContributions are not judged either → filed 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.
  • Contract review owed: the narrowing face is CONTRACT_REVIEW_TIER (contract-review.md); no record on this head yet → needs:contract-review on the PR.
  • Landing: after a same-form PASS on this head: ready → auto-merge through the queue.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 1139c31f460731decb0dc8419a9b6d9e531067ea · 2026-10-29T00:46Z

Triage seat, taking needs:contract-review on this head (#27062 (a)). The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1139c31f460731decb0dc8419a9b6d9e531067ea
Local-runs: none
Scope: packages/cli/test/normalized-call-sites.test.ts, packages/cli/src/utils/collect-docs.ts, packages/cli/src/utils/collect-docs.nav-target.test.ts + packages/cli/src/utils/artifact-packages.ts, packages/core/src/stack-collections.ts (resolveStackCollection), packages/cli/src/utils/stack-collections.ts (packageOwnedCollectionKeys), packages/spec/src/stack.zod.ts (composeStacks steps 3a and 8, the manifest: 'preserve' option docblock), packages/spec/src/ui/app.zod.ts (DocNavItemSchema), packages/cli/src/commands/compile.ts, packages/cli/src/commands/validate.ts, packages/cli/src/commands/lint.ts (the three collectAndLintDocs doors), packages/cli/src/utils/collect-docs.flat-docs-owner.test.ts, skills/objectstack-ui/rules/pages.md, content/docs/ui/apps.mdx, docs/adr/0130-release-artifact-as-co-ownership-boundary.md (2026-09-22 addendum). The changeset is the prose face: read for ②, not reviewed.

Inputs read: card #27210 body and all 4 comments (triage 6467195085, claim 6479705900, os-dev-report 6480884619, addendum 6482045655); PR #27342 body, file list (4 files, +139/−10), net diff vs main at the head; the 57 check-runs on 1139c31f (37 success, 20 skipped, 0 pending, 0 failure; all on this head); precedent PR #27204 (merge 91a4036a) and its changeset 27090-embed-ref-package-scope.md, and sibling card #27343, both by gh api GET. Tree read with git show prr-27342:… and origin/main; nothing checked out, built or run.

① Derived judgments

  1. Accept-set change (the one the PR declares) — right. lintDocNavTargets (collect-docs.ts L1183–1185) now reads stack.apps when the top level carries an array, otherwise artifactPackages(stack).flatMap(body.apps). On a multi-package composeStacks(…, { manifest: 'preserve' }) artifact the composer deletes every package-owned key from the top level (stack.zod.ts step 8: for (const key of packageOwnedCollectionKeys()) delete artifact[key] once ≥2 packages carry and reproduce the collections), so a type: 'doc' nav item in a package body's app was never walked and is now judged. All three doors hand the un-folded stack to collectAndLintDocs (compile.ts L992 and validate.ts L759 pass result.data; lint.ts L1029 passes normalizeStackInput(config), not the authoringRuleUnionStack fold, which lintConfig applies internally), so the refusal is newly reached on os build, os validate and os lint alike — the changeset's three doors are the right three. Confirmed by the new pin: composed('crm_lead_guide').apps is undefined on the real composer, and the dead target is refused at apps/crm_app/navigation/nav_page (Test Core green on the head).
  2. Unchanged set — right. A stack with no packages[] takes Array.isArray(stack.apps) exactly as before; a stack whose top level carries apps (single-package, or an additive-shape composition where step 8's three conditions fail) is judged exactly as before because the top-level array wins, which is resolveStackCollection's rule verbatim (core/stack-collections.ts L167–171: top array first, bodies only when the key is absent).
  3. Resolution sets — right. A package app's doc resolves against artifactNames (every doc the artifact carries, body and disk docs included, collectAndLintDocs L1420–1426, the cli: read package docs from each package directory under an ADR-0130 layout — the widening half of #18170, blocked on two contract questions #18431 C2 ruling), its book against top-level books ∪ each body's books ∪ package ids — the same candidate sets a single-package app's items already used. No new resolution rule is invented.
  4. No public-surface change — right. lintDocNavTargets's signature, the DocIssue shape, rule id docs/nav-target, message text and path spelling are untouched; packages/spec and every governed path are untouched (Governed Surface Queue Guard: success). Metadata-protocol: none is right: no key an author can write is added, removed or re-meant.
  5. Test tooling (codeOnly) — right, no accept set. The masker now blanks template TEXT only (interpolations are code at any depth via a per-hole brace count), masks regex-literal bodies and decides regex-vs-division by the last code character. Branch order read: // and /* */ are taken before the regex branch, so a // after an operator never enters the regex scan. The census's own no read is unclassified test now sees utils/collect-docs.ts :: stack.apps, and the new top-level row makes it classified; the no row is stale test accepts it because the read is live at L1183. The why edit on the stack.books row changes classification nothing (still top-level, still correct: L1135 reads the top-level half of a union on purpose).
  6. Card premise, re-judged on the tree — right. grep stack.(objects|flows|permissions|books|apps) over collect-docs.ts at the head returns only L1135 (stack.books) and L1183–1184 (stack.apps): the three reads the card named are gone on main (fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204), so no row for them is the correct outcome and an added row would have failed no row is stale.

② Semver level

  • .changeset/27210-doc-nav-target-package-apps.md: '@objectstack/cli': patch, title fix(cli)!: …, line-initial Clause-②: yes (narrowing) — … naming the exact shape (multi-package preserve, package-body app, dead doc/book, three doors), Metadata-protocol: none, one adr-0087: not-required (no-migration-prescription) marker, BREAKING banner with before / now / unchanged / remedy. The PR body carries the same two declaration lines plus Skills-docs: none.
  • Level matches what the diff publishes: AGENTS.md §Post-Task Checklist 3 — "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". Metadata-protocol: none ⇒ patch; (narrowing) ⇒ BREAKING !. yes (narrowing) is a well-formed arm of the closed pair (no (widening) is the malformed one). Form is byte-for-byte the fix(cli)!: docs/metadata-embed-ref judges a package doc against its package and declared dependencies #27204 precedent (fix(cli)!, patch, yes (narrowing), same disposition marker), which merged as 91a4036a.
  • Clause-②: line judged: the arm is correct. This is a narrowing, not a no: a concrete authorable shape that built clean on main (ebfadb02) is refused at the head. The dev's open question (A no vs B narrowing) was ruled B by the seat and applied in 1139c31f; the ruling is the right one by the AGENTS.md definition and the precedent.
  • Gate verdicts on the head: Check Changeset ×3 success; Lint & Repo Gates (the check-adr-0087-registration family, lint.yml) success; Governed Surface Queue Guard success.

③ Boundary flags

  • open_questions[0] (declaration no vs narrowing) — answered: seat ruled B; 1139c31f carries it in the changeset and the PR body; verified in the diff and the body. Closed.
  • Deviation: the mechanism was a regex literal, not only a nested template — answered: the masker fix covers both shapes and both are pinned (keeps its place past a nested template literal and a regex literal, masks a nested template exactly); the test-file header paragraphs state the new bound. The card's Expected ("code inside ${…} is scanned as code at any depth") is met. Closed.
  • Deviation: card premise partly stale (three reads gone, stack.books already rowed) — answered on the tree (① item 6). Closed.
  • Deviation: packages/cli/src edit under the dispatch conditional — answered: the claim (6479705900) permitted it "only if classifying a hidden read proves a real unclassified read must change in source; it is declared in the report"; stack.apps is exactly that, +9/−2, declared. Triage's "no allowlist" (6467195085) is held: no allowlist entry was added; the top-level row is a classification with a measured why, which is the census's own mechanism. Closed.
  • Deviation: declaration lines first copied as no, then rewritten — superseded by the addendum head. Closed.
  • Deviation: stray /lint-files.log on the dev container root — not in the tree (file list is 4 paths); no action for this review. Closed.
  • Deviation: commit trailers / PR body PATCHed by the dev at the seat's direction / worktree upstream — process notes with no bearing on the contract; none touches the tree. Closed.
  • out_of_scope_findings[0] (a non-owner package's manifest.navigationContributions on a preserve artifact are not judged) — escalated: filed as 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 (open, awaiting triage). Not fixed here, correctly: the census cannot see it and it is a separate reader. The changeset's "Unchanged" paragraph does not over-claim it.
  • Skills-docs: none — verified on main: skills/objectstack-ui/rules/pages.md L355–356 ("os build refuses a target the package lacks (docs/nav-target)") and content/docs/ui/apps.mdx L246–248 ("os build, os validate and os lint refuse a doc item whose book or doc names nothing in the package") state the rule with no single-package limit. Nothing to update; the Docs Drift Check on the head listed nothing. Closed.
  • Dogfood battery (owed by the narrowing arm) — on the head, Dogfood Regression Gate (1..3/3) and Dogfood Verify CLI are skipped by design (ci.yml L2318: the shards run outside pull_request / merge_group events only; the bare Dogfood Regression Gate context counts zero attestations on a PR and is success). CI therefore did NOT run dogfood on this head; the only dogfood reading is the dev's self-reported local run (348 files / 2806 tests, 0 failed, addendum 6482045655). Stated as such, not adopted as a gate verdict. No pending check: 57/57 completed.
  • Author — PR user.login is objectstack-fleet[bot]; no relay-stripped login to name.

④ Design judgment

The narrowing, read against the published spec on main:

  • It makes the spec's own refusal real; it takes no declared capability away. DocNavItemSchema (app.zod.ts L757–760) declares: "Whether the named book / doc EXISTS is a package-level question … checked by the CLI's docs publish lint (docs/nav-target), the one step that holds the complete doc set." The skill and the docs page (③ above) teach the same refusal with no artifact-shape limit. The preserve option (stack.zod.ts L5826–5840) declares the collections "live in packages[i].manifest and nowhere else" and that the maintainer ruled this "READERS FIRST: every reader resolves packages[] before this emitter stopped feeding the flat copy"; ADR-0130's 2026-09-22 addendum names the failure mode exactly: "a reader nobody enumerated, and it is SILENT: nothing throws, the collection is simply absent. ⛔ A missed reader is a new reader card". lintDocNavTargets's apps read was such a missed reader. The capability the consumer loses — shipping a package app whose doc item opens nothing — was never declared; the spec declared the opposite. So this is the implementation gap being implemented, not a narrowing at the consumer of something declared.
  • The rule chosen is the published one. Top array wins, bodies only when the key is absent, is resolveStackCollection verbatim; it is also why the additive shape is not double-walked. Using artifactPackages instead of calling resolveStackCollection is defensible on the cited code: resolveStackCollection → stackPackageBodies → resolveArtifactPackageOrder, which parses every entry whole and refuses DUPLICATE_ARTIFACT_PACKAGE (core/artifact-packages.ts L24, L339), while collect-docs.flat-docs-owner.test.ts L225–231 and L339–349 pin that a two-entries-same-id artifact flows through docsPackageRefs / the build without throwing. The books walk three lines above already uses artifactPackages, so one function now reads bodies by one walk. Cost: positional rather than topological body order, which changes issue ORDER only, never the verdict.
  • Tried to break it:
    • Top-level apps present as an empty array with apps in bodies (hand-written artifact): bodies are not walked. Same answer resolveStackCollection gives every other reader; by design, not a finding.
    • Non-array packages reaches artifactPackages as []: collectAndLintDocs L1429 already runs declaredPackageEntries(stack.packages) on the same stack before the call, so a malformed value is refused upstream; and the pre-existing books walk had the same shape. Not a finding.
    • Per-package docs/nav-target issues are not passed through underPackage(...) and the path is apps/APP/navigation/ITEM: two packages carrying same-named apps would yield indistinguishable paths. Pre-existing for the books half; app-name duplication is itself a distinct defect. should-fix (low): prefix the body-sourced issues the way underPackage prefixes per-package doc issues, or fold into 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's follow-up.
    • regexMayStart treats + and - as regex openers, so n++ / d … / m on one line would blank d … as a regex body; ) always divides, so if (x) /'/.test(s) would leave a quote in code. The docblock states the operator/keyword bound but not these two. The dev's 179-file comparison against a TypeScript scanner (0 diffs) is a one-time scratch reading, not pinned. Test-tooling only; the census is a positive-control test, and the stated bounds section is where such a limit belongs. should-fix (low): name the ++/-- and )-before-regex bounds in the header's bounds paragraph.
    • Sibling gap (navigationContributions off the top-level manifest only): real, measured by the dev, filed as 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; the changeset's Before/Now/Unchanged text claims nothing about contributions. Not a must-fix for this head.
  • Must-fix: none.

Implemented-by: claude/project-thread-cgmcnw
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli census: normalized-call-sites codeOnly loses its place in a nested template literal and hides package-owned reads (false green)

2 participants