fix(docs): upstream the harness docs-site fixes — nested anchors, dead graph links, baseUrl, ADR casing - #224
Conversation
protectedRanges() guarded the reference transforms with `<[^>]+>`, which ranges a bare tag only. That covers an artifact ID sitting in an anchor's href but not one sitting in its link text — and the link text is the half that actually produces a nested anchor, so Prior art: <a href="/decisions/ADR-0001-x" className="rfc-ref">ADR-0001</a> still had its label wrapped a second time on a later pass. Range whole <a>…</a> elements alongside bare tags. Back-ported from joestump/claude-plugin-sdd#224, which upstreamed this guard and made that one change to it; the two copies now match. Tests: the plugin suite grows a `delta` fixture domain carrying every shape the linkifier must leave alone — a reference in inline code, in a markdown link, and inside an emitted anchor — plus a line mixing a protected span with a bare mention, pinning the guard as span-scoped rather than line-scoped. Only the anchor case fails without this change.
597a1e7 to
326141e
Compare
Skill Eval ResultsMode:
|
…an anchor
transformSpecReferences and transformAdrReferences rewrote every ADR-NNNN /
SPEC-NNNN match on a line, including matches that were already inside inline
code, a markdown link, or an anchor emitted by an earlier pass. The markdown
case is the common one: `[ADR-0006](adr-0006-thing.md)` had its label wrapped a
second time, producing
<a href="/specs/gamma/spec#adr-0001"><a href="/decisions/ADR-0001">ADR-0001</a></a>
which is invalid HTML and which some minifiers reject outright rather than
repair.
Port protectedRanges/matchOffset/isProtected from the harness docs site, where
this was found and fixed, into all three copies of the transform: the docs-site
scripts, the vendored Docusaurus plugin template, and the sync-spec-docs
integration lib.
The guard is span-scoped, not line-scoped, so a bare mention elsewhere on a
line with a protected span still resolves.
Ranged separately from harness: whole <a>...</a> elements as well as bare tags.
`<[^>]+>` alone protects an ID sitting in an href but not one sitting in the
link text, and the link text is the half that actually nests.
Tests cover all three shapes in each of the three copies, plus an end-to-end
pass over the plugin template's own fixture. Nine of them fail without the
guard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-0023 and SPEC-0018 are the SDD plugin's own artifacts describing the frontmatter DAG. The generators hardcoded links to them in three places — the per-artifact "Related Artifacts" mini-DAG, the ADR/spec index "Hierarchy" section, and the Architecture Graph page — so every consumer repo that does not happen to have those two artifacts got dead links on every ADR page, every index page, and the graph page. Docusaurus's onBrokenLinks is 'warn' by default, so the build stayed green and nobody noticed. Port citeGraphArtifacts() from the harness docs site: link them when the repo actually has them, name them as plain text otherwise. The load-bearing half of the sentence is the `/sdd:graph` hint, which is true everywhere. Both lookups go by content rather than by number — the ADR by its filename slug, the spec by its directory — so a repo that renumbered them still gets links. Applied to the vendored Docusaurus plugin template and the docs-site scripts, where the helper lives in the shared graph-data module. Deliberately not applied to templates/integration/sync-spec-docs: that copy already emits plain text, and its pages are written under a pathPrefix that these site-absolute links would not respect, so adding it there would introduce the dead links rather than remove them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…only regex Cross-reference chips are emitted as raw `<a href>` attributes. Docusaurus does not rewrite those — unlike markdown links, which it resolves against baseUrl itself — so the chips have to carry the prefix already. Both baseUrl readers looked for a `baseUrl: '...'` string literal in docusaurus.config.ts and fell back to '' when they did not find one. Neither ever found one: this repo's config and the one in templates/docusaurus both assign `baseUrl: BASE_URL` from a const, so CI can override it per host, and there is no literal on that line to capture. Every chip was therefore emitted at the host root. In this repo that is 944 links, all 404 on the published site. Nothing caught it. The dev server and `npm run serve` both mount at baseUrl, so it looks right locally, and onBrokenLinks does not inspect raw href attributes, so CI stayed green. Two fixes, because the two readers have different information available: - The vendored Docusaurus plugin now reads `context.siteConfig.baseUrl` and `context.siteConfig.title`. Docusaurus hands every plugin the fully-evaluated config; scraping the file was never the supported path. Ported from harness. - The docs-site scripts run outside Docusaurus and have no siteConfig, so they keep parsing the file — but through a shared readBaseUrl() that follows a const assignment to its declaration. templates/integration/sync-spec-docs already read context.siteConfig.baseUrl and needed no change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Repos name their ADR files either `ADR-0001-thing.md` or `adr-0001-thing.md`. Every filename regex in the generators was case-sensitive, so a lowercase-naming repo silently lost: - all ADR graph nodes, hence all edges, every per-artifact mini-DAG, and the Architecture Graph page entirely - the badge header on every ADR page (isNumberedAdr never matched) - every entry in the ADR link mapping, so ADR-NNNN mentions stayed plain text Make the filename patterns case-insensitive and normalize the extracted ID to uppercase, since the graph keys its nodes by a constructed `ADR-NNNN` and a lowercase key would miss on lookup. That third failure was masking a fourth. The badge header renders `decision-makers`, which the MADR template writes as a list — YAML hands that back as an array, and escapeJsxAttr called .replace on it. Fixing the casing alone converts a silent omission into "str.replace is not a function" at build time, so toDisplayString() lands with it: arrays join, Dates render as YYYY-MM-DD rather than gaining a midnight-UTC time the author never wrote. Both ported from harness. The casing fix spans all three copies; toDisplayString applies only to the plugin template, which is the copy that parses frontmatter into real YAML types — the other two extract with line regexes and always get strings back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
citeGraphArtifacts built the spec link as /specs/<dir>/spec, the nested route. #223 made that route conditional: a domain holding only spec.md renders flat at /specs/<dir>, and every other consumer -- the mapping, the specs index, the transform itself -- now reads the layout instead of assuming it. This one still assumed, so a project whose artifact-graph domain carries no design.md got a dead link on every ADR page, every index page, and the graph page: precisely the class of bug the citation change was introduced to remove. Both copies fixed, with a regression test covering nested and flat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
326141e to
64514d2
Compare
Code review —
|
Upstreams four fixes that
stump.wtf/harnessmade in its vendored copy ofsdd-contentand never sent back, so every other consumer still carries them. Reconciles the template against harness's copy end to end — what is left diverging is harness's own homepage hero (branding, not a fix) and #222's spec-keying changes going the other way.Stacked on #222 — its
spec-references.test.jsis the suite these tests extend. GitHub retargets this tomainwhen #222 merges.1. Nested anchors (the reported bug)
transformSpecReferences/transformAdrReferencesrewrote everyADR-NNNN/SPEC-NNNNmatch on a line, including matches already inside inline code, a markdown link, or an anchor from an earlier pass.[ADR-0006](adr-0006-thing.md)had its label wrapped a second time:Invalid HTML; some minifiers reject it rather than repair it. Ports
protectedRanges()/matchOffset()/isProtected()into all three copies. The guard is span-scoped, so a bare mention elsewhere on a protected line still resolves.One deliberate change from harness: whole
<a>…</a>elements are ranged as well as bare tags.<[^>]+>alone protects an ID sitting in anhrefbut not one sitting in the link text — and the link text is the half that actually nests. Worth pulling back into harness.2. Dead
ADR-0023/SPEC-0018linksThose are the plugin's own artifacts. Three call sites linked them unconditionally, so any consumer repo without them got dead links on every ADR page, every index page, and the graph page.
onBrokenLinksdefaults towarn, so builds stayed green.citeGraphArtifacts()links them when present and names them as plain text otherwise. Both lookups go by content — the ADR's filename slug, the spec's directory — rather than by number, so a repo that renumbered them still gets links.Not applied to
templates/integration/sync-spec-docs: that copy already emits plain text, and its pages are written under apathPrefixthese site-absolute links would not respect, so adding it there would introduce the dead links.3.
baseUrlnever resolved — 944 broken links in this repoBoth readers looked for a
baseUrl: '...'literal indocusaurus.config.tsand fell back to''. Neither ever matched: this repo's config andtemplates/docusaurus's both assignbaseUrl: BASE_URLfrom a const. Every cross-reference chip was emitted at the host root.Measured on
mainbefore the fix, then after:Nothing caught it. The dev server and
npm run serveboth mount atbaseUrl, so it looks right locally, andonBrokenLinksdoes not inspect rawhrefattributes, so CI stayed green. Markdown links masked it further — Docusaurus resolves those againstbaseUrlitself; the raw<a href>chips these transforms emit are not resolved and need the prefix baked in.Two fixes, because the readers have different information:
context.siteConfig.baseUrl/.title. Docusaurus hands every plugin the fully-evaluated config; scraping the file was never the supported path. (Harness's fix.)docs-site/scriptsrun outside Docusaurus and have nositeConfig, so they keep parsing the file — through a sharedreadBaseUrl()that follows a const to its declaration.templates/integration/sync-spec-docsalready readcontext.siteConfig.baseUrl.4. Lowercase ADR filenames, and the crash they were hiding
Repos name ADRs either
ADR-0001-thing.mdoradr-0001-thing.md. Every filename regex was case-sensitive, so a lowercase-naming repo silently lost all ADR graph nodes (hence all edges, every mini-DAG, and the graph page entirely), the badge header on every ADR page, and every ADR link mapping.That third failure masked a fourth: the badge header renders
decision-makers, which the MADR template writes as a list and YAML hands back as an array —escapeJsxAttrcalled.replaceon it. Fixing the casing alone converts a silent omission intostr.replace is not a functionat build time, sotoDisplayString()lands with it. Arrays join; Dates render asYYYY-MM-DDrather than gaining a midnight-UTC time the author never wrote.Beyond harness:
status.toUpperCase()gets the same coercion, since it crashes on a non-string status one call beforeescapeJsxAttrwould.Verification
make test lint scangreen. Docs site builds; the only broken links reported are two pre-existing/skills/indexones, unchanged by this branch.readBaseUrl's const case fails against the old regex.base-url.test.js,adr-filenames.test.js. Both exercise every copy that has the defect, asspec-references.test.jsalready does.