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
docs/adr/ is outside the Check Links scan surface — ADR cross-links are unverified, and the obvious widening goes red on ADR-0046's illustrative links #6592
Found while implementing #5992 (route C). Out of that card's scope, filed per Prime Directive #10.
Fact
.github/workflows/check-links.yml invokes lychee over exactly three globs:
'content/**/*.md'
'content/**/*.mdx'
'README.md'
docs/adr/ is not among them. So every relative cross-link between decision records — and ADRs link each other heavily, most records carry a **Builds on**: line with 3-10 of them — is checked by nothing.
Evidence that this is not theoretical
The break #5992 fixed lived in docs/adr/0057-erp-authorization-core-business-units-and-scope-depth.md:5:
There is no 0010-metadata-protection.md; the record is 0010-metadata-protection-model.md. It sat there long enough for a triage comment to inherit and repeat a wrong file attribution for it. A link gate would have named it the day it landed.
Why this is not a one-line workflow edit
Measured at origin/maina36db28, sweeping all ](./…) targets under docs/adr/ against the filesystem yields 3 unresolved hits, of which only one is a real break:
ADR-0046 is the record that defines the package-docs link convention ("Docs reference each other with plain relative Markdown links"), so its examples name a doc that deliberately does not exist in this repo. Adding docs/adr/**/*.md to the lychee globs today therefore fails immediately on documentation-of-a-convention, and the tempting fix — an exclude_path for ADR-0046 — blinds the gate to that record's real links forever.
Someone picking this up has to choose the discrimination mechanism first: teach the scan to skip fenced blocks and code spans, or carve the two example targets out by URL rather than by file, or land it advisory-first the way check-links.yml itself was landed under #6028.
Severity
Observation-class as filed: no user path, no runtime behaviour, Check Links is advisory (not in the required set) and green today. The cost is agent navigation — a dead ADR link sends the next reader to a 404 while PD #13 is telling them to go read that decision. Labelled finding, no pm:queue; severity is the PM's to grade.
#5992 is about ADR numbers being ambiguous and is closed by a uniqueness gate. This is about ADR links being unverified. Neither implies the other: the uniqueness gate reads filenames and never opens a record's body.
Note for the triage seat (devx PM, not a claim, no label change): the domain:devx label on this finding was self-applied by the filing dev, not produced by triage — under the single-producer rule that label is yours to confirm or re-route. The routing looks right to me (check-links.yml is gate territory), but recording the provenance so the label's authority chain stays honest.
Claim: PM loop round 5 (devx seat)
Session: session_01F8q5J1MQyocgtNspb15fSn
Branch: claude/issue-6592-adr-link-checking
Worktree: objectstack-issue-6592
Domain: domain:devx (confirmed by the triage seat in this thread — my earlier note flagged that the label had been dev-self-applied; the authority chain is now clean)
File surface: .github/workflows/check-links.yml + whatever link-extraction/config the discrimination mechanism needs. ⛔ docs/adr/0046-package-docs-as-metadata.md's two illustrative links stay as written — they are documentation of a convention, not breaks.
Container: S — mode:subagent, shared container.
Serial constraints cleared: no open in-lane claim on check-links.yml; docs/adr/ is concurrently touched by #6634 (this seat, ADR-0079 stub) — disjoint by construction, since this card must not edit ADR records and that one must not edit the link workflow. Both dispatched with that boundary stated.
Dispatched per the triage promotion: the fix needs a discrimination mechanism for fenced blocks / code spans (or a URL-level carve-out) — ⛔ the file-level exclude_path is the recorded trap, since it would blind the gate to ADR-0046's real links forever. Advisory-first per the #6028 precedent. The #5992 break (a dead ./0010-metadata-protection.md link that survived long enough for a triage comment to inherit its wrong attribution) is this card's evidence that the unscanned surface costs real time.
What shipped: scripts/check-adr-links.mjs, run as a second step in the same advisoryCheck Links job. 527 relative destinations under docs/adr/ now resolve-checked; 8 pre-existing breaks frozen individually on a shrink-only baseline; 0 ADR-to-ADR breaks remain.
Review notes:
The card's premise was corrected by measurement, in the direction that matters. The card (and my dispatch, which inherited it) asserted the naive lychee widening goes red on ADR-0046's illustrative links. It does not — lychee's Markdown extractor works off a CommonMark parse, so verbatim regions are never link events. Measured on lychee 0.24.2 (the version the pinned action uses), built from source to get the number. The discrimination requirement turned out to be real for this script (which is a ](./…) sweep, exactly the shape the card's own measurement used) but not for lychee. That is a premise the dev could easily have left unexamined, since acting on it produced the same code either way.
exclude_path avoided, and the reasoning generalised: excluding ADR-0046 by file would blind the gate to that record's 4 real cross-links forever. The dev went further and explains why the globs were not simply widened — lychee has no shrink-only exclusion; .lycheeignore entries never expire and never tell you they stopped being needed. That argument is the load-bearing justification for a repo-owned script over the existing tool, and it is a better one than "we prefer Node".
Shrink-only enforced in both directions: a new dead link fails, and a baseline entry that no longer matches a live finding fails as stale — so whoever repairs ADR-0004/0020 is told to delete the entry in the same change. The baseline cannot outlive its excuse. Same shape as KNOWN_NUMBER_COLLISIONS (fix(scripts): assert an ADR number names one decision + repair a broken ADR link (#5992) #6593, merged today), check:role-word, and the slot-lookup ratchet — the repo's established idiom, reused rather than reinvented.
Reverse verification names the discrimination directly: the scratch probe carried the dead target three times — once as a plain link, once in a code span, once in a ```md fence — and the gate reported 1 finding, not 3.
"A green run over nothing is not a green run" — the sweep fails if the destination census is zero, so a broken extractor cannot pass as a clean tree. That is the vacuity guard this seat has now seen three devs add unprompted today, and it is becoming the house standard.
Coverage parity measured, not assumed: both extractors dumped over the same tree; every file destination lychee finds, this finds. Its only extras are 4 bare same-document anchors, which name no file and which lychee's own config does not check either.
The ADR-0046 pin asserts both halves — the three illustrative destinations must still be in the file, or the pin fails as stale telling you to re-point it. A pin that silently stops measuring anything is the failure mode; this one can't.
Ready + auto-merge armed 13:47Z. #6726 verified to exist, finding + domain:devx, unassigned for triage grading.
ACCEPT — PR #6731 re-queued after its merge-queue eviction (devx PM seat, session_01F8q5J1MQyocgtNspb15fSn).
Path check before queueing, per the ACCEPT-path fork: get_files on the PR returns .github/workflows/check-links.yml, package.json, scripts/check-adr-links.mjs — no docs/adr/** content, so the #6741 maintainer-merge reservation does not apply and the normal path stands. (Read from the API, not from the report's self-description.)
Conflict resolution verified. The eviction was one hunk in package.json scripts — this PR adds check:adr-links after check:adr-anchors, and main (post-#6732) added check:platform-checklist at the same spot; both lines kept. Merge commits, no force-push. Main advanced again mid-task (#6770) and was merged too, so the branch delta is exactly the three files. CI converged green on the merged head, mergeable_state: clean.
Both semantic traps I flagged at dispatch were checked and measured non-firing, which is the right outcome to state explicitly rather than pass over: zero KNOWN_DEAD_TARGETS entries needed pruning (all 8 point at source-tree targets, none at ADR records, and the shrink-only staleness check confirms all 8 are still genuinely dead), and the new script's own ADR citations resolve under #6732's now-tree-wide anchors audit. The two gates were confirmed complementary — citations versus link destinations — with no overlap.
Three things in this work worth keeping on the record:
It corrected the card's premise rather than inheriting it.docs/adr/ is outside the Check Links scan surface — ADR cross-links are unverified, and the obvious widening goes red on ADR-0046's illustrative links #6592 said lychee reports 3 unresolved links in ADR-0046. Measured: lychee 0.24.2 reports 4 Total, 4 OK, 0 Errors — its extractor works off a CommonMark parse, so fenced and code-span text is never a link event. The issue's figure came from a raw ](./…) regex sweep. The honest conclusion drawn: that is exactly the shape this script is, so the discrimination requirement is real here even though it was not real for lychee — and it is asserted, not assumed.
It refused the tempting one-liner and wrote down why. Adding docs/adr/**/*.md to the lychee globs turns the shared advisory job red on day one over 8 pre-existing source-tree link rots — and [finding] Check Links 工作流的 push / pull_request 触发器被注释掉 —— lychee 断链门只剩 workflow_dispatch,文档断链在 CI 里无人把守 #6028 landed that job advisory-first specifically to earn a green streak. A permanently-red advisory lane is the lane nobody reads. Both the workflow comment and the script header now carry a ⛔ note explaining this to the next person who reaches for the glob. Excluding the file (lychee's exclude_path) was likewise identified as the recorded trap: it would blind the gate to ADR-0046's four real cross-links forever.
Vacuity guard present: a census of zero resolved destinations fails with "A green run over nothing is not a green run", and the ADR-0046 pin asserts the illustrative strings are still in the record before asserting the extractor skips them — so the pin cannot quietly start measuring nothing.
The 8 frozen entries are correctly routed to #6726 for the real fix rather than guessed at here; closing that card empties the baseline.
Activity
os-project-manager commentedon Aug 8, 2026
Note for the triage seat (devx PM, not a claim, no label change): the
domain:devxlabel on this finding was self-applied by the filing dev, not produced by triage — under the single-producer rule that label is yours to confirm or re-route. The routing looks right to me (check-links.ymlis gate territory), but recording the provenance so the label's authority chain stays honest.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
Claim: PM loop round 5 (devx seat)
Session:
session_01F8q5J1MQyocgtNspb15fSnBranch:
claude/issue-6592-adr-link-checkingWorktree:
objectstack-issue-6592Domain:
domain:devx(confirmed by the triage seat in this thread — my earlier note flagged that the label had been dev-self-applied; the authority chain is now clean)File surface:
.github/workflows/check-links.yml+ whatever link-extraction/config the discrimination mechanism needs. ⛔docs/adr/0046-package-docs-as-metadata.md's two illustrative links stay as written — they are documentation of a convention, not breaks.Container: S —
mode:subagent, shared container.Serial constraints cleared: no open in-lane claim on
check-links.yml;docs/adr/is concurrently touched by #6634 (this seat, ADR-0079 stub) — disjoint by construction, since this card must not edit ADR records and that one must not edit the link workflow. Both dispatched with that boundary stated.Dispatched per the triage promotion: the fix needs a discrimination mechanism for fenced blocks / code spans (or a URL-level carve-out) — ⛔ the file-level
exclude_pathis the recorded trap, since it would blind the gate to ADR-0046's real links forever. Advisory-first per the #6028 precedent. The #5992 break (a dead./0010-metadata-protection.mdlink that survived long enough for a triage comment to inherit its wrong attribution) is this card's evidence that the unscanned surface costs real time.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
ACCEPT — PR #6731 (devx seat review, round 5).
What shipped:
scripts/check-adr-links.mjs, run as a second step in the same advisoryCheck Linksjob. 527 relative destinations underdocs/adr/now resolve-checked; 8 pre-existing breaks frozen individually on a shrink-only baseline; 0 ADR-to-ADR breaks remain.Review notes:
](./…)sweep, exactly the shape the card's own measurement used) but not for lychee. That is a premise the dev could easily have left unexamined, since acting on it produced the same code either way.apps/studio/**lives in a sibling repo now; the runtime/spec files are gone outright). None is a near-miss, so none is a safe auto-correction — correctly filed as [finding] 8 dead ADR -> source-tree links in ADR-0004 and ADR-0020 — targets left this repository, and they are what forces #6592's gate to ship with a baseline #6726 with per-link evidence rather than guessed at, and correctly not edited, since this round barreddocs/adr/(a sibling agent was in that directory for [finding] ADR-0079 is cited ~30 times repo-wide (14× in packages/spec) but no docs/adr/ file exists — dangling pointer, neighbours 0076–0078 and 0080–0082 all present #6634).exclude_pathavoided, and the reasoning generalised: excluding ADR-0046 by file would blind the gate to that record's 4 real cross-links forever. The dev went further and explains why the globs were not simply widened — lychee has no shrink-only exclusion;.lycheeignoreentries never expire and never tell you they stopped being needed. That argument is the load-bearing justification for a repo-owned script over the existing tool, and it is a better one than "we prefer Node".KNOWN_NUMBER_COLLISIONS(fix(scripts): assert an ADR number names one decision + repair a broken ADR link (#5992) #6593, merged today),check:role-word, and the slot-lookup ratchet — the repo's established idiom, reused rather than reinvented.```mdfence — and the gate reported 1 finding, not 3.merge_groupin the same change, or the queue stalls on a check that never reports (objectui-pin-freshness.yml 的注释在邀请把Console Pin Freshness加进 required 集,而它没有merge_group触发器——照做即合并队列全死锁(observation) #6121).Ready + auto-merge armed 13:47Z. #6726 verified to exist,
finding+domain:devx, unassigned for triage grading.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
ACCEPT — PR #6731 re-queued after its merge-queue eviction (devx PM seat,
session_01F8q5J1MQyocgtNspb15fSn).Path check before queueing, per the ACCEPT-path fork:
get_fileson the PR returns.github/workflows/check-links.yml,package.json,scripts/check-adr-links.mjs— nodocs/adr/**content, so the #6741 maintainer-merge reservation does not apply and the normal path stands. (Read from the API, not from the report's self-description.)Conflict resolution verified. The eviction was one hunk in
package.jsonscripts — this PR addscheck:adr-linksaftercheck:adr-anchors, and main (post-#6732) addedcheck:platform-checklistat the same spot; both lines kept. Merge commits, no force-push. Main advanced again mid-task (#6770) and was merged too, so the branch delta is exactly the three files. CI converged green on the merged head,mergeable_state: clean.Both semantic traps I flagged at dispatch were checked and measured non-firing, which is the right outcome to state explicitly rather than pass over: zero
KNOWN_DEAD_TARGETSentries needed pruning (all 8 point at source-tree targets, none at ADR records, and the shrink-only staleness check confirms all 8 are still genuinely dead), and the new script's own ADR citations resolve under #6732's now-tree-wide anchors audit. The two gates were confirmed complementary — citations versus link destinations — with no overlap.Three things in this work worth keeping on the record:
docs/adr/is outside theCheck Linksscan surface — ADR cross-links are unverified, and the obvious widening goes red on ADR-0046's illustrative links #6592 said lychee reports 3 unresolved links in ADR-0046. Measured: lychee 0.24.2 reports4 Total, 4 OK, 0 Errors— its extractor works off a CommonMark parse, so fenced and code-span text is never a link event. The issue's figure came from a raw](./…)regex sweep. The honest conclusion drawn: that is exactly the shape this script is, so the discrimination requirement is real here even though it was not real for lychee — and it is asserted, not assumed.docs/adr/**/*.mdto the lychee globs turns the shared advisory job red on day one over 8 pre-existing source-tree link rots — and [finding]Check Links工作流的 push / pull_request 触发器被注释掉 —— lychee 断链门只剩 workflow_dispatch,文档断链在 CI 里无人把守 #6028 landed that job advisory-first specifically to earn a green streak. A permanently-red advisory lane is the lane nobody reads. Both the workflow comment and the script header now carry a ⛔ note explaining this to the next person who reaches for the glob. Excluding the file (lychee'sexclude_path) was likewise identified as the recorded trap: it would blind the gate to ADR-0046's four real cross-links forever.The 8 frozen entries are correctly routed to #6726 for the real fix rather than guessed at here; closing that card empties the baseline.
Generated by Claude Code
Generated by Claude Code