CRUMB a card from devarno-cloud

Outranks DAG and docs-invariants CI

petrova advanced 8 min read

ELI5

Every doc carries a tag saying which other docs it outranks. The CI builds the directed graph of those tags and refuses to merge if it finds a loop. This is what stops backlog notes from quietly overruling north-star intent.

Technical Deep Dive

Two DAGs, one CI workflow

docs-invariants.yml enforces two independent DAGs and four other invariants:

flowchart TB
subgraph CI[".github/workflows/docs-invariants.yml"]
iso["Validate ISO dates in decision-doc filenames (MR-4)"]
fnd["Validate findings filenames YYYYMMDD[-HHMM]-<slug>.md (MR-4)"]
ax["Validate AGENTS.xml schema — required children, event catalog (MR-6)"]
hg["AGENTS.xml handoff graph is a DAG (intra-phase only — exempt: audit.fail, phase.close)"]
rg["Rank graph from outranks: front-matter is a DAG (MR-1)"]
xref["Milestone-ID cross-refs in AGENTS.xml exist in MILESTONES.md (MR-8)"]
sup["Superseded docs link to successor (MR-7)"]
end
trigger["pull_request to docs/, AGENTS.xml, MILESTONES.md, CLAUDE.md"]
trigger --> CI
CI --> verdict{all pass?}
verdict -->|yes| merge["mergeable"]
verdict -->|no| block["block"]

The rank graph (MR-1)

Every doc in docs/ declares its tier and what it outranks:

---
rank: north-star
outranks: [decisions, findings, runbooks, backlog]
---

The CI loads every .md under docs/, parses front-matter, and adds an edge (rank → lower) for each entry in outranks. Then:

import networkx as nx
g = nx.DiGraph()
# ... add edges ...
if not nx.is_directed_acyclic_graph(g):
cycle = list(nx.simple_cycles(g))[:1]
print(f"MR-1 violation: rank graph has a cycle: {cycle}")
sys.exit(1)

Early-state escape hatch: “If g.number_of_nodes() == 0, print ‘No ranked docs found yet (early project state); skipping DAG check.’ and exit 0.” So a fresh repo doesn’t fail before any front-matter exists.

The handoff graph (AGENTS.xml)

The agent handoff graph is also required to be a DAG, but with two principled exemptions:

TriggerReason exempt
audit.faillegitimate remediation back-edge: auditor → implementer
phase.closelegitimate phase-boundary loop: reviewer → planner

Both close real loops in the workflow but neither is an intra-phase forward-progress cycle. The CI filters them out, then DAG-checks what remains:

intraphase_exempt = {'audit.fail', 'phase.close'}
forward = nx.DiGraph()
for u, v, d in g.edges(data=True):
if d.get('trigger') not in intraphase_exempt:
forward.add_edge(u, v)
if not nx.is_directed_acyclic_graph(forward):
sys.exit(1)

Filename validators (MR-4)

Two distinct grammars, both enforced:

Terminal window
# decision docs
[[ "$base" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9-]+\.md$ ]]
# findings
[[ "$base" =~ ^[0-9]{8}(-[0-9]{4})?-[a-z0-9-]+\.md$ ]]

Decisions use hyphenated dates, findings use compact YYYYMMDD plus optional HHMM. Both refuse uppercase, refuse non-kebab slugs.

Cross-reference validator (MR-8)

Every milestone ID referenced in AGENTS.xml must exist in MILESTONES.md:

declared = set(re.findall(r'\bM\d+(?:\.\d+){1,2}\b', milestones_text))
referenced = set(re.findall(r'\bM\d+(?:\.\d+){1,2}\b', agents_text))
referenced = {r for r in referenced if not r.startswith('M{')} # templates exempt
missing = referenced - declared

M{N}.{n} placeholders (template form) are filtered out — only resolved IDs are checked. This means the template can ship with M{N}.{n} and not fail CI; instantiated consumer repos must use real IDs.

AGENTS.xml schema validator (MR-6)

Each <subagent> requires the six children: name, spawns_on, reads, writes, output_contract, refusal_conditions. Each <spawns_on> must contain <event> children whose IDs match <event_catalog>. <refusal_conditions> must have at least one <condition> (“subagents that ‘never refuse’ are a smell” — MR-6 spirit).

How adding a new doc category interacts with the DAG

Adding a new tier (say roles between findings and backlog) means:

  1. Pick the rank name and place in the hierarchy.
  2. Decide what it outranks (must not introduce a cycle).
  3. Update one or more existing docs’ outranks: lists if higher-tier docs should outrank the new tier.
  4. Open a decision doc explaining the new tier (MR-1 implies tier additions earn a doc).

The CI re-runs on the PR; the DAG check catches accidental cycles immediately.

Failure surfaces

When CI fails, the failure message names the MR being violated:

  • “MR-4 violation: docs/decisions/foo.md does not match YYYY-MM-DD-.md”
  • “MR-1 violation: rank graph has a cycle: [[‘decisions’, ‘north-star’, ‘decisions’]]”
  • “AGENTS.xml: subagent X: <refusal_conditions> empty (MR smell)”
  • “MR-7 violation: docs/decisions/old.md status=superseded but no Superseded-by link”

The Summary step at the end prints: “If this job failed, see META-RULES.md for the rule (MR-N) being enforced.”

Key Terms

  • Rank-graph DAG — directed graph of (rank → lower-tier) edges from outranks: front-matter; required acyclic per MR-1.
  • Handoff-graph DAG (intra-phase) — directed graph of <edge from=... to=... trigger=...> in <handoff_graph>, filtered to non-loop triggers, required acyclic.
  • Template-form M-ID — placeholder like M{N}.{n} in AGENTS.xml.tmpl; filtered out before milestone cross-ref check.

Q&A

Q: Which two AGENTS.xml handoff triggers are exempt from the intra-phase DAG check? A: audit.fail (auditor → implementer remediation) and phase.close (reviewer → planner phase-boundary). Both close legitimate loops; the DAG check filters them before testing acyclicity.

Q: What does the rank-graph CI step do when zero ranked docs exist? A: Prints “No ranked docs found yet (early project state); skipping DAG check.” and exits 0. This is the bootstrap escape hatch — a fresh repo with no rank: front-matter yet shouldn’t fail CI before any tier is declared.

Q: What pattern flags an MR-4 violation in a decision-doc filename? A: Anything that does not match ^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9-]+\.md$. Uppercase letters, missing date, non-kebab slug, or wrong date separators all fail. _template.md, README.md, and _changelog.md are exempt; everything else under docs/decisions/ is checked.

Examples

A PR adds docs/decisions/2026-05-06-Cool-Feature.md (uppercase). CI fails: “MR-4 violation: docs/decisions/2026-05-06-Cool-Feature.md does not match YYYY-MM-DD-.md”. Author renames to 2026-05-06-cool-feature.md; CI passes. Separately, a planner adds M9.1.1 to AGENTS.xml without first writing it into MILESTONES.md — the milestone-cross-ref step fails with “AGENTS.xml references milestone IDs not in MILESTONES.md: [‘M9.1.1’]”. Author lands MILESTONES.md edit in the same commit; CI passes.

neighbors on the map