Contracts
adr-invariance-contract.md
Contract: ADR invariance — OBSOLETE
OBSOLETE post-rebase — the byte-identity ADR invariance gate (TestContentInvariance, _EXPECTED_INVARIANT, _SANCTIONED_SELF_AMENDMENT, migrate_adr_body_links) was retired upstream by commit ccd278061 (3.2.4 cycle). Lane C is now: FR-008 plain ADR link repair (WP05) + FR-011 census widen 117→119 (WP06). See spec.md Scope Change section for the full rationale.
This file is kept so links from other documents do not dangle.
changelog-sync-contract.md
Contract: CHANGELOG canonical→root sync
Surface: scripts/docs/sync_changelog.py (NEW) + a blocking test.
generate_root(canonical_text: str) -> str
- Input: full canonical
docs/changelog/CHANGELOG.mdtext (YAML frontmatter + body). - Output: root
CHANGELOG.mdtext = canonical body after the frontmatter block, byte-stable. - Must produce a valid Keep-a-Changelog document consumable by
scripts/release/extract_changelog.py(reads root at repo cwd,utf-8-sig).
CLI
python scripts/docs/sync_changelog.py --write→ regenerates root from canonical.python scripts/docs/sync_changelog.py --check→ exit1ifread(root) != generate(read(canonical)), naming the divergence.
Invariant (FR-007 / INV-4)
read(root) == generate(read(canonical)) at all times. Link fixes (FR-006) are applied to the canonical and flow to root via regeneration.
Tests
- Red-first divergence test (SC-003): the current files diverge (frontmatter + the stale
architecture/2.x/05_ownership_map.mdline) →--checkfails; after FR-006 fix + regenerate → passes. - Editing one file without the other →
--checkfails.
gate-contract.md
Contract: the body-link gate
Surface: scripts/docs/relative_link_fixer.py::check_dead_body_links + --check CLI + tests/docs/test_relative_link_fixer.py::TestLiveTreeGate.
check_dead_body_links(repo_root: Path) -> list[Unresolvable]
- Scans every Markdown file under
docs/not matched byEXCLUDE_PREFIXES. - For each inline body link that
is_bare_relative(skipshttp(s),mailto:,#anchor, absolute/…, reference-style, raw HTML), resolvesrepo_root / normpath(file_dir / target)and records anUnresolvable(file, link, line)if absent. - Post-mission:
EXCLUDE_PREFIXES == ()→ covers all ofdocs/incl.docs/adr/,docs/changelog/,docs/architecture/,docs/archive/,docs/plans/user_journey/. - Determinism (NFR-002): returns findings sorted by
(file, line, link). - Non-vacuity (FR-004/INV-2): raises/fails if zero files or zero links were examined.
--check CLI
python scripts/docs/relative_link_fixer.py --check --repo-root .→ exit0iff no dead links; exit1otherwise.- Failure output (NFR-003): one line per offender as
file:line -> target, enumerating all offenders (not a summary count).
Gate-unmask dry-run (C-007 / INV-5)
- A mode/flag (or test) runs
check_dead_body_linkswithEXCLUDE_PREFIXES=()over the integrated branch and requires zero dead links before merge — the gate cannot validate its own unmask within its PR.
Tests
TestLiveTreeGate(@pytest.mark.fast, blocking): zero dead links on the live tree (full scope post-flip);_KNOWN_GAPSre-pinned tofrozenset().test_gate_excludes_immutable_subtrees(:264): inverted — post-mission the gate must NOT excludedocs/adr/.- Deliberate-breakage test (SC-002): ≥2 planted bad links → all reported with correct
(file, line, target).