Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Read and follow AGENTS.md in this repository — it is the single operational
law for AI agents here (workflow, verification trio, documentation policy,
ADR triggers, canonical sources).
13 changes: 13 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!--
Body must START with "Fixes #N" (or "Refs #N" for a partial step) so the
sidebar links the issue and merge auto-closes it.
-->

Fixes #

## Checklist

- [ ] `go vet ./...` / `go test ./... -race -count=1` / `go test ./... -cover -count=1` all pass (AGENTS.md)
- [ ] `docs/<pkg>.md` updated for any behavior/contract change; `docs/README.md` index row added for a new package
- [ ] `bash scripts/check-docs.sh` passes if `docs/`, `AGENTS.md`, or `CONTRIBUTING.md` changed
- [ ] ADR: ____ (number) / none — reason: ____ (see docs/adr/README.md for the T1–T5 triggers)
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ jobs:
fi
working-directory: .

docs:
name: docs governance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: stable
check-latest: true

- name: check-docs
run: bash scripts/check-docs.sh

cross-build:
name: cross-build ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ explicit `node.Run(..., schemes...)` registration — never `init()`.
cross-package convention, or rejects a significant alternative, include an
Architecture Decision Record in the same PR. See `docs/adr/README.md` for
the trigger list (T1–T5), the template, and the lifecycle.
- When you touch `docs/`, `AGENTS.md`, or `CONTRIBUTING.md`, also run
`bash scripts/check-docs.sh` — CI enforces the documentation floor checks
(index sync, test-name and exported-symbol presence, required sections,
ADR integrity, canonical-copy lockstep).

## Mandatory workflow for any code change

Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Claude in this repository

Read and follow [AGENTS.md](./AGENTS.md) — it is the single operational law
for AI agents here (workflow, verification trio, documentation policy, ADR
triggers, canonical sources). This file intentionally adds nothing else.
5 changes: 4 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ for `scripts/check-docs.sh` — keep it in sync with the prose.
package: Overview; Concepts; Public API; Invariants & Guarantees; Concurrency & Call Permissions; Stability Contract; Testing; Limitations & TODO
cmd: Overview; Usage; Behavior & guarantees (with Concurrency & lifecycle and Stability Contract as subsections); Testing; Limitations & TODO
multi: Overview; per-package API sections; Concurrency & Call Permissions; Stability Contract; Testing; Limitations & TODO
assignments: docs/dart.md=cmd; docs/observability.md=multi
policy: free-form policy document (no required sections)
assignments: docs/dart.md=cmd; docs/observability.md=multi; docs/design-assumptions.md=policy
heading-aliases: Concepts <=> Wire form; Determinism / Stability Contract <=> Stability Contract
-->

Expand All @@ -72,6 +73,8 @@ heading-aliases: Concepts <=> Wire form; Determinism / Stability Contract <=> St
- **multi** (assigned: `docs/observability.md`): one document covering
several packages uses per-package API sections instead of unified
Concepts/Public-API/Invariants sections.
- **policy** (assigned: `docs/design-assumptions.md`): a free-form policy
document; the section template does not apply.
- **Heading aliases**: the Concepts section may be titled by its domain
content (e.g. peer.md's "Wire form"); the Stability Contract may be titled
"Determinism / Stability Contract".
Expand Down
2 changes: 1 addition & 1 deletion docs/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ front-end satisfies the other from cache. Requests are routed on `RequestURI`
rather than by an `http.ServeMux`, because the prefix paths embed a full URL and
a ServeMux would path-clean the `//` in `https://`.

## 2. What is cacheable, and why only that
## 2. Concepts: what is cacheable, and why only that

**Only digest-addressed blobs are cached.** A blob URL names its own content
hash, so it is immutable: caching it can never serve the wrong bytes, and the
Expand Down
239 changes: 239 additions & 0 deletions scripts/check-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
#!/usr/bin/env bash
# check-docs.sh — documentation governance floor checks.
#
# What this enforces (hard failures):
# 1. docs/README.md index <-> docs files stay in sync
# 2. backticked Test*/Benchmark*/Example*/Fuzz* names in docs exist as test
# functions in *_test.go files
# 3. every exported top-level identifier and exported method (go doc -all)
# is named (word-boundary match) in its package document
# 4. every docs/*.md carries the required sections of its sanctioned shape
# (DOC-SHAPES block in docs/README.md is the single source of truth)
# 5. ADR integrity: unique numbers, required metadata fields, Supersedes
# symmetry, index coverage, Binds path existence
# 6. CANONICAL-COPY blocks are identical to their CANONICAL source
# (modulo leading indentation)
#
# Plus one warning (never fails): relative .md links that point nowhere.
#
# This is a FLOOR check: it guarantees presence and lockstep, not quality or
# semantics. Known limits: check 3 covers single-line const/var declarations
# and methods, not grouped const members without individual doc comments, and
# a symbol only appearing in example code still counts as "documented";
# check 4 matches required words at non-letter boundaries in heading text
# (first-word aliases included), so "## Instability notes" does not satisfy
# Stability but "## 6. Determinism / Stability Contract" does.
# Meaning stays with human/agent review.
#
# Requires: bash, grep, awk, go (for check 3). No network, no LLM.

set -u
cd "$(dirname "$0")/.."

fail=0
err() { echo "FAIL: $*" >&2; fail=$((fail+1)); }
warn() { echo "WARN: $*" >&2; }
ok() { echo "ok: $*"; }

# ---------------------------------------------------------------- check 1
# Index <-> files.
index_links=$(grep -oE '\]\(\./[A-Za-z0-9_/-]+\.md\)' docs/README.md | sed 's/^](\.\///; s/)$//' | sort -u)
for f in docs/*.md docs/adr/README.md; do
rel=${f#docs/}
[ "$rel" = "README.md" ] && continue
if ! grep -qx "$rel" <<<"$index_links"; then
err "check1: $f is not linked from the docs/README.md index"
fi
done
while IFS= read -r rel; do
[ -z "$rel" ] && continue
[ -f "docs/$rel" ] || err "check1: index links docs/$rel, which does not exist"
done <<<"$index_links"
ok "check1: index <-> files"

# ---------------------------------------------------------------- check 2
# Backticked test names in docs exist as test functions.
testfiles=$(find internal cmd providers -name '*_test.go' 2>/dev/null)
[ -n "$testfiles" ] || err "check2: no *_test.go files found at all"
for doc in docs/*.md; do
names=$(grep -oE '`(Test|Benchmark|Example|Fuzz)[A-Za-z0-9_]+`' "$doc" | tr -d '`' | sort -u)
while IFS= read -r n; do
[ -z "$n" ] && continue
if ! grep -qE "func ${n}\(" $testfiles 2>/dev/null; then
err "check2: $doc mentions \`$n\`, but no 'func $n(' exists in any *_test.go"
fi
done <<<"$names"
done
ok "check2: documented test names exist"

# ---------------------------------------------------------------- check 3
# Exported symbols appear in their package document.
doc_for_pkg() {
case "$1" in
metrics|admin) echo "docs/observability.md" ;;
*) echo "docs/$1.md" ;;
esac
}
for d in internal/*/; do
pkg=${d%/}; pkg=${pkg#internal/}
doc=$(doc_for_pkg "$pkg")
[ -f "$doc" ] || { err "check3: no document for internal/$pkg (expected $doc)"; continue; }
syms=$(go doc -all "./internal/$pkg" 2>/dev/null | awk '
/^(func|type|var|const) [A-Z][A-Za-z0-9_]*/ { s=$2; sub(/\(.*/, "", s); print s }
/^func \([a-z]+ \*?[A-Z][A-Za-z0-9_]*\) [A-Z][A-Za-z0-9_]*/ {
s=$0; sub(/^func \([a-z]+ \*?[A-Z][A-Za-z0-9_]*\) /, "", s); sub(/\(.*/, "", s); print s
}' | sort -u)
while IFS= read -r s; do
[ -z "$s" ] && continue
if ! grep -qE "(^|[^A-Za-z0-9_])${s}([^A-Za-z0-9_]|\$)" "$doc"; then
err "check3: exported symbol $s (internal/$pkg) is not named in $doc"
fi
done <<<"$syms"
done
ok "check3: exported symbols named in package docs"

# ---------------------------------------------------------------- check 4
# Required sections per sanctioned shape, sourced from the DOC-SHAPES block.
shapes=$(awk '/<!-- DOC-SHAPES/{on=1; next} /-->/&&on{exit} on' docs/README.md)
[ -n "$shapes" ] || { err "check4: DOC-SHAPES block missing in docs/README.md"; shapes=""; }

shape_line() { grep -E "^$1:" <<<"$shapes" | sed "s/^$1: *//"; }
# Canonical keyword -> regex of heading alternatives (aliases included).
kw_regex() {
local alts="$1"
# first words of every side of every alias pair that mentions this keyword
local pairs
pairs=$(grep -E '^heading-aliases:' <<<"$shapes" | sed 's/^heading-aliases: *//' | tr ';' '\n')
while IFS= read -r pair; do
case "$pair" in
*"$1"*)
while IFS= read -r side; do
side=$(echo "$side" | sed 's/^ *//; s/ *$//')
[ -n "$side" ] && alts="$alts|${side%% *}"
done <<<"$(echo "$pair" | sed 's/<=>/\n/g')"
;;
esac
done <<<"$pairs"
echo "$alts"
}
# keyword list: template-concept -> heading regex (first-word match, aliases applied)
KEYWORDS="Overview Concepts API Invariants Concurrency Stability Testing Limitations Usage Behavior"
for doc in docs/*.md; do
base=${doc#docs/}
[ "$base" = "README.md" ] && continue
shape="package"
a=$(grep -E '^assignments:' <<<"$shapes" | sed 's/^assignments: *//')
for kv in ${a//;/ }; do
[ "${kv%%=*}" = "docs/$base" ] && shape=${kv#*=}
done
line=$(shape_line "$shape")
[ -n "$line" ] || { err "check4: shape '$shape' (for $doc) is not defined in DOC-SHAPES"; continue; }
for kw in $KEYWORDS; do
case " $line " in
*" $kw"*) : ;; # keyword appears in this shape's spec -> required
*) continue ;;
esac
rx=$(kw_regex "$kw")
# Alternatives are anchored on non-letter boundaries: "## Instability
# notes" must NOT satisfy the Stability requirement. The numeric
# "N. " prefix used by convention is optional, so a plain "## Overview"
# (no number) still matches.
if ! grep -qiE "^#{1,4} +([0-9.]+ +)?(.* )?($rx)([^A-Za-z]|$)" "$doc"; then
err "check4: $doc (shape $shape) lacks a required '$kw' section heading"
fi
done
done
ok "check4: required sections per sanctioned shape"

# ---------------------------------------------------------------- check 5
# ADR integrity.
adr_readme=docs/adr/README.md
[ -f "$adr_readme" ] || err "check5: $adr_readme missing"
nums=""
for f in docs/adr/[0-9][0-9][0-9][0-9]-*.md; do
[ -e "$f" ] || continue
n=$(basename "$f" | cut -c1-4)
case " $nums " in *" $n "*) err "check5: duplicate ADR number $n";; esac
nums="$nums $n"
for field in 'Status: ' 'Date: ' 'Supersedes: ' 'Binds: '; do
grep -q "^- $field" "$f" || err "check5: $f lacks a '- $field...' metadata line"
done
status=$(sed -n 's/^- Status: //p' "$f" | head -1)
case "$status" in
proposed|accepted|rejected) : ;;
"superseded by ADR-"[0-9][0-9][0-9][0-9])
sn=${status#superseded by ADR-}
sup=$(ls docs/adr/${sn}-*.md 2>/dev/null | head -1)
[ -n "$sup" ] || err "check5: $f is superseded by ADR-$sn, which does not exist"
[ -n "$sup" ] && ! grep -q "^- Supersedes: ADR-$n" "$sup" \
&& err "check5: $f says superseded by ADR-$sn, but $sup does not say 'Supersedes: ADR-$n'"
;;
*) err "check5: $f has an unrecognized Status: $status" ;;
esac
sup=$(sed -n 's/^- Supersedes: //p' "$f" | head -1)
case "$sup" in
none) : ;;
ADR-[0-9][0-9][0-9][0-9])
ls docs/adr/${sup#ADR-}-*.md >/dev/null 2>&1 || err "check5: $f supersedes $sup, which does not exist" ;;
*) err "check5: $f has an unrecognized Supersedes: $sup" ;;
esac
if [ -f "$adr_readme" ] && ! grep -q "$n" "$adr_readme"; then
err "check5: ADR-$n is not listed in $adr_readme"
fi
# Binds path existence: path-like tokens must resolve. (Process
# substitution, not a pipe: err() must run in THIS shell — a piped loop is
# a subshell and its failure count would be silently lost.)
while IFS= read -r tok; do
tok=${tok%,}; tok=${tok%.}
if [ "${tok%.md}" != "$tok" ]; then
[ -f "$tok" ] || err "check5: $f binds $tok, which does not exist"
else
[ -d "$tok" ] || [ -f "$tok" ] || err "check5: $f binds $tok, which does not exist"
fi
done < <(sed -n 's/^- Binds: //p' "$f" | grep -oE '(docs|internal|cmd|providers)/[A-Za-z0-9_/.,-]+' || true)
done
ok "check5: ADR integrity"

# ---------------------------------------------------------------- check 6
# Canonical copies match their sources (modulo leading indentation).
canon_files=$(grep -rl '<!-- CANONICAL' AGENTS.md CONTRIBUTING.md docs 2>/dev/null || true)
ids=$(grep -rhoE '<!-- CANONICAL id="[a-z0-9-]+" -->' $canon_files 2>/dev/null | sed 's/.*id="//; s/".*//' | sort -u)
dedent() { sed 's/^[[:space:]]*//'; }
block() { # file, open-regex, id
awk -v id="$3" -v open="$2" '
$0 ~ open && $0 ~ "id=\"" id "\"" { on=1; next }
on && $0 ~ "<!-- /CANONICAL" && $0 ~ "id=\"" id "\"" { on=0; next }
on' "$1"
}
for id in $ids; do
srcfile=$(grep -l "<!-- CANONICAL id=\"$id\" -->" $canon_files 2>/dev/null | head -1)
[ -n "$srcfile" ] || { err "check6: no CANONICAL source for id '$id'"; continue; }
src=$(block "$srcfile" '<!-- CANONICAL id=' "$id" | dedent)
for f in $canon_files; do
# every copy of this id in file f
srcattr=$(grep -oE "<!-- CANONICAL-COPY source=\"[^\"]+\" id=\"$id\" -->" "$f" 2>/dev/null | sed 's/.*source="//; s/".*//' | sort -u)
[ -z "$srcattr" ] && continue
[ "$srcattr" = "$srcfile" ] || err "check6: $f copies id '$id' from '$srcattr', but the CANONICAL lives in $srcfile"
copy=$(block "$f" '<!-- CANONICAL-COPY source=' "$id" | dedent)
[ "$copy" = "$src" ] || err "check6: CANONICAL-COPY '$id' in $f has drifted from its source $srcfile"
done
done
ok "check6: canonical copies in lockstep"

# ------------------------------------------------- link warning (soft)
for doc in docs/*.md docs/adr/*.md AGENTS.md CONTRIBUTING.md README.md; do
[ -f "$doc" ] || continue
grep -oE '\]\(\./[^)#]+\.md(#[^)]*)?\)' "$doc" 2>/dev/null | sed 's/^](\.\///; s/#.*//; s/)$//' | sort -u | \
while IFS= read -r rel; do
[ -z "$rel" ] && continue
target="$(dirname "$doc")/$rel"
[ -e "$target" ] || warn "link: $doc links ./$rel, which does not exist"
done
done

echo
if [ "$fail" -gt 0 ]; then
echo "check-docs: $fail check failure(s)" >&2
exit 1
fi
echo "check-docs: all floor checks passed"