# PR-* test plan template

> **Use this when:** you're adopting the PRESENTATION_LAYER standard
> (`mgf-fed standards PRESENTATION_LAYER`; PR-01..PR-07) in a new
> SDK + app pair and want to ship the five hardened-pair pins listed
> in §8.1.
>
> **Reference impl:** [`mgf-hrb`](https://codeberg.org/magogi-admin/mgf-hrb)
> (SDK side) + [`hrb-app`](https://codeberg.org/magogi-admin/hrb-app)
> (app side). Both use this template's exact shape.

This template gives you a paste-able skeleton for every test
the standard requires, plus the supporting infrastructure
(scripts, CI wires, conformance-tracker shape). Lift the files,
rename `mgf.hrb` → `mgf.<your-sibling>` + `hrb_app` →
`<your-app>`, and your pair is hardened-bound for PR-*.

---

## Files to copy / adapt

### SDK-side (`<sibling-sdk>` repo)

| Target path | Source (mgf-hrb) | What it pins |
|---|---|---|
| `tests/unit/<pkg>/test_fmt.py` | `tests/unit/mgf/hrb/test_fmt.py` | Boundary values per band; TypeError on bool/float/str/None |
| `tests/unit/<pkg>/test_fmt_property.py` | `tests/unit/mgf/hrb/test_fmt_property.py` | Hypothesis property tests; output is bounded + safe-for-logs |
| `tests/unit/<pkg>/test_fmt_concurrency.py` | `tests/unit/mgf/hrb/test_fmt_concurrency.py` | 16 threads × 5k calls; no race, no exceptions |
| `tests/contract/test_fmt_canonical_output.py` | `tests/contract/test_fmt_canonical_output.py` | Public-API-stable strings (glyphs, uptime, duration, rss) |
| `tests/perf/test_fmt_helpers_latency.py` | `tests/perf/test_fmt_helpers_latency.py` | p99 < 1 µs per helper; cold-import < 5 ms |
| `tests/conformance/test_pr_standard_sdk.py` | same | Per-rule pin (PR-01, PR-04, PR-06, PR-07) |
| `tests/conformance/test_import_linter_alive.py` | same | Deliberate-violation meta-test |
| `tests/conformance/test_conformance_tracker_shape.py` | same | Tracker §PR shape validator |
| `scripts/audit_transitive_deps.py` | same | PR-01 supply-chain audit |
| `scripts/verify_wheel_contents.sh` | same | Built-wheel hygiene check |

### App-side (`<sibling>-app` repo)

| Target path | Source (hrb-app) | What it pins |
|---|---|---|
| `src/<app>/core/_sdk_compat.py` | `src/hrb_app/core/_sdk_compat.py` | Cross-version feature-detection at import time |
| `tests/conformance/test_pr_standard_app.py` | same | Per-rule pin (PR-02..PR-07 app side, T8 cross-version) |
| `tests/conformance/test_conformance_tracker_shape.py` | same | Tracker §PR shape validator |

### Federation-side (`mgf-common`)

The drift-detector script
[`scripts/audit_pr_conformance.py`](../../scripts/audit_pr_conformance.py)
is federation-shared — no copy needed; invoke it from each
sibling's CI:

```yaml
# .woodpecker.yml (or equivalent)
- name: audit_pr_conformance
  image: python:3.13-slim
  commands:
    - python /path/to/mgf-common/scripts/audit_pr_conformance.py .
```

---

## pyproject.toml additions

### Import-linter contracts (both sides)

```toml
[tool.importlinter]
root_packages = ["mgf.<sibling>"]   # or ["<app-pkg>"]
include_external_packages = true

# PR-01 — SDK forbids UI framework imports
[[tool.importlinter.contracts]]
name = "PR-01 — SDK forbids UI framework imports"
type = "forbidden"
source_modules = ["mgf.<sibling>"]
forbidden_modules = [
    "PySide6", "qasync",     # Qt
    "textual", "rich",       # TUI
    "prompt_toolkit",
    "click", "typer", "fire", # CLI frameworks
    # Add web frameworks (flask / fastapi / django) if your
    # SDK could plausibly reach for them.
]

# PR-06 — fmt module is stricter still
[[tool.importlinter.contracts]]
name = "PR-06 — fmt module forbids UI frameworks (strict)"
type = "forbidden"
source_modules = ["mgf.<sibling>.fmt"]
forbidden_modules = [
    # Same list as PR-01.
]
```

For the **app side**, add `layers` + horizontal `forbidden` contracts:

```toml
# PR-03 / PR-04 — vertical layer hierarchy
[[tool.importlinter.contracts]]
name = "<app-pkg> layered: ui -> view -> core"
type = "layers"
layers = ["<app-pkg>.ui", "<app-pkg>.view", "<app-pkg>.core"]

# PR-01 inside the app — only ui/ may import the UI framework
[[tool.importlinter.contracts]]
name = "PySide6 lives only in <app-pkg>.ui"
type = "forbidden"
source_modules = ["<app-pkg>.core", "<app-pkg>.view"]
forbidden_modules = ["PySide6", "qasync"]

# PR-05 — sibling UIs MUST NOT import each other
[[tool.importlinter.contracts]]
name = "PR-05: ui/cli MUST NOT import ui/qt"
type = "forbidden"
source_modules = ["<app-pkg>.ui.cli"]
forbidden_modules = ["<app-pkg>.ui.qt"]

[[tool.importlinter.contracts]]
name = "PR-05: ui/qt MUST NOT import ui/cli"
type = "forbidden"
source_modules = ["<app-pkg>.ui.qt"]
forbidden_modules = ["<app-pkg>.ui.cli"]
```

### Dev deps

```toml
[project.optional-dependencies]
dev = [
    # Upper bound on import-linter — major-version bumps may
    # change contract syntax silently. Bump deliberately.
    "import-linter>=2.0,<3.0",
    "hypothesis>=6.100",     # for property tests
    "pytest-timeout>=2.3",
    # ... rest of your dev deps
]
```

### Pytest markers

Register the 13-layer marker taxonomy (federation standard per
TS-19/-20) so `--strict-markers` catches typos:

```toml
[tool.pytest.ini_options]
addopts = "--strict-markers --strict-config -m 'not perf and not conformance and not slow'"
markers = [
    "smoke: minimal post-install sanity",
    "contract: wire-format + SDK-surface pins",
    "property: invariant-under-all-inputs (Hypothesis)",
    "concurrency: real-threads contention",
    "conformance: design-promise tests; release-gated",
    "perf: hot-path latency pins",
    # ... rest from TS-19/-20
]
```

---

## CI wiring (`scripts/gates` or `.woodpecker.yml`)

```sh
# 1. The four canonical green gates
run_gate "ruff" .venv/bin/ruff check src tests
run_gate "mypy --strict" .venv/bin/mypy --strict src
run_gate "pytest" .venv/bin/pytest -q --cov --cov-fail-under=80
run_gate "import-linter" .venv/bin/lint-imports --config pyproject.toml

# 2. PR-* additional gates
run_gate "PR-01 transitive deps" .venv/bin/python scripts/audit_transitive_deps.py
# Wheel verification runs after `python -m build`:
# scripts/verify_wheel_contents.sh
```

For the conformance-marker tier (nightly / release-gated), add:

```sh
.venv/bin/pytest -m "conformance or contract or property or concurrency or perf"
```

---

## Conformance tracker §PR section

Both projects ship `docs/inprogress/MGF_STANDARDS_CONFORMANCE.md`
with a `### §2.X PR — PRESENTATION_LAYER` section. Use this row
shape — the `test_conformance_tracker_shape.py` validator pins
that ✅ rows cite evidence (file path / test name / contract
name / commit SHA):

```markdown
### §2.X PR — PRESENTATION_LAYER

| Rule | Status | Notes |
|---|---|---|
| PR-01 SDK forbids UI frameworks | ✅ | Enforced via `import-linter` contract `PR-01 — SDK forbids UI framework imports` in [`pyproject.toml`](../../pyproject.toml); gate runs in `scripts/gates`. |
| PR-04 import-linter enforces layering | ✅ | `lint-imports` runs as a hard gate; meta-test in `tests/conformance/test_import_linter_alive.py` proves the gate is alive (synthetic violation rejected). |
| PR-06 stdlib-only fmt helpers | ✅ | Reference impl at [`src/mgf/<sibling>/fmt.py`](../../src/mgf/<sibling>/fmt.py); 60+ pin tests across unit / property / concurrency / contract / perf. |
| PR-07 tracker §PR section | ✅ | This section. Shape validated by `tests/conformance/test_conformance_tracker_shape.py`. |
```

---

## What "hardened pair" means for you

Once you ship the items above, your pair can declare PR-*
**binding for itself** (not waiting for federation-wide
promotion). Update both trackers' §1 to drop the "DRAFT pending
second adopter" caveat and add an audit-log row citing your
hardening commit SHAs. The standard itself stays federation-DRAFT
until a second pair adopts; that's PR-02's promotion criteria,
not yours to gate.

If you DO end up being the second pair: drop the DRAFT marker
from the PRESENTATION_LAYER standard in mgf-standard §8 + open
a PR to mgf-standard describing your adoption. The standard
promotes to L1-binding federation-wide on merge.
