Skip to content

fix: reconcile package and contract documentation drift #236

fix: reconcile package and contract documentation drift

fix: reconcile package and contract documentation drift #236

Workflow file for this run

# SPDX-License-Identifier: Apache-2.0
# Doc hygiene: fail if an internal Markdown link points at a file that doesn't
# exist. The docs (AGENTS.md, context/, planning/) are heavily cross-linked, and
# a rename silently breaks them. Also fails on:
# - a [^label] citation footnote whose label matches no sources[].id in the
# same file's frontmatter. That label is the join key OKF resolves
# attribution through, so a dangling one is a citation pointing nowhere —
# and the spec leaves the case undefined, so consumers can't be relied on
# to flag it.
# - a positional citation (`path.ext:NN`) — CONVENTIONS § Citations bans them.
# Ignores links inside fenced blocks, inline code, and HTML comments, so
# documented link *syntax* examples don't trip it.
#
# Despite the name this now checks citation integrity as well as links. Two
# prepared texts, and each check must use the right one — see link_text and
# cite_text below.
name: link-check
on:
push:
pull_request:
jobs:
links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Check internal Markdown links
run: |
python3 - <<'PY'
import re, os, glob, sys
bad = 0
concepts = {os.path.splitext(os.path.basename(p))[0]
for p in glob.glob('context/**/*.md', recursive=True)}
for f in sorted(glob.glob('**/*.md', recursive=True)):
# Skip only root build directories, not tracked root documents
# such as a future build-guide.md.
top = f.split(os.sep, 1)
if len(top) == 2 and (top[0] == 'build' or top[0].startswith('build-')):
continue
raw = open(f, encoding='utf-8').read()
# sources[].id values declared in this file's frontmatter
fm = re.match(r'---\n(.*?)\n---\n', raw, flags=re.DOTALL)
ids = set(re.findall(r'^\s*-?\s*id:\s*(\S+)', fm.group(1), flags=re.M)) if fm else set()
# TWO prepared texts. They differ in one step, and which one a
# check reads decides whether it can see anything at all — so name
# them rather than letting a later check inherit the wrong one.
#
# link_text: for the link and footnote checks. Inline code goes,
# because a doc that *documents* link syntax writes examples there.
# Order matters: code first, comments last. A doc that documents
# comment syntax has a bare `<!--` inside a code block, and
# stripping comments first pairs it with a `-->` far below,
# swallowing the fences in between and blinding every check that
# follows. Removing code spans first leaves only real comments.
link_text = raw
link_text = re.sub(r'```.*?```', '', link_text, flags=re.DOTALL) # drop fenced code
link_text = re.sub(r'`[^`]*`', '', link_text) # drop inline code
link_text = re.sub(r'<!--.*?-->', '', link_text, flags=re.DOTALL) # drop HTML comments
#
# cite_text: for the positional-citation check only. Inline code is
# KEPT — a citation is written `like/this.cpp:42`, so stripping it
# would blind the check completely. Fenced blocks still go, because
# compiler and sanitizer output legitimately carries file:line.
cite_text = re.sub(r'```.*?```', '', raw, flags=re.DOTALL)
cite_text = re.sub(r'<!--.*?-->', '', cite_text, flags=re.DOTALL)
base = os.path.dirname(f)
for m in re.findall(r'\]\(([^)]+)\)', link_text):
t = m.split('#')[0].strip()
if not t or t.startswith(('http://', 'https://', 'mailto:')):
continue
# Root-absolute links resolve against the site root, not the
# bundle root, so they work nowhere. CONVENTIONS bans them —
# flag them, never silently remap them onto context/.
if t.startswith('/'):
print(f'ROOT-ABSOLUTE {f} -> {m} (use a ./relative path)')
bad += 1
continue
if not os.path.exists(os.path.normpath(os.path.join(base, t))):
print(f'BROKEN {f} -> {m}'); bad += 1
# [[slug]] wikilinks resolve by concept name, not path. Not the
# house style here, but a stray one should still be checked.
for w in re.findall(r'\[\[([^\]]+)\]\]', link_text):
slug = w.split('|')[0].split('#')[0].strip()
if slug not in concepts:
print(f'BROKEN WIKILINK {f} -> [[{w}]]'); bad += 1
# [^label] citation footnotes join to sources[].id in this file.
for lab in set(re.findall(r'\[\^([^\]]+)\]', link_text)):
if lab not in ids:
print(f'DANGLING FOOTNOTE {f} -> [^{lab}] (no sources[].id)'); bad += 1
# Positional citations (`path.ext:NN`) — banned by CONVENTIONS
# § Citations. A line number into a file that later gets edited
# comes to point at the wrong text while still resolving and still
# looking right, so nothing downstream catches it: not review, not
# the checks above. Cite a section or a symbol instead.
# log.md is exempt — its entries are dated snapshots frozen at
# write time, the same place a specific number is allowed to live.
if os.path.basename(f) != 'log.md':
for c in re.findall(r'`([\w./-]+\.(?:md|hpp|cpp|h|py|toml|txt|yml|cmake|sh):\d+)`', cite_text):
print(f'POSITIONAL CITATION {f} -> `{c}` (cite a section or symbol)'); bad += 1
print('all internal links resolve' if not bad else f'{bad} bad link(s)')
sys.exit(1 if bad else 0)
PY