Skip to content

fix(cli): read doc tags from src/docs frontmatter, and report what it cannot read - #10976

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10486-doc-tags-frontmatter-list
Aug 22, 2026
Merged

fix(cli): read doc tags from src/docs frontmatter, and report what it cannot read#10976
os-elon merged 1 commit into
mainfrom
claude/issue-10486-doc-tags-frontmatter-list

Conversation

@os-elon

@os-elonos-elon commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10486

DocSchema.tags was declared in 17.0.0 (#4509, ADR-0049) as the enforce half of enforce-or-remove: the resolver side already compared against it (matchesInclude in packages/spec/src/system/book.zod.ts) and the REST book-tree route already forwarded it. But packages/cli/src/utils/collect-docs.ts read frontmatter with frontmatterScalar alone — single-line scalars — and had no case for tags at all. On the flat src/docs/*.md path the docs actually recommend, a tags: block was dropped without a word.

This enforces that existing ruling on the path it was declared for. No packages/spec change — the key was already declared; only the collector could not produce it.

The reproduced drop (at branch head, before any change)

Two docs, one per frontmatter spelling, run through collectDocsFromSrc and then resolveBookTree with include: { tag: 'tutorial' }:

--- collected DocItems ---
[ { "name": "crm_block", "label": "Block" },
{ "name": "crm_inline", "label": "Inline" } ] ← no `tags` on either
--- issues ---
[] ← and not one word about it
--- resolveBookTree with include: { tag: "tutorial" } ---
{ "key": "tutorials", "label": "Tutorials", "entries": [] }
{ "key": "uncategorized", "label": "Uncategorized",
"entries": [ { "doc": "crm_block" }, { "doc": "crm_inline" } ] }
--- VERDICT ---
tutorials group entry count: 0

Silent in both directions, exactly as the card describes: no build warning that tags: was dropped, and no resolver diagnostic that an include rule matched nothing.

Half 1 — a minimal frontmatterList

Reads the two ordinary YAML sequence spellings and no others, wired through DocItem.tags:

tags: [tutorial, beginner] # inlinetags: # block
- tutorial
- beginner

Details worth naming: the block sequence ends at the next frontmatter key, so a group: following a tags: block still parses (pinned); items are unquoted the same way frontmatterScalar unquotes; and an authored tags: [] parses and means what it says — no tags, and no complaint, because nothing was dropped.

After the change, the same repro yields tutorials entries: ["crm_block","crm_inline","crm_quoted"].

Half 2 — the loud DocIssue, which is the half that matters

The reader is deliberately minimal and is not becoming a YAML engine. That minimalism is only honest if what it cannot read is reported rather than dropped, so a tags: present in any other spelling now raises a docs/frontmatter-tags warning naming the consequence and quoting what it actually found:

[warning] docs/frontmatter-tags @ src/docs/crm_scalar.md
Frontmatter `tags:` in "crm_scalar.md" is not a list this reader understands, so no
tags were collected and a book group's `include: { tag }` cannot match this doc.
Write either `tags: [tutorial, beginner]` or a block of `- item` lines under `tags:`.
Found: tags: tutorial

Three unreadable spellings are pinned: a bare scalar, an unterminated inline sequence, and a key with nothing under it. The doc is still collected — only its tags are missing. Warnings surface through the paths that already print DocIssues: os lint (forwarded verbatim), os validate, and os compile (printWarning).

One bounded addition inside the same defect class, named here rather than left silent: the same warning fires when a locale variant — a name.locale.md sibling such as crm_guide.zh.md — declares tags:. DocTranslationItem carries only label/description/content, so a parseable tags list on a variant would have vanished exactly as unparsed ones used to — the identical silent drop, one file over. The warning points at the base file. Evidence for the shape is the sibling declaration itself (DocTranslationItem) plus DocSchema.tags being doc-level; no new verification surface, same file, same rule name, same gate family. Flagging it for the reviewing seat as a deliberate call, not an oversight.

Reverse-verification — both legs, one lock acquisition

Predicted RED before running (the new tests assert positively on collected tags and on exactly one warning). Reverted only the production file, kept the tests:

########## LEG 1: production change reverted (tests kept) — predict RED ##########
packages/cli/src/utils/collect-docs.ts | 129 +----------------------- (4 insertions, 125 deletions)
LEG1_EXIT=1
Test Files 1 failed (1)
Tests 8 failed | 26 passed (34)
########## RESTORE ##########
restored-clean=yes
########## LEG 2: restored — predict GREEN ##########
LEG2_EXIT=0
Test Files 1 passed (1)
Tests 34 passed (34)

All 8 failures are the 8 new tests, by name — the three reading tests, the three unreadable-spelling warnings, the locale-variant warning, and the resolver-level pin. Direction was as predicted, with no inversion and no diagnostic-count surprise.

Tests

Eight new tests in packages/cli/src/utils/collect-docs.test.ts, including a resolver-level pin that imports the real resolveBookTree from @objectstack/spec/system and asserts a group's include: { tag } matches docs authored in either list form — the end of the claim this card makes, not just the collector's output. A warning nothing asserts is a warning that can silently stop firing, so each unreadable spelling asserts severity, rule, path, the named consequence, and the quoted text found.

Full package suite, under scripts/pm/os-verify-lock.sh:

> @objectstack/cli@17.1.0 typecheck → tsc --noEmit, exit 0
Test Files 151 passed (151)
Tests 1675 passed (1675)
os-verify-lock: VERDICT command-exit 0

Gate union — derived and run at the final commit 51c5e2af87

node scripts/pm/dispatch-gates.mjs (no paths passed; it derived the 3-file change set from the merge base itself). All green, quoting each gate's own verdict line:

GateVerdict line
check:nul-bytesOK (scanned 6323 text file(s) … no raw ASCII control bytes)
check:changeset-gate-self-testsexit 0
check:objectui-changesetexit 0
check:slot-lookupslot-lookup ratchet holds: 107 unswept site(s) … none new
check:test-source-aliasOK — 72 packages with tests scanned; 61 registered …
check:type-source-resolutionOK — 76 packages with a tsconfig.json scanned …
check-adr-0087-registration.mjsexit 0
check-changeset-no-major.mjsexit 0
check-empty-changeset.mjsNo empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)
check-ci-filter-parity.mjsOK: all 82 declared cross-package glob(s) (71 unique) are covered …
check-plugin-teardown-shape.mjsexit 0
docs-audit/check-affected-docs.mjsexit 0
check:query-options-erasureratchet holds: 67 unswept non-test site(s) … none new
check:engine-double-contractOK — 376 pinned, 133 in the DEBT ledger, 2 exempt
check:where-matcherconformance holds: 275 matcher(s) … 275 answer correctly or refuse loudly
check:type-check-coverageOK — 64/77 workspace packages type-checked …
check:type-check-debt (--re-measure)OK — 33 ledger entr(ies) re-measured in 199.0s, 1908 raw tsc error(s) total, none above its recorded number

The ratchet was run properly built (turbo run build over the workspace: Tasks: 70 successful, 70 total). No ledger or baseline was edited. Its standing plugin-auth surplus note (TEST_DEBT records 109, tsc now reports 97) is a pre-existing condition on main in a package this PR does not touch, and the gate itself marks it ℹ … Not an error. Worth noting the ratchet's @objectstack/cli entry measures the hidden sibling test/ tree; the new test file sits under src, which include: ["src"] already compiles — so it is covered by the green tsc --noEmit above rather than by the ledger.

Filed, not folded — #10966

Triage flagged that the interim warn callout in content/docs/ui/doc-pages.mdx (from PR #10483) states that tags are not read from Markdown frontmatter. Once this merges that callout becomes false, so it is filed as a linked docs chore — #10966, unassigned, labelled finding + domain:cli (labels verified as landed), Blocked-by: #10486 since the callout is correct until this lands. It is kept out of this PR so the file surface stays packages/cli/src/utils/collect-docs.ts + its test + the changeset. It names the exact callout, the stale table row beside it, and the "three scalars" sentence further down that also needs the word scalars corrected.

⛔ Left as draft, not enqueued — the PM seat reviews and merges.


Generated by Claude Code

…it cannot read
`DocSchema.tags` was declared in 17.0.0 (#4509, ADR-0049) as the enforce half
of enforce-or-remove: the resolver already compared against it (`matchesInclude`
in `book.zod.ts`) and the REST book-tree route already forwarded it. But
`collect-docs.ts` read frontmatter with `frontmatterScalar` alone — single-line
scalars — and had no case for `tags`. On the flat `src/docs/*.md` path the docs
recommend, a `tags:` block was dropped without a word: every doc reached
`resolveBookTree` with `tags === undefined`, so a group declaring
`include: { tag: 'tutorial' }` matched nothing and rendered empty.
Two halves, both needed:
- `frontmatterList` reads the two ordinary YAML sequence spellings — inline
`tags: [a, b]` and the block form of `- item` lines — wired through
`DocItem.tags`. The block sequence ends at the next frontmatter key, so a
`group:` following a `tags:` block still parses. An authored `tags: []`
parses and means what it says.
- A `docs/frontmatter-tags` warning fires whenever `tags:` is present in a
spelling the reader cannot parse (bare scalar, unterminated inline sequence,
key with nothing under it). The reader stays deliberately minimal and is not
becoming a YAML engine; the warning is what keeps that minimalism honest, by
turning the next unanticipated spelling from a silent drop into a visible
report. The same warning fires when a locale variant declares `tags:`, since
tags belong to the doc rather than to one translation and
`DocTranslationItem` carries no such field.
No schema change: `DocSchema.tags` already declared the key, and only the
collector could not produce it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 23 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 af1636c760879215f28eb2d9a720c3e393027a80packageMentionDocs.

Which tree this was computed on

This run read content/docs from 59fff57c36b0a363cb16ea0d82779baa964daf57 — the merge of head 51c5e2af873ecd25b4cad6c0a5de3f4c807f02fe into base af1636c760879215f28eb2d9a720c3e393027a80, 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 59fff57c36b0a363cb16ea0d82779baa964daf57 && git checkout 59fff57c36b0a363cb16ea0d82779baa964daf57
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin af1636c760879215f28eb2d9a720c3e393027a80 51c5e2af873ecd25b4cad6c0a5de3f4c807f02fe && git checkout -B drift-repro af1636c760879215f28eb2d9a720c3e393027a80 && git merge --no-ff 51c5e2af873ecd25b4cad6c0a5de3f4c807f02fe
node scripts/docs-audit/affected-docs.mjs --json af1636c760879215f28eb2d9a720c3e393027a80

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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32546382385 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.82s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 65 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 0d0fcafAug 22, 2026
35 checks passed
@os-elon
os-elon deleted the claude/issue-10486-doc-tags-frontmatter-list branch August 22, 2026 02:44
os-zhuang pushed a commit that referenced this pull request Aug 23, 2026
…callout
PR #10483 added an interim warn callout stating that `tags` cannot be set
from a `src/docs/*.md` file. #10976 has since taught the collector to read
it, so every sentence of that callout is now false and the page steers
authors away from the path that works.
Three sites in content/docs/ui/doc-pages.mdx, all verified against the
merged collector rather than transcribed from the filing card:
- the warn callout: replaced (not deleted — leaving nothing swaps one
false statement for no steer at all) with the two accepted YAML
spellings, the reported-not-dropped behaviour of anything else, the
surfaces that raise `docs/frontmatter-tags`, the locale-variant rule,
and the name-convention preference that survives as a preference;
- the frontmatter table row: "not read from frontmatter" becomes
frontmatter `tags:`, matching the `order:` / `group:` rows above it;
- the "three scalars" sentence: `tags` is a list, not a scalar.
Documentation only — no source file touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-elon@claude