From 15f8bd7e2656bfc262d48d9a16dcf8f0f0ec40d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Thu, 27 Aug 2026 16:03:16 +0200
Subject: [PATCH 1/4] fix(ci): repair the link-check URL extraction (#3)
The extractor trimmed trailing markup in a fixed order: strip one
unbalanced `)`, then strip trailing punctuation. A URL that ends a
markdown link inside a sentence or a bold run carries both at once, so
the `)` survived and lychee checked a truncated URL. Twelve of the
seventeen errors in the report were this, not a broken link.
Trim in a loop instead. Each pass strips trailing punctuation, then one
unbalanced `)`, until the string stops changing. Backticks join the
punctuation set, so a URL inside an inline code span is trimmed too.
Also drop percent-encoded resolver templates (`%7B`/`%7D`), which the
literal `{}` filter missed, and skip `node_modules` so a local
`data/node_modules` cannot pull dependency READMEs into the list.
Exclude `iso.org` and `loc.gov` in lychee.toml: both answer a bot with
403 and neither link is broken.
The report drops from 17 errors to 3. The remaining three are
`blob/main` links to ADR-0001, ADR-0002 and ADR-0006, which `main` does
not carry yet. Merging staging into main for v0.1.0 resolves them.
Closes #3
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_0148cB4MGy2zPeuhemebM38h
---
.github/workflows/links.yml | 35 ++++++++++++++++++++++++++++-------
lychee.toml | 2 ++
2 files changed, 30 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml
index 5cb72a6..bd534eb 100644
--- a/.github/workflows/links.yml
+++ b/.github/workflows/links.yml
@@ -28,15 +28,36 @@ jobs:
- name: Collect external URLs from sources
run: |
mkdir -p lychee
- # Allow `)` inside URLs (Wikipedia disambiguation titles), then strip
- # a single trailing `)` only when unbalanced (handles markdown
- # `[text](url)`). Drop resolver templates containing `{placeholder}`.
- # Restrict to YAML/MD so lockfiles inside data/ are not scanned.
+ # A URL may legitimately contain `(` and `)`, because Wikipedia and
+ # Wikisource put disambiguation titles there. The match therefore
+ # keeps parentheses and the trailing markup is trimmed afterwards.
+ #
+ # The trim runs as one loop, not as a fixed sequence. A URL at the
+ # end of a markdown link, a bold run, or a sentence carries several
+ # layers of trailing markup at once — `…/4.0/).`, `…/arks.org/)**`,
+ # `…/rfc3987))` — so each pass strips punctuation, then one
+ # unbalanced `)`, until the string stops changing. Doing this in a
+ # single pass leaves a `)` behind whenever punctuation follows it,
+ # which is what produced the false 404s in #3.
+ #
+ # Resolver templates are dropped: `{placeholder}` literally, and
+ # `%7B`/`%7D` for the percent-encoded spelling.
+ #
+ # Restrict to YAML/MD, and skip node_modules, so lockfiles and
+ # dependency READMEs inside data/ are not scanned.
grep -rhoE --include='*.yaml' --include='*.yml' --include='*.md' --include='*.mdx' \
+ --exclude-dir=node_modules \
'https?://[^]["'\''<>[:space:]]+' data/ src/content/docs/ \
- | awk '{ n=gsub(/\(/,"&"); m=gsub(/\)/,"&"); if (m>n) sub(/\)$/,""); print }' \
- | grep -v '[{}]' \
- | sed -E 's/[.,;:]+$//' \
+ | awk '{
+ do {
+ changed = 0
+ if (sub(/[.,;:!?*_`]+$/, "")) changed = 1
+ n = gsub(/\(/, "&"); m = gsub(/\)/, "&")
+ if (m > n && sub(/\)$/, "")) changed = 1
+ } while (changed)
+ if (length($0) > 0) print
+ }' \
+ | grep -viE '[{}]|%7b|%7d' \
| sort -u > lychee/urls.txt
echo "Collected $(wc -l < lychee/urls.txt) unique URLs"
diff --git a/lychee.toml b/lychee.toml
index f2f8688..ff0f57f 100644
--- a/lychee.toml
+++ b/lychee.toml
@@ -34,6 +34,8 @@ exclude = [
"^https?://([a-z0-9-]+\\.)*twitter\\.com",
"^https?://([a-z0-9-]+\\.)*x\\.com",
"^https?://([a-z0-9-]+\\.)*opencollective\\.com",
+ "^https?://([a-z0-9-]+\\.)*iso\\.org",
+ "^https?://([a-z0-9-]+\\.)*loc\\.gov",
"^https?://localhost(:\\d+)?(/|$)",
]
From 9da2e7ff8f7756175743d54120a04bca417e3ef5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Thu, 27 Aug 2026 16:03:28 +0200
Subject: [PATCH 2/4] fix(site): render a deprecated record as a tombstone
(#99)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`deprecated` is one of the three tombstone statuses in the model, but no
record template treated it as one. ADR-0004, `specification.md:284` and
the `Status` enum all say so, and `RETIRED_STATUSES` in the compiler
agrees, yet both derivation sites tested `withdrawn` and `blocked` only.
A `deprecated` record therefore rendered with no banner and dropped its
`superseded_by` link, while the `.json` sibling published it.
Decision: the three statuses stay one tombstone category with one
presentation. They differ in reach, not in kind — a `deprecated` record
is retained and still resolves, while a `withdrawn` or `blocked` record
is additionally excluded from `Work` mapping projections — so the
user-facing copy distinguishes them and the markup does not. The
`superseded_by` treatment is the normal one.
This is latent, not live: no record in the registry is `deprecated`
today. The cost lands the first time a promoted record is corrected.
The copy still lives in two templates. #100 single-sources it, and this
decision is what its shared status module has to encode.
Closes #99
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_0148cB4MGy2zPeuhemebM38h
---
src/components/Tombstone.astro | 18 ++++++++++++++++--
src/pages/id/ref/[uuid]/index.astro | 22 ++++++++++++++++++++--
2 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/src/components/Tombstone.astro b/src/components/Tombstone.astro
index 0a3cd14..a51c744 100644
--- a/src/components/Tombstone.astro
+++ b/src/components/Tombstone.astro
@@ -6,8 +6,15 @@ interface Props {
const { status, supersededBy } = Astro.props;
+// ADR-0004 and specification.md §12 make `deprecated`, `withdrawn` and `blocked`
+// one tombstone category, and the compiler agrees (`RETIRED_STATUSES` in
+// scripts/compile.ts). The three differ in reach, not in kind: a `deprecated`
+// record is retained and still resolves, while a `withdrawn` or `blocked` record
+// is additionally excluded from `Work` mapping projections. The banner therefore
+// stays one presentation, and the copy below states which of the three applies.
const isDraft = status === 'draft';
-const isTombstone = status === 'withdrawn' || status === 'blocked';
+const isTombstone =
+ status === 'deprecated' || status === 'withdrawn' || status === 'blocked';
---
{
@@ -33,9 +40,16 @@ const isTombstone = status === 'withdrawn' || status === 'blocked';
{status === 'blocked'
? 'This record is blocked by a rights, trust, or policy dispute.'
- : 'This record has been withdrawn.'}
+ : status === 'deprecated'
+ ? 'This record is deprecated.'
+ : 'This record has been withdrawn.'}
+ {status === 'deprecated' && (
+
+ It is retained and it still resolves, but it is no longer recommended.
+
+ )}
{supersededBy ? (
Superseded by {supersededBy}.
diff --git a/src/pages/id/ref/[uuid]/index.astro b/src/pages/id/ref/[uuid]/index.astro
index 1a55f99..d7a47cd 100644
--- a/src/pages/id/ref/[uuid]/index.astro
+++ b/src/pages/id/ref/[uuid]/index.astro
@@ -81,8 +81,18 @@ const escapeHtml = (s: string) =>
.replace(/"/g, '"');
const chicagoHtml = escapeHtml(chicago).replace(/\*([^*]+)\*/g, '$1');
+// ADR-0004 and specification.md §12 make `deprecated`, `withdrawn` and `blocked`
+// one tombstone category, and the compiler agrees (`RETIRED_STATUSES` in
+// scripts/compile.ts). The three differ in reach, not in kind: a `deprecated`
+// record is retained and still resolves, while a `withdrawn` or `blocked` record
+// is additionally excluded from `Work` mapping projections. The banner therefore
+// stays one presentation, and the copy below states which of the three applies.
+// Kept in step with src/components/Tombstone.astro by hand until #100 lands.
const isDraft = ref.status === 'draft';
-const isTombstone = ref.status === 'withdrawn' || ref.status === 'blocked';
+const isTombstone =
+ ref.status === 'deprecated' ||
+ ref.status === 'withdrawn' ||
+ ref.status === 'blocked';
---
{ref.status === 'blocked'
? 'This record is blocked by a rights, trust, or policy dispute.'
- : 'This record has been withdrawn.'}
+ : ref.status === 'deprecated'
+ ? 'This record is deprecated.'
+ : 'This record has been withdrawn.'}
+ {ref.status === 'deprecated' && (
+
+ It is retained and it still resolves, but it is no longer
+ recommended.
+
+ )}
{ref.superseded_by ? (
Superseded by {ref.superseded_by}.
From 0bb63f98decb7cc8179d52bdb8f601ad4cfd2043 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Thu, 27 Aug 2026 16:03:29 +0200
Subject: [PATCH 3/4] refactor(site): delete the orphaned CitationBox component
(#48)
`src/components/CitationBox.astro` has no consumers. The reference page
was the only one, and #30 replaced it with inlined Chicago and COinS
rendering against `src/lib/citation.ts`.
`git grep CitationBox` now returns nothing, and `npm run build:fast`
produces identical `dist/` output.
Closes #48
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_0148cB4MGy2zPeuhemebM38h
---
src/components/CitationBox.astro | 134 -------------------------------
1 file changed, 134 deletions(-)
delete mode 100644 src/components/CitationBox.astro
diff --git a/src/components/CitationBox.astro b/src/components/CitationBox.astro
deleted file mode 100644
index ec1a9a7..0000000
--- a/src/components/CitationBox.astro
+++ /dev/null
@@ -1,134 +0,0 @@
----
-import type {
- Work,
- CanonicalReference,
- CitationSystem,
-} from '../../standard/schema/index.js';
-import { toCSL, formatChicago, toCOinS } from '../lib/citation.ts';
-
-interface Props {
- work: Work | undefined;
- ref: CanonicalReference;
- system: CitationSystem | undefined;
-}
-
-const { work, ref, system } = Astro.props;
-const csl = toCSL(work, ref, system);
-const chicago = formatChicago(csl);
-const coins = toCOinS(csl);
-const cslJson = JSON.stringify(csl, null, 2);
-
-const escapeHtml = (s: string) =>
- s
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"');
-
-const chicagoHtml = escapeHtml(chicago).replace(/\*([^*]+)\*/g, '$1');
----
-
-
-
Cite this reference
-
Chicago (notes-bibliography)
-
-
{chicago}
-
-
-
-
-
-
-
-
-
-
-
From e4e3fb88b7941ef9410f7029bea62beaa9e6891f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Thu, 27 Aug 2026 16:21:37 +0200
Subject: [PATCH 4/4] fix(github): label the docs issue template with a label
that exists
`docs.yml` asked for `docs`. The repository has no such label; it has
`documentation`, which is what #68, #79 and #81 already carry. GitHub
applies the labels an issue form names only when they exist, and it does
not create the missing one, so every issue filed through this template
would arrive with `triage` alone and miss a `label:documentation` query.
Every other template in the directory names a label that exists. This is
the one that does not.
Latent so far: no issue in the repository carries `triage`, so no docs
issue has been filed through the form yet. The tag freezes `.github/`,
which makes a one-line fix cheaper now than after.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_0148cB4MGy2zPeuhemebM38h
---
.github/ISSUE_TEMPLATE/docs.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml
index 5bc9820..3a99de4 100644
--- a/.github/ISSUE_TEMPLATE/docs.yml
+++ b/.github/ISSUE_TEMPLATE/docs.yml
@@ -1,7 +1,7 @@
name: 📚 Documentation issue
description: Something on the site or in the README is missing, wrong, or unclear.
title: 'docs: '
-labels: [docs, triage]
+labels: [documentation, triage]
body:
- type: input
id: url