# Plan — OBPI-0.0.27-05-citation-contract

**Brief:** `docs/design/adr/foundation/ADR-0.0.27-exemplar-corpus-doctrine/obpis/OBPI-0.0.27-05-citation-contract.md`
**Parent ADR:** ADR-0.0.27-exemplar-corpus-doctrine
**Lane:** Heavy + Foundation (brief-level Gate 5 required)

## Context

OBPI-0.0.27-04 has landed the first distilled-characteristics document
(`docs/governance/complexity/distilled-characteristics-2026-05-04.md`) with
frontmatter `corpus_revision: 1`. The canonical complexity rule
(`.gzkit/rules/complexity-doctrine.md`) already carries a "Citation Contract
(binding)" prose section authored under OBPI-01, but it lacks: (a) the formal
tuple definition `(distilled_characteristics_path, section_anchor,
corpus_revision)`, (b) the percentile + absolute-number pairing rule for
portability across refresh, and (c) the refresh-portability rule the
link-integrity validator (OBPI-07) consumes. This OBPI extends the section
with those three formalizations and lands the parser surface OBPI-07
mechanically validates.

Destination-in-mind disclosure: the obvious shape is a Pydantic
`Citation(distilled_characteristics_path, section_anchor, corpus_revision)`
frozen model with `extra="forbid"`, a JSON Schema co-generated from it, a
`parse_citation` constructor that accepts the canonical string form, and an
`is_portable(citation, current_revision, supported_window)` function that
returns true within the supported window and false outside it. Rejected
alternatives during exploration: (1) free-form regex parser without Pydantic
— rejected because the rule body declares an `extra="forbid"`-equivalent
schema and bypassing Pydantic loses the schema/model isomorphism downstream
ADRs depend on; (2) implicit corpus-revision derivation from the file
frontmatter — rejected because the citation must be self-contained at the
citing-ADR site, not require a round-trip read of the cited file; (3)
percentile-OR-absolute pairing (operator picks one) — rejected because the
brief mandates AND, since the load-bearing portability invariant requires
both forms cited together.

## Files

Canonical edits (vendor mirrors regenerated by `gz agent sync
control-surfaces` after the canonical surfaces land — never edited
directly per `.gzkit/rules/skill-surface-sync.md`):

- `.gzkit/rules/complexity-doctrine.md` — extend "Citation Contract (binding)"
  section with formal tuple definition, percentile + absolute pairing rule,
  refresh-portability rule; bump rule-version `0.1.0 → 0.2.0` in both the
  body comment and visible block quote.
- `src/gzkit/complexity/citation.py` — new module:
  - frozen Pydantic `Citation` model (`ConfigDict(frozen=True, extra="forbid")`)
  - `parse_citation(text: str) -> Citation` constructor (raises
    `ValidationError` on missing fields)
  - `is_portable(citation: Citation, current_revision: int,
    supported_window: int = 2) -> bool`
- `src/gzkit/schemas/complexity_citation.json` — JSON Schema mirroring the
  Pydantic model (`additionalProperties: false`; `distilled_characteristics_path`
  is `"^docs/governance/complexity/.+"` regex; `section_anchor` is slug;
  `corpus_revision` is positive integer).
- `tests/complexity/test_citation.py` — unittest module decorated with
  `@covers("REQ-0.0.27-05-NN")` per REQ; `tempfile`-backed fixtures for any
  rule-body parsing assertions.

Brief evidence section updates:

- `docs/design/adr/foundation/ADR-0.0.27-exemplar-corpus-doctrine/obpis/OBPI-0.0.27-05-citation-contract.md`
  — fill Implementation Summary, Key Proof, Closing Argument; mark Acceptance
  Criteria.

## Steps

1. **Red phase — author failing tests first (REQ-derived).** Create
   `tests/complexity/test_citation.py` covering all six test obligations
   from REQ-06: parser accepts canonical tuple shape (`@covers
   REQ-0.0.27-05-03`); parser rejects citations missing any of the three
   fields (`@covers REQ-0.0.27-05-04`); portability returns true within
   current revision (`@covers REQ-0.0.27-05-05`); portability returns false
   when revision is older than supported window (`@covers REQ-0.0.27-05-05`);
   rule body parsing detects the citation contract section
   (`@covers REQ-0.0.27-05-01` and `REQ-0.0.27-05-02`); vendor-mirror sync
   produces empty diff (`@covers REQ-0.0.27-05-07`). Add explicit assertion
   tying the rule body's `<!-- rule-version: 0.2.0 -->` HTML comment and
   visible block-quote `> **Rule version:** \`0.2.0\`` together
   (`@covers REQ-0.0.27-05-06`). Confirm RED.

2. **Green phase — citation module.** Author
   `src/gzkit/complexity/citation.py` with the frozen `Citation` Pydantic
   model and `parse_citation` / `is_portable` functions. Canonical string
   form: `<distilled_characteristics_path> § <section_anchor> (corpus
   revision <N>)` matching the example in the rule body. Constructor parses
   that form, raises `ValidationError` on missing fields. `is_portable`
   accepts the citation, the current revision, and a supported window
   (default 2 — i.e. revision N is portable when current revision is N or
   N+1; revision N is non-portable when current revision is N+2 or later).
   Run unittest — confirm GREEN for citation-module tests.

3. **Author JSON Schema co-generated with the Pydantic model.** Land
   `src/gzkit/schemas/complexity_citation.json` with `additionalProperties:
   false`; constraints exactly mirror the Pydantic field constraints. Add a
   schema-shape test in `test_citation.py` asserting the schema is valid
   Draft 2020-12 JSON Schema and that the model's `model_json_schema()` is
   equivalent (semantic equivalence, not byte-identical — order may differ).
   Confirm GREEN.

4. **Extend `.gzkit/rules/complexity-doctrine.md`.** Replace the existing
   "Citation Contract (binding)" prose section with an extended version:
   formalize the tuple definition explicitly (`Citation =
   (distilled_characteristics_path, section_anchor, corpus_revision)`,
   referencing `src/gzkit/complexity/citation.py`); add the percentile +
   absolute pairing rule with worked example (`p90 = CC 7` referencing the
   landed distilled doc's `radon_cc` boundary); add the refresh-portability
   rule naming the supported-window default and OBPI-07's flag-not-rewrite
   behavior; bump `<!-- rule-version: 0.1.0 -->` to `<!-- rule-version:
   0.2.0 -->` and the visible block-quote to `> **Rule version:** \`0.2.0\``
   with a one-sentence rationale. Confirm rule-body-parsing tests GREEN.

5. **Run vendor-mirror sync and confirm idempotence.**
   `uv run gz agent sync control-surfaces` — propagates canonical rule to
   `.claude/rules/`, `.agents/rules/`, `.github/instructions/`. Re-run sync
   — diff is empty. Confirm sync-idempotence test GREEN.

6. **Refactor pass.** Tighten any duplication, ensure complexity stays under
   project ceilings, verify no operator-PII tokens were introduced.
   `uv run gz arb ruff` and `uv run gz arb typecheck` — confirm clean.

7. **OBPI-scoped verification — REQ → @covers parity.**
   `uv run gz covers OBPI-0.0.27-05-citation-contract --json` — confirm
   `summary.uncovered_reqs == 0`. If any REQ is uncovered, return to step 1
   to add the missing decorator + assertion before proceeding.

8. **Heavy-lane verification.**
   `uv run gz arb step --name unittest -- uv run -m unittest tests/complexity/test_citation.py -v`
   `uv run gz arb step --name mkdocs -- uv run mkdocs build --strict`
   `uv run gz validate --documents --surfaces`
   `uv run gz validate --advisory-scorecard`

9. **Stage 4 evidence assembly + Gate 5 attestation.** Brief Gate 5 walks
   through the foundation + heavy walkthrough at brief level. Attestation
   text passes operator's verbatim words enriched with concrete session
   evidence (rule-version bump diff hunk, REQ-coverage table, ARB receipt
   IDs).

## Verification

```bash
uv run gz arb ruff
uv run gz arb typecheck
uv run gz arb step --name unittest -- uv run -m unittest tests/complexity/test_citation.py -v
uv run gz arb step --name mkdocs -- uv run mkdocs build --strict
uv run gz validate --documents --surfaces
uv run gz validate --advisory-scorecard
uv run gz agent sync control-surfaces  # diff empty
uv run gz covers OBPI-0.0.27-05-citation-contract --json
```

## Notes

- BDD waiver registered at `data/behave_coverage_waivers.json` for OBPI-05
  per brief Gate 4 — downstream-citation BDD scenarios land at OBPI-07's
  link-validator scope, not here. Add the waiver row if absent.
- Rule-version bump follows `.gzkit/rules/skill-surface-sync.md`: minor bump
  for governance-rule extension (formalizing tuple, percentile pairing,
  portability rule).
- `is_portable` default `supported_window=2` is the agent-drafted starting
  value matching the rule body's "supported window" parameterization.
  Operator may amend at attestation if practitioner-eye observation
  suggests a different window for the corpus refresh cadence.
- No operator-PII tokens permitted in rule body, schema, code, fixtures,
  attestation text, or commit messages per AGENTS.md § Local Agent Rules.
- Sibling-ADR scope-collision advisories (202 overlaps) are advisory per
  the audit code — most are speculative pending OBPIs in pool/pending ADRs
  and do not block. Real conflict surfaces only when those OBPIs activate.
