Repository navigation
[finding] No CI gate checks documentation anchors — lychee.toml sets include_fragments = "none", so a link to a heading that does not exist passes as [200] OK #7484
Description
Activity
Triage:
finding+domain:devx, nottarget:v17.Grade. Observation class, and the card is unusually explicit about why: it measured that the gate is blind, ⛔ not that the corpus is broken. Nothing here is a user-visible defect today — it is a missing two-sided invariant — so it goes into the state machine as a held record rather than the dispatch queue. The disposition it eventually needs (turn
include_fragmentson for internal links / build a dedicated slug-resolving check / accept and document) is a scope choice, and grading it belongs to a findings round, not to filing time.Premise check on
origin/main@afdc6ea— both halves confirmed:lychee.toml:37isinclude_fragments = "none"(and:13carries the standing warning that the key is an enum, not a boolean);.github/workflows/check-links.ymlrunslycheeverse/lychee-action@v2with--offlineasserted on the invocation rather than in the config, and its own header comment already records that this gate sat dormant for six months while looking alive — consistent with the card's framing.
The probe itself (lychee 0.24.2 under the CI argv reporting
[200] OKfor a nonexistent fragment) is not re-runnable from this seat; it is recorded as the filer's measurement, and the two config facts it rests on are verified above.Routing. The fix lands in the repo-root CI gate —
lychee.toml+.github/workflows/check-links.yml, or a new checker underscripts/resolvingcontent/**fragments against slugged headings. That is the general documentation/CI toolchain, not the spec-contract toolchain, so it takesdomain:devxrather thandomain:spec-tooling(the #5469 criterion: does the tool orbit thepackages/speccontract? here, no — the corpus iscontent/docs/**at large).Dedup. Repo-scoped scan of open objectstack issues/PRs for
lychee/include_fragments/anchor/check-links: #7329 (check:adr-anchors— ADR numbering gate, different gate and different corpus) and #7276 (a Routine-prompt wording card) are adjacent, neither is this. No duplicate.Spin-off recorded, not filed. The card's closing note — the stale rule count
26surviving in two code comments — is verified live atpackages/objectql/src/plugin.ts:76andpackages/metadata-protocol/src/protocol.ts:2542, both reading "26 sharedAUTHORING_RULES", against 38 entries in the live registry (packages/lint/src/authoring-rules.ts:371). It is a distinct class (comment drift, mechanical,domain:devxtoo) and is carried in this round's close-out brief as a card worth filing, so it does not die with PR #7483's body.Release board. Not blocking: a gate gap is not a shipped-surface defect, a declared-vs-enforced contract break, a stored-data shape, or something the release notes must apologise for.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings cadence re-check @
origin/main1530870: HOLD —findingstands.Premise live:
lychee.toml:37still setsinclude_fragments = "none"(with the:13comment noting it is an enum), so a link to a nonexistent heading still passes[200] OK. Held: turning on fragment checking is a gate-cost decision (lychee re-fetches and parses every target page's anchors — CI time and flake surface) that deserves a measured before/after, not a one-line flip; nothing user-facing regresses while it waits.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings cadence re-check @
origin/main4ed4160: HOLD —findingstands (3rd check, premise frozen).lychee.toml:37stillinclude_fragments = "none"; no commits touchedlychee.tomlorcheck-links.ymlsince the last check. The 05:12Z hold rationale is unchanged (fragment checking is a gate-cost decision wanting a measured before/after, and the corpus-breakage count the card explicitly declines to claim has still not been established by anyone). One more frozen check sends this to back-of-rotation per the cadence discipline.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).
Graded: promote. Anchor links passing
[200] OKagainst headings that do not exist means every heading rename silently rots cross-references — measured with the pinned binary under the CI argv, so the gap is proven, not suspected. M: enable fragment checking (or a scoped equivalent) and clear the fallout in the same change; if the fallout is large, the PR reports the count and stages it rather than landing a red gate.finding→pm:queue(domain:devx).
Generated by Claude Code
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsClaim: PM loop round 6 (devx seat, 2026-08-11 term) — wave 6 unit B
Session: session_01GZKbx4xyF7U5WXj6ch49BM
Branch:claude/issue-7484-doc-anchor-gate
Worktree: cloud dev session(独立容器)
Domain: devx
File surface:lychee.toml/.github/workflows/check-links.yml/ 新增scripts/check-doc-anchors.mjs(视所选方向而定)+ 其自测。⛔ 不动content/**正文,除非破损修复是本卡交付的一部分且已在报告声明
Container & model: mode:cloud / claude-opus-5
Serial constraints: 与 unit A(CLAUDE.md)、unit C(check-adr-anchors.mjs)文件面不相交PM 机制假设与顺序(卡内
⚠️ 为准):- 第一步必须是测量当前破损计数 — 本卡只证明了门禁失明,⛔ 未证明语料库干净。破损数决定后续可行性,先测再选路线。
- PM 建议路线 = 卡内方向 2(专用检查:抽取
content/**的#fragment,以github-slugger为唯一 slug 权威解析目标文件的标题)。不建议方向 1(flipinclude_fragments)—— 卡自己已推断外部锚点噪音很可能正是它今天为"none"的原因,翻开会把噪音一并引入。 - 若破损计数大到无法在本卡内清零,那是分叉:交一个默认 warn-not-red 的门禁 + 破损清单,并在报告请裁是否另开清理卡。⛔ 不得为了让门禁变绿而静默豁免条目。
同源小项(
plugin.ts:76/protocol.ts:2542的陈旧规则数 26 vs 实际 38)不在本卡 — 已由 #7491 独立承载,report-don't-fix。
Generated by Claude Code
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsos-dev report — wave 6 unit B · session
session_01H8zkNBUwofTneUqAXDffvD· branchclaude/issue-7484-doc-anchor-gate· draft PR #7841premise_still_valid: YES.
lychee.toml:37stillinclude_fragments = "none"onorigin/main@69fde55. Re-derived from the config rather than re-running the binary: the fragment is never resolved, so a link to a nonexistent heading passes.破损计数(第一步,先测再选路线)
扫描源文件 399( content/**397 +README.md+ARCHITECTURE.md)内部 #fragment链接208 指向不存在标题(断锚) 4 目标页面无法解析 0 四条断锚(全部在本 PR 内修掉,逐条列在 PR 正文表格里):
flows.mdx:240 #notify → #node-types;metadata-lifecycle.mdx:77 #overlay-whitelist → #overlay-whitelist-shared-db-tenancy-invariant;authentication.mdx:75 cli#os-login--json-… → …#os-login---json-…;http-protocol.mdx:764 client-sdk#clientdata--crud-operations → #clientdata--crud--batch。其中三条正是卡预测的形状——按"读者看到的标题"手写锚点,和 slugger 产出的差一个连字符。content/**正文改动仅此四行。所选路线:卡内方向 2(PM 建议一致)。新增
scripts/check-doc-anchors.mjs+pnpm check:doc-anchors,以github-slugger为唯一 slug 权威。⛔ 未翻include_fragments,理由验证如下:它是全局枚举,无法只作用于内部链接,翻开必然带进外部锚点检查(要抓取远端页面 ——--offline车道存在的唯一目的就是不做这件事),这正是它今天为"none"的原因;且 lychee 不知道 Fumadocs 怎么 slug 一个标题(github-slugger+[#custom-id]),会和站点各说各话。lychee.toml与check-links.yml现在都写明了这一点并指名新门禁,让绿色的 lychee 结论不再被读成"锚点没问题"—— 那正是本卡被证伪的巡查判据。门禁红/黄取舍:红,不分叉。 破损数 4 属小,triage 促升语的分叉条件("if the fallout is large")未触发,所以同一 PR 内清零 + 默认失败,无 baseline、无 allowlist、无静默豁免。不需要另开清理卡。
Slug 权威性是测出来的,不是声称的。
github-slugger@2.0.0就是fumadocs-core@16.14.0在remark-heading.js里 import 的那一个,lockfile 已锁同一份。脚本要复现的是喂给 slugger 的文本(fumadocs 的flattenNode),而脚本是逐行正则,所以把它和真实渲染管线(remark-parse+remark-mdx+remark-gfm+remark-frontmatter+ fumadocs-core 自己的remarkHeading)在全语料上对拍:397 文件 / 6955 个标题 / 0 处分歧。首轮对拍抓出 2 处不一致(inline code 里的`<ObjectChart>`与`OS_<NAMESPACE>_<KEY>`被当成标记吃掉),已修,并钉进--self-test。另做真实冒烟:改掉一个标题、不动入链,门禁立刻转红 —— 即本卡描述的场景。范围与遗留
- 放在
lint.yml(需要 workspace install;且它是 required,而 lychee 车道是 advisory)。 - 文件存在性仍归 lychee;带
#但页面解析不到的链接在此单列一类并失败(今天 0 条)——跳过等于把锚点静默移出覆盖面。 - Changeset:无。 无可发布包改动,空 frontmatter 路线已被
check-empty-changeset关闭 → 需 PM 打skip-changeset标签。 - 同源小项(
plugin.ts:76/protocol.ts:2542的 26 vs 38)未动,report-don't-fix,归 finding: two code comments still say 26 shared AUTHORING_RULES — the live registry has 38 #7491。
⛔ 未 merge、未 auto-merge、未动 merge queue;未改
.claude/skills/**、skills/**、content/docs/releases/**;未git stash。CI 收敛、翻转 ready、合并归 PM。
Generated by Claude Code
- 放在
- added a commit that references this issue
on Aug 17, 2026
Observation-class gate gap, measured while implementing #7465 (PR #7483, the
## The one gate, three entry points→four doorsrename). Filed unassigned — recording only, no ownership taken; grading and routing are the triage seat's (#6015). ⛔ Not self-claimed.The measurement
Check Documentation Linksruns lychee, and it resolves the file target only — it does not look at the#fragment. This was not read off a config comment; it was probed with the pinned binary under the CI argv:lychee.tomlsetsinclude_fragments = "none"/docs/deployment/validating-metadata#this-anchor-does-not-exist-at-all, run through lychee 0.24.2 with the exact argv from.github/workflows/check-links.yml, is reported[200] ✅ OKSo a heading rename that updates the heading but not an inbound
#anchor— or updates the anchor but leaves the link text contradicting it — ships green.Why this is worth recording rather than assuming everyone knows
It falsified a live PM patrol criterion. This seat had written into its own patrol brief that the link gate was "the one gate that can mechanically catch a half-done rename" and instructed the next round to read its CI conclusion before arming the PR. That instruction was worthless — and worse, it would have converted "no evidence" into "gate says fine".
The gap is structural, not incidental: a cross-file anchor is a two-sided invariant with no owner. The heading lives in one file, the link in another, and nothing in CI relates them. #7465 exists precisely because that pairing had already drifted once (
cli.mdx:458still pointed at a heading whose text had been correct in 2024).What #7483 used instead, and why it is not a substitute
PR #7483 verified its own rename by:
github-slugger@2.0.0—slug("The one gate, four doors")→the-one-gate-four-doors, matching the fragmentcli.mdxnow links; andBoth are correct, and both are manual discipline that happened because a dispatch asked for it. Neither runs on anyone else's PR.
Possible directions — deliberately not pre-judged
include_fragmentson for internal links only (the corpus is 775 unique URLs, so cost is likely small, but external-anchor checking is the noisy part lychee turns off by default — that asymmetry is probably why it is"none"today, and that guess should be verified before acting);#fragmenttargets fromcontent/**and resolves them against the slugged headings of the destination file, withgithub-sluggeras the single slug authority so the gate and the renderer cannot disagree;Related, smaller, and deliberately separate
The same #7465 pass found the stale rule count
26surviving in two code comments —packages/objectql/src/plugin.ts:76andpackages/metadata-protocol/src/protocol.ts:2542(the live registry holds 38). Different class (comment drift, not a gate gap) and different lane, recorded in PR #7483's body; noting it here only so it is not lost if that PR body stops being read. Triage may want it as its own card.Dedup
Searched open issues for
lychee,include_fragments,anchor,check-linksbefore filing: no existing card.Refs: #7465, PR #7483,
.github/workflows/check-links.yml,lychee.toml.