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.md text (YAML frontmatter + body).
  • Output: root CHANGELOG.md text = 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 → exit 1 if read(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.md line) → --check fails; after FR-006 fix + regenerate → passes.
  • Editing one file without the other → --check fails.

gate-contract.md

Surface: scripts/docs/relative_link_fixer.py::check_dead_body_links + --check CLI + tests/docs/test_relative_link_fixer.py::TestLiveTreeGate.

  • Scans every Markdown file under docs/ not matched by EXCLUDE_PREFIXES.
  • For each inline body link that is_bare_relative (skips http(s), mailto:, #anchor, absolute /…, reference-style, raw HTML), resolves repo_root / normpath(file_dir / target) and records an Unresolvable(file, link, line) if absent.
  • Post-mission: EXCLUDE_PREFIXES == () → covers all of docs/ 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 . → exit 0 iff no dead links; exit 1 otherwise.
  • 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_links with EXCLUDE_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_GAPS re-pinned to frozenset().
  • test_gate_excludes_immutable_subtrees (:264): inverted — post-mission the gate must NOT exclude docs/adr/.
  • Deliberate-breakage test (SC-002): ≥2 planted bad links → all reported with correct (file, line, target).