You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6d7634a
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli)!: the census masker sees past nested templates and regex literals; docs/nav-target judges package-body apps (#27342)
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKURKuRGF1ewEVVZKx7U1i4)_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 级,同 #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-#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 #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`.
<details><summary>Red, then green: the masker</summary>
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`.
</details>
<details><summary>Red, then green: docs/nav-target</summary>
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)`.
</details>
<details><summary>Package and repo checks</summary>
- `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)`.
</details>
<details><summary>Dogfood battery (owed by the narrowing arm)</summary>
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.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CXc5hequf76uJfzrdK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli)!: `docs/nav-target` judges the apps a package body carries on a multi-package artifact
6
+
7
+
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`).
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type of the package's public surface changes: the build-time docs lint now reaches the apps a package body carries, judging them by the rule it already applies to a stack's own apps. A refused nav item is fixed in the app's navigation; there is nothing for a migration to rewrite. -->
11
+
12
+
**BREAKING** accept-set narrowing in the build-time docs lint (`os build`, `os validate`, `os lint`).
13
+
14
+
-**Before.** The `docs/nav-target` check walked only the artifact's top-level `apps`. A multi-package `preserve` artifact keeps its apps inside the package bodies and has none at its top level, so a nav item in a package's app was never judged: a dead `doc` or `book` target built clean and degraded at runtime.
15
+
-**Now.** The check walks the top level's apps when the stack carries the key, otherwise each package body's, the same rule `resolveStackCollection` applies. A package app's `doc` item resolves against every doc the artifact carries, and its `book` item against the declared books and package ids, as a single-package app's always did.
16
+
-**Unchanged.** A stack with no `packages[]`, and any stack whose top level carries `apps`, is judged exactly as before.
17
+
18
+
**The remedy.** For a refused item, name a doc in `src/docs/*.md` or a declared book (a package id is its own implicit book), or remove the item. The refusal message names the nearest match.
why: '`lintDocNavTargets` resolves a nav item\'s `book` against the artifact\'s own top-level `books` AND each package body\'s (`addBooks(body.books)` over `artifactPackages(stack)`), so the top-level read is one half of the union on purpose (#27090 measured it: the read was there before, hidden from this census by `codeOnly` losing its place in a nested template literal).',
490
+
why: '`lintDocNavTargets` resolves a nav item\'s `book` against the artifact\'s own top-level `books` AND each package body\'s (`addBooks(body.books)` over `artifactPackages(stack)`), so the top-level read is one half of the union on purpose (#27090 measured it: the read was there before, hidden from this census by `codeOnly` losing its place; #27210 measured the cause there as a regex literal holding a quote).',
491
+
},
492
+
'utils/collect-docs.ts :: stack.apps': {
493
+
reads: 'top-level',
494
+
why: '`lintDocNavTargets` walks the top level\'s apps when it carries the key, otherwise each package body\'s (`artifactPackages(stack)`): `resolveStackCollection`\'s rule, over a walk that refuses no entry shape. Measured on a two-package `preserve` app in `collect-docs.nav-target.test.ts` (#27210: before, a body\'s app was never judged, and a nested template literal hid this read from the census).',
0 commit comments