# Presentation Layer

> **Conformance:** L1 (every Magogi-foundation domain library that
> grows a UI consumer).
> **Audience:** Maintainers designing a domain SDK that will have
> multiple UI shapes (CLI today, GUI tomorrow, web/TUI later);
> consumer-app maintainers structuring the app side; AI agents
> scaffolding either side.
> **Reference consumers:** [`mgf-hrb`](https://codeberg.org/magogi-admin/mgf-hrb)
> (the domain SDK, owns the `fmt` helpers + import-linter fence) +
> [`hrb-app`](https://codeberg.org/magogi-admin/hrb-app) (the
> consumer app with three-layer separation + CLI + Qt GUI absorbed
> into one repo).
> **Relationship to [`GUI_CONSUMERS.md`](GUI_CONSUMERS.md):**
> PR-* is the **framework-neutral umbrella** — applies to every
> UI shape (CLI, Qt GUI, TUI, web). `GUI_CONSUMERS.md` (GUI-*) is
> the **Qt-specific specialisation** — when the UI happens to be
> PySide6, the GUI-* rules add the Qt-specific bar (worker
> pattern, excepthook bridge, `pytest-qt`, etc.) on top of PR-*.
> Adopting GUI-01..GUI-06 IS adopting PR-01, PR-03, PR-04, PR-05
> for the Qt sub-tree; the two doc sets cite each other but
> never disagree.
> **Propagation (DOC-18):** changing a rule here ripples — walk DOCUMENTATION.md §10.4 and update its dependents in the same commit (README *Index of MUSTs*, the `mgf-fed standards` / `agents` CLI, and any parallel copies / pins the matrix names).

> **Scope.** Where presentation logic lives when a domain library
> has multiple UI consumers, and how the SDK ↔ app boundary is
> structured so neither side can drift. The rules here are small
> but **L1-binding** because presentation drift is the most
> expensive class of long-tail tax in a federation: once a CLI
> computes uptime one way and a GUI computes it another, the
> operator-visible disagreement outlives every contributor who
> wrote it.

---

## Rules box (read this first)

| ID | Rule |
|---|---|
| <a id="PR-01"></a>**PR-01** | **A domain SDK MUST NOT import any UI framework.** No `PySide6`, `PyQt6`, `textual`, `rich`, `prompt_toolkit`, `click`, `flask`, `fastapi`, `django` at module level anywhere under the SDK's import root. The rule MUST be enforced mechanically via `import-linter` (or equivalent). The SDK exists to serve UIs; it MUST NOT pick one. This generalises [`GUI_CONSUMERS.md`](GUI_CONSUMERS.md) GUI-01 (which is the Qt specialisation). |
| <a id="PR-02"></a>**PR-02** | **When a sibling SDK has 2+ UI consumers, the presentation logic SHOULD live in a single dedicated repo, conventionally named `<sibling>-app`.** Concentrating presentation in one app repo (rather than scattering across `<sibling>-cli`, `<sibling>-gui`, `<sibling>-web` siblings) lets the view-models be shared across every UI, lets one owner enforce the layering, and lets per-framework adapters stay small. A single UI consumer MAY skip this — the rule activates at the second one. The reference shape is [`hrb-app`](https://codeberg.org/magogi-admin/hrb-app), which absorbed [`mgf-hrb`](https://codeberg.org/magogi-admin/mgf-hrb)'s CLI as `ui/cli/` alongside its existing Qt GUI in `ui/qt/`. |
| <a id="PR-03"></a>**PR-03** | **An app repo with multiple UIs MUST separate UI-framework code from the shared-rendering layer.** The canonical layout is `src/<app>/{core,view,ui/<framework>}/` — **`core/`** for the domain adapter (typed facade over the SDK, polling, events; no UI deps), **`view/`** for framework-free view-models (plain dataclasses + pure transforms; the "what to render" shape), **`ui/<framework>/`** for the framework-specific renderer. **Accepted equivalent shape:** projects whose `core/` and `view/` would collapse into one MAY ship `src/<app>/{presentation,<framework>}/` where `presentation/` carries both responsibilities (domain adapter + framework-free view-models) and each `<framework>/` (e.g. `cli/`, `gui/`) is a top-level sibling rather than nested under `ui/`. This variance is endorsed for apps without a long-lived domain-event stream that genuinely needs its own layer — the load-bearing invariant is *"a framework-free shared layer sits below each UI-framework layer"*, not the layer count. Reference shapes: [`hrb-app`](https://codeberg.org/magogi-admin/hrb-app) for the three-layer canonical; [`vm-vmanager-app`](https://codeberg.org/magogi-admin/vm-vmanager-app) for the `presentation/<framework>` variant. |
| <a id="PR-04"></a>**PR-04** | **The layer hierarchy MUST be enforced mechanically via `import-linter` (or equivalent).** Two contracts at minimum: (a) `core` and `view` MUST NOT import any UI framework (PR-01 inside the app); (b) `ui/<framework>` MUST import only its own framework + `view/` + the SDK — never `ui/<other-framework>/`. The contracts MUST run in CI as a blocking gate. A layer rule that ships without mechanical enforcement is a comment, not a standard. |
| <a id="PR-05"></a>**PR-05** | **Each `ui/<framework>/` adapter MUST consume `view/` only — never another `ui/<other>/` adapter.** UIs are siblings, not parents. The CLI MUST NOT `import` from the Qt UI, and vice-versa. Shared rendering logic belongs in `view/` (framework-free) or in the SDK's `fmt` helpers (PR-06) — never in another UI. The rule prevents the "borrow this widget from the other UI" anti-pattern that couples renderers across frameworks and makes either UI unremovable. |
| <a id="PR-06"></a>**PR-06** | **The SDK MAY expose pure `fmt` helpers for medium-agnostic formatting (uptime → `"Hh Mm Ss"`, bytes → `"42.3 MiB"`, severity → glyph).** When it does, the binding OUTCOME is that those helpers **import the standard library only** — no third-party imports, no `mgf.common`, no logging, no I/O. That outcome MUST be mechanically pinned by a **stdlib-allowlist meta-test** (parse the helper module's AST, collect every `import` / `from … import`, assert the top-level package of each is in `sys.stdlib_module_names` — the canonical primary check, because it expresses the full "no `mgf.common`" clause that `import-linter` provably cannot, see §4). An `import-linter` forbidden-modules contract SHOULD additionally fence the obvious third-party packages as **defense-in-depth**, but it is not the load-bearing check. Helpers SHOULD live at `<sdk-pkg>.fmt` for discoverability. Reference: [`mgf.hrb.fmt`](https://codeberg.org/magogi-admin/mgf-hrb/src/branch/main/src/mgf/hrb/fmt.py). The rule is SHOULD (not MUST) because some SDKs have no medium-agnostic formatting need; when they do, fragmenting the implementation across consumers guarantees drift. |
| <a id="PR-07"></a>**PR-07** | **Conformance trackers MUST classify PR-* rules per the [audit-outcomes taxonomy](principles/audit-outcomes.md) (✅ / ❌ / ⚪ / 🚫 / ⏳ / N/A), same discipline as DP-* / EH-* / etc.** A sibling at L1+ with a UI consumer MUST have a `PR — PRESENTATION_LAYER` section in its `docs/inprogress/MGF_STANDARDS_CONFORMANCE.md`. A pure-SDK sibling with zero UI consumers MAY mark PR-02..PR-05 as `N/A` (rule activates at the first UI). PR-01 always applies. |

---

## 1. Why this standard exists

A domain SDK with one consumer is structurally simple: the SDK
owns the types, the consumer owns the rendering, and any helper
that's "kind of UI but kind of domain" can live wherever the
single owner finds convenient. The moment a second UI lands —
say, a CLI plus a desktop GUI — that convenience breaks in three
ways that compound:

1. **Duplicated formatting.** Both UIs need to render "uptime"
   as `"3h 14m"`. If the helper lives in the CLI, the GUI
   either re-implements it (drift) or imports from the CLI
   (the GUI now depends on the CLI; pulling either one is
   blocked by the other).
2. **Framework contamination of the SDK.** A well-meaning
   contributor adds a Qt-aware convenience to the SDK ("just a
   tiny `to_qt_model()` method on this dataclass"). Now every
   consumer of the SDK pays for PySide6 in its dependency tree
   — including the headless CLI, the curl-driven test
   harnesses, and the eventual web dashboard.
3. **Test-fixture sprawl.** Each UI grows its own test
   harness with its own mock of the SDK. They drift. Bugs that
   only show up under specific render paths slip past CI
   because no test exercises the shared rendering shape.

The PR-* rules exist to make those three failure modes
**structurally impossible** rather than caught-in-review:

- PR-01 makes (2) a CI failure, not a judgement call.
- PR-03..PR-05 make (1) a layout violation, not a code-smell.
- PR-06 gives the legitimate "shared formatting" need a single
  home in the SDK, fenced so it can't grow framework deps.

---

## 2. The canonical layout

```
<sdk>/                              ← the domain SDK (e.g. mgf-hrb)
├── src/<sdk-pkg>/
│   ├── __init__.py                ← typed Client, dataclasses
│   ├── fmt.py                     ← stdlib-only formatting helpers (PR-06)
│   └── ...                        ← NO ui frameworks anywhere (PR-01)
└── pyproject.toml                  ← import-linter forbids ui-framework imports

<app>/                              ← the consumer app (e.g. hrb-app)
├── src/<app-pkg>/
│   ├── core/                      ← domain adapter; imports <sdk> only
│   │   ├── broker.py
│   │   └── events.py
│   ├── view/                      ← view-models; imports core/ + <sdk>.fmt
│   │   ├── device_tree.py
│   │   ├── doctor.py
│   │   └── ...
│   └── ui/                        ← framework adapters; each imports view/ only
│       ├── cli/                   ← argparse / click; the CLI rendererset
│       │   ├── __main__.py
│       │   ├── scan.py
│       │   └── ...
│       └── qt/                    ← PySide6; the GUI widget set
│           ├── main_window.py
│           └── ...
└── pyproject.toml                  ← import-linter: core/view/ no qt; ui/* no peers
```

Dependencies go strictly down:

```
ui/<framework>  ──▶  view  ──▶  core  ──▶  <sdk>
       │                                      │
       └─────────────────────┐                ▼
                             │           <sdk>.fmt
                             └──────────▶  (stdlib only)
```

Never up. Never sideways between `ui/<a>` and `ui/<b>`.

---

## 3. What the SDK MUST NOT do (PR-01 elaboration)

The forbidden-import set for any SDK under `mgf.*`:

| Category | Forbidden modules |
|---|---|
| Qt / desktop GUI | `PySide6`, `PyQt6`, `PyQt5`, `PySide2` |
| Async-Qt bridges | `qasync`, `asyncqt` |
| TUI | `textual`, `rich`, `prompt_toolkit`, `urwid` |
| CLI argument parsing as a framework | `click`, `typer`, `fire` (argparse is stdlib, allowed) |
| Web | `flask`, `fastapi`, `django`, `starlette`, `aiohttp.web`, `bottle` |
| HTML / template engines | `jinja2`, `mako`, `chameleon` |

`argparse` is stdlib and not a framework — using it inside an
SDK's own debug entrypoint (e.g. `python -m <sdk>` for a
self-doctor) is fine. The bar is "no THIRD-PARTY UI framework",
not "no UI code at all."

`logging` is allowed because it's stdlib and not a UI framework
— log records are observable from any UI or none.

**Enforcement template** for `.importlinter` (or
`pyproject.toml` `[tool.importlinter]`):

```ini
[importlinter:contract:no-ui-frameworks]
name = SDK must not import any UI framework
type = forbidden
source_modules = <sdk-pkg>
forbidden_modules =
    PySide6
    PyQt6
    qasync
    textual
    rich
    prompt_toolkit
    click
    typer
    flask
    fastapi
    django
```

---

## 4. What `<sdk>.fmt` MAY contain (PR-06 elaboration)

The legitimate medium-agnostic formatting kinds the SDK MAY ship:

- **Time** — `format_uptime(ms: int) -> str` → `"3h 14m 27s"`;
  `format_duration(us: int) -> str` → `"123µs"` / `"4.5ms"` /
  `"1.2s"` / `"5m12s"`.
- **Size** — `format_rss(bytes: int) -> str` → `"42.3 MiB"` /
  `"1.2 GiB"` (binary prefixes — IEC; decimal SI prefixes if
  the domain calls for them, e.g. network throughput).
- **Severity / status glyphs** — `format_severity_glyph(s) ->
  str` → the canonical 5-char band (`"  OK "`, `" WARN"`,
  `" FAIL"`, `" ... "`) used by every renderer that shows a
  status column.
- **Domain-specific compact forms** — e.g. `format_urn_short()`,
  `format_bdf()`, anything where the SDK has the type and the
  rendering shape is operator-canonical.

What `<sdk>.fmt` MUST NOT contain:

- **Color codes / ANSI escapes** — those are CLI-specific
  decoration, not formatting. They belong in `ui/cli/`.
- **HTML / markdown emission** — those are web/GUI-specific.
- **Multi-line table layout** — that's a renderer concern;
  `view/` or `ui/<framework>/` owns it.
- **I/O** — no logging, no file reads, no network. Pure
  functions on primitives.
- **Third-party imports** — not even `mgf.common`. Stdlib only.
  The fence is mechanically checked.

**Enforcement template** for the stdlib-only fence:

```ini
[importlinter:contract:fmt-is-stdlib-only]
name = <sdk>.fmt must not depend on non-stdlib
type = forbidden
source_modules = <sdk-pkg>.fmt
forbidden_modules =
    pydantic
    pydantic_settings
    PySide6
```

(Listing `mgf.common` here is rejected by import-linter as
"subpackages of external packages are not valid" — which is exactly
why the **stdlib-allowlist meta-test is the primary check** per PR-06
(parse the module AST, assert every import's top-level package is in
`sys.stdlib_module_names`; it expresses the full "no `mgf.common`"
clause import-linter cannot). This forbidden-set contract is kept as
**defense-in-depth** covering every third-party import any sibling
currently has.)

---

## 5. The three-layer split (PR-03 elaboration)

### `core/` — the domain adapter

What goes here: the typed facade that the rest of the app talks
to, plus polling loops, event fan-out, connection lifecycle,
retry / circuit-breaker behaviour against the SDK.

Imports allowed: the SDK (`<sdk>`), `mgf.common`, stdlib,
async libraries (`asyncio`, `anyio`). NO UI frameworks. NO
imports from `view/` or `ui/`.

The smell test: `core/` MUST be importable + testable in a
pure-Python REPL with zero UI deps installed. If it isn't,
something has leaked.

### `view/` — framework-free view-models

What goes here: plain dataclasses + pure functions that turn
`core/`'s output into the shape a renderer wants. "Take the
broker's `StateSnapshot` and return a `DoctorViewModel(headline:
str, sections: tuple[Section, ...], severity_counts: ...)`."

Imports allowed: `core/`, the SDK (especially `<sdk>.fmt`),
stdlib. NO UI frameworks. NO imports from `ui/`.

The smell test: every `view/` module MUST be testable with
plain `pytest` and zero `pytest-qt` / `httpx` / `click.testing`
machinery. If a test needs a framework harness, the logic
belongs in `ui/<framework>/`, not in `view/`.

### `ui/<framework>/` — the per-framework adapter

What goes here: argparse trees + `print()` calls for CLI; `QWidget`
subclasses + signals/slots for Qt; route handlers + templates
for web. The framework's idiom, applied to a `view/` view-model.

Imports allowed: the framework, `view/`, the SDK (only for
typing imports + `fmt`; never `core/` directly — go through
`view/`), stdlib. NO imports from any peer `ui/<other>/`.

The smell test: removing any one `ui/<framework>/` subtree
MUST leave the other framework UIs (and all tests under
`tests/unit/{core,view}/`) passing untouched.

### 5.5 The `presentation/<framework>` variant (PR-03 endorsement)

Some apps don't benefit from separating `core/` and `view/`
— the domain adapter and the framework-free view-model would
share the same module surface. Those apps MAY collapse the two
into one `presentation/` layer with each `<framework>/` as a
top-level sibling:

```
<app>/
├── presentation/                    ← domain adapter + view-models
├── cli/                             ← framework-free except for argparse
├── gui/                             ← Qt / PySide6
└── observability/                   ← optional shim to mgf.common
```

The CRITICAL invariants are preserved:

1. **`presentation/` is framework-free.** Same constraint as
   `view/` in the canonical: NO UI frameworks. Mechanical
   `import-linter` check: `presentation/` forbids `PySide6` /
   `qasync` / `textual` / etc.
2. **`cli/` and `gui/` are independent.** Same constraint as
   PR-05. Mechanical `import-linter` `independence` contract:
   neither imports the other (except for a lazy entry-point edge
   like `cli.groups.gui → gui.app`, listed in `ignore_imports`).
3. **`presentation/` does not import either `cli/` or `gui/`.**
   Mechanical contract; same shape as the canonical's
   "view/ does not import ui/".

**When to use which.** Canonical `core/view/ui/<framework>/` is
the right shape when the app has a long-lived domain-event stream
(polling loops, subscriptions, retries against the SDK) that's
genuinely separate from the rendering shape. The
`presentation/<framework>` variant is the right shape when the
domain interaction is mostly stateless per-command (CLI verbs +
GUI signals dispatching directly to SDK calls without an
intermediate event-broker layer). Reference shapes are
deliberately cited in both forms — adopters pick the one that
matches their app's interaction model, not a layout convention.

The federation will sometimes use both layouts; cross-sibling
comparison reasons about the IDENTICAL invariant set (PR-01 in
the SDK, framework-free shared layer, sibling UI isolation,
import-linter enforcement), not about layout naming.

---

## 6. The boundary between PR-* and GUI-*

| Concern | Where it lives |
|---|---|
| "SDK MUST NOT import Qt" | **GUI-01** (Qt-specific) — a specialisation of **PR-01** (framework-neutral) |
| "SDK MUST NOT import any UI framework" | **PR-01** |
| "Three-layer separation in app repo" | **PR-03** (framework-neutral) |
| "Qt is permitted only in presentation layer" | **GUI-01** — Qt instance of **PR-01** |
| "Presentation widgets have no business logic" | **GUI-06** (Qt-specific phrasing) — specialisation of the **PR-03 / PR-04** layer hierarchy (which is what *makes* widget isolation enforceable) |
| "Sibling UIs MUST NOT import each other" | **PR-05** (framework-neutral; no GUI-* equivalent — sibling-UI isolation is only meaningful when ≥2 UIs exist, which GUI-* doesn't presume) |
| "Worker pattern off the main thread" | **GUI-03** (Qt-specific; no PR-* equivalent because async/threading shape is per-framework) |
| "Qt excepthook bridge" | **GUI-05** (Qt-specific; no PR-* equivalent) |
| "Stdlib-only `fmt` helpers" | **PR-06** (framework-neutral; no GUI-* equivalent) |
| "import-linter enforces layering" | **PR-04** (framework-neutral, all UIs) — **GUI-01** cites the same enforcement for the Qt sub-tree |

A project with only a Qt GUI follows PR-01 + GUI-01..GUI-10. A
project with a CLI + Qt GUI follows PR-01..PR-07 AND GUI-01..GUI-10
(the GUI-* rules apply to the `ui/qt/` sub-tree; PR-* covers the
cross-UI shape). Adoption order: PR-* first (the layout), then
GUI-* (the per-framework specifics).

---

## 7. Adoption guide

### For a brand-new sibling + app pair

1. Scaffold `<sdk>` with the import-linter contract from §3 wired
   from the first commit. Cheaper to add now than later.
2. If you anticipate any medium-agnostic formatting, create
   `<sdk-pkg>/fmt.py` early (even with one helper) so consumers
   know where the convention lives.
3. Scaffold `<sdk>-app/src/<app>/{core,view,ui/}/` from day one
   with the per-layer import-linter contracts from §5. Add the
   first `ui/<framework>/` subtree only when you start rendering.
4. Declare PR-* status in `docs/inprogress/MGF_STANDARDS_CONFORMANCE.md`
   alongside DP-*, EH-*, etc.

### For an existing SDK that's about to grow a second UI

1. Land the PR-01 import-linter contract on the SDK FIRST. It
   should pass immediately (if it doesn't, the SDK already has
   a leak — fix that before adding the second UI).
2. Extract any medium-agnostic helpers from the existing single
   UI into `<sdk>.fmt`. Verify the existing UI by replacing its
   inline copy with the SDK helper ("dog-food the helper before
   shipping it").
3. Pick the app repo (existing `<sdk>-app` or new). Build the
   three-layer shape (PR-03) with import-linter (PR-04). Absorb
   the existing UI as `ui/<existing-framework>/`. Add the second
   UI as a sibling `ui/<new-framework>/` consuming the same
   `view/`.
4. Deprecate the old in-SDK UI entrypoint per [AP-04]
   (`DeprecationWarning` + one MINOR overlap window) before
   removing it.

### For a sibling without an app today

PR-01 still applies (the SDK MUST NOT import UI frameworks even
if no UI exists yet — protects the future you). PR-02..PR-05
classify as `⏳ activates at first UI consumer` in the conformance
tracker. PR-06..PR-07 follow when there's a reason.

---

## 8. Promotion criteria

Per [`README.md`](README.md) §"Versioning", a Magogi standard
ships when it's grounded in shipped code in two or more siblings.
The PR-* standard satisfies that bar via:

1. **`mgf-hrb` + `hrb-app`** — the reference pair. `mgf-hrb`
   ships the `fmt` module + the import-linter fence; `hrb-app`
   ships the three-layer split + the absorbed CLI alongside its
   Qt GUI.

The standard publishes as **DRAFT** until a second independent
sibling pair (`mgf-cpu` / `mgf-cpu-app`, or a hypothetical
`mgf-network` / `mgf-network-app`) adopts it; on the second
adoption the DRAFT marker drops and the rules earn the same
L1-binding status as the rest of the standards.

Until then, the conformance trackers may carry PR-* under a
"DRAFT — pending second adopter" note; that's accurate, not
hedging.

**LOCK v1 status (2026-07-04):** PR-01..PR-07 are **GATED — excluded
from the locked corpus** (see [`../../LOCK.md`](../../LOCK.md)). The
second adoption promotes them to ACTIVE via an **EXTEND** amendment
(ratify-as-built); nothing about this family's DRAFT mechanics
reopens the lock.

### 8.1 Hardened-pair binding (this project pair)

A specific adopter pair can declare PR-* **binding for itself**
without waiting for federation-wide promotion, provided the pair
ships all of:

1. **Per-rule conformance tests** (one test per applicable PR-*).
2. **Adversarial-input pins** on any `<sdk>.fmt` module — at
   minimum, type-validation tests (TypeError on `bool` / `float` /
   `None` / `str` where ints are expected) and property tests
   (Hypothesis or equivalent) covering the full input domain.
3. **import-linter deliberate-violation meta-test** — proves the
   gate is alive, not vestigial; a fixture violates the contract,
   the test asserts `lint-imports` rejects.
4. **Cross-version feature-detection** — the app side asserts at
   import time that the SDK exposes the symbols the app consumes.
5. **Conformance-tracker drift detector** — wired in CI; rejects a
   tracker row that says `PR-XX ✅` while the matching
   import-linter contract is broken.

The reference pair is `mgf-hrb` + `hrb-app` (the hardened pair). The pair's
trackers drop the "DRAFT pending second adopter" caveat from
§1 once the five items above are shipped. The standard itself
remains federation-DRAFT until the second pair adopts; this
sub-section codifies how *adopters* gain certainty about their
own conformance ahead of federation-wide promotion.

A copy-paste-ready test-plan template lives at
[`../recipes/pr-standard-test-plan-template.md`](../recipes/pr-standard-test-plan-template.md)
— the second-adopter shortcut.

### 8.2 Federation adoption status

**Current net status:** PR-* is **DRAFT pending a second adopter.**
The hardened reference pair is `mgf-hrb` + `hrb-app` (canonical
`core/view/ui/{cli,qt}`, all of §8.1's five hardening items).
PR-* drops the DRAFT marker — becoming federation-wide L1-binding —
when a second sibling pair ships all five §8.1 hardening items.

The `mgf-vm-sdk` + `vm-vmanager-app` pair (variant
`presentation/{cli,gui}` per §5.5) is the natural candidate. The
SDK-side gaps a promotion session must close, verified against the
live repos at adopt-time (not against any frozen snapshot here):

1. **PR-01 contract.** Add an `[importlinter:contract]` to
   `mgf-vm-sdk/pyproject.toml` forbidding UI frameworks
   (`PySide6`, `qasync`, `textual`, `rich`, `prompt_toolkit`,
   `click`, `typer`) from `mgf.vm.sdk`.
2. **PR-06 `fmt` module decision.** Either ship `mgf.vm.sdk.fmt`
   with at least one shared helper, OR explicitly mark PR-06 as
   N/A in the tracker if vm has no medium-agnostic formatting need.
3. **PR-07 sections in both trackers** (`mgf-vm-sdk` +
   `vm-vmanager-app` `MGF_STANDARDS_CONFORMANCE.md`) per the
   audit-outcomes taxonomy.
4. **The five §8.1 hardening items** (per-rule conformance tests,
   fmt adversarial-input pins, import-linter alive meta-test,
   cross-version feature-detection, conformance-tracker drift
   detector).

**Not on the candidate list:** `mgf-fastapi`, `mgf-django`,
`mgf-alembic`, `mgf-sqlalchemy`, `mgf-apiprobe`, `mgf-http`,
`mgf-test-supervisor`, `mgf-common` — federation libraries with no
multi-UI consumer. PR-01 still applies to each; PR-02..PR-07 are
N/A until a multi-UI consumer materialises.

---

## 9. Edge cases and clarifications

Four edge cases the rules above don't address head-on but that
recur often enough to deserve explicit guidance:

### 9.1 Test-time imports of UI frameworks in SDK tests

**PR-01 applies to `src/`, not to `tests/`.** An SDK MAY import
a UI framework in its own `tests/` tree if a test legitimately
needs it (e.g. a pytest-qt fixture that exercises a fake Qt
backend the SDK ships for downstream testing). The
`import-linter` contract MUST target the SDK's source root only:

```ini
source_modules = mgf.hrb        # not mgf  — leaves tests/ free
```

If a test-time import of a UI framework is needed, that's a
signal the SDK is shipping UI-aware testing scaffolding —
document it in the test module's docstring so future
contributors don't mistake it for a leak.

### 9.2 Federation dependencies (`mgf.common`) vs peer SDKs

The SDK MAY depend on `mgf.common` (the federation foundation —
shared exceptions, config, logging, observability). This is
NOT a violation of PR-01 because `mgf.common` is not a UI
framework; it's the L1-cornerstone library every Magogi-
foundation sibling shares.

The SDK MUST NOT depend on a **peer SDK** (e.g. `mgf.hrb` MUST
NOT import `mgf.vm`). Peer-SDK dependencies create a circular
federation topology that turns "I'd like to use this one
library" into "I have to install half the federation." If two
SDKs genuinely need shared types, lift them to `mgf.common`.

PR-06's `fmt` fence (stdlib-only) is **stricter** than this —
`mgf.hrb.fmt` MAY NOT import even `mgf.common`. The fence is
docstring-discipline + the third-party forbidden list (because
`import-linter` cannot mechanically reject subpackages of a
namespace it doesn't own). Reviewer's contract: if a `fmt`
helper grows a non-stdlib import in a PR, reject and lift the
function elsewhere.

### 9.3 Generated UI code (Qt Designer `.ui` → `.py`)

Code generated from Qt Designer files and **committed to
version control** falls under the same `import-linter` contracts
as hand-written code — typically lives in `ui/qt/_generated/`
and obviously imports `PySide6`. PR-04 enforcement passes
because the generated file is under `ui/qt/`.

Code generated **at build time** (not committed; produced by a
`pyside6-uic` step in CI / `pyproject.toml` build hooks) is
exempt from `import-linter` because it doesn't exist in the
source tree the linter sees. The buildsystem should write the
generated files into `ui/qt/_generated/` so when they DO appear
locally for an IDE / debugger, the layer rule still places them
correctly.

### 9.4 SDK self-doctor and `python -m <sdk>` entrypoints

An SDK MAY ship a minimal `python -m <sdk>` entrypoint for
self-diagnostics (per [`OPERABILITY.md`](OPERABILITY.md) OP-02
`doctor` discipline). For argument parsing, use **`argparse`
(stdlib)** — it's not a "UI framework" for PR-01 purposes.

Heavier CLI libraries (`click`, `typer`, `fire`) are
forbidden by PR-01 even for the self-doctor entrypoint, because
they exceed the "minimal stdlib" bar and start to define the
SDK's API shape (decorators, command classes). When you find
yourself wanting one of those, the answer is: that surface
belongs in the `<sibling>-app` repo as `ui/cli/`, not in the
SDK.

The bar for "is this a UI framework for PR-01?" is **"does it
ship widgets, layouts, decorators, or any kind of opinionated
program-structure?"** stdlib `argparse` is just argument
parsing; `click` ships a whole framework.

### 9.5 What PR-01 enforcement MUST cover vs MAY skip

PR-01's forbidden-import list (§3) is **comprehensive for the
SDK's ecosystem**, not exhaustive across every UI framework
ever published. An SDK serving headless infrastructure (e.g.
`mgf-hrb` — hardware broker, never serves HTTP) does not need
to enforce `flask` / `fastapi` / `django` in its contract; an
SDK that's plausibly within a web framework's reach (e.g. a
hypothetical `mgf-http`) does.

The minimum-viable forbidden set for any SDK:

- **Qt family** — `PySide6`, `PyQt6` (and their async bridges
  `qasync`, `asyncqt`)
- **TUI** — `textual`, `rich`, `prompt_toolkit`
- **CLI framework** — `click`, `typer`, `fire` (argparse OK)

Optionally, when the SDK's domain plausibly touches that shape:

- **Web** — `flask`, `fastapi`, `django`, `starlette`
- **Templating** — `jinja2`, `mako`

If a third-party UI framework not listed in §3 appears in your
ecosystem and there's any chance an SDK could accidentally
import it, add it to the contract. The list is a **floor**, not
a ceiling.

---

## 10. Self-review checklist

For an SDK PR:
- [ ] Does anything new under `src/<sdk-pkg>/` `import` from a
  third-party UI framework? (PR-01) — if yes, reject.
- [ ] If a new `fmt` helper was added: stdlib-only? Pure function?
  Tested at boundary values? (PR-06)
- [ ] Did `scripts/gates` run import-linter? Green? (PR-04)

For an app PR:
- [ ] Is the change in the right layer? `core/` for domain
  adapter, `view/` for framework-free shape, `ui/<framework>/`
  for the renderer? (PR-03)
- [ ] Does anything under `core/` or `view/` import a UI
  framework? (PR-04) — if yes, reject.
- [ ] Does the new `ui/<framework>/` code import from a peer
  `ui/<other>/`? (PR-05) — if yes, lift to `view/` or `<sdk>.fmt`.
- [ ] Did `scripts/gates` run import-linter? Green? (PR-04)

For a tracker PR:
- [ ] Does the project's `MGF_STANDARDS_CONFORMANCE.md` have a
  PR-* section with rationale per row? (PR-07)
