# Plan: OBPI-0.0.28-01-threshold-rule-file

**OBPI:** OBPI-0.0.28-01-threshold-rule-file
**Parent ADR:** ADR-0.0.28 (foundation, heavy lane)
**Objective:** Author `.gzkit/rules/complexity-thresholds.md` codifying the per-metric threshold table (each metric with mandatory `block` band + optional `warn` / `advise`), the fixed three-value trigger-semantic vocabulary (`block` / `warn` / `advise`), the binding percentile + absolute-number pairing requirement, the citation tuple form pointing at OBPI-0.0.27-04's distilled-characteristics document (corpus_revision 1), and the operator-amendable mapping protocol section. Vendor-mirror via `gz agent sync control-surfaces`. Land scorecard entry classifying the new rule as **Mechanical** (enforced by OBPI-0.0.28-03 validator).

## Allowed Files

- `.gzkit/rules/complexity-thresholds.md` — new canonical rule file
- `.claude/rules/complexity-thresholds.md` — vendor mirror (generated by sync)
- `.agents/rules/complexity-thresholds.md` — vendor mirror (generated by sync)
- `.github/instructions/complexity-thresholds.md` — vendor mirror (generated by sync)
- `docs/governance/advisory-rules-audit.md` — scorecard entry
- `tests/governance/test_complexity_thresholds_rule.py` — REQ-derived tests
- `data/behave_coverage_waivers.json` — BDD waiver registration (Gate 4 requirement; sibling OBPI-0.0.27-01 used the same path; brief Allowed Paths gap noted)
- `docs/design/adr/foundation/ADR-0.0.28-complexity-threshold-doctrine/**` — brief evidence updates only

## Context

### Source-of-truth artifacts read

- ADR-0.0.28 — § Decision (rationale 1-6 binding), § Threshold table shape (binding example: `(p75=advise, p90=warn, p95=block)`), § Trigger-semantic vocabulary (binding three-value enum), § Mechanical surfaces (rule path, validator path, scorecard target).
- OBPI-0.0.28-01 brief — 13 numbered REQs, 7 acceptance criteria.
- `docs/governance/complexity/distilled-characteristics-2026-05-04.md` — corpus_revision 1, 12 per-metric sections with p50/p75/p90/p95/p99 numbers.
- `.gzkit/rules/complexity-doctrine.md` — § Citation contract (binding: `(distilled_characteristics_path, section_anchor, corpus_revision)` tuple; percentile + absolute pairing; refresh-portability window).
- `src/gzkit/complexity/citation.py` — `Citation` Pydantic model (`frozen=True`, `extra='forbid'`); `parse_citation` is the consumer this rule body must satisfy.
- Sibling precedent: `.claude/plans/complexity-doctrine-rule-OBPI-0.0.27-01.md` (validated rule-OBPI plan; identical structure pattern).

### Measured corpus numbers (corpus_revision 1, 13 projects)

Direct citations from `distilled-characteristics-2026-05-04.md`:

| Metric | p50 | p75 | p90 | p95 | p99 | Polarity | Status |
|--------|-----|-----|-----|-----|-----|----------|--------|
| `radon_cc` | 2.00 | 4.00 | 7.00 | 11.00 | 24.00 | high=worse | citable |
| `radon_mi` | 59.53 | 79.58 | 100.00 | 100.00 | 100.00 | low=worse | inverted polarity + clamped — bootstrap |
| `radon_hal_volume` | 233.19 | 946.89 | 2740.93 | 5549.80 | 14981.93 | high=worse | citable |
| `radon_hal_difficulty` | 4.35 | 8.13 | 11.54 | 12.46 | 17.80 | high=worse | citable |
| `radon_hal_effort` | 1030.14 | 7975.79 | 30805.01 | 74805.40 | 216567.24 | high=worse | citable |
| `radon_raw_nloc` | 105.00 | 311.75 | 733.20 | 1031.90 | 3143.82 | high=worse | citable |
| `radon_raw_lloc` | 90.50 | 238.25 | 518.00 | 811.70 | 2502.73 | high=worse | citable |
| `lizard_nloc` | 6.00 | 13.00 | 25.00 | 37.00 | 77.00 | high=worse | citable |
| `lizard_param_count` | 2.00 | 3.00 | 4.00 | 5.00 | 11.00 | high=worse | citable |
| `lizard_nesting_depth` | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | high=worse | **measurement defect** — bootstrap |
| `lizard_ccn` | 2.00 | 4.00 | 8.00 | 11.00 | 25.00 | high=worse | citable |
| `cohesion_lcom4` | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | high=worse | **measurement defect** — bootstrap |

**9 metrics citable directly** with `(p75, p90, p95)` → `(advise, warn, block)` mapping per ADR § Threshold table shape worked example.

**3 metrics require REQ-11 bootstrap carve-out:**
- `radon_mi` — inverted polarity (low=bad) is not modellable by the current `band_for` contract (assumes high=bad). Compounded by clamping (p90+ all = 100). Use bootstrap conservative absolutes; flag with explicit annotation; file follow-up GHI for polarity-aware threshold-model amendment landing in OBPI-02 or as ADR-0.0.28 amendment.
- `lizard_nesting_depth` — all-zero baselines indicate measurement-pipeline parser defect (already flagged by OBPI-0.0.27-04 closeout). Use bootstrap conservative absolutes drawn from canon (Martin: nesting depth > 3 is the extract-method/guard-clause signal). File follow-up GHI for parser fix.
- `cohesion_lcom4` — same all-zero defect as nesting_depth. Bootstrap absolutes; same GHI follow-up scope or separate.

### Trigger-semantic mapping (operator-amendable doctrine; default chosen for this OBPI)

ADR § Threshold table shape worked example: `(p75, advise), (p90, warn), (p95, block)`. This OBPI adopts that mapping as the default for the 9 high=worse metrics with non-zero distributions. Rationale recorded in rule body's "Operator-amendable mapping protocol" section: amendments flow through the doctrine-amendment-protocol pool stub forward-referenced from ADR-0.0.27 OBPI-02.

### Schema constraints (RuleFrontmatter, src/gzkit/rules.py)

- Required frontmatter: `id` (kebab-case), `paths` (list[str], min_length=1), `description` (str)
- `extra="forbid"` — never add `skill-version:` to rule frontmatter
- Body-level marker: `<!-- rule-version: 0.1.0 -->` HTML comment + `> **Rule version:** \`0.1.0\`` block quote (per `.gzkit/rules/skill-surface-sync.md`)

### Citation tuple constraint (Citation Pydantic model)

Every band's percentile + absolute MUST be paired; the rule body's "Citation" section at top must carry the canonical tuple naming the distilled-characteristics document. Citation model fields:
- `distilled_characteristics_path` — relative path under `docs/governance/complexity/`, `.md`-ending
- `section_anchor` — slugified anchor matching a metric section in the cited document
- `corpus_revision` — positive int matching the cited document's frontmatter

## Steps

### Step 1: File two follow-up GHIs (in-flight defects, brief-flagged)

Per AGENTS.md § Prime Directive #6 (every defect must be trackable), file:

1. **GHI: Measurement-pipeline parser produces all-zero baselines for `lizard_nesting_depth` and `cohesion_lcom4`** — rooted at OBPI-0.0.27-03 measurement pipeline; surfaced in OBPI-0.0.27-04 closeout evidence; affects threshold-table authoring in this OBPI. Label `defect`. Reference OBPI-0.0.27-04's evidence section line on this finding.
2. **GHI: Polarity-aware threshold model for inverted-polarity metrics (`radon_mi`)** — `band_for(metric, value)` semantics assume higher = worse; `radon_mi` is lower = worse. Affects OBPI-0.0.28-02 loader contract. Label `enhancement`. Either landed as a separate OBPI under ADR-0.0.28 amendment, or addressed inside OBPI-02's loader.

Both GHIs cited in the rule body's bootstrap annotations and in the OBPI evidence section. **GHIs are filed via `gh issue create`, not `gz issue file`** (this is the gzkit repo itself, not a consumer repo).

### Step 2: Update brief Allowed Paths (administrative fix)

Add `data/behave_coverage_waivers.json` to OBPI-0.0.28-01 brief Allowed Paths section (sibling OBPI-0.0.27-01 used the identical path for its Gate 4 waiver; the gap is administrative, not architectural).

### Step 3: Write failing tests — TDD RED phase

Create `tests/governance/test_complexity_thresholds_rule.py` with REQ-derived tests:

- `REQ-0.0.28-01-01` — frontmatter validates (id, paths, description); body-level rule-version marker (`<!-- rule-version: 0.1.0 -->`) matches visible block quote (`> **Rule version:** \`0.1.0\``).
- `REQ-0.0.28-01-02` — trigger-semantic vocabulary section declares exactly three values (`block`, `warn`, `advise`); no fourth value present.
- `REQ-0.0.28-01-03` — for each canonical metric (12-tuple from `gzkit.complexity.measurement.CANONICAL_METRICS`), per-metric threshold table has at least a `block` band row.
- `REQ-0.0.28-01-04` — every band row carries both `corpus_percentile` and `absolute_number` (absence of either fails); structural test parsing the rule's per-metric tables.
- `REQ-0.0.28-01-05` — Citation section at top of rule body contains canonical tuple `(distilled_characteristics_path, section_anchor, corpus_revision)`; the cited path resolves; `parse_citation` round-trips; `corpus_revision` matches the cited document's frontmatter.
- `REQ-0.0.28-01-06` — operator-amendable mapping protocol section present; references doctrine-amendment-protocol stub; declares silent edits forbidden.
- `REQ-0.0.28-01-07` — scorecard at `docs/governance/advisory-rules-audit.md` carries a `complexity-thresholds` entry classified Mechanical with citations to ADR-0.0.28 (parent) and OBPI-0.0.28-03 (validator).
- `REQ-0.0.28-01-08` — `uv run gz validate --advisory-scorecard` exits 0.
- `REQ-0.0.28-01-09` — `uv run gz agent sync control-surfaces` post-sync diff is empty (vendor mirror parity).
- `REQ-0.0.28-01-10` — bootstrap carve-out section names exactly the three known-bootstrap metrics (`radon_mi`, `lizard_nesting_depth`, `cohesion_lcom4`) with concrete cited GHI numbers; no others.

Each test decorated with `@covers("REQ-0.0.28-01-NN")`. Tests load rule content from disk; no string-pinning of currently-observed bytes per `.gzkit/rules/tests.md` § "Tests assert semantics, not strings" (Invariant 6f).

### Step 4: Author `.gzkit/rules/complexity-thresholds.md` — TDD GREEN phase

Sections, in order:

1. **Frontmatter** — `id: complexity-thresholds`, `paths: [".gzkit/rules/complexity-thresholds.md", ".claude/rules/complexity-thresholds.md", ".agents/rules/complexity-thresholds.md", ".github/instructions/complexity-thresholds.md"]`, `description: <one sentence>`. No `skill-version` (forbidden by schema).
2. **Body-level rule-version marker** — `<!-- rule-version: 0.1.0 -->` HTML comment + `> **Rule version:** \`0.1.0\`` block quote with one-sentence rationale.
3. **`# Complexity Thresholds (gzkit)`** title.
4. **`## Citation`** — canonical tuple naming `docs/governance/complexity/distilled-characteristics-2026-05-04.md`, `corpus_revision: 1`. Plain-text canonical-string form per `parse_citation` shape. Reference `.gzkit/rules/complexity-doctrine.md` § Citation contract for the binding.
5. **`## Invariant`** — one-paragraph statement: gzkit publishes one canonical threshold table; vocabulary fixed; per-metric mapping operator-amendable; downstream surfaces consume.
6. **`## Trigger-Semantic Vocabulary (binding)`** — exactly three values. Each value's effect on downstream consumer surfaces (`block` → xenon-as-gate exit 3; `warn` → advisor recommendation; `advise` → authoring-guidance hint, no build effect).
7. **`## Per-metric threshold tables`** — twelve subsections, one per `CANONICAL_METRIC`. Each subsection carries:
   - Per-metric markdown table with columns `Trigger` | `Corpus percentile` | `Absolute number` | `Cited section`.
   - For 9 citable metrics: three rows (advise/warn/block) at (p75/p90/p95) percentiles citing the metric's section in the distilled document. Absolute numbers from the distillation table above. `Cited section` column = slugified metric anchor (e.g. `radon-cc`).
   - For 3 bootstrap metrics: bootstrap rows with absolute numbers chosen conservatively (see § Bootstrap absolutes below) and explicit `Bootstrap (GHI #N)` annotation in the `Cited section` column.
8. **`## Bootstrap absolutes (REQ-11 carve-out — one-shot)`** — names the three bootstrap metrics (`radon_mi`, `lizard_nesting_depth`, `cohesion_lcom4`); names each by GHI number; states the carve-out is one-shot (validator skips portability checks against bootstrap rows until removed). Bootstrap absolute defaults (subject to operator amendment):
   - `radon_mi` (lower=worse, clamped at 100): bootstrap `block=50` (well below corpus p50=59.53), `warn=70`, `advise=85`. Polarity-aware semantics deferred to GHI #N (polarity-aware model).
   - `lizard_nesting_depth` (parser defect): bootstrap from canon — Martin/Clean Code names depth > 3 as the guard-clause signal. `advise=2`, `warn=3`, `block=4`.
   - `cohesion_lcom4` (parser defect): conservative — `advise=2`, `warn=4`, `block=8` (LCOM4 measures cohesion fracture; values reflect class fragmentation).
9. **`## Operator-amendable mapping protocol`** — amendments to `(metric, band, trigger)` mappings flow through doctrine-amendment-protocol pool stub (forward-referenced from ADR-0.0.27 OBPI-02). Silent edits forbidden by validator (OBPI-0.0.28-03). Scope of amendment: per-metric mapping only; the trigger-semantic vocabulary itself is foundation doctrine and amendable only via ADR-0.0.28 ceremony.
10. **`## Refresh portability`** — when corpus refresh produces a new distilled-characteristics document, the table's percentile column remains; absolute numbers are re-cited; the citation `corpus_revision` updates. Link-integrity validator (OBPI-0.0.27-07) flags out-of-date citations.
11. **`## Anti-patterns`** — explicit list (silent edits, missing block band, percentile-only without absolute, citation outside the canonical tuple form, mixing fourth state into vocabulary).
12. **`## Related`** — links to ADR-0.0.28, ADR-0.0.27, OBPI-0.0.28-02 (loader), OBPI-0.0.28-03 (validator), `.gzkit/rules/complexity-doctrine.md`.

### Step 5: Update scorecard in `docs/governance/advisory-rules-audit.md`

Add new section after the last rule section:

```markdown
### Complexity Thresholds (`.gzkit/rules/complexity-thresholds.md`)

| # | Rule | Score | Why |
|---|------|-------|-----|
| <next> | gzkit publishes one canonical threshold table whose every entry is a (metric, percentile-band, absolute-number, trigger-semantic) tuple cited from the current distilled-characteristics document. | **Mechanical** | Enforced by `uv run gz validate --complexity-thresholds` (OBPI-0.0.28-03). Parent ADR-0.0.28. |
```

Update Summary table counts. Run `uv run gz validate --advisory-scorecard` to confirm exit 0.

### Step 6: Register BDD waiver in `data/behave_coverage_waivers.json`

Pattern from sibling OBPI-0.0.27-01:

```json
"OBPI-0.0.28-01-threshold-rule-file": {
  "rationale": "adr-0.0.28-foundation-bdd-deferred",
  "status_at_landing": "Draft"
}
```

Named rationale:

```json
"adr-0.0.28-foundation-bdd-deferred": "ADR-0.0.28 (complexity-threshold-doctrine) is foundation/heavy-kind doctrine codifying the per-metric threshold table. OBPI-01 is a rule-only OBPI with no CLI surface to scenario-test; verification is via Python unit tests asserting rule-body content. BDD coverage for the CLI surface (gz validate --complexity-thresholds) is deferred to OBPI-03 (validator), which provides the scenario-testable surface. Same pattern as ADR-0.0.27 / OBPI-01."
```

### Step 7: Run `uv run gz agent sync control-surfaces`

Propagate `.gzkit/rules/complexity-thresholds.md` to all four vendor mirrors. Verify post-sync diff is empty.

### Step 8: Validate

```bash
uv run gz lint
uv run gz typecheck
uv run gz test
uv run gz validate --advisory-scorecard
uv run gz validate --documents --surfaces
uv run gz agent sync control-surfaces  # diff must be empty
uv run gz arb step --name unittest -- uv run -m unittest tests/governance/test_complexity_thresholds_rule.py -v
uv run gz covers OBPI-0.0.28-01-threshold-rule-file --json  # parity gate must show uncovered=0
```

## Verification

```bash
uv run gz arb ruff
uv run gz arb typecheck
uv run gz arb step --name unittest -- uv run -m unittest -q
uv run gz arb step --name unittest -- uv run -m unittest tests/governance/test_complexity_thresholds_rule.py -v
uv run gz arb step --name mkdocs -- uv run mkdocs build --strict
uv run gz validate --advisory-scorecard
uv run gz validate --documents --surfaces
uv run gz covers OBPI-0.0.28-01-threshold-rule-file --json
```

Heavy-lane evidence: ARB receipts for ruff, typecheck, unittest, mkdocs.

## Notes

- **Scope collision advisory:** `tests/governance/test_complexity_thresholds_rule.py` will be flagged by `gz plan audit` as contested with 217 sibling-ADR test paths. This is advisory-only noise (any new test under `tests/governance/**` collides with every governance OBPI). Same as OBPI-0.0.27-01 sibling precedent.
- **GHI filing surface:** This is gzkit itself — use `gh issue create --label defect|enhancement` directly. The `gz issue file` wrapper is for consumer repos filing against gzkit.
- **Brief REQ-11 reading:** Bootstrap is "one-shot," not "permanent escape hatch." For 2 measurement-defect metrics, bootstrap is the one-shot until the parser GHI lands. For 1 inverted-polarity metric, bootstrap is the one-shot until the polarity-aware model GHI lands. Both are tracked.
- **Operator decision points encoded as defaults:**
  - Trigger mapping default: `(p75=advise, p90=warn, p95=block)` — ADR worked example.
  - Bootstrap absolutes for `radon_mi`: 50/70/85 — drawn conservative below corpus p50.
  - Bootstrap absolutes for `lizard_nesting_depth`: 2/3/4 — drawn from Martin canon.
  - Bootstrap absolutes for `cohesion_lcom4`: 2/4/8 — conservative LCOM4 thresholds.
  These defaults are operator-amendable per the rule body's protocol section; they ship as the agent's best-effort starting point with explicit attestor review at Gate 5.
