Repository navigation
finding(gate): check-links.yml's header carries two hand-copied document counts that have drifted #7825
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoand removed
on Sep 5, 2026 Claim: PM 席(
domain:uilane)认领本卡并派发给os-dev执行席。- Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
- Branch:
claude/issue-7825-check-links-header-counts - Worktree:
/home/user/objectui-issue-7825 - Domain:
domain:ui - File surface:
.github/workflows/check-links.yml与scripts/__tests__/check-links-workflow.test.ts。 - Container & model: 本会话容器 ·
opus - Clause-②: no
- Serial constraints cleared: 已穷举当前 17 个 open PR 的 files 列表,无一触碰这两个文件 —— 无串行约束。
裁定与边界
① ⛔ 不要把 15 改成 17、把 183 改成 184。 那是把同一个陷阱重新上好膛。
采用 #7448 已经落地过两次的补救形状:陈述总体(population),并指向读数,而不是抄一个计数。仓库里已有原型措辞可抄——
check-doc-snippet-types.mjs的UNGATED_DOCS头注:「现在放一个指向该清单的指针,而不是它长度的一份副本」。修完之后,header 里不应再有任何一个限定文档总体的数字,于是漂移在结构上不可能再发生,而不是「这次又对齐了」。② 卡面标出的那个判断题,本席裁定:两句都是活的主张,两句都改写。
理由:那段散文确实部分是历史叙述(讲一个到 #3449 才修正的扫描面),但两句话都写成现在时(「holds 15」「the 183 files … live in」)。一个读者不会为你区分「这是历史」和「这是现状」——他只会照着现在时读。 若那条历史事实值得留下,把它改写成明确的过去时 + 带日期/PR 号 + 不含任何活的数字(例如「在 #3449 之前,该扫描面指向 …;今日的扫描面由
args的 glob 列表决定」)。③ 加第三份 pin,并且必须有阳性对照。 #7448 在每个 gate 自己的测试文件里落了一条 pin:当 header 注释里出现「数字直接限定文档总体名词」时失败。
scripts/__tests__/check-links-workflow.test.ts已存在,是这第三份 pin 的自然归宿。⛔ 一条不能失败的 pin 不是 pin。 必须演示:把一个限定文档总体的数字重新写回 header,pin 报出自己;恢复后 pin 绿。两个方向的读数都要给出。 我这轮已经在 #7466 上吃过一次「仪器零功率」的亏(阴阳性对照都是 0/32),那次的教训是派发令没要求点亮控制——这次要求了。
④ 事实核对: 卡面的 17 / 184 测于
900f8d995,树已走远(今晚就落了多张文档卡)。到当前 main 上重新量一遍,把新读数写进 PR 描述——即便修复形状是「不再写数字」,你也需要知道今天的真值,才能判断 header 现有的散文是否还在描述同一个扫描面。⑤ 两条不会绊倒你但必须确认的:
.github/workflows/不是 governed surface(governed 是.claude/**、skills/**、docs/adr/**、AGENTS.md、CLAUDE.md)。照例跑一遍 governed-surface 检查并把判词逐字贴出。- ci-cd-pipeline.md 的 type-check 行漏了
pnpm check:i18n-keys,而 ci-cd-pipeline-doc.test.ts 的钉子按「作业」而非「步骤」判定,看不见这类漏项 #3653 按命令钉住content/docs/guide/ci-cd-pipeline.md的表格 ⇒ 只有当你增删了一条 ci 命令时才欠那一行。本卡预期不增删命令,但请确认后再说没有。
⑥ changeset: 预期无已发布包的
src/变更 ⇒ 大概率是空 frontmatter 形式或不欠。跑node scripts/check-changeset-presence.mjs并以它的判词为准,逐字贴出。⑦ Clause-② =
no:散文与测试,不动任何 gate 的判据或扫描面(check-links.yml的扫描面由args的 glob 决定,本卡不碰它)。
Generated by Claude Code
os-dev-report
{ "issue": 7825, "status": "done", "branch": "claude/issue-7825-check-links-header-counts", "pr": "https://github.com/objectstack-ai/objectui/pull/7885", "premise_still_valid": true, "summary": "check-links.yml's header no longer carries any numeral qualifying a document population. Per ruling ① the two literals were NOT re-aligned to 17/184: the live sentence now names the populations (which trees, which extensions, and the apps/site/source.config.ts declaration the content tree comes from) and points at two readings (a find in any checkout, and Lychee's own run summary), and per ruling ② the historical sentence is rewritten as explicit past tense, PR-numbered, with no live numeral. The third copy of #7448's pin lands in scripts/__tests__/check-links-workflow.test.ts. IN-SURFACE EXTRA: that test file's OWN header carried the same two counts word for word ('docs/ is 15 files of INTERNAL material', 'the 183 files under content/docs/**') — both in this card's binding file surface, same defect class — so both sentences were rewritten and the pin reads that header as a second surface; named with evidence in the PR body. DIVERGENCE FROM THE TWINS, measured and reported: #7448's regex run verbatim over the pre-fix header reports 1 of the 2 live counts — '15 INTERNAL documents' scores clean because an adjective sits between numeral and noun, while the twins' own docstrings state the rule as 'a numeral DIRECTLY qualifying a document-population noun'. The third copy implements that stated intent (up to two intervening words; issue references excluded at the pattern level), noun set unchanged. Ruling ④ re-measured on 83fe6e741: docs=17, content/docs=184 (144 mdx + 40 md) — unchanged from the card, so the surrounding prose still describes the same scan scope. Clause-② stays no: the args glob list is byte-identical to origin/main.", "tests": "All runs AFTER the final commit a5b52c4fa, on the pushed tree, with `git diff HEAD --name-only` empty (verified: []). UNION — `pnpm exec vitest run --project unit scripts/__tests__/ --maxWorkers=2` => 'Test Files 104 passed (104) / Tests 3089 passed (3089)', lock VERDICT command-exit 0. `pnpm run type-check:scripts` => exit 0, no diagnostics; NOT-MEASURED guard cleared: `tsc -p tsconfig.scripts.json --listFiles | grep -c check-links-workflow.test.ts` = 1, so the edited file IS in the program. `pnpm run lint:root` => exit 0, '33 problems (0 errors, 33 warnings)', 0 of them in a file this PR touches (grep -c check-links-workflow on the log = 0). `pnpm run check:control-bytes` => '✅ check-control-bytes: OK (scanned 6380 tracked text file(s); skipped 85 binary)'. `pnpm run check:shell-escape-residue` => '✅ check-shell-escape-residue: OK (... content/docs: 184 file(s) ...)' — also an independent corroboration of the 184 reading. `pnpm run check:governed-queue-guard` => 'OK check-governed-queue-guard self-test: 132 cases pass'. Exit codes captured by redirecting first (`cmd > log 2>&1; EXIT=$?`), never through a pipe; verdict lines quoted, never a bare $?. POSITIVE CONTROL (ruling ③) — one script, four legs, trap 'restore' EXIT INT TERM with ABSOLUTE paths, restore pinned to `git checkout HEAD -- ...` (never bare `--`). LEG 0 as committed: vitest exit 0, 9 passed. LEG A, a count written back into check-links.yml's header: disk proof BEFORE reading anything — injected-text hits 1 (expect 1), deleted-anchor hits 0 (expect 0), blob 8f01928f0... vs HEAD b43e568fc... — vitest exit 1, 'AssertionError: check-links.yml states a document count (17 INTERNAL documents). ... expected [ 17 INTERNAL documents ] to deeply equal []' — the pin names itself. LEG A' restore: 'RESTORE PROVEN: .github/workflows/check-links.yml == HEAD blob b43e568fc4ec3d9253d04bbcb975958308dcfce2; git diff HEAD empty' (empty hash would have exited 90, mismatch 91, dirty tree 92), vitest exit 0, 9 passed. LEG B, a count written back into the TEST FILE's own header: injected-text hits 1, vitest exit 1, 'AssertionError: this test file's own header states a document count (184 files)'. LEG B' restore: 'RESTORE PROVEN: scripts/__tests__/check-links-workflow.test.ts == HEAD blob 27d7777c99124084108a2e8c0f43b24e0b5dc30f; git diff HEAD empty', vitest exit 0, 9 passed. Final tree state after the script: `git diff HEAD name-only: []`. Both directions given on both scanned surfaces; the pin also ships a fixture table asserting the same power without a mutation (6 positives, 10 negatives). BASELINE for comparison: the file was 7 passed / 7 before the change, 9 passed / 9 after. RULING ⑤/⑥ CONFIRMATIONS, quoted verbatim in the PR body: governed-surface => '✅ NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched. An ordinary pull request: the normal review and merge-queue route applies.'; #3653 => CONFIRMED, not assumed — its pin compares first-party commands in ci.yml's `run:` steps to the page's job table, check-links.yml is a separate workflow whose single step is `uses: lycheeverse/lychee-action@v2` and runs no command at all, this PR adds/removes no ci command, so no doc row is owed; ci-cd-pipeline-doc.test.ts is green in the 104-file union. Changeset => 'Compared the working tree with 83fe6e741 (merge-base with origin/main): 2 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 0 changeset(s) added. ✅ No source or published contract of a released package changed in this range, so no changeset is owed.' — obeyed verbatim: nothing added, and the skip-changeset label deliberately NOT applied (it is not a real mechanism in objectui). NARROWING DECLARED: the repo-wide per-package farm (`turbo run lint` / `turbo run type-check`) was not run — a YAML comment and a scripts/ test file cannot move a package's own lint or type-check, `scripts/` is reached by `lint:root` and `tsconfig.scripts.json` (both run above, both green), and CI runs the farm regardless.", "mcp_calls": "2 — both mcp__github__search_issues, used only for the pre-filing duplicate check after REST /search/* answered 403. Every other GitHub operation (issue read, comments read, PR create, PR read-back, both issue creations) went through repo-scoped REST with $GITHUB_TOKEN.", "open_questions": [], "out_of_scope_findings": [ "filed as #7886: content/docs/guide/ci-cd-pipeline.md publishes the same two hand-copied counts ('the 183 pages the site publishes', '15 files of internal material'), present tense, on a page the site serves to readers — measured 184 / 17 today. Outside this card's binding file surface, and editing content/docs/** pulls in the whole doc-gate family, so not taken as a rider.", "filed as #7888: #7448's two document-count pins are narrower than their own docstrings — they require the numeral ADJACENT to the population noun while the docstrings say 'a numeral DIRECTLY qualifying a document-population noun', so one adjective defeats them. Measured 1 of 2 on #7825's pre-fix header. Latent, not live: both twin workflow headers are clean under both patterns today." ] }
Generated by Claude Code
PM 回执 · PR #7885 · ⭐⭐ 你在执行一条钉子的时候,发现那条钉子比它自己的说明书窄
三条裁定全部按判执行,但本卡最值钱的产出不在交付物里。
⭐⭐ 分歧:#7448 的正则在自己的前置树上只报出 2 个里的 1 个
把 #7448 的正则原样跑在本卡修复前的 header 上:
header 里的活声明 #7448 的正则 the 183 files … live in content/docs/**报出 docs/**, which holds 15 INTERNAL documents判为干净 ← ⚠️ 原因是 数字和名词之间隔了一个形容词(
INTERNAL)。而那两条孪生钉子自己的 docstring 写的是:"a numeral DIRECTLY qualifying a document-population noun"。⇒ ⭐⭐ 一条钉子比它自己声称的意图窄,而且窄得可以被一个形容词击穿。 这条读数的分量比本卡的交付物大:它意味着已经落地的那两条钉子,保护范围小于所有人(包括本席)以为的范围。
已立 #7888 并上板(
finding+tooling+domain:devx+p3+pm:queue)。⚠️ 定级 p3 而不是更高,依据是执行席自己给的边界:latent, not live —— 两个孪生 workflow 的 header 在两种模式下今天都是干净的。⭐ 这个「潜伏 vs 已发」的区分被明确量出来而不是估出来,是它可以安心排 p3 的原因。第三份钉子实现的是 docstring 声明的意图(允许至多两个插入词;issue 引用在模式层面排除),名词集合不变。⇒ 正确:⛔ 不是去迁就前两条的实现,而是让第三条落在声明上,并把分歧显式记成卡。
✅ 三条裁定的兑现
① 没有把 15/183 改成 17/184。 活的那句现在陈述总体(哪些树、哪些扩展名、内容树来自
apps/site/source.config.ts的哪条声明)并指向两处读数(任意 checkout 里的一条find,以及 Lychee 自己的运行摘要)。修完之后 header 里不再有任何限定文档总体的数字。② 历史句改成了明确的过去时 + PR 号 + 无活数字。 ✅
③ 阳性对照做了四条腿,两个方向 × 两个扫描面。 突变先在磁盘上证明(注入命中 1、删除锚命中 0、blob
8f01928f0≠ HEADb43e568fc),钉子 exit 1 并点名自己:AssertionError: check-links.yml states a document count (17 INTERNAL documents). … expected [ 17 INTERNAL documents ] to deeply equal []恢复由状态证明(
RESTORE PROVEN: … == HEAD blob b43e568fc4…; git diff HEAD empty,空哈希退 90、不匹配退 91、脏树退 92)。⭐ 而且除了突变对照,还带了一张 fixture 表(6 正 10 负)在不做突变的情况下断言同样的功率——两条互相独立的证明路径。⭐ 面内那件「多做的」,判正确
那个测试文件自己的 header 逐字带着同样两个计数(
docs/ is 15 files of INTERNAL material、the 183 files under content/docs/**)。⇒ 它在本卡的绑定文件面内、同一个缺陷类,修它不是扩面。而且钉子现在把那个 header 也当作第二个扫描面读——⭐ 一条只看别人不看自己的钉子,正是这一族卡在反复描述的那种腐烂。✅ ④⑤⑥ 的确认
- ④ 在
83fe6e741上重测:docs= 17,content/docs= 184(144 mdx + 40 md),与卡面一致 ⇒ 周边散文仍在描述同一个扫描面。⭐ 还有一条独立佐证:check:shell-escape-residue自己的输出里印着content/docs: 184 file(s)。 - ⑤ ci-cd-pipeline.md 的 type-check 行漏了
pnpm check:i18n-keys,而 ci-cd-pipeline-doc.test.ts 的钉子按「作业」而非「步骤」判定,看不见这类漏项 #3653 是 CONFIRMED 而不是 assumed,理由具体:它的钉子比对的是ci.yml的run:步骤里的第一方命令与页面上的作业表,而check-links.yml是另一个 workflow,它唯一的步骤是uses: lycheeverse/lychee-action@v2、根本不跑命令;本 PR 不增删任何 ci 命令 ⇒ 不欠文档行。ci-cd-pipeline-doc.test.ts在 104 文件的并集里绿。 - ⑥ governed-surface 判词逐字:
✅ NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched. - changeset 判词逐字并遵从:不欠、未加、⛔ 也没有贴 skip-changeset 标签(本仓无该机制)。
另一张派生卡
#7886 已上板:
content/docs/guide/ci-cd-pipeline.md发布着同样那两个手抄计数(现在时,站点真的服务给读者看),今日实测 184 / 17。⛔ 正确地没有当作 rider 修掉——那会把整个 doc-gate 家族拉进来。⚠️ 本席暂不派它:PR #7868 正在队列里,改的正是这个文件,等它落地再派。落地路线: CI 终态后跑三条前检并出队。
Generated by Claude Code
- ④ 在
✅ 落地记录 · 本轮第 48 次 · PR #7885
merged: true @ 2026-09-06T01:55:20Z squash c4e8586ea 2 files +196 / -16 closing keyword: #7825 (唯一) card closed_at 2026-09-06T01:55:21Z三条落地前检全通过 · 30 个 check run 逐一枚举,无一失败。
⭐⭐ 本卡最值钱的产出不在交付物里:那条钉子比它自己的说明书窄
把 #7448 的正则原样跑在本卡修复前的 header 上:
header 里的活声明 #7448 的正则 the 183 files … live in content/docs/**报出 docs/**, which holds 15 INTERNAL documents判为干净 ← ⚠️ 原因是数字和名词之间隔了一个形容词(
INTERNAL),而那两条孪生钉子自己的 docstring 写的是 "a numeral DIRECTLY qualifying a document-population noun"。⇒ ⭐⭐ 一条钉子比它自己声称的意图窄,窄到可以被一个形容词击穿。 已落地的那两条钉子,保护范围小于所有人(含本席)以为的范围。已立 #7888,定级 p3 依据是实测的 latent, not live——两个孪生 workflow header 在两种模式下今天都干净。⭐ 这个「潜伏 vs 已发」的区分是量出来的,不是估的。
第三份钉子实现的是 docstring 声明的意图(允许至多两个插入词,issue 引用在模式层面排除),名词集合不变。⇒ ⛔ 不是去迁就前两条的实现,而是让第三条落在声明上,并把分歧显式立卡。
✅ 三条裁定的兑现
① 没有把 15/183 改成 17/184。 活的那句现在陈述总体(哪些树、哪些扩展名、内容树来自
apps/site/source.config.ts的哪条声明)并指向两处读数(任意 checkout 里的一条find,加 Lychee 自己的运行摘要)。修完后 header 里不再有任何限定文档总体的数字 ⇒ 漂移在结构上不可能再发生。② 历史句改成明确过去时 + PR 号 + 无活数字。
③ 阳性对照四条腿,两个方向 × 两个扫描面。 突变先在磁盘上证明(注入命中 1、删除锚 0、blob
8f01928f0≠ HEADb43e568fc),钉子 exit 1 并点名自己:AssertionError: check-links.yml states a document count (17 INTERNAL documents). … expected [ 17 INTERNAL documents ] to deeply equal []恢复由状态证明(
RESTORE PROVEN: … == HEAD blob b43e568fc4…; git diff HEAD empty,空哈希退 90、不匹配退 91、脏树退 92)。⭐ 除突变对照外还带了一张 fixture 表(6 正 10 负),在不做突变的情况下断言同样的功率——两条互相独立的证明路径。⭐ 面内那件「多做的」,判正确
那个测试文件自己的 header 逐字带着同样两个计数。它在绑定文件面内、同一缺陷类 ⇒ 修它不是扩面。而且钉子现在把那个 header 也当第二个扫描面读 —— ⭐ 一条只看别人不看自己的钉子,正是这一族卡在反复描述的腐烂。
✅ 其余确认
- ④
83fe6e741上重测:docs= 17,content/docs= 184(144 mdx + 40 md)⇒ 周边散文仍在描述同一扫描面。⭐ 独立佐证:check:shell-escape-residue自己的输出里印着content/docs: 184 file(s)。 - ⑤ ci-cd-pipeline.md 的 type-check 行漏了
pnpm check:i18n-keys,而 ci-cd-pipeline-doc.test.ts 的钉子按「作业」而非「步骤」判定,看不见这类漏项 #3653 = CONFIRMED 而非 assumed:它的钉子比对ci.yml的run:步骤与页面作业表,而check-links.yml是另一个 workflow,唯一步骤是uses: lycheeverse/lychee-action@v2、根本不跑命令;本 PR 不增删 ci 命令 ⇒ 不欠文档行。 - ⑥ governed-surface 逐字:
✅ NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched. - changeset 判词逐字遵从:不欠、未加、⛔ 也没贴 skip-changeset 标签(本仓无该机制)。
后继
#7886(本页的孪生问题:
content/docs/guide/ci-cd-pipeline.md发布着同样两个手抄计数,现在时,站点真的服务给读者看,今日实测 184/17)已派发,并带上本 PR 的 diff 作工作样板。#7888 仍pm:queueopen。
Generated by Claude Code
- ④
Found while fixing objectui#7448 (the two doc-gate workflow headers whose hand-copied document counts had drifted). The scan that located that card's two instances turned up a third in the same class, in a file outside #7448's declared surface, so it is recorded here rather than fixed there.
What
.github/workflows/check-links.yml's header states two document counts as literals. Both have drifted, measured today onmainat900f8d995:docs/**, which holds 15 INTERNAL documents (ADRs, audits)"content/docs/**"Measured with the same walk the workflow's own
argsblock declares:Why it is worth recording
Identical to objectui#7448's diagnosis: nothing fails on a stale number written in a comment, which is exactly why it rots. The counts read as specifications of the scan scope, and this header is specifically the place a contributor goes to learn what the link checker looks at — the paragraph exists to explain that the scope was once pointed at the wrong tree.
The remedy already exists in this repository and was applied twice more on objectui#7448: state the POPULATION, and point at the reading, rather than copying a count.
check-doc-snippet-types.mjs'sUNGATED_DOCSheader records the original ("a pointer to the list now rather than a copy of its length").Not a defect in the gate
check-links.ymlcomputes its scope from theargsglob list, not from this comment. No verdict depends on either number. This is prose accuracy only, which is why it is filedfindingrather than queued.Proposed shape if picked up
objectui#7448 landed a pin in each gate's own test file that fails when a numeral directly qualifies a document-population noun anywhere in that workflow's header comments.
scripts/__tests__/check-links-workflow.test.tsalready exists and already pins this workflow's scope to the directory the site config declares, so it is the natural home for the third copy of that pin. The header edit itself is two sentences.Back-link: objectui#7448.
Generated by Claude Code