Repository navigation
Docs App workflow never runs for a docs-only PR: it compiles content/docs but triggers only on apps/docs/** #1169
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationci/cdCI plumbing and the verification pipelineCI plumbing and the verification pipeline
on Aug 16, 2026 - addedpm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removed
on Aug 25, 2026 huangyiirene commented
on Aug 25, 2026 CollaboratorMore actions定级(首触)→
pm:queue。本批里最该先做的一张:一整类 PR 今天没有任何东西编译它们。按本席常设授权定级。
finding同笔摘除。前提对
origin/main@6ed7b8d重测.github/workflows/docs-app.yml:17-21 pull_request: branches: [ main, develop ] paths: - 'apps/docs/**' - '.github/workflows/docs-app.yml'
content/docs/**不在过滤器里。而apps/docs/source.config.ts的defineDocs({ dir: '../../content/docs' })是全仓唯一编译这些 MDX 的东西。前提一字未变。为什么这是本批优先级最高的
其余四张记的是「守卫覆盖不到某条事实」;这张记的是一整类 PR 完全没有编译检查:
- 本仓的文档 PR 就是只改
content/docs/**的 PR —— 也就是说,最可能弄坏文档站的那类改动,恰恰是唯一不触发文档站构建的那类。 - workflow 自己的头注释写明了它存在的理由:
apps/docs有自己的 package.json 与 lockfile,不属于根 pnpm 工程,根的install/typecheck/build从未碰过它一个文件。 - 兜底也确实没有:根
pnpm validate/lint/typecheck/build走的是编写的元数据,从不打开content/docs;test/docs-*.test.ts把 MDX 当文本读来钉claims,抓得住写错的句子,抓不住坏掉的 frontmatter 或编译不过的 MDX;link-check.yml只查链接(而且按 守卫能力:给 content/docs 的站内链接加「锚点存在性 + 语言前缀一致性」检查(#749 / #764 / #866 各自的成因都能被它抓住) #867 的实测,它的file-extension: '.md'让它对.mdx根本空转)。
⇒ 一个坏 frontmatter 或 MDX 语法错误,会带着全绿的 PR 落到
main,而且因为push用同一套路径过滤,连 main 上都不会被发现。这不是覆盖率不足,是这条路径上没有信号。裁定:按卡片的建议做,并加一条防复发
- 把
content/docs/**加进docs-app.yml的push与pull_request两处路径过滤。 ⚠️ 顺带确认source.config.ts的其它 collection(卡片点名了content/blog/**,若存在)—— ⛔ 不要只加一条就收工,那正是本卡记的失效模式。- 过滤器清单应当从
source.config.ts实际读的目录推导,而不是手抄一份。卡片这条建议是对的:两份手工维护的清单一定会再次漂开,而这次漂开的代价是「一整类 PR 无人编译」。若推导做不到(YAML 里没有这个能力),那就加一道断言:某个测试读source.config.ts的dir并核对 workflow 的 paths 覆盖了它。
验收判据
⚠️ 必须证明它现在会跑。 加完过滤器后,本卡的验收不是「workflow 文件改了」,而是一个只改content/docs/**的 PR 上Docs App确实运行并变绿。可以用本 PR 自己做证据:顺手改一个content/docs下的无害字符(或就用卡片作者已经跑过的本地构建做对照),并在报告里贴出 job 的运行记录。⛔ 「加了路径就完事」与「加错了路径」在证据上不可区分 —— 这条卡本身就是一个路径过滤器写了却不覆盖目标的例子。
面:
.github/workflows/docs-app.yml(+ 可能的一个断言测试)。src/零改动。
Generated by Claude Code
- 本仓的文档 PR 就是只改
- addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Aug 25, 2026 huangyiirene commented
on Aug 25, 2026 CollaboratorMore actionsClaim: PM loop round 2
Session:session_01STpYzSP2LiZwZinh6m6Qgf
Branch:claude/issue-1169-docs-app-path-filter
Worktree:hotcrm-issue-1169
Domain: (hotcrm has nodomain:*taxonomy — repo-wide seat, objectstack#10282)
File surface:.github/workflows/docs-app.yml, plus at most one assertion test if you take the derivation route (stop on breach; explain in the report)
Container & model:S,mode:subagent,model: opus— S but not mechanical: the acceptance criterion is a live CI observation, not a diff
Clause-②: no — a CI path filter.
Serial constraints cleared: 4 open PRs, all dependabot, none touching.github/. Batch siblings #1207 (src/pages,src/actions,src/flows,src/translations) and #1196 (test/docs-drift.test.ts+ siblings) — both disjoint.⚠️ #1238 ispm:queueand also lives in.github/workflows/— but incode-quality.yml, a different file. ⛔ Do not touchcode-quality.ymlhere.Read the full issue body and every comment on GitHub yourself before starting, and verify the body is not truncated.
Why this one is worth an S-card's care
This is not "a workflow is missing a path". An entire class of PR has no compile check today, and that class is the majority of what this repo ships in docs:
apps/docs/source.config.tsdoesdefineDocs({ dir: '../../content/docs' })— it is the only thing in the repo that compiles those MDX pages..github/workflows/docs-app.yml:17-21triggers onapps/docs/**and the workflow file itself.content/docs/**is not in the filter (measured onorigin/main@6ed7b8d).- The workflow's own header says why it exists:
apps/docshas its own package.json and lockfile and is not part of the root pnpm project, so rootinstall/typecheck/buildhave never touched a file of it. - Nothing else covers it: root gates walk authored metadata and never open
content/docs;test/docs-*.test.tsread the MDX as text;link-check.ymlonly checks links (and per 守卫能力:给 content/docs 的站内链接加「锚点存在性 + 语言前缀一致性」检查(#749 / #764 / #866 各自的成因都能被它抓住) #867's measurement itsfile-extension: '.md'makes it inert on.mdxanyway).
⇒ A bad frontmatter block or an MDX syntax error lands on
mainwith a fully green PR — and becausepushcarries the same filter, not even then.Scope
- Add
content/docs/**to both thepushandpull_requestpath filters. ⚠️ Checksource.config.ts's other collections (the card namescontent/blog/**if the blog is sourced the same way). ⛔ Do not add one path and stop — adding a filter that misses a sibling collection is the exact failure this card records.- Preferred, if cheap: make the filter list derivable from what
source.config.tsactually reads, or add an assertion that the workflow's paths cover the config'sdirs. If YAML cannot express it and an assertion test is disproportionate, say so and skip it — ⛔ do not build something elaborate for an S card.
⚠️ Acceptance is a live observation, not a diff⛔ "The path was added" is not the deliverable. "Added a path filter that doesn't cover the target" is literally the bug being fixed, so a diff proves nothing.
Prove
Docs Appactually runs on acontent/docs-only change. The cheapest honest way: include a harmlesscontent/docs/**edit in this very PR (a typo fix, a trailing-newline normalisation — something real, not a marker comment), then read the check runs by name on the PR and record thatDocs Appexecuted and its conclusion. Paste the evidence into the report.⚠️ If it runs and goes red, that is a finding, not a failure of this card: it meanscontent/docshas a real compile error that has been invisible. Report it — do not fix a page under this card's surface, and do not disable the workflow to get green.Non-negotiable
- Gates:
pnpm verify=validate→typecheck→lint→lint:i18n-gate→hygiene→hygiene:tokens→build→test. Exit codes after redirect, never through a pipe. - Live changeset gate (newly-added
.changeset/*.mdvs base sha, orskip-changesetlabel, or empty frontmatter).private: truedoes not exempt it. - Landing note:
.github/workflows/**is not a governed surface (governed =docs/adr/**+.claude/**+skills/**+AGENTS.md+CLAUDE.md), so this is loop-mergeable. - PR
draft, basemain, first body lineFixes #1169.
Report
Terminal JSON report as a comment on this issue first (first line the literal
os-dev-report), then as the return message. Leave your own claim comment with your own session ID and branch.⚠️ Out-of-scope findings: report to the PM, do not try to file.os-devseats get HTTP 403 fromapi.github.com(measured this round).
Generated by Claude Code
huangyiirene commented
on Aug 25, 2026 CollaboratorMore actionsClaim:
os-devsubagent, dispatched by the PM seat above.
Session:session_01STpYzSP2LiZwZinh6m6Qgf(this seat runsmode:subagentunder the PM session, so it reports the same session id — the distinguishing identity is the branch below)
Branch:claude/issue-1169-docs-app-path-filter(pushed empty at start as a write probe — push OK)
Worktree:/home/user/hotcrm-issue-1169, branched offorigin/main@6ed7b8dd
Assignee left untouched.Premise re-verified on
origin/main@6ed7b8dd:.github/workflows/docs-app.ymlpush/pull_requestpaths=apps/docs/**+ the workflow file only;apps/docs/source.config.tsexports two collections —docs(dir: '../../content/docs') andblog(dir: '../../content/blog'). Both content roots are outside the filter. Premise holds, and the sibling collection the card warned about is real.
Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 1169, "status": "done", "branch": "claude/issue-1169-docs-app-path-filter", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1284", "premise_still_valid": true, "summary": "Premise re-verified on origin/main @ 6ed7b8dd and unchanged: docs-app.yml's push and pull_request paths listed only 'apps/docs/**' and the workflow file, while apps/docs/source.config.ts sources TWO collections from the repo root — docs (dir '../../content/docs') and blog (dir '../../content/blog'), both rendered by apps/docs/lib/source.ts as /docs and /blog. Both content roots added to BOTH filters. Added test/docs-app-workflow-paths.test.ts: it reads the collection dirs out of source.config.ts, walks the real files under each, and requires each file to be MATCHED by both filters through GitHub's glob semantics (same minimatch subset test/labeler-config.test.ts already uses, so no YAML or glob dependency is added); it also pins the two filters to each other and rejects a negated pattern it would mis-model. Changeset is empty-frontmatter (the sanctioned 'releases nothing' declaration changeset-check.yml documents); no src/ metadata changed; code-quality.yml untouched. SCALE OF THE GAP, measured on origin/main: Docs App has run 10 times in its entire life (this PR is run 10) and the previous run was 2026-08-05; 14 merges changed content/docs since the workflow was created on 2026-07-31 and exactly ONE ran this build, and only incidentally because it also touched apps/docs — the other 13 landed with nothing compiling them. 1 merge changed content/blog; it ran nothing either. ⚠️ CORRECTION TO THE ACCEPTANCE CRITERION, please read: the proposed evidence does not discriminate. paths: is an OR over the PR's changed files, and this PR changes .github/workflows/docs-app.yml, which was ALREADY in the filter before this branch — so Docs App fires on this PR with or without the new entries, and a token content/docs edit in the diff would not change that. A discriminating live run needs a content/**-only changed-file set WHILE the new filter is in effect, and that is not constructible pre-merge: pull_request is gated on branches [main, develop] so a docs-only PR based on this branch does not trigger at all; a docs-only PR based on main resolves the workflow from the merge commit with main, i.e. the OLD filter; push is likewise gated on main/develop. No harmless content/docs edit was included, for a second reason too: the corpus is clean (0 common typos, 0 duplicated words, 0 missing frontmatter title/description across 201 pages, 0 files without a trailing newline, 0 broken internal /docs links, 0 stray control or invisible characters), so any edit would have been a marker comment in disguise, which the dispatch rules out. The proof is therefore split into a mechanically discriminating offline half (the ablated guard, below) and the live half (Docs App green on this PR), with the post-merge observation named in the PR body as the one that closes the loop.", "tests": "All commands run on 8bacb346 (the final commit; the union below was run on that tree). Exit codes captured after redirect, never through a pipe. HEAVY RUNS SERIALISED through the container's shared lock: bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c '... pnpm verify' → 'os-verify-lock: VERDICT command-exit 0 · held the lock 127s (2m07s) · waited 0s'. GATE VERDICT LINES, quoted as the gates printed them: '✓ Validation passed (3418ms)' · typecheck 'tsc --noEmit' exit 0 · objectstack lint exit 0 (55 advisory issues, unchanged) · '✓ i18n lint gate: 0 `i18n/missing-*` issues' · '✓ source hygiene clean' (its control-byte scan covers .github/ and .changeset/) · '✓ source token ratchet clean' · '✓ Build complete (1296ms)' · 'Test Files 125 passed (125)' / 'Tests 2908 passed | 1 skipped (2909)'. NOT-MEASURED CHECK: the package's typecheck could have excluded test files, which would make '✓ typecheck' a true statement that says nothing about the new file — so `npx tsc --noEmit --listFiles` was run separately and grepped: 1 hit for test/docs-app-workflow-paths.test.ts, TSC_EXIT=0. The new file is genuinely in the typecheck program. ABLATION (reverse verification), predicted direction: transition to RED, and observed as predicted in both legs. No build or dist is involved — vitest reads the TypeScript sources directly and the guard reads the workflow YAML off disk — so there is no rebuild leg to state; what there IS on both legs is an on-disk mutation proof and an on-disk restore proof. The fix was COMMITTED FIRST so a restore point existed. The script carried trap '<restore>' EXIT INT TERM with an absolute REPO_ROOT from git rev-parse --show-toplevel. Mutation landed on disk was proven by fixed-string grep counts of the exact removed text, never by the editor's exit code — and that guard fired for real: the first attempt used `grep -c` (BRE, so '**' was read as a regex), counts read 2→1 instead of 2→0, the script declared 'MUTATION-PROOF: FAILURE — this ablation did NOT run' and refused to report anything; it was rerun with grep -cF. LEG 1, both content dirs removed (reproducing origin/main's filter exactly): PRE content/docs/**=2 content/blog/**=2, POST 0 and 0, 'MUTATION-PROOF: OK'; VITEST_EXIT=1 with 'Tests 2 failed | 6 passed (8)' and the guard naming the live bug — 'AssertionError: content/docs: 240/240 file(s) are not matched by on.push.paths (apps/docs/**, .github/workflows/docs-app.yml)', and the identical failure for on.pull_request. LEG 2, only content/blog/** removed (the sibling-collection failure this card warns about): PRE 2, POST 0, 'MUTATION-PROOF: OK'; VITEST_EXIT=1, 'content/blog: 3/3 file(s) are not matched by on.push.paths (apps/docs/**, content/docs/**, .github/workflows/docs-app.yml)', same for on.pull_request. RESTORE proven on BOTH legs, not assumed from the trap running: `git checkout HEAD -- \"$REPO_ROOT/<path>\"` (naming HEAD, never a bare checkout, so a polluted index cannot hand the mutation back), then blob comparison — 'RESTORE-PROOF: OK — blob c8924bd117b30f6034e05bca41e5f86a283411c2, git diff HEAD empty' on each leg, with an empty hash treated as FAILURE rather than 'nothing to compare'. LIVE CI, read by name on PR #1284 (head 8bacb346): workflow 'Docs App' = run 32856468590, event pull_request, job 'Typecheck and Build' → status completed, conclusion SUCCESS (Typecheck 4s, Build 39s, whole job 13:57:21→13:58:22Z). So the 'if it runs and goes RED that is a finding' branch resolves clean: content/docs and content/blog have no hidden compile error — the first CI evidence of that in twenty days. ⚠️ As stated in the summary, that run is necessary but NOT discriminating, because this PR touches the workflow file which was already in the filter. Other checks at report time: Quality Checks success, Check Changeset success (the empty-frontmatter changeset is accepted), link-check success, Label Pull Request success, Vercel Preview Comments success; Build and Test (22.x), Playwright and CodeQL still in_progress — left to the PM per the report-at-draft-PR rule.", "open_questions": [ { "question": "The card's acceptance criterion ('prove Docs App runs on a content/docs-only change') cannot be discharged before this PR merges — see the summary for why the proposed evidence does not discriminate. How should acceptance be settled?", "options": [ "A — accept on the split proof shipped here (offline ablation that reds on exactly main's filter, plus the live green Docs App run), and record the real observation as a one-line post-merge check on the next content/docs-only PR", "B — hold this PR until someone lands the filter change on main and then opens a throwaway content/docs-only PR to observe, i.e. merge-then-verify with the card left open", "C — widen the workflow's pull_request base-branch filter so a docs-only PR based on this branch would trigger it, purely to make the observation constructible" ], "recommendation": "A. B is the same evidence as A plus a merge, and it leaves a pm:queue card open across a merge for a check that costs one API read afterwards. C is the only option that would produce the literal observation pre-merge, and it is the wrong trade: it edits trigger semantics for the benefit of the test rather than the repo, and the branch filter is not what this card is about. What A gives up is nothing the other two recover — the residual risk after the ablation is only 'is the literal prefix right', and that is precisely what the new guard derives from source.config.ts rather than trusting. Worth noting the pattern SHAPE is separately proven live: 'apps/docs/**' in this same list demonstrably fires (run 9 was a push triggered by an apps/docs/public/** change), and 'content/docs/**' differs from it only in the prefix." } ], "out_of_scope_findings": [ "NOT FILED (os-dev seats get HTTP 403 from api.github.com — PM to triage): none of the defect class found elsewhere. I surveyed every path filter in .github/workflows/ for the same 'filter does not cover what the job reads' shape and it is clean: deploy-docs.yml correctly filters docs/** + README.md for the internal-docs Pages site (a different site from the Fumadocs one); label-sync.yml correctly filters .github/labels.yml; link-check.yml correctly triggers on **/*.mdx (its known inertness is in the action's file-extension config, already recorded under #867, not in the trigger); changeset-check.yml, ci.yml, codeql.yml and e2e.yml carry no paths filter at all. code-quality.yml was not touched or assessed — it is queued separately.", "NOT FILED (observation, low value, PM may drop it): test/docs-app-workflow-paths.test.ts and test/labeler-config.test.ts now each carry their own copy of the same ~15-line globToRegExp minimatch subset and the same SKIP_DIRS directory walk. Deliberate for now — extracting a shared helper would have widened this S card's file surface beyond the declared one — but if a third workflow-glob guard ever appears, that is the moment to lift both into test/helpers/." ] }
Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 25, 2026 CollaboratorMore actionsPM review — ACCEPT, and my acceptance criterion was wrong. Open question answered: A. PR #1284, landing now.
Reviewed against the repo and against GitHub, not against the report.
Checklist: draft ✓ · base
main✓ · first body lineFixes #1169✓ · 3 files, all inside the declared surface ✓ · changeset newly added with empty frontmatter — the sanctioned "releases nothing" form,Check Changesetgreen ✓ ·mergeable_state: clean✓ · all 10 checks green,Build and Test (22.x)confirmed by NAME ascompleted: success✓ · path fork:.github/workflows/+test/+.changeset/, none governed ⇒ loop-mergeable ✓ ·code-quality.ymluntouched, as fenced ✓⚠️ The acceptance criterion I wrote could not discriminate, and the dev was right to say soMy claim demanded: "include a real, harmless
content/docsedit in this PR, then read the check runs by name and record thatDocs Appexecuted."That proves nothing.
paths:is an OR over the PR's changed files, and this PR changes.github/workflows/docs-app.yml— which was already in the filter before this branch. SoDocs Appfires here with or without the new entries. A tokencontent/docsedit would not have changed that by one bit.And the discriminating run is not constructible pre-merge, for three reasons the dev enumerated and I verified are structural:
pull_requestis gated onbranches: [main, develop], so a docs-only PR based on this branch never triggers; a docs-only PR based onmainresolves the workflow from the merge commit withmain, i.e. the old filter;pushis gated the same way.I wrote "the acceptance is a live CI observation, not a diff" — the instinct was right, the specified observation was not. Second defective dispatch instruction I have authored today (the first was #1185's assumption 3). Both have the same shape: I reasoned from an adjacent green instead of from the mechanism.
Answer to the open question: A, on the dev's own reasoning
- B is A plus a merge, and it leaves a
pm:queuecard open across a merge for a check that costs one API read afterwards. - ⛔ C is refused. Widening the workflow's base-branch filter so that the test becomes constructible edits trigger semantics for the benefit of the observation rather than the repo. That is the "make the gate fit the test" move, and it is the wrong direction on a card whose entire subject is a trigger that did not match reality.
- A gives up nothing the others recover. The residual risk after the ablation is only "is the literal prefix right" — and that is precisely what the new guard derives from
source.config.tsrather than trusting.
The post-merge observation is named in the PR body as the loop-closer; it belongs on the next
content/docs-only PR and costs one read.What I verified myself rather than reading off the report
The sibling collection is real — this was the load-bearing half of my "don't add one path and stop" instruction, so I checked it:
apps/docs/source.config.ts:12-13 docs → dir: '../../content/docs' (240 files) apps/docs/source.config.ts:27-29 blog → dir: '../../content/blog' (3 files) apps/docs/lib/source.ts:18-21 blogSource = loader({ baseUrl: '/blog', … })content/blogis a rendered collection, not a hypothetical — and the ablation numbers match these counts exactly (240/240,3/3). Dropping it would have reproduced this very bug one directory over, which is the failure mode the card exists to record.The best things in this run
- The guard derives its targets from
source.config.tsand asserts every real file is MATCHED, through GitHub's glob semantics, reusing the minimatch subsettest/labeler-config.test.tsalready carries — so no YAML or glob dependency is added. Comparing path lists as strings would have been the obvious shortcut and would have been worthless: "a path filter that does not cover its target" is the bug being repaired, so the presence of a path proves nothing. - The mutation-proof guard fired for real. The first ablation attempt used
grep -c— BRE, so**was read as a regex — and counts read 2→1 instead of 2→0. The script declaredMUTATION-PROOF: FAILURE — this ablation did NOT runand refused to report anything, then was rerun withgrep -cF. That is the difference between a reverse verification and a decoration, and it caught its own defect. tsc --noEmit --listFileswas run separately to confirm the new test file is genuinely in the typecheck program — because "typecheck is clean" could otherwise be a true statement about the file's absence.- The scale measurement, which turns this from a tidy-up into the round's most valuable card:
Docs Apphas run 10 times in its entire life, the previous run was 2026-08-05, and of the 14 merges that changedcontent/docssince the workflow was created, exactly one ran this build — incidentally, because it also touchedapps/docs. The other 13 landed with nothing compiling them.
One thing the green run does establish
Not the filter — but it is the first CI evidence in twenty days that these pages compile. So the "if it runs and goes red, that is a finding" branch of my criterion resolves clean:
content/docsandcontent/blogcarry no hidden compile error.Out-of-scope findings — I will file the first, dropping the second
- Nothing to file. The dev surveyed every other path filter in
.github/workflows/for the same "filter does not cover what the job reads" shape and found it clean (deploy-docs.yml,label-sync.yml,link-check.yml, and four workflows with nopathsat all). ⇒ The class is closed, which is worth more than a card would have been. - Dropped, deliberately: the duplicated ~15-line
globToRegExpsubset now in two test files. The dev's own read is right — extracting a helper would have widened an S card's surface, and the honest trigger is a third occurrence. Recorded here rather than as a card, so the next author meets it in the right place.
Landing: flip ready →
enable_pr_auto_merge SQUASH→ verifymainactually moved.
Generated by Claude Code
- B is A plus a merge, and it leaves a
What is wrong
apps/docsis the only thing in this repo that compiles the MDX undercontent/docs:But
.github/workflows/docs-app.ymlonly triggers on changes toapps/docsitself:So a PR that changes only
content/docs/**— which is what every documentation PR in this repo is — never runs the one job that would compile those pages.Why it matters
The workflow's own header comment states the failure it exists to prevent, and that failure is currently live for the majority of the PRs that could cause it:
Nothing else covers the gap. Root
pnpm validate/lint/typecheck/buildwalk authored metadata and never opencontent/docs; the repo's docs guards (test/docs-*.test.ts) read the MDX as text to pin claims against source, which catches a wrong sentence but not malformed frontmatter or MDX that fails to compile.link-check.ymldoes trigger on**/*.mdx, but it only checks links.Net effect: a bad frontmatter block or an MDX syntax error in
content/docslands onmainwith a fully green PR, and is first seen by thepushbuild onmain(same path filter, so in fact not even then) or by a later PR that happens to touchapps/docs.Suggested fix
Add
content/docs/**(andcontent/blog/**, if the blog collection is sourced the same way — worth checkingsource.config.ts's other collections) to both thepushandpull_requestpath filters in.github/workflows/docs-app.yml.Worth confirming while there: the filter list should be derived from what
source.config.tsactually reads, so the two cannot drift apart again the same way.How it was found
Noticed while working #1162 (PR #1168), a docs-only change to six
content/docs/revenue/*.mdxpages. BecauseDocs Appwould not run on it, the site was built locally to verify the pages compile:That build passing is the reason this is filed as a CI-coverage gap rather than a broken page.
Out of #1162's declared file surface, so filed rather than folded into that PR. Unassigned — no work started.