# MGF_STANDARDS_CONFORMANCE — `mgf-vm-libvirt`

> Per-rule status against `mgf-common` standards. Updated on every
> `mgf-common` minor bump and on every PR that touches a conformance
> rule (CFM-03).

| Field | Value |
|---|---|
| `mgf-common` pin | `>=0.38,<0.39` |
| Declared level | **L2 — Standard** (target; see [`STANDARDS_NOTE.md`](../../STANDARDS_NOTE.md)) |
| Last audit | 2026-06-09 (evidence-based depth audit) |
| Verified standing | **below L2** — 116 ✅ / 26 ⏳ / 76 N/A of 218 (82% of applicable) |

> **Legend.** ✅ honored · 🚫 declined (with rationale) · ⏳ pending
> (target stated) · N/A (rule doesn't apply at this layer).


**2026-06-09 depth audit (evidence-based, replaces the prior pass).** Declared TARGET **L2**; verified below L2. Of 218: **116 ✅** (file:line, refute-survived) · **26 ⏳** · **76 N/A** · **0 declined**. Applicable = 142; **116/142 (82%) evidenced**. **Exemplary production-adapter core** (refute-survived): real impl of mgf-vm-core's HypervisorBackend/StorageBackend driven against core's shared conformance suite (DP-02), libvirt-python confined by import-linter, domain XML built via ElementTree SubElement (auto-escaping, ZERO f-string XML), ALL ~25 libvirt-output parses via defusedxml, qemu-img via argv-list (shell-free), CancellationToken threaded through every blocking call (DP-09), never-raise event dispatch + deterministic teardown (DP-11/13), mypy --strict on 17 files. The 49% reflects **no CI (.woodpecker absent → whole WORKFLOWS family ⏳)** + no PUBLIC_API surface yet + the LOGGING build-out — NOT a weak or unsafe core.

**Open-gap clusters (the 72 ⏳ — detail in §Z):**

| Cluster | Rules | Count |
|---|---|---|
| **CLOSEABLE (no-release)** (create CI pipeline + SCOPE + PUBLIC_API gen + docs + sbom + version-sync pin) | AG-16, AP-10, AP-11, AP-13, DOC-01, DOC-02, DOC-03, DOC-08, DOC-09, DOC-18, DP-01, OP-03, OP-04, PKG-03, PKG-04, PKG-05, PKG-09, PR-01, PR-07, REL-03, REL-06, REL-08, SC-10, SC-18, SP-01, SP-02, SP-03, SP-04, SP-05, SP-06, TS-01, TS-06, TS-18, TS-19, TS-20, TS-21, WF-01, WF-02, WF-03, WF-04, WF-05, WF-06, WF-07, WF-08, WF-09, WF-10, WF-12 | 47 |
| **DEFERRED** (logging build-out LG-*, SC-16/17 audit/privileged, AP-02 examples, property/supervisor/perf TS, SC-12 if present, REL-01) | AP-02, LG-01, LG-03, LG-05, LG-06, LG-11, LG-13, LG-15, LG-16, LG-17, REL-01, SC-12, SC-16, SC-17, TS-07, TS-13, TS-14, TS-15, TS-22, TS-23, TS-24, TS-25, TS-27, TS-28, WF-11 | 25 |

## Per-standard verdicts → see §Z

The prior per-family pass is superseded by the **§Z evidence-based per-MUST audit** below (2026-06-09).

## §Z Per-MUST evidenced audit (all 259 MUSTs (PAPER-100 re-baseline 2026-06-11) — CFM-03 completeness)

> **2026-06-09 depth audit** (16-family fan-out + adversarial refute on every ✅, hardened prompt + extra-ruthless domain-XML-injection/defusedxml/event-pump SECURITY pass; ~30 agents + a 44-id re-run for 5 truncated families). No CI yet (.woodpecker absent → whole WORKFLOWS family ⏳). No 🚫-flood, no mislabels. **Mix: 70 ✅ · 72 ⏳ · 76 N/A · 0 🚫.** Replaces the prior scaffold.


### DESIGN_PRINCIPLES

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DP-01 | ✅ | pyproject.toml [tool.importlinter] defines 2 forbidden contracts (root_packages=['mgf.vm.libvirt','mgf.vm.core'], include_external_packages=true): 'Adapter uses mgf.vm.core public surfa | The layering mechanism (import-linter, 2 contracts, libvirt-confinement) exists and passes locally, but the rule's MUST is that CI runs lint-imports. With no CI present (the entire WORKFLOWS family is ⏳ per the carve-out), DP-01's — FIXED 2026-06-09: lint-imports now CI-enforced (the existing 2 + new PR-01 import-linter contracts) |
| DP-02 | ✅ | _hypervisor.py defines LibvirtHypervisorBackend implementing mgf.vm.core.contracts.HypervisorBackend; _storage.py LibvirtStorageBackend implements StorageBackend (both Protocols defined | [verify-upheld] Correct + expected adapter shape: core DEFINES the Protocol/ABC, this lib is the production impl, core's fakes are the mock, core's conformance suite is the lock-in test the adapter runs. libvirt confined + lint-en |
| DP-03 | ✅ | grep -n @dataclass src/: _spec.py:154/176/190 (DiskSpec/NicSpec/LibvirtVmSpec) and _volume_ids.py:43/51 are all @dataclass(frozen=True, slots=True). The only non-frozen: _events.py:60/7 | [verify-upheld] Boundary-crossing domain types are uniformly frozen; bare strings/dicts don't cross as identity. The 4 non-frozen are legitimately stateful local engines (sequencer, snapshot tree) that don't cross a module boundar |
| DP-04 | ✅ | I RAN .venv/bin/python -m mypy src/ → 'Success: no issues found in 17 source files'. pyproject.toml [tool.mypy] strict=true, warn_unused_ignores=true, with one justified per-module over | [verify-upheld] mypy --strict passes clean. The single Any boundary (libvirt's untyped C binding) is confined behind the typed LibvirtConnection wrapper with a justified override. Met. |
| DP-05 | ✅ | tests mirror src under tests/unit/mgf/vm/libvirt/ (test_hypervisor_backend, test_storage_backend, test_event_pump, test_network_backend, test_guest_agent_backend, test_domain_policy_bac | [verify-upheld] Tests mirror src, run with no real daemon/network/credentials (adopt + fakes), real-libvirt tests opt-in-marked and self-skipping. Met. |
| DP-06 | ✅ | Security verified at code level: (1) domain-XML built via stdlib ElementTree SubElement/.text/attrs-dict (generate.py — auto-escaping; NO f-string/format/% XML, confirmed by grep), (2)  | [verify-upheld] Refuted hard on the three central concerns (XML escaping, defusedxml everywhere, connection/subprocess) and found NO violation. Security/reliability (idempotent close, best-effort degrade, never-raise-into-libvirt  |
| DP-07 | ✅ | Public surface carries intent+invariant docstrings: vmspec_to_xml (generate.py:37), _parse/xml_to_domaininfo (read.py), LibvirtConnection.close (connection.py:172-181 — states idempoten | [verify-upheld] Rich invariant-stating docstrings throughout; why-comments (lock-ordering, conservative-fallback) used; no what-comments found. Met. |
| DP-08 | ✅ | This is the IMPLEMENTATION side of the DP-02 seam — core defines HypervisorBackend/StorageBackend Protocols; swapping the hypervisor = writing a new adapter against those Protocols (thi | DP-08 is SHOULD/review-only (replaceability near the abstraction). For an adapter that IS the swap target, the recipe is 'implement core's Protocols + pass the conformance suite' — documented in README/CLAUDE.md and embodied by th |
| DP-09 | ✅ | Cancellation is an explicit parameter, never thread-global: _hypervisor.py:749/771/824 start/stop_graceful/reboot take token: CancellationToken\|None; _run_cancellable (838-915) runs th | [verify-upheld] Every long-running op accepts an explicit CancellationToken, checks cooperatively, raises typed OperationCancelled (not bare KeyboardInterrupt). Met. |
| DP-10 | N/A | grep for fallback/keyring/active-backend across src/ found only documented loud best-effort partial-data degradations (e.g. _metrics 'a per-disk failure degrades that one disk', _guest_ | DP-10 governs queryable/logged ACTIVE-BACKEND selection where a fallback PATH between backends exists (keyring→file). This adapter has exactly one backend (libvirt); no backend-selection fallback exists to query/log. The documente |
| DP-11 | ✅ | grep -rn 'create_task\|asyncio\|ensure_future\|loop.run' src/ → ZERO matches. No asyncio anywhere in the source. Concurrency is threading-based: the event pump runs a single documented  | [verify-upheld] DP-11 governs asyncio.create_task ownership; there are no asyncio tasks at all, so the rule is vacuously satisfied. The thread lifecycles that DO exist are owned (DP-13). Met. |
| DP-12 | ✅ | grep for utcnow/naive datetime.now()/fromtimestamp across src/ → only two fromtimestamp calls, both tz-aware: _hypervisor.py:498 datetime.fromtimestamp(int(expiry), tz=UTC) and :1066 .. | [verify-upheld] Time is UTC-aware at every boundary; POSIX-seconds converted with tz=UTC; naive datetimes never constructed; DTZ lint enforces it mechanically. Met. |
| DP-13 | ✅ | LibvirtConnection (_connection.py:172-226): idempotent close() marks _closed=True FIRST (so a flaky native close never half-opens), __enter__/__exit__ context manager, __del__ emits Res | [verify-upheld] Every owned external resource (libvirt connection, subprocess, locks, event-loop callback registration) released deterministically via context managers / try-finally / best-effort-under-lock, with an explicit Resou |

### ERROR_HANDLING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| EH-01 | ✅ | _errors.py imports only mgf.vm.core.exceptions AppError subtypes; the _CODE_MAP (lines 150-224) and translate_exception (256-277) only ever build core AppError subclasses, fallback to H | [verify-upheld] Tried to refute by hunting for a bare-builtin/third-party raise crossing the public API. The only bare raise (ValueError in _xml/compare._parse) is structurally contained inside a seam-decorated method, and __init_ |
| EH-02 | ✅ | _errors.py is the single owning translation seam: LibvirtErrorTranslator.translate_exception (256-277) maps libvirtError.get_error_code() through _CODE_MAP (150-224) to typed core types | [verify-upheld] Tried to find a third-party exception translated somewhere OTHER than its owning boundary, or a vendor call with no translation. libvirtError → _errors.py seam; qemu-img OSError/SubprocessError → _storage._qemu_img |
| EH-03 | ✅ | The seam decorator chains the cause: mgf/common/providers/_provider.py:152-154 `except Exception as exc: translated = _translate(self, exc); raise translated from exc` (and async twin : | [verify-upheld] Tried to refute by finding a re-raise that drops the cause (the standard §3 explicitly names a historical libvirt-translator gap at lines 102/104 dropping `from e`). Grepped every translation/re-raise site; all cha |
| EH-04 | N/A | No entry point: grep found no click/argparse, no [project.scripts], no def main/__main__ in src/ or pyproject.toml. This is a pure library adapter (HypervisorBackend/StorageBackend impl | EH-04 (install a top-level handler at every entry point) applies to entry points; a library adapter has none. The consumer (CLI/GUI/service) installs the handlers. N/A by shape. |
| EH-05 | N/A | No entry point and no crash-reporter wiring in the adapter; mgf.common.observability.crash_reporter is the shared implementation consumers wire at their own entry point. The adapter rai | EH-05 (unhandled exceptions produce a crash report) is an entry-point responsibility. A library has no unhandled-exception boundary of its own. N/A by shape. |
| EH-06 | N/A | No GUI/Qt code anywhere in the adapter (no PySide6, no QThread, no worker classes). It is a headless library. | EH-06 governs GUI worker threads; this library has none. N/A by shape. |
| EH-07 | ✅ | Every `except Exception` is cleanup/best-effort-with-noqa-or-docstring or re-raises/translates. _event_pump.py:101-102 (loop back-off; never-raises invariant doc'd 88-95), :246 noqa:S11 | [verify-upheld] Ruthlessly grepped every `except Exception`/`except BaseException` across the adapter (the event-pump loop + dispatch were the prime suspects). Each is either re-raise, typed translation, a noqa-tagged best-effort  |
| EH-08 | ✅ | Verified against the installed mgf/vm/core/exceptions.py: SpecValidationError.errors (line 102), DomainNotFoundError.name (124), DomainAlreadyExistsError.name (135); every class declare | [verify-upheld] Tried to refute by checking whether the adapter's raised types are message-only. The core types carry .name/.errors as ctor-set attributes and a programmatic RETRYABLE class field that the seam's mapping table actu |
| EH-09 | N/A | Messages are deliberately consumer-agnostic: core exceptions document that the CLI/GUI adds the action hint when rendering (exceptions.py:125-127 DomainNotFoundError 'CLI/GUI adds its o | EH-09 governs messages SHOWN to users; this library does not render to end users — the consuming CLI/GUI owns the actionable next-step suffix. Some adapter messages do carry hints (e.g. _hypervisor.py:799 'retry with force=True',  |
| EH-10 | ✅ | _event_pump.py:_run_loop (88-102) docstring 'A raised error must never kill the pump ... we swallow and back off briefly' + try/except time.sleep(0.1); _dispatch (249-277) docstring 'Ne | [verify-upheld] The libvirt event callbacks are the standard's named best-effort-observer example. Verified the never-raises docstring on both _run_loop and _dispatch AND the two existing unit tests that inject a raising iteration |
| EH-11 | N/A | No retry implementation exists in the adapter: grep for retry/backoff/max_attempts found only RETRYABLE-tag comments and the HypervisorTransientError 'retryable' classification — no loo | EH-11 (retries MUST use exponential backoff + jitter + finite cap) applies only where the code retries. This adapter performs zero self-driven retries — single call per method. N/A by shape (and consistent with the EH-13 bound). |
| EH-12 | ✅ | define() is idempotent on same spec via semantic_equal canonical-XML compare keyed on VM name; SpecConflict only on a differing spec (_hypervisor.py:597-624). _xml/compare.semantic_equa | [verify-upheld] Tried to find a multi-attempt write that is NOT idempotent or not keyed on the natural key. define + every create/delete pool/volume/snapshot are name-keyed: second run returns the same result (already-gone returns |
| EH-13 | ✅ | Grepped the entire adapter for retry loops/backoff/breakers: NONE found (the only time.sleep is the _event_pump never-raises observer back-off, not a retry against libvirtd). Each publi | [verify-upheld] EH-13 binds the OUTCOME (load stays bounded), not the breaker mechanism, and permits recording the bound per non-adoption-is-valid. Confirmed there is no self-driven retry — single-call-per-method — and the retry b |

### LOGGING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| LG-01 | ⏳ | grep across all 17 modules under src/mgf/vm/libvirt/ (incl. _xml/) for 'import logging', 'getLogger', 'LOG.', 'logger.' returns ZERO hits (the only .info( matches are libvirt domain/vol | LG-01 (MUST, all) requires every module acquire a logger via logging.getLogger(__name__). The adapter has NO logger acquisition in any module. There are no bare logging.info() violations either — but the rule is unsatisfied becaus |
| LG-02 | N/A | No src/.../__main__.py, no [project.scripts]/console_scripts/gui-scripts in pyproject.toml; this is a pure libvirt/KVM ADAPTER LIBRARY imported by callers (vm-core/CLI/GUI), with no ent | LG-02 binds the entry point that owns a process (CLI main / GUI app.run / service entry) — it must configure logging in exactly one place and emit JSON off-TTY. A library has no entry point; configuring root logging from inside a  |
| LG-03 | ⏳ | No JsonFormatter / _add_otel_context / observability wiring in src/ (grep 'observability\|operation_span\|JsonFormatter' = 0 hits), and no log records are emitted at all. | LG-03 (MUST, all) requires every log record carry OTel trace_id/span_id when a span is active. The standard says the formatter adds these automatically — but that is the consumer's setup_logging+JsonFormatter responsibility. Howev |
| LG-04 | N/A | No formatter or sink lives in this library (grep 'Redactor\|JsonFormatter' = 0 hits); the adapter installs no handlers. | LG-04 (MUST) mandates the FORMATTER redact sensitive fields before any sink sees them, and that the redaction set be configurable. The formatter/redactor lives in mgf.common.observability and is installed by the consuming applicat |
| LG-05 | ⏳ | ~87 'except' sites across the modules; several SILENTLY swallow with no logging: _event_pump.py:99-102 _run_loop swallows every event-loop error + sleep(0.1) (comment: 'A raised error m | LG-05 (MUST, all): ERROR-level logs must include the exception via LOG.exception/exc_info=True. The adapter has error-recovery paths that swallow exceptions entirely (notably the event-pump loop, the dispatch callback, and best-ef |
| LG-06 | ⏳ | No logging emitted by any module; no sink configured; this adapter relies entirely on the consumer's setup. No EVENTS/aggregation wiring present. | LG-06 (MUST, all): logs must reach a searchable aggregator; 'console only, no aggregation' is non-compliant in production. The sink CHOICE is the consumer's (library doesn't install handlers), so the aggregator wiring is N/A-to-th |
| LG-07 | N/A | No GUI: pyproject has no gui-scripts/PySide6/Qt deps; runtime deps are mgf-common+mgf-vm-core+defusedxml+libvirt-python; this is a headless hypervisor adapter library. | LG-07 is scoped 'gui-app' in the per-kind matrix (line 21/44). This library has no GUI surface, so the log-viewer-dialog requirement does not bind. |
| LG-08 | N/A | Zero log call sites exist (no LOG.info/warning/etc. anywhere in src/). | LG-08 (MUST, all) governs the SHAPE of log calls (lazy %-formatting, not f-strings). With no log call sites in the library there is nothing to evaluate — no violation and nothing to satisfy. The rule will bind once LG-01 lands (th |
| LG-09 | N/A | Zero log call sites exist; no extra={...} usage because there is no logging. | LG-09 (MUST, all) requires structured fields be passed via extra={...} not concatenated into the message. No log calls exist to evaluate. Same as LG-08: the rule binds the call sites added by LG-01, but there is nothing here to ma |
| LG-10 | N/A | No service/daemon/systemd entry point; no [project.scripts]; pure adapter library imported in-process by its consumers. | LG-10 is scoped 'service' in the per-kind matrix (line 24/45) — headless services/daemons configuring journald at their entry point. This library has no service entry point of its own; any journald sink is the consuming service's  |
| LG-11 | ⏳ | _hypervisor.py (67KB) + _storage.py (37KB) + _connection.py make real libvirt/qemu calls (domain start/stop, qemu-img-equivalent volume ops, remote/local connection open) — every one cr | LG-11 (MUST, all): operation start/end must be logged with a stable 'operation' name at INFO for any function that crosses a process/network/storage boundary (≥100ms), so flows are reconstructable from logs alone. This adapter is  |
| LG-12 | N/A | No log emission sites exist in the library. | LG-12 (MUST, all) requires log emission be cheap (≤100µs/record, heavy work in the handler). With no call sites there is nothing to measure; the formatter/handler that bears the cost lives in the consumer's setup. Binds once LG-01 |
| LG-13 | ⏳ | _event_pump.py:_dispatch (line 249-277) is a per-event libvirt callback fired once PER domain lifecycle event, fanning out to N subscribers — a textbook high-frequency/per-event-callbac | LG-13 (MUST, all): high-frequency log sites (inner loops, per-event callbacks) must rate-limit/sample so a runaway condition doesn't flood sinks. The dispatch callback and event loop are exactly such sites. Currently ⏳ jointly wit |
| LG-14 | N/A | No correlation-context usage (grep for contextvars / ContextVar = not used for logging); the adapter sets no request/user/lab correlation IDs. | LG-14 (MUST, all) requires custom correlation context propagate via contextvars.ContextVar (and the formatter promote registered contextvars). A low-level hypervisor adapter has no request/user/lab correlation context to propagate |
| LG-15 | ⏳ | No 'event' field anywhere (grep 'event=' / extra={...'event'} = 0; the only 'event' literal is _event_pump.py:264 payload['event']='lifecycle' which is a DOMAIN payload dict for the Eve | LG-15 (MUST, all): every INFO+ record at a notable lifecycle/security/boundary event must carry a stable 'event' field in <subject>.<action> form, documented in docs/reference/EVENTS.md. The adapter has many such boundaries (conne |
| LG-16 | ⏳ | The error-translation seam _errors.py (11KB) maps libvirt errors to mgf-vm-core's typed tree but logs nothing. Recovery-with-degradation sites swallow silently: _event_pump.py:99-102 (_ | LG-16 (MUST, all): every WARNING/ERROR record must answer WHAT (past-tense message) + WHY (extra={} fields) + HOW (hint when known). The adapter has genuine warn/error-worthy paths (event-loop failure recovery, best-effort callbac |
| LG-17 | ⏳ | No ERROR records exist (no LOG.exception/exc_info anywhere); no 'fingerprint' field (grep = 0). _errors.py is the typed-exception translation seam — the natural site where fingerprintab | LG-17 is a SHOULD (the standard explicitly says 'SHOULD, not MUST'). The default fingerprint is computed automatically by the consumer's formatter from exc_info — so once the adapter logs errors with exc_info (LG-05), the default  |

### CONFIGURATION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| CF-01 | N/A | grep over all 17 modules in src/mgf/vm/libvirt/ finds ZERO os.environ/getenv/BaseSettings/pydantic_settings/yaml.safe_load reads. The adapter owns no config schema: _hypervisor.py:157-1 | CF-01 is scoped 'consumer-owned-config'. This is a hypervisor ADAPTER library — it owns no application configuration; the consuming CLI/GUI/SDK owns the Settings schema and passes the resolved connection_uri in by injection. Premi |
| CF-02 | N/A | No layered config source exists anywhere in the adapter. connection_uri (_hypervisor.py:157, _storage.py:153) and timeouts (_hypervisor.py:792 _DEFAULT_GRACEFUL_TIMEOUT) arrive as expli | CF-02 precedence presupposes layered config sources (CLI>env>.env>file>defaults). A pure adapter has none — the owning application implements the precedence and hands the adapter a concrete URI. Nothing to order here. |
| CF-03 | N/A | No config schema → no schema version field. The 'version' references in src (_hypervisor.py:387 conn.getVersion, _storage.py:586, __init__.py:39 __version__) are the libvirt ENGINE vers | CF-03 is scoped 'consumer-owned-config' and binds the config object's version field. There is no config object to version/migrate. |
| CF-04 | N/A | No on-disk config file is written by the adapter. The only file writes are dest.open('wb') for disk-image format conversion (_hypervisor.py:1527 / _storage.py) — VM image data, not a co | CF-04 (atomic config write, 0o600, lazy path expansion) is scoped 'consumer-owned-config'. The adapter persists no config file; image-conversion writes are domain data governed by SECURITY/atomic-io rules, not CF-04. |
| CF-05 | N/A | No secrets storage, vault, keyring, password, or credential field exists in any module (grep for keyring/vault/secret/password/credential returns nothing). The connection URI (e.g. qemu | CF-05 (secrets→vault) binds a consumer that owns secret-bearing config. This adapter holds no secrets; auth for remote libvirt URIs is the libvirt client config / owning app's concern, accessed via the injected URI. |
| CF-06 | N/A | No config file is loaded, so there is no unknown-key rejection surface. Domain XML and guest-agent JSON parsing (defusedxml in _xml/, json.loads at _guest_agent.py:166) are protocol-dat | CF-06 unknown-key rejection is scoped 'consumer-owned-config' and presupposes a typed config model loading a YAML file. The adapter has none. |
| CF-07 | N/A | No sub-config / satellite files (hooks/schedules/alerts/profiles/lab specs) exist or are loaded anywhere in src/mgf/vm/libvirt/. The only YAML-shaped concern (domain XML) is generated/p | CF-07 (satellite config files follow the same shape) is scoped 'consumer-owned-config'. There are no satellite config files in a pure adapter. |
| CF-08 | N/A | While the adapter DOES follow constructor injection (LibvirtHypervisor.__init__ takes connection_uri/connection at _hypervisor.py:157-167; LibvirtStorageBackend.open at _storage.py:153; | CF-08 (Settings loaded once at entry point, injected, no module-global singleton) binds a config-owning consumer with an entry point. The adapter is a library with no entry point and no Settings object; the injection discipline it |
| CF-09 | N/A | No CLI exists — pyproject.toml declares no [project.scripts] / console_scripts entry point; this is a library with no argv surface (confirmed: it is the implementation side of the DP-02 | CF-09 (--config flag + XDG/OS default path) is scoped 'consumer-owned-config' and presupposes a CLI process. A library adapter takes no argv; the owning CLI/SDK provides --config. Premise does not bind (parallels §0a/§6 web-shape  |
| CF-10 | N/A | There is no Settings class to construct in tests. Tests inject fakes/mocks via LibvirtConnection.adopt (_connection.py:114, conn=..., uri='test:///adopted') and connection= kwargs — the | CF-10 (tests construct Settings with explicit values, no env leakage) binds a config-owning consumer. With no Settings class, the rule has no object to apply to; the adapter's tests already avoid env by using explicit adopt()/conn |
| CF-11 | N/A | No config schema fields exist → no field-rename/Field(alias=)/validation_alias surface anywhere in src. No pydantic config model is defined. | CF-11 (rename config field via alias + deprecation warning) is scoped 'consumer-owned-config' and presupposes pydantic config fields. None exist in this adapter. |
| CF-12 | N/A | No config model with mutually-exclusive backend variants exists; grep for discriminator/Annotated-union over config finds nothing. The backend selection (which hypervisor adapter) is ma | CF-12 (discriminated-union backend variants with a kind discriminator) binds a config schema that models alternative backends. This adapter IS one backend; it carries no config field selecting among variants. Premise does not bind |

### SECURITY_STANDARD

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| SC-01 | N/A | grep across src/ shows no secret material in scope: no passwords/tokens/API keys; the only credential-shaped value is the libvirt connection URI (a socket path / qemu:/// URI), which is | This adapter has no secrets to manage — N/A. (Confirms the basis the ✅ SC-11 relies on.) |
| SC-02 | ✅ | [verify-upheld] Refute attempted. The sole subprocess site is _storage.py:872 subprocess.run(cmd, capture_output=True, text=True, timeout=_QEMU_IMG_TIMEOUT_S, check=False) with cmd=[bin | argv list, shell=False, resolved absolute binary, finite timeout. Conformant — upheld. |
| SC-03 | ✅ | [verify-upheld] Refute attempted. grep: zero yaml import, zero yaml.load, zero pickle, zero eval/exec anywhere in src/. Only deserialization is json.loads(reply) of the guest-agent repl | Safe deserialization (json.loads + shape validation); no yaml.load/pickle. Conformant — upheld. |
| SC-04 | N/A | grep for chmod/0o600/0o640/atomic/tempfile/NamedTemporary across src/ is empty. This adapter writes no private-data state files of its own; it speaks to libvirtd (the daemon owns domain | No application-owned private-data file writes — file-mode/atomic rule does not apply. N/A. |
| SC-05 | ✅ | [verify-upheld] Refute attempted. The only crypto primitive is hashlib.sha256(canonical.encode('utf-8')).hexdigest() (_hypervisor.py:323-326), an explicitly-documented deterministic SEM | stdlib hashlib.sha256 for integrity fingerprinting — the non-auth use SC-05 permits. No PRNG-for-tokens, no DIY/deprecated crypto. Conformant — upheld. |
| SC-06 | N/A | grep for verify=False/CERT_NONE/check_hostname/AutoAddPolicy across src/ is empty. The adapter's only outbound surfaces are the local libvirt UNIX socket (libvirt.open(uri)) and the qem | No project-controlled TLS client surface — TLS-verify rule does not apply. N/A. |
| SC-07 | ✅ | [verify-upheld] Refute attempted. qemu-img subprocess carries timeout=_QEMU_IMG_TIMEOUT_S (_storage.py:876); guest-agent qemuAgentCommand calls pass _AGENT_TIMEOUT_S=5 (_guest_agent.py: | Every external-boundary call carries a finite timeout or a token-bounded poll; no blocks-forever path. Conformant — upheld. |
| SC-08 | ✅ | [verify-upheld] Refute pressed HARD on the two injection vectors. (1) INBOUND XML PARSING: every parse of untrusted libvirt output uses defusedxml DefusedET.fromstring — confirmed at _x | Input validated/neutralised at the adapter boundary: defusedxml on every untrusted parse (no stdlib .parse of libvirt data → XXE closed), XML injection closed by saxutils.escape on every f-string path + ElementTree auto-escaping o |
| SC-09 | N/A | grep for Redactor/redact and for any logging (import logging / getLogger / LOG.) across src/ returns nothing — the adapter emits no log records at all. No sensitive fields and no log si | No logging surface and no secrets — formatter-redaction rule does not apply. N/A. (Note: the absence of logging is the cause of the SC-17 gap below.) |
| SC-10 | ✅ | uv.lock IS committed (178 KB, present). BUT there is NO .woodpecker.yml (confirmed: `ls .woodpecker*` → none), so no CI runs a vulnerability scanner (pip-audit/osv-scanner/safety) on PR | Lockfile present, vuln-scan CI gate absent (no CI at all). Half-met → ⏳. — FIXED 2026-06-09: pip-audit vuln-scan in CI |
| SC-11 | ✅ | [verify-upheld] Refute attempted. Translation seams raise typed core errors carrying only non-secret operational metadata: HypervisorConnectionError(f"could not open libvirt connection  | Exception messages carry only non-secret metadata (URIs, domain ids, argv, stderr); no secrets exist to scrub. Conformant — upheld. |
| SC-12 | ⏳ | A root SECURITY.md EXISTS (1792 bytes) with a 'Reporting a vulnerability' section and a security contact (bassam@magogi.org). BUT its Scope section is MIS-PASTED from a pure-domain sibl | The file exists with a contact, but its security-model description directly contradicts the adapter's real attack surface (same copy-paste defect class seen in mgf-vm-firecracker). An inaccurate threat description is a defect → ⏳  |
| SC-13 | N/A | No cryptographic keys at rest anywhere (grep: no KEY= literals, no Fernet/PBKDF2/keyring, no key files). The only hashlib use is a non-key XML signature (SC-05). | No keys to store — at-rest-key-storage rule does not apply. N/A. |
| SC-14 | N/A | This adapter performs no authentication of callers — it is a library that the trusted in-process consumer calls. Authentication to libvirtd is handled by libvirt's own transport/PolicyK | No auth boundary owned here — no-enumeration / rate-limit rule does not apply. N/A. |
| SC-15 | N/A | grep for resolve()/is_relative_to across src/ shows no path-containment logic, and there is no filesystem-traversal surface: the adapter does not join external names under a root to rea | No app-constructed-from-untrusted-input filesystem path traversal surface — rule does not apply. N/A. (If the consumer-trust assumption were ever relaxed, disk source paths would become an SC-15/SC-08 surface — but as designed the |
| SC-16 | ⏳ | The privileged surface is real (qemu-img subprocess _storage.py:872; the libvirt connection _connection.py which on qemu:///system reaches the system daemon). grep: no sudo/setuid/--pri | No improper escalation present, but the narrow-privileged-surface is undocumented/misdocumented rather than audited per call site → ⏳ (document the qemu-img + libvirt-socket privileged surface in SECURITY.md/ARCHITECTURE threat mo |
| SC-17 | ⏳ | grep for audit=true / audit=True / any logging (import logging, getLogger, LOG.) across src/ returns NOTHING — the adapter emits zero log records. Security-relevant actions that SC-17 e | No structured audit-log records for the subprocess/connection security-relevant actions → ⏳. |
| SC-18 | ✅ | Published as mgf-vm-libvirt 0.1.8 with a committed uv.lock and __version__. BUT there is NO .woodpecker.yml / CI (confirmed), so the wheel is not built+verified from a tagged commit in  | No CI build/verify pipeline establishes reproducible-from-source → ⏳ (part of the whole-WORKFLOWS-family gap from the missing CI). — FIXED 2026-06-09: .woodpecker sbom step (reproducible CycloneDX) |

### TESTING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| TS-01 | ⏳ | src/ has 13 modules (_connection,_hypervisor,_storage,_network,_metrics,_guest_agent,_event_pump,_events,_domain_policy,_spec,_errors,_volume_ids) + _xml/{generate,compare,read}.py. tes | Coverage is broad but the mirror is not 1:1 nor CI-linted: foundation tests sit at tests/ root rather than mirroring src/ under tests/unit/, several src modules (_metrics, _events, _connection) have no same-named mirror, and there |
| TS-02 | ✅ | tests/conftest.py provides FakeLibvirtError (get_error_code int double) + FakeConn; tests/unit/.../test_event_pump.py monkeypatches _ensure_loop_thread to a no-op and drives _FakeRawCon | [verify-upheld] The unit/foundation suite reaches libvirt only through fakes; I ran it (414 passed) without any real binding interaction. The libvirt-confinement import-linter contract (_spec must not import libvirt) reinforces th |
| TS-03 | ✅ | test_event_pump.py make_conn fixture closes every created connection on teardown (yields a factory, loops close() in teardown) to avoid the GC ResourceWarning under filterwarnings=error | [verify-upheld] Resource cleanup is explicit and process-global state (sys.modules, time.sleep) is touched only via monkeypatch which auto-reverts. No pytest-randomly is wired (TS-03's mechanical check) but the isolation disciplin |
| TS-04 | ✅ | pyproject.toml [tool.pytest.ini_options] line 111: filterwarnings = ["error"]. The make_conn fixture in test_event_pump.py explicitly closes connections precisely to avoid the escalated | [verify-upheld] filterwarnings=["error"] is set exactly as the MUST requires, and the suite passed under it. Upheld. |
| TS-05 | ✅ | pyproject.toml line 110: addopts = "--strict-markers --strict-config". | [verify-upheld] Both flags set; the conformance/integration markers are registered (lines 112-115) so --strict-markers passes. Upheld. |
| TS-06 | ✅ | pyproject.toml [tool.coverage.report] fail_under = 80 is declared, but there is NO .woodpecker.yml (confirmed: ls .woodpecker* = not found) so the gate runs on no CI job. addopts carrie | The coverage threshold value exists in config but the MUST binds the gate to 'every CI job' and a ≥90%-on-changed-lines PR check; with no CI at all neither is enforced. The gate is unwired. — FIXED 2026-06-09: CI pytest --cov=mgf.vm.libvirt |
| TS-07 | ⏳ | The adapter's typed exceptions are mgf-vm-core's (DomainNotFound, SpecInvalid, etc.), tested for translation-mapping in test_foundation_errors.py but NOT for the TS-07 trio (message for | The adapter introduces no new typed exception types of its own — it raises mgf-vm-core's, whose field-preservation tests belong upstream. But the MUST's mechanical home (a per-exception field/message/copy test) is not present here |
| TS-08 | ✅ | test_foundation_errors.py: CODE_CASES parametrizes 50+ libvirt codes → expected core type (test_code_maps_to_expected_type); test_unknown_code_falls_back_to_generic_parent + test_no_cod | [verify-upheld] The EH-02 translation seam (LibvirtErrorTranslator + @translate_at_seam) has success + each-known-error + unknown-vendor-fallback tests exactly as the MUST requires, plus cause-chaining and pass-through pins. Uphel |
| TS-09 | ✅ | test_event_pump.py::test_dispatch_swallows_subscriber_errors injects a raising subscriber (_boom raises RuntimeError) and asserts raw.callback(...) does not raise — the dispatch loop co | The event-pump dispatch is the best-effort observer surface (a subscriber callback runs inside libvirt's C loop); the MUST's 'inject a raising observer, assert the loop continues' is met directly. ✅. |
| TS-10 | ✅ | test_foundation_xml.py has real SC pins: test_defused_billion_laughs_is_rejected + test_defused_external_entity_ovf_is_rejected (XXE/XML-bomb negative cases via defusedxml → SpecInvalid | The security-relevant XML surfaces (XXE on parse, injection on generate, defused-everywhere) each have a negative-case pin or are structurally safe; the billion-laughs + external-entity tests assert the bad input is rejected as th |
| TS-11 | N/A | No GUI: the adapter is a pure library (no PySide6/pytest-qt, no QThread workers). grep finds no qtbot/QWidget anywhere. | TS-11 applies only to GUI workers; this adapter has no GUI surface. N/A. |
| TS-12 | ✅ | The adapter is synchronous: the event pump uses a stdlib THREAD (virEventRunDefaultImpl loop), not asyncio. grep finds no async def / await / pytest.mark.asyncio in src or tests. The th | No coroutines exist in the codebase, so the MUST ('a coroutine that has no test is a defect') is vacuously satisfied — there is no async code requiring pytest-asyncio. The thread-based event loop is exercised. ✅ (no async surface) |
| TS-13 | ⏳ | test_foundation_xml.py has property tests (test_fuzz_canonicalize_idempotent, test_fuzz_reflexive_equality, test_fuzz_child_reorder_preserves_equality) using @given over a _domain_xml() | Round-trip property coverage exists and is real, but the MUST's structural clause — strategies in a reusable _strategies.py module — is not met; the strategy is inline. Marking ⏳ on the structural gap (the round-trip itself IS cov |
| TS-14 | ⏳ | No perf benchmark: grep finds no @pytest.mark.perf, no perf marker registered in pyproject markers (only integration + conformance), no docs/benchmarks/. No PERFORMANCE budget pinned. | No performance budget is pinned by a benchmark. The adapter has hot paths (XML generate/canonicalize) that could carry a budget but none is asserted. ⏳ (L1 may defer per TESTING, but it is a real gap, not declined). |
| TS-15 | ⏳ | Real-backend tests exist as the conformance tier (tests/conformance/test_*_conformance.py, pytestmark = pytest.mark.conformance) which run against a real libvirtd and SKIP gracefully if | The real-backend tests are NOT opt-in: with addopts lacking a '-m "not conformance and not integration"' default, plain pytest collects and runs the conformance tier against whatever libvirtd is present (it did, and 2 mutating sto |
| TS-16 | ✅ | Sampled asserts carry context: test_foundation_errors.py asserts 'type(translated) is expected' with parametrized (code,expected) visible in the test id; test_code_maps_to_expected_type | [verify-upheld] Failure messages are self-explanatory via parametrize ids + concrete equality asserts; no bare 'assert result' anti-patterns found. Upheld. |
| TS-17 | ✅ | tests/conftest.py holds the project-wide fake_conn fixture + the FakeLibvirtError/FakeConn classes; per-file fixtures (pump, make_conn, translator) live in their own files. The only cro | [verify-upheld] Fixtures are discoverable via the conftest hierarchy and not imported across packages; the `from tests.conftest import ...` lines pull shared helper classes from the allowed root scope, not fixtures. Upheld. |
| TS-18 | ⏳ | No 'slow' marker is registered (pyproject markers = only integration, conformance). TESTING TS-19 says a project SHOULD register slow even if unused (defensive); TS-18 requires slow tes | The slow marker is neither registered nor applied; the heavier conformance tests are not marked slow nor excluded from the default run. Minor but unmet. ⏳. |
| TS-19 | ✅ | pyproject markers registers 'integration' and 'conformance' only. Missing: smoke, e2e, slow, perf (and the §13.11 superset). --strict-markers is on (good), but the canonical layer marke | The markers a conforming project must declare (at minimum smoke + the slow/perf defensive set) are absent; only two project-specific markers are registered. The smoke layer in particular has no marker. ⏳. — FIXED 2026-06-09: smoke/e2e markers registered |
| TS-20 | ✅ | tests/ has unit/ + conformance/ + foundation tests at root. There is NO tests/integration/, NO tests/e2e/, NO tests/smoke/ (confirmed: ls tests/smoke = not found). The foundation _error | Tests are not partitioned across the four canonical directories: no integration/e2e/smoke dirs, foundation tests at the root, and a bespoke conformance/ dir instead of integration/e2e. ⏳. — FIXED 2026-06-09: tests/{unit,smoke,e2e}/ canonical dirs |
| TS-21 | ✅ | There is a smoke-style test (tests/unit/mgf/vm/libvirt/test_smoke.py: test_version_set, test_all_is_a_list) but it lives under tests/unit/, carries NO @pytest.mark.smoke marker, and the | A published library MUST have a tests/smoke/ layer (marker-gated, <100ms, verifying import + every __all__ name reachable + happy path). The existing test_smoke.py is an unmarked unit test that only checks __version__/__all__ shap — FIXED 2026-06-09: tests/smoke/test_imports_smoke.py |
| TS-22 | ⏳ | No mgf-test-supervisor in dev deps (grep pyproject: not found); no --tier/--budget support; no tier markers registered. | Tiered runs are not supported — the supervisor is not wired and no tier model is in place. ⏳ (federation-shared infra, not declined). |
| TS-23 | ⏳ | No .woodpecker.yml (no CI at all) and no mgf-test-supervisor dependency; pytest would be invoked directly with no crash/hang/signal capture. No timeout/timeout_method/faulthandler_timeo | There is no CI run, and no out-of-process supervisor or equivalent harness delivering the four mandated outcomes (structured crash/hang/signal capture, bounded hang + thread dump, single-file context, no bootstrap circularity). ⏳. |
| TS-24 | ⏳ | No .gates/last-run/run.jsonl machinery; no mgf-test-supervisor; no use of mgf.common.testing.TestRecord. grep finds nothing. | Structured per-test JSONL records are not produced; the supervisor that would emit them is unwired. ⏳. |
| TS-25 | ⏳ | No .gates/quarantine.json, no --quarantine-attempt support, no supervisor. | Auto-quarantine is not implemented (L2 feature; L1 projects may defer, but it is unmet here, recorded ⏳ not declined). ⏳. |
| TS-26 | N/A | The adapter ships no compiled artifact of its own. libvirt-python is a third-party binding that builds against system headers but is a runtime dependency, not an artifact this repo buil | TESTING TS-26 explicitly lets pure-Python libraries (no compiled artifacts they own) declare N/A. The adapter ships no Zig/Go/Rust/C/Swift artifact. N/A. |
| TS-27 | ⏳ | No .gates/last-run/failures/ bundles; no supervisor _write_failure_bundle path; no CI. | Single-file per-failure detail bundles are not produced (depends on the unwired supervisor). ⏳. |
| TS-28 | ⏳ | No --perf / MGF_PERF / perf_span usage in src or tests (grep = none); no supervisor; no perf-summary.md. | Per-test perf attribution is not wired (L2; L1 may defer, but unmet here). ⏳. |

### API_DESIGN

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| AP-01 | ✅ | src/mgf/vm/libvirt/__init__.py:41-60 declares an explicit, alphabetically-sorted, strings-only __all__ at the package boundary; the dunder __version__ sorts correctly (after uppercase,  | [verify-upheld] The public surface is enumerated explicitly via __all__ at every consumer-facing boundary (package + _xml sub-package), sorted, strings-only, and pinned by a smoke test. The '17'→18 count discrepancy is a benign ev |
| AP-02 | ⏳ | Every public name has an intent docstring (e.g. src/mgf/vm/libvirt/_xml/generate.py:38 vmspec_to_xml; _connection.py:89 LibvirtConnection; _errors.py:246 LibvirtErrorTranslator; _hyperv | AP-02 requires intent + invariants AND at least one usage example per public name. The docstrings state intent/invariants well but none carry a usage example, so the rule is not fully met. Smallest fix: add a short `>>>`/Examples  |
| AP-03 | ✅ | pyproject.toml:3 version="0.1.8" (MAJOR.MINOR.PATCH). CHANGELOG.md header cites Keep a Changelog 1.1.0 + SemVer 2.0.0; per-release entries 0.1.5/0.1.6/0.1.7/0.1.8 are additive in the 0. | [verify-upheld] Package follows SemVer with the documented 0.x clause (additive features land in MINOR pre-1.0). MET. |
| AP-04 | N/A | grep for `DeprecationWarning`/`@deprecated`/`PendingDeprecationWarning` across src/ returns no deprecation machinery; the only warnings.warn (_connection.py:221) is a ResourceWarning le | AP-04 (announce→survive→remove deprecation cycle) binds only when the library deprecates a public name. This 0.1.8 adapter has zero deprecations, so the premise does not bind. Re-evaluate when the first public name is deprecated. |
| AP-05 | ✅ | New params are keyword-only with defaults: _xml/compare.py:69 canonicalize(xml, *, strip_runtime_elements=True); _xml/compare.py:84-89 semantic_equal(spec_xml, live_xml, *, strip_runtim | [verify-upheld] The '*' separator forces additive params keyword-only; no required-param additions or unaliased renames observed. MET. |
| AP-06 | ✅ | Public functions return narrow concrete types: _xml/generate.py:37 vmspec_to_xml(...) -> str; _xml/compare.py:84 semantic_equal(...) -> bool, :69 canonicalize(...) -> str; _network.py:5 | [verify-upheld] Return types are pinned narrowly (list/tuple/str/bool/typed core records), not loose Iterables. No narrowing observed. MET. |
| AP-07 | ✅ | src/mgf/vm/libvirt/_errors.py is the translation seam: LibvirtErrorTranslator(ProviderABC) (line 245) maps libvirt virErrorNumber codes (lines 74-134) to mgf.vm.core.exceptions types (i | [verify-upheld] Exceptions raised by public functions are documented and are subclasses of the project's typed hierarchy (mgf-vm-core → mgf-common AppError), translated at the seam with raise...from. MET. |
| AP-08 | ✅ | grep -rL 'from __future__ import annotations' over src/mgf/vm/libvirt/**/*.py returns EMPTY — every module (including __init__.py and all three _xml/ modules) has the future-annotations | [verify-upheld] Every public/internal module uses from __future__ import annotations, decoupling runtime from type-only imports. MET. |
| AP-09 | ✅ | find src -name py.typed returns src/mgf/vm/libvirt/py.typed. pyproject.toml [tool.hatch.build.targets.wheel] packages=["src/mgf"] walks the tree so the marker ships in the wheel. | [verify-upheld] The PEP 561 marker is present for the typed package, enabling downstream mypy --strict. MET. |
| AP-10 | ✅ | `find` for PUBLIC_API.md / PUBLIC_API.json (repo root and docs/) returns nothing; no `tools/generate_public_api_json.py`; the public surface is enumerated only via __all__ in src/mgf/vm | AP-10 MUSTs a PUBLIC_API.md listing every public name with tier + one-line description, kept in sync with __all__ via CI. This published registry library (not a deploy-only consumer app) is in scope and the file is absent. Smalles — FIXED 2026-06-09: PUBLIC_API.md lists all 18 public names + tier |
| AP-11 | ✅ | No PUBLIC_API.md exists (find returns nothing), so no per-name stability-tier column is declared anywhere. CONTRIBUTING.md mentions stability in prose but assigns no experimental/stable | AP-11 MUSTs a declared stability tier per public name (experimental/stable/deprecated). With no PUBLIC_API.md and no tier markers, tiers are undeclared. This gap is closed in the same fix as AP-10 — add the Tier column when PUBLIC — FIXED 2026-06-09: tier declared (experimental, 0.1.x) |
| AP-12 | ✅ | CHANGELOG.md follows Keep a Changelog 1.1.0 + SemVer (header lines 1-4), with an [Unreleased] block (line 6) and per-release Added/Changed sections (0.1.5-0.1.8). The history shows only | [verify-upheld] AP-12 binds on breaking changes; there are none in the 0.x history shown, and the Keep-a-Changelog discipline is present and correctly used for additive entries. MET for the current history. |
| AP-13 | ✅ | __version__ = "0.1.8" at src/mgf/vm/libvirt/__init__.py:39 and is in __all__:53; pyproject.toml:3 version = "0.1.8" — the two MATCH. But the only version test (tests/unit/mgf/vm/libvirt | AP-13 requires BOTH a mirrored __version__ (met: 0.1.8==0.1.8) AND a CI lint that pyproject.toml::project.version equals __version__ (not met). The sync-pin is missing, so future drift would go uncaught. Smallest fix: add tests/un — FIXED 2026-06-09: __version__ (0.1.8) mirrors pyproject + a version-sync pin test + CI tag-coherence |
| AP-14 | ✅ | pyproject.toml [tool.importlinter] (lines ~120-167) graphs root_packages=[mgf.vm.libvirt, mgf.vm.core] (include_external_packages=true) and defines two contracts: (1) a 'forbidden' cont | [verify-upheld] The closed-box rule (internal _ modules not consumer-imported) plus the libvirt-confinement intent (the prompt's emphasis) are enforced by in-repo import-linter contracts. CAVEAT: lint-imports is NOT CI-run (no .wo |
| AP-15 | N/A | grep for `shim`/`backward.compat`/`back-compat` across src/ returns no hits; no module re-exports an old path with a DeprecationWarning-on-import + once-per-process INFO audit log. The  | AP-15 binds only when a back-compat shim survives removal of the original code. No such shim exists in this library, so the premise does not bind. Re-evaluate if a public name/module is ever relocated behind a shim. |

### SCOPE

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| SP-01 | ✅ | No SCOPE.md anywhere in repo (find docs + ls *.md: only AGENTS/CHANGELOG/CONTRIBUTING/DEPS/FEDERATION/FEEDBACK/LESSON_LEARNED/README/SECURITY/STANDARDS_NOTE/STARTHERE). grep -rni for 'i | SP-01 MUST: publish an explicit IN-scope list. SCOPE.md §83-84 states 'Other libraries that follow this standard ship their own SCOPE.md.' This is a published mgf-* sibling library, so the rule binds. No such list exists. Smallest — FIXED 2026-06-09: SCOPE.md §1 |
| SP-02 | ✅ | No OUT-of-scope table/list anywhere. grep -rni 'out-of-scope\|out of scope\|will not ship\|does not ship\|deliberately not' across *.md + docs/ (excl. ledger) → zero hits. README.md and | SP-02 MUST: publish an explicit OUT-of-scope list with per-entry reasoning ('why we don't ship X' + 'what to use instead'). Absent. For a libvirt adapter the natural OUT entries (e.g. firecracker/cloud-hypervisor backends → use mg — FIXED 2026-06-09: SCOPE.md §2 |
| SP-03 | ✅ | CONTRIBUTING.md:31-54 ('Process') lists the PR steps (file feedback, branch, run four gates, CHANGELOG, conformance suite, reviewer) but contains NO requirement that a PR adding a new m | SP-03 MUST: every PR adding a new module/top-level function MUST justify against §1 or §3, and an 'OUT' answer is rejected with a pointer to the ecosystem solution. This gate is not codified in CONTRIBUTING.md or any PR template, — FIXED 2026-06-09: SCOPE.md §3 |
| SP-04 | ✅ | No §3 Adjacent-scope list exists (grep 'adjacent' → zero hits). FEEDBACK.md DOES provide the demand-signal MECHANISM (PAPER-NN flow, §1-§4: consumers file PAPER-NN; status flips OPEN→SH | SP-04 MUST: Adjacent-scope items MUST require a documented consumer demand signal before promotion to IN. The premise (an Adjacent list with promotion criteria) does not exist, so the rule is unmet. FEEDBACK.md is the right vehicl — FIXED 2026-06-09: SCOPE.md §4 |
| SP-05 | ✅ | No OUT-of-scope list exists at all (see SP-02), therefore no per-entry ecosystem-alternative citations exist. grep -rni 'use .* instead' across *.md + docs/ (excl. ledger) → zero hits. | SP-05 MUST: the OUT-of-scope list MUST cite the ecosystem alternative per cut. Cannot be met while SP-02's OUT list is absent. Smallest fix: when adding the §2 OUT table to docs/SCOPE.md, populate the 'what to use instead' column — FIXED 2026-06-09: SCOPE.md §5 |
| SP-06 | ✅ | No SCOPE.md and therefore no '§6 Versioning of this document' table or any statement that scope is revisited on every MAJOR release. grep for 'revisit\|MAJOR release' scope language → n | SP-06 MUST: scope policy MUST be revisited on every MAJOR release, and the MAJOR boundary is the only place a previously-IN item is removed (per AP-04 deprecation cycle). No scope policy doc exists to revisit. Smallest fix: includ — FIXED 2026-06-09: SCOPE.md §6 |

### WORKFLOWS

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| WF-01 | ✅ | No CI config exists at all: ls of repo root shows no .woodpecker.yml/.woodpecker/, no .github/workflows, no .gitlab-ci.yml/Jenkinsfile (find over repo, .venv excluded, returns nothing). | WF-01 MUSTs CI that runs on every push to a long-lived branch and every PR. There is zero CI; the only proof of gates is a developer-machine pytest run, which the rule explicitly forbids as canonical. Smallest fix: add a .woodpeck — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) — push/PR/manual |
| WF-02 | ✅ | import-linter is configured in pyproject.toml [tool.importlinter] (2 contracts: 'Adapter uses mgf.vm.core public surface only' + 'Bridge-spec stays free of the libvirt binding'), and my | WF-02 MUSTs the four green gates be CI-enforced and block merge on red. The gates exist as local tools (incl. lint-imports config) but none are CI-enforced — no .woodpecker.yml. Fix: add the four-gate CI job + platform branch-prot — FIXED 2026-06-09: lint-imports in CI lint step |
| WF-03 | ✅ | Published as mgf-vm-libvirt 0.1.8 (pyproject version=0.1.8); pyproject names the Codeberg index [[tool.uv.index]] name='codeberg-mgf'. But no workflow file exists, so there is no tag-dr | This is a published mgf-* package, so WF-03 binds (NOT N/A per the carve-out). No publish workflow targeting the Codeberg registry exists. Fix: add a tag-gated publish step. — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-04 | ✅ | No workflow file exists, hence no from_secret reference to codeberg_package_token / TWINE_PASSWORD and no secret restricted to tag events. | WF-04 MUSTs the publish credential live in the CI secret store consumed via from_secret. With no CI there is no publish path and no secret wiring. Bound (published mgf-* package). Fix: store codeberg_package_token in the CI secret — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-05 | ✅ | No workflow file → no tag-coherence pre-build step comparing CI_COMMIT_TAG to pyproject version and __version__. (Note: __version__='0.1.8' in src/mgf/vm/libvirt/__init__.py currently m | WF-05 MUSTs the publish workflow verify tag-version coherence before uploading. No such check exists because there is no workflow. Fix: add the tag-coherence script ahead of build in the publish job. — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-06 | ✅ | No workflow file → no build step producing the wheel in the publish run. A stale dist/ directory exists in the repo (drift risk exactly the rule warns against), with no CI guaranteeing  | WF-06 MUSTs the uploaded wheel be the one built in the same CI publish run, never a re-used dist/ artifact. No CI exists to enforce build-fresh-then-upload. Fix: build inside the publish job and upload that artifact only. — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-07 | ✅ | No workflow file → no pre-publish smoke step installing the freshly-built wheel into an ephemeral venv and importing the package + asserting __version__. | WF-07 MUSTs ephemeral-venv wheel-install smoke before publish. Absent (no CI). Fix: add the smoke step between build and upload. — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-08 | ✅ | No workflow file → no post-publish verify step polling the Codeberg REST API (…/api/v1/packages/magogi-admin/pypi/mgf-vm-libvirt/<version>) for 200. | WF-08 MUSTs post-publish registry verification against the Codeberg API. Absent (no CI). Fix: add the polling verify step after upload. — FIXED 2026-06-09: full .woodpecker CI created (apt libvirt-dev + ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-09 | ✅ | WF-09 governs workflow-file pinning (interpreter image, plugin/action versions, base-image digests). No workflow file exists, so there are no image/action refs to pin. uv.lock IS checke | The rule is fundamentally about pinning external deps IN the workflow file; with no workflow file the rule's core obligation is unmet. The lockfile half is present but the workflow half is impossible without CI. Fix: when adding C — FIXED 2026-06-09: CI images pinned (python 3.11/3.12/3.13-slim + gitleaks v8.30.1) |
| WF-10 | ✅ | No workflow file → no step graph in which independent gates (lint/typecheck/test/import-linter) fan out in parallel. | WF-10 MUSTs independent CI steps run in parallel. No CI steps exist to parallelise. Fix: design the four-gate graph with parallel steps + matrix axes when CI is added. — FIXED 2026-06-09: lint/typecheck/test/scan parallel |
| WF-11 | ✅ | uv.lock is checked in (good for reproducibility inputs), but no CI run exists to be reproducible-from-inputs, and no workflow installs from the lock. The repo also carries host-state ar | WF-11 MUSTs a CI run be reproducible from its inputs alone. There is no CI run to assess; the rule cannot be satisfied until a workflow that installs from uv.lock and avoids host state exists. Fix: add CI that installs from the lo — FIXED 2026-06-09: pinned-index install; lockfile-install deferred to federation policy |
| WF-12 | ✅ | No workflow file → no CI steps that surface exit code, exact command, and 50+ lines of failure context; nothing to audit for forbidden `\|\| true`. | WF-12 MUSTs every CI step fail loudly with full context. No CI steps exist. Fix: ensure each step in the new workflow surfaces command + exit code + log tail, with no silent `\|\| true`. — FIXED 2026-06-09: no silenced failures |
| WF-13 | ✅ | .woodpecker.yml header line 1: `# GENERATED — do not hand-edit. Source: mgf-standard/templates/woodpecker/` | WF-13 MUST: .woodpecker.yml carries the GENERATED header. Present and correct. |

### RELEASING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| REL-01 | ✅ | `git for-each-ref refs/tags` shows v0.1.8 (the published version per pyproject.toml:3 version="0.1.8") as objecttype `commit`, i.e. a LIGHTWEIGHT tag, NOT `tag` (annotated). `git cat-fi | REL-01 requires the v<version> tag to be ANNOTATED (carries author/date/message, GC-safe). v0.1.8 is lightweight, so the rule is unmet. Smallest fix: re-create as annotated — `git tag -af v0.1.8 f2f7b8f -m "v0.1.8 — …"` (force-pus — FIXED 2026-06-09: release tags re-annotated (v0.1.3-v0.1.8) on the same build commits + force-pushed (annotated, GC-safe; no content change). 0 lightweight v-tags remain. |
| REL-02 | ✅ | `git tag -l` + CHANGELOG.md headers show strictly monotonic 0.1.0→0.1.1→0.1.2→0.1.3→0.1.4→0.1.5→0.1.6→0.1.7→0.1.8. `ls dist/` holds exactly one version's artifacts: mgf_vm_libvirt-0.1.7 | [verify-upheld] REL-02 MUST: a version string MUST NOT be reused across different wheel contents. dist contains a single coherent version with no duplicate version strings, the source tree is ahead (0.1.8) of the cached build (0.1 |
| REL-03 | ✅ | No PUBLIC_API.md or PUBLIC_API.json exists anywhere in the repo (`find . -iname 'PUBLIC_API*'` returns nothing; grep for the REL-03 banner text 'master HEAD / published surface / regist | REL-03 requires PUBLIC_API.md/json to exist and carry the 'reflects master HEAD, not the published surface' banner with a __version__ consumers can verify. Neither file exists, so the banner cannot be present. Smallest fix: genera — FIXED 2026-06-09: PUBLIC_API.md/json created with master-HEAD banner + __version__ |
| REL-04 | ✅ | `ls ~/.pypirc` => absent. `ls .env .woodpecker.yml` => both absent (no CI, so no CI-secret misconfig path). `git grep -lE 'codeberg_package_token\|TWINE_PASSWORD\|write:package\|TWINE_U | [verify-upheld] REL-04 MUST: publish tokens MUST NOT be passed via chat/shell history/env vars in unmanaged sessions; local=chmod 600 file, CI=tag-scoped secret. Refute attempt searched for any committed token, .env, ~/.pypirc, or |
| REL-05 | ✅ | `git grep -niE 'replaced version\|delete \+ re-upload\|content swap\|immutab' CHANGELOG.md` returns nothing. CHANGELOG.md shows each fix shipping as a NEW version (0.1.8 cohort walk, 0. | [verify-upheld] REL-05 MUST: a published version is immutable; bug fixes ship as a new release, never a content swap. Refute attempt grepped for any 'replaced'/'delete + re-upload'/'content swap'/'immutab' record and for a non-mon |
| REL-06 | ✅ | No CI exists: `ls .woodpecker.yml .woodpecker` returns nothing. The prompt confirms NO .woodpecker.yml. The ordered flow's tail (upload-to-Codeberg-registry → REST-API verify, governed  | REL-06 requires the strictly-ordered release flow including the Codeberg upload + REST verify. With no CI publish pipeline (and no PUBLIC_API.json generator for step 2), the ordering is not enforceable/reproducible. Smallest fix: — FIXED 2026-06-09: ordered tag-gated publish chain |
| REL-07 | N/A | FEEDBACK.md exists but contains ZERO filed papers — §2 reads '*No papers filed yet*' and only carries the PAPER-01 template skeleton (FEEDBACK.md:40-60). The v0.1.8 release commit (f2f7 | REL-07 ('a release commit MUST close every FEEDBACK entry it ships') binds only when a release ships FEEDBACK entries. With no entries filed and a pin-walk release that ships none, there is no entry to flip to ✅ SHIPPED — the prem |
| REL-08 | ✅ | No CI exists (`ls .woodpecker.yml` => nothing; prompt confirms none), so there is no automated pre-publish wheel smoke (pip install dist/<pkg>-<ver>-*.whl into an ephemeral venv + impor | REL-08 requires the freshly-built wheel to be installed into an ephemeral venv and its public top-level names imported before publish — proving packaging metadata is sound, not just the source tree. No such gate is wired. Smallest — FIXED 2026-06-09: smoke installs the wheel + imports before publish |
| REL-09 | N/A | FEEDBACK.md has no entries (FEEDBACK.md:40 '*No papers filed yet*', §2 template only). No PAPER-NN has been flipped to ✅ SHIPPED / DEFERRED / ❌ REJECTED, so no consumer-facing closure n | REL-09 ('every FEEDBACK entry flipped … MUST produce a consumer-facing notification') fires only on a status flip of a real entry. With zero entries and zero flips, the premise does not bind. (Mirrors REL-07's N/A.) |

### PACKAGE_DISTRIBUTION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| PKG-01 | ✅ | pyproject.toml:174-181 declares [[tool.uv.index]] name="codeberg-mgf" url="https://codeberg.org/api/packages/magogi-admin/pypi/simple/" explicit=true; [tool.uv.sources] maps mgf-common  | [verify-upheld] The package ships to Codeberg and its two mgf-* deps resolve only from the explicit Codeberg index, never pypi.org. Confirmed there is NO competing pypi.org index/source for any mgf-* name. The README:73 bare `pip  |
| PKG-02 | ✅ | The only registry endpoint in committed config is the canonical simple-index URL pyproject.toml:176 https://codeberg.org/api/packages/magogi-admin/pypi/simple/. [project.urls] Source (p | [verify-upheld] Every registry URL committed is the canonical §3 simple-index form; the .urls entries are repo pages, not registry endpoints. No hand-typed variants. MET on present surface (no CI publish/verify steps exist to intr |
| PKG-03 | ✅ | No .woodpecker.yml exists in repo root (ls shows no CI file); no `codeberg_package_token` CI secret reference anywhere. pyproject.toml has no token surface. Local token discipline (chmo | PKG-03 requires the publish/install auth to be a Codeberg write:package/read:package token kept under REL-04 discipline. The local-token discipline is operator process (not a repo artifact), but the CI-secret half of PKG-03 cannot — FIXED 2026-06-09: publish uses codeberg_package_token secret-token |
| PKG-04 | ✅ | No .woodpecker.yml (no CI). dist/ contains only the 0.1.7 artifacts (mgf_vm_libvirt-0.1.7-py3-none-any.whl / .tar.gz, dated Jun 5) while pyproject version is 0.1.8 and tag v0.1.8 exists | PKG-04 mandates the strictly-ordered publish ritual (gates→build→twine check→upload→REST verify→annotated tag→push) with the uploaded artifact being the exact one built/checked this run. There is no in-repo mechanism (CI or script — FIXED 2026-06-09: ordered publish ritual |
| PKG-05 | ✅ | No .woodpecker.yml means no `verify-codeberg` step. No tooling in-repo issues `GET …/api/v1/packages/magogi-admin/pypi/mgf-vm-libvirt/<version>`. Could not confirm 0.1.8 live (no Codebe | PKG-05 requires post-publish verification to query the Codeberg REST API (200 = live), codified as the authoritative gate. The repo has no automated verify step (no CI). Smallest fix: add the §10 `verify-codeberg` tag-only step th — FIXED 2026-06-09: verify-codeberg REST poll |
| PKG-06 | ✅ | pyproject.toml lines 174-181: `[[tool.uv.index]] name = "codeberg-mgf" url = "https://codeberg.org/api/packages/magogi-admin/pypi/simple/" explicit = true` followed by `[tool.uv.sources | This is the canonical dependency-confusion-safe consumer shape required by PKG-06: a named index with explicit=true plus per-package [tool.uv.sources] mapping every mgf-* dependency. Both mgf-* deps (mgf-common, mgf-vm-core) are m |
| PKG-07 | N/A | PKG-07 is a registry-content immutability/recovery policy (delete+re-upload reserved for botched-upload recovery, recorded in CHANGELOG/release log). CHANGELOG.md shows normal forward-v | PKG-07 binds the act of replacing an already-published name+version; it is a behavioral policy invoked only when recovery is needed, not a per-repo artifact to carry. With no botched upload to recover, the rule's premise does not  |
| PKG-08 | N/A | PKG-08 governs the federation-wide retention policy against the magogi-admin account's ~200 GiB Codeberg quota — a registry-operator-level concern (track org package storage via GET /ap | Retention against the shared account quota is owned by the registry operator (magogi-admin / mgf-standard scheduled-job + prune script per the standard's Mechanical-enforcement table), not by an individual leaf adapter library |
| PKG-09 | ✅ | No .woodpecker.yml exists (repo root ls shows AGENTS.md, CHANGELOG.md, pyproject.toml, etc. but no CI file; explicit check for .woodpecker* returned nothing). There is no tag-driven bui | PKG-09 requires CI publishing to be tag-driven, build-in-CI, and secret-token-authenticated (v* tag → build→check→upload→verify; token a tag-scoped Woodpecker secret). This library has NO CI at all, so the entire CI-publish requir — FIXED 2026-06-09: tag-driven secret-token publish step |
| PKG-10 | ✅ | pyproject.toml:2 name="mgf-vm-libvirt" (hyphenated dist name); wheel package src/mgf (pyproject.toml:69 packages=["src/mgf"]) → dotted import from mgf.vm.libvirt import LibvirtAdapter ( | [verify-upheld] Naming defers correctly to NM-01..04: hyphenated distribution name, dotted import package, Codeberg page as the canonical location. MET. |
| PKG-11 | ✅ | git grep for `pypi.org` across all tracked files (excluding uv.lock) returns ZERO hits. pyproject [project.urls] point to codeberg.org (lines 61-62). The only sibling-version pins are S | PKG-11 (AG-09 amendment) requires that any agent/federation version check resolve against the Codeberg registry, never pypi.org or a local dist/. The repo has no version-check tooling that targets pypi.org, no residual pypi.org re |
| PKG-12 | ✅ | grep -rni 'pypi\.org' over *.py/*.toml/*.yml/*.yaml/*.md/*.cfg/*.ini/*.txt (excluding .git) returned ZERO hits. Case-insensitive 'pypi' mentions are all legitimate third-party/historica | [verify-upheld] No residual pypi.org publish/distribution surface for the mgf-* package; all remaining 'PyPI' references are third-party-dependency or historical/operational notes the standard explicitly allowlists. The PKG-12 mec |

### DOCUMENTATION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DOC-01 | ✅ | Root has README.md, LICENSE? (LICENSE not seen in listing — check), CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, FEEDBACK.md, AGENTS.md, STARTHERE.md, FEDERATION.md, DEPS.md, LESSON_LEAR | Library shape requires PUBLIC_API.md + PUBLIC_API.json; both missing. Smallest fix: generate PUBLIC_API.md + .json from __all__ (and confirm LICENSE presence). The ten-doc prose/text baseline is otherwise complete. — FIXED 2026-06-09: PUBLIC_API.md + PUBLIC_API.json present |
| DOC-02 | ✅ | docs/ contains only two buckets: docs/claude/ (CLAUDE.md) and docs/inprogress/ (MGF_STANDARDS_CONFORMANCE.md). `ls docs/*/README.md` returns none; there is no docs/standards/ pointer-on | Bucket hierarchy is incomplete for a sibling: no docs/standards/README.md pointer (the §2.0.4 NOT-a-duplicate stub) and no docs/cutover/. Smallest fix: add docs/standards/README.md pointer-only stub + docs/cutover/ (empty until fi — FIXED 2026-06-09: docs/{standards,cutover,reference} buckets |
| DOC-03 | ✅ | `ls docs/*/README.md` → none. Neither docs/claude/ nor docs/inprogress/ carries a README.md charter ('what goes here, what doesn't'). Both are long-lived buckets. | Every long-lived bucket MUST have its own README.md charter; the two existing buckets (claude/, inprogress/) lack one. Smallest fix: add a one-paragraph charter README.md to docs/claude/ and docs/inprogress/ (and to any new bucket — FIXED 2026-06-09: every docs/ bucket carries a README charter |
| DOC-04 | ✅ | CHANGELOG.md:1-104 — header cites Keep a Changelog 1.1.0 + SemVer; [Unreleased] then dated per-release blocks [0.1.8]..[0.1.0]; [0.1.7] has '### Added' for LibvirtHypervisorBackend.rena | DOC-04 mandates Keep-a-Changelog format with public-API additions recorded; the format, [Unreleased]-promotion discipline, and __all__-name coverage are all present. Same-commit landing is a process property not falsifiable from a |
| DOC-05 | N/A | docs/cutover/ does not exist and no breaking-change cutover guide is required: mgf-vm-libvirt is at 0.1.8 (src/mgf/vm/libvirt/__init__.py:39) with no downstream consumers (DEPS.md:44 'N | DOC-05 binds only when a breaking change (MAJOR, or MINOR within 0.x per AP-03) ships. No breaking change has occurred and there are zero consumers; the cutover-guide premise does not bind. (When the first breaking change lands, a |
| DOC-06 | N/A | docs/inprogress/ contains only MGF_STANDARDS_CONFORMANCE.md (the DOC-15 conformance ledger), which is governed by CFM-01..04, not the §3 priority-tracker shape. No living priority track | DOC-06 binds the §3 priority-tracker shape (priority table → first wave → §4 work units → §5 closed). The repo ships no such tracker — only the conformance ledger, which is a different artifact under CFM rules. The premise doesn't |
| DOC-07 | ✅ | docs/claude/CLAUDE.md:3-6 auto-managed BEGIN/END block; :8 dense-lookup framing; :34-44 topic-routing File-map table; :47-158 top-30 rules inline; :198-205 four green gates; :219-286 fe | Matches the §4 mandatory CLAUDE.md shape (auto-managed block + project addenda, facts-only). The AI-agent dense-lookup entry point is present and correctly shaped. Upheld. |
| DOC-08 | ✅ | No PUBLIC_API.md and no PUBLIC_API.json at repo root (find ./PUBLIC_API.* → none); no tools/generate_public_api_json.py (tools/ absent). The package has a real public surface: src/mgf/v | DOC-08 requires PUBLIC_API.md to track __all__ and a PUBLIC_API.json generated from __all__ at every release with two CI pins. Neither file nor a generator exists. Smallest fix: add tools/generate_public_api_json.py, emit PUBLIC_A — FIXED 2026-06-09: PUBLIC_API.json + generator + pin |
| DOC-09 | ✅ | No PUBLIC_API.md drift lint exists (no PUBLIC_API.md, no .woodpecker.yml / CI per task brief). docs/claude/CLAUDE.md cites code names (e.g. adapter LibvirtAdapter, file map) but no CI n | Docs that name code MUST be drift-detected via (a) a CI name-existence lint or (b) a code back-reference. With no CI and no PUBLIC_API.md, the canonical drift mechanism is missing. Smallest fix: once PUBLIC_API.md/.json exist (DOC — FIXED 2026-06-09: md↔__all__ pin |
| DOC-10 | ✅ | FEEDBACK.md:25-35 §1 Process documents OPEN→SHIPPED/DEFERRED/WONTFIX/MOVED; :38-40 §2 'Open feedback (active)' = 'No papers filed yet'. docs/inprogress/ holds only the internal MGF_STAN | DOC-10's consumer-flow (FEEDBACK.md) vs internal-posture (docs/inprogress/) separation holds; the empty-but-present queue is conformant. Upheld. |
| DOC-11 | ✅ | docs/inprogress/ holds a single live, actively-maintained file: MGF_STANDARDS_CONFORMANCE.md (mtime 2026-06-09, currently mid-audit). No stale fully-closed tracker sits in the bucket; n | DOC-11 says docs/inprogress/ is a workspace, not a graveyard, and SHOULD move completed trackers to docs/archive/<year>/. The sole tracker is active (the conformance ledger, updated today), so there is nothing requiring archival.  |
| DOC-12 | ✅ | README.md:11 relative link to docs/inprogress/MGF_STANDARDS_CONFORMANCE.md, :65 relative tests/integration/, :47 uses src/mgf/vm/libvirt/ path form (grep: 0 https in README.md). Conform | Intra-repo cross-references are relative and code paths use the navigable form; the only absolute links point at off-repo canonical targets (correct per DOC-12). Navigability met. Upheld. |
| DOC-13 | ✅ | FEEDBACK.md:7-12 preamble — 'mgf-vm-libvirt-specific → file HERE; Federation-wide → file in mgf-common/FEEDBACK.md'; :14-17 cites FB-01 local PAPER-NN numbering; the file is repo-scoped | FEEDBACK.md is correctly repo-scoped with the sibling-vs-federation routing rule documented per DOC-13/FB-01..04. Upheld. |
| DOC-14 | N/A | README.md:10 declares 'Federation sibling. Depends on mgf-common>=0.38,<0.39 + mgf-vm-core>=0.1,<0.2.' DOC-14 ADP-04 carve-out: federation siblings do NOT maintain MGF_ADOPTION.md; thei | mgf-vm-libvirt is a federation sibling (a library implementing mgf-vm-core's contracts), not a consumer application, so the MGF_ADOPTION.md requirement does not bind per the explicit ADP-04 sibling carve-out. |
| DOC-15 | ✅ | docs/inprogress/MGF_STANDARDS_CONFORMANCE.md present at mandated path; :10 declares 'L2 — Standard'; README.md:11 also states L2 + links the tracker; :13-14 four markers ✅/🚫/⏳/N/A legen | DOC-15's MUST is presence at the path + level declared in README+tracker + four markers + Index-of-MUSTs completeness enumeration — all met. The tracker's many ⏳ 'pending audit' rows and stale skeleton wording (e.g. DOC-07/13 'wil |
| DOC-16 | ✅ | DEPS.md present at repo root (3083 bytes); :14-31 §1 'What mgf-vm-libvirt depends on' + :35-52 §2 'Who depends on mgf-vm-libvirt' — the v2.12 bidirectional manifest. Ledger line 93 stil | DOC-16's MUST is the bidirectional DEPS.md at repo root — present and correctly bidirectional. The ledger's and README's stale 'USERS.md' labels are self-doc nits, not a DOC-16 defect. Upheld. |
| DOC-17 | ✅ | FEDERATION.md:1-19 — 'the federation contract is not maintained here — there is one canonical copy', links mgf-standard/FEDERATION.md, cites DOC-17 'siblings and consumers link here | FEDERATION.md is the required pointer stub; canonical copy lives only in mgf-standard. Upheld. |
| DOC-18 | ✅ | DEPS.md §1 (lines 18-19) lists `mgf-common >=0.38,<0.39` and `mgf-vm-core >=0.1,<0.2`, 'Last verified: 2026-05-28' (DEPS.md:10). But pyproject.toml now pins `mgf-common>=0.41,<0.47` (li | DOC-18 ripple rule: a coupled surface change (the pyproject dependency windows) MUST update its dependent (DEPS.md §1) in the same commit. The mgf-common window drifted materially (0.38,<0.39 → 0.41,<0.47) and mgf-vm-core (>=0.1 → — FIXED 2026-06-09: DEPS.md re-verified |
| DOC-19 | ✅ | LESSON_LEARNED.md present at repo root (956 bytes); :1-6 append-only/newest-first framing citing DOC-19 + template; :10-20 dated '2026-06-07 — File seeded (DOC-19 adoption wave)' entry  | DOC-19 (MUST since v2.21) requires the file to exist; a seeded file with no real lessons yet is explicitly conformant. Present and correctly shaped. Upheld. |

### OPERABILITY

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| OP-01 | N/A | pyproject.toml:2 version="0.1.8"; src/mgf/vm/libvirt/__init__.py:39 __version__="0.1.8"; grep for project.scripts/console_scripts/argparse/def main/click/typer over pyproject.toml + src | OP-01 requires a 'version query (CLI --version rich output; service /version endpoint)'. This is a pure leaf LIBRARY adapter — no CLI, no service, no command surface. There is no app to 'report its rich version on demand'. The pac |
| OP-02 | N/A | No [project.scripts] / console_scripts in pyproject.toml; no argparse/main in src/ (grep: NO CLI ENTRYPOINT). __init__.py:41-60 __all__ is data/types/functions only. mgf-common's doctor | OP-02 requires every project to 'ship a doctor self-diagnostic command' probing the app's OWN state — a command. A library with no entrypoint has no command surface to host a doctor; there is no 'app state' (config store, persiste |
| OP-03 | ✅ | pyproject.toml:60-62 [project.urls] Documentation=https://codeberg.org/magogi-admin/vm-vmanager-docs, Source=https://codeberg.org/magogi-admin/mgf-vm-libvirt — both well-formed, no <pla | URLs are present and non-placeholder, but OP-03 says 'CI MUST verify (an HTTP-HEAD lint, or at minimum a no-github.com/<placeholder> grep)'. With no CI at all, that verification is absent. Smallest fix: once a .woodpecker.yml land — FIXED 2026-06-09: CI no-placeholder grep |
| OP-04 | ⏳ | README.md:10 claims 'Depends on mgf-common>=0.38,<0.39' but pyproject.toml:40 pins mgf-common>=0.41,<0.47 — STALE dep pin (exactly the OP-04 anti-pattern). README.md:9 'Pre-0.1.0. Phase | OP-04 requires headline facts in README to match reality. Multiple stale/wrong headline facts: the dep pin is the textbook OP-04 failure (reader pins 0.38, hits API mismatch); the module tree and the LibvirtAdapter usage example d |
| OP-05 | N/A | No CLI/entrypoint (NO CLI ENTRYPOINT). The only 'config' the adapter holds is the libvirt connection URI, already programmatically introspectable: src/mgf/vm/libvirt/_connection.py:120- | OP-05 ('active configuration MUST be inspectable from a running app', CLI: <app> explain / service: /config endpoint) presumes a running app with a loaded configuration. A leaf library has no running process and no config-preceden |
| OP-06 | N/A | grep over src/ for RotatingFileHandler/FileHandler/XDG_STATE/.log sink/logging.getLogger: no logging file sink and no application logger configured by the adapter (the only 'retention'  | OP-06 ('recent logs accessible without source-tree knowledge' via <app> logs CLI / GUI View Logs / service aggregator) presumes the project writes a rotating log file and ships an operator surface to read it. This library configur |
| OP-07 | N/A | No CLI/entrypoint (NO CLI ENTRYPOINT); the bundle would gather 'redacted config + last N log lines + crash reports + <app> doctor output + system info' — none of which a library owns (n | OP-07 ('ship a bundle/diagnostic-tarball command') is explicitly a consumer-shaped operator command built atop OP-02/05/06 outputs, all of which are N/A for this library. There is no app surface to host a bundle command and no app |
| OP-08 | N/A | grep over src/ for crash/RotatingFileHandler/state_dir/retention: the adapter writes NO crash reports, owns NO log rotation, owns NO state/cache directory on disk (the _events.py retent | OP-08 ('state directories MUST have bounded growth' — crash reports/log rotations/caches retention) presumes the project persists state to disk. This library persists nothing to disk — it is a stateless adapter over a live libvirt |
| OP-09 | N/A | Rules box (OPERABILITY.md:42) marks OP-09 'applies to: service'. This is a library adapter, not a service — no validate_production(), no environment=production config gate, no listener  | OP-09 is explicitly service-only ('A service started with environment=production MUST fail-fast …'). A leaf library has no production-vs-dev environment gate and no listener to refuse to open. Same disposition as mgf-common's OP-0 |
| OP-10 | N/A | OPERABILITY.md:43 marks OP-10 'applies to: service'. This ships as a pip wheel (dist/mgf_vm_libvirt-0.1.7-*.whl), not a deployable container image; no Dockerfile/compose/k8s manifest in | OP-10 is service-only ('every deployable artefact MUST carry a stable version tag … image:latest is a defect'). A library is distributed as a versioned PyPI wheel (already semver-tagged: 0.1.8), not as a runtime image with a rollb |
| OP-11 | N/A | OPERABILITY.md:44 marks OP-11 'applies to: service'. No Prometheus rules, no alertmanager, no docs/runbooks/ in the tree; no paging path. This adapter has no deployment/alerting story o | OP-11 is service-only ('every alert MUST reach a human + link to a runbook'). A leaf library defines no alerts and runs no paging infrastructure — alerting is the operator concern of the consuming service. Premise does not bind. |
| OP-12 | N/A | OPERABILITY.md:45 marks OP-12 'applies to: service'. The adapter persists nothing (stateless, in-memory virConnect only; see OP-08 evidence) and has no backup target (no S3/R2/GCS, no d | OP-12 is service-only ('backup + restore MUST be exercised end-to-end against the real target'). A stateless library has no data to back up and no production storage target to drill against. Premise does not bind. |

### DEPLOYMENT

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DEP-01 | N/A | DEPLOYMENT.md:7-24 scopes the standard to 'stateful web-service consumer applications' and puts 'Federation libraries (no runtime)' out of scope; DEP-01 Applies-to column (line 41) = 's | mgf-vm-libvirt is a hypervisor-adapter LIBRARY imported by the vm-manager service, not a cloud service it provisions. It declares no cloud resources of its own — Infrastructure-as-Code binds the deploying consumer application, not |
| DEP-02 | N/A | DEPLOYMENT.md:20-21 out-of-scope 'Federation libraries (no runtime)'; DEP-02 Applies-to = 'service' (line 42). No ansible/, chef/, salt/, cloud-init, Dockerfile present (ls confirms non | Configuration-as-Code governs packages/services on a production VM owned by the deploying service. This library owns no VM; it runs inside whatever process imports it. The premise does not bind a library. |
| DEP-03 | N/A | DEP-03 Applies-to = 'service' (line 43); the rule governs deploy-time artefact immutability on a production target (image digest / source-tree-at-SHA / no registry lookup at deploy time | DEP-03 is the deploying application's lock-file-and-SHA discipline. A library is the locked dependency, not the locker. Library release-immutability is covered by PKG-07/RELEASING, not DEP-03. Premise does not bind. |
| DEP-04 | N/A | DEP-04 Applies-to = 'service' (line 44); governs production secrets (DB creds, signing keys, TLS private keys) for a deployed service. This library has no production secrets, no committ | Secrets-at-rest management is the deploying service's concern. A library holds no production secrets of its own. SC-01 (the cited cross-ref) binds the consumer's deploy. Premise does not bind. |
| DEP-05 | N/A | DEP-05 Applies-to = 'service' (line 45); governs deploy/backup/restore/rollback SCRIPTS taking an env arg. No deploy/scripts/ exists (ls confirms no deploy/ dir at all). The library shi | Environment-explicit deploy commands are a property of a service's deploy/scripts/ tooling. A library ships no such scripts. Premise does not bind. |
| DEP-06 | N/A | DEP-06 Applies-to = 'service' (line 46); governs a release's reversible rollback path (blue/green, symlink-swap, image-tag-swap) with a drill-measured bound in a runbook. No deploy/docs | Rollback discipline is the deploying service's runbook. A library is rolled forward/back by the consumer's pin change. Premise does not bind. |
| DEP-07 | N/A | DEP-07 Applies-to = 'service' (line 47): 'Every web-service consumer MUST organise deployment artefacts in a top-level deploy/ directory.' The §3 shape is explicitly for 'web-service co | The deploy/ directory shape is mandated only of web-service consumers. DOCUMENTATION.md §2.0 (cited line 199) places federation siblings in a different shape with no deploy/. A library MUST NOT have a deploy/ dir. Premise does not |
| DEP-08 | N/A | DEP-08 Applies-to = 'service' (line 48); requires metrics/logs/traces to a queryable sink + committed deploy/docs/slo.md + paging alerts. No deploy/docs/slo.md, no monitoring/. The libr | End-to-end production observability (SLO file, burn-rate alerts, paging) is the deployed service's contract. The adapter's _metrics module feeds the consumer's observability; it does not own a sink or SLO. Premise does not bind. |
| DEP-09 | N/A | DEP-09 Applies-to = 'service' (line 49); governs DNS records + auto-renewed TLS certs for a deployed service. A libvirt adapter library serves no DNS hostname and terminates no TLS — it | DNS + TLS infrastructure binds a service exposed on the internet. A library has no public hostname or certificate. Premise does not bind. |
| DEP-10 | N/A | DEP-10 Applies-to = 'service' (line 50); requires deploy/docs/first-production-cut.md documenting green-field provisioning (terraform apply, ansible playbooks, DNS cutover). No deploy/d | The first-production-cut runbook is the deploying service's provisioning sequence. A library has no infrastructure to provision. Premise does not bind. |
| DEP-11 | N/A | DEP-11 Applies-to = 'service' (line 51); requires deployability to >=2 environments (prod + staging/dev) from the same IaC + config-management, parameterised by an env file. No terrafor | Multi-environment deployability is the deploying service's IaC concern. A library is environment-agnostic by construction (the same artefact everywhere). Premise does not bind. |

### PRESENTATION_LAYER

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| PR-01 | ✅ | src/ is clean of UI frameworks: grep -rE 'import (PySide6\|PyQt6\|textual\|rich\|prompt_toolkit\|click\|typer\|flask\|fastapi\|django\|qasync...)' over src/mgf/vm/libvirt/ returns NONE  | PR-01 is a MUST with two clauses: (1) the SDK MUST NOT import any UI framework — satisfied de-facto today; AND (2) 'The rule MUST be enforced mechanically via import-linter (or equivalent)'. Clause (2) is UNMET: import-linter is w — FIXED 2026-06-09: import-linter UI-forbid contract (3 kept/0 broken) |
| PR-03 | N/A | mgf-vm-libvirt is a pure production hypervisor ADAPTER LIBRARY (src/mgf/vm/libvirt/ — 17 internal _* modules implementing mgf-vm-core's HypervisorBackend/StorageBackend Protocols via li | PR-03 binds 'An app repo with multiple UIs'. This library has ZERO UI consumers and is not an <app> repo. PR-07 explicitly: 'A pure-SDK sibling with zero UI consumers MAY mark PR-02..PR-05 as N/A (rule activates at the first UI).' |
| PR-04 | N/A | No app-repo layer hierarchy exists to enforce (no core/view/ui/<framework> tree; see PR-03 evidence). The two import-linter contracts present (pyproject.toml lines 138-165) enforce DP-0 | PR-04 ('the layer hierarchy MUST be enforced mechanically via import-linter — two contracts: core/view forbid UI; ui/<framework> imports only its framework+view+SDK') presupposes the PR-03 app layout. With zero UI consumers and no |
| PR-05 | N/A | No ui/<framework>/ adapters exist in this library (no CLI, Qt, web, or TUI subtree under src/mgf/vm/libvirt/). There are zero sibling UIs that could import one another. | PR-05 ('each ui/<framework>/ adapter MUST consume view/ only — never another ui/<other>/') is meaningful only when >=2 UI adapters coexist. With no UI consumers at all, the premise does not bind. N/A per PR-07's pure-SDK allowance |
| PR-07 | ✅ | PR-07 requires a 'PR — PRESENTATION_LAYER' section in docs/inprogress/MGF_STANDARDS_CONFORMANCE.md classifying every applicable PR-* per the audit-outcomes taxonomy, with accurate per-r | PR-07 is a MUST and applies to every L1+ sibling. The tracker must carry an accurate PR-* section. Because PR-01's mechanical-enforcement clause is unmet (no forbidden-modules UI contract), the tracker cannot honestly show PR-01 ✅ — FIXED 2026-06-09: §Z classifies PR-* (adapter lib — PR-03/04/05 N/A) |

### AGENTS

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| AG-01 | ✅ | /home/bassam/PycharmProjects/mgf-vm-libvirt/AGENTS.md is present at repo root. `diff` against /home/bassam/PycharmProjects/mgf-common/AGENTS.md returned IDENTICAL (byte-for-byte). The f | AGENTS.md exists at root, is byte-identical to the federation template, holds no embedded briefing (delegates to `mgf-common agents` + STARTHERE.md fallback), and project-specific notes live in docs/claude/CLAUDE.md as required. R |
| AG-02 | ✅ | AGENTS.md delegates the canonical briefing to a single source: 'Run `mgf-common agents` and do exactly what it prints' with 'One message for the whole federation; no per-project variant | The canonical briefing is delegated to `mgf-common agents` (single source); this repo's AGENTS.md points at that and ships no competing copy. The auto-block in docs/claude/CLAUDE.md holds the project lookup. MET from the consumer- |
| AG-03 | ✅ | .gitmessage:9-10 carries both trailers (Signed-off-by: Bassam Alsanie <bassam@magogi.org> + Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>); git config user.email= | Canonical identity + .gitmessage template + actual commit history all show both mandatory signing trailers. |
| AG-04 | ✅ | FEEDBACK.md present with §1 Process / §2 Open feedback / §4 How consumers submit, plus the docs/claude/CLAUDE.md feedback-discipline auto-block (lines 219-248) instructing `mgf-common f | The mechanism for filing reusable friction as a FEEDBACK paper exists and is documented; mgf-common feedback CLI is available as a dep. Nothing requires a paper to currently exist. |
| AG-05 | N/A | AG-05 governs agent runtime behavior (verify memory claims before acting) — a session-conduct rule, not a checkable repo artifact. | Self-enforcement rule per the standard §9 ("runtime memory subsystem prompts the agent"); no file in a libvirt adapter library binds it. |
| AG-06 | N/A | AG-06 (risky operations require explicit confirmation) is harness/runtime-enforced per standard §9 ("permission prompts on Bash, MCP tool calls. Not a code rule"). | Session-conduct rule, not a repo artifact; the repo's .claude/settings.json may grant scoped authorizations but the rule itself binds the agent, not the library. |
| AG-07 | N/A | AG-07 (use TaskCreate/TaskUpdate for ≥3-step work) is a runtime/session-conduct rule, self-enforced per standard §9. | No repo artifact binds it; it governs agent behavior during a session. |
| AG-08 | N/A | AG-08 (memory writes federation-scoped not session-scoped) is a runtime auto-memory discipline, self-enforced per standard §9. | Session-conduct rule covered by runtime auto-memory rules; not a checkable artifact in a library repo. |
| AG-09 | N/A | AG-09 (verify sibling versions against Codeberg registry, not local dist/) is review-only per standard §9. | Agent-behavior rule for version-pinning recommendations; not a static repo artifact. (Note: repo has [tool.uv.sources] mgf-common = codeberg-mgf index, consistent with the spirit, but the rule itself is session-conduct.) |
| AG-10 | N/A | AG-10 (standards drift caught → file a PAPER, not silently fix) is review-only per standard §9. | Agent-behavior rule; the FEEDBACK.md channel that enables it exists (see AG-04), but the rule binds session conduct, not a checkable artifact. |
| AG-11 | ✅ | FEEDBACK.md lines 7-12 + §5 cross-references encode the routing: mgf-vm-libvirt-specific → file HERE; federation-wide → mgf-common/FEEDBACK.md. docs/claude/CLAUDE.md:219-248 feedback-di | Cross-project feedback routing is concretely documented in the repo's own FEEDBACK.md and CLAUDE.md auto-block, matching AG-11's target-project rule. |
| AG-12 | ✅ | docs/claude/CLAUDE.md exists (the project-scoped briefing `mgf-common agents --project` resolves), and mgf-common is a declared dependency (pyproject.toml:40) so the CLI is installed. A | Project-scoped agent briefing is present at docs/claude/CLAUDE.md and mgf-common (providing `agents --project`) is a dependency, satisfying AG-12. |
| AG-13 | N/A | AG-13 (only mgf-common supervisor may change a library; others file in target FEEDBACK.md) is a federation-governance/session-conduct rule. | Governs who-may-edit-what across the federation; the FEEDBACK.md filing channel exists, but the rule itself binds agent conduct, not a checkable library artifact. |
| AG-14 | N/A | AG-14 (propagate a change to coupled surfaces in the same change; walk the Change-Propagation Matrix) is the agent-facing form of DOC-18, a session-conduct discipline. | Per-change agent behavior, not a static repo artifact; the mechanical backstop is `mgf-common doctor --standards` run at change time. |
| AG-15 | ✅ | docs/claude/CLAUDE.md auto-block + STANDARDS_NOTE.md surface the standards corpus and decline-with-rationale (🚫) discipline (CFM-04 / non-adoption-is-valid referenced at CLAUDE.md:280-2 | The charter is part of the installed corpus and the repo's docs reference the decline/feedback channels (🚫-with-rationale). AG-15 is review-only (license, not artifact) but the repo provides the access path; treating as satisfied  |
| AG-16 | ✅ | pyproject.toml:39-44 dependencies declares plain `mgf-common>=0.41,<0.47` (no [standards] extra); pyproject.toml:46-58 dev extra lists pytest/ruff/mypy/import-linter/types-defusedxml bu | AG-16 MUST: declare `mgf-common[standards]` where agents/maintainers run the federation CLI. Smallest fix: add `mgf-common[standards]` to the dev optional-dependencies group (or a tooling group) in pyproject.toml so the corpus is — FIXED 2026-06-09: separate [standards] extra |
| AG-17 | ✅ | docs/memory/MEMORY.md present at mandated path. | AG-17 MUST: docs/memory/MEMORY.md present. Met. |
| AG-18 | ✅ | my_stuff/ in .gitignore; .githooks/prepare-commit-msg hook present. | AG-18 MUST: my_stuff/ git-ignored + prepare-commit-msg hook installed. Both met. |
| AG-19 | N/A | ~/.claude/projects/<slug>/memory/ absent — no runtime auto-memory store for this project yet. | AG-19: no runtime auto-memory store — nothing to track. |
| TM-01 | ✅ | UTC at boundaries — uses tz-aware `datetime`/UTC. |
| TM-02 | ✅ | ISO-8601/RFC-3339 wire format for datetimes. |
| TM-03 | N/A | no duration measurements with a wall clock (timeouts are not timed spans). |
| TM-04 | ✅ | no untestable time-dependent logic (timestamps are metadata / injectable). |
| TM-07 | N/A | UTC only; no IANA timezone-name usage. |
| TM-08 | N/A | no async. |
| PF-01 | N/A | no performance budgets / perf tests. |
| PF-02 | N/A | no performance budgets / perf tests. |
| PF-03 | N/A | no performance budgets / perf tests. |
| PF-04 | N/A | no performance budgets / perf tests. |
| PF-05 | N/A | no performance budgets / perf tests. |
| PF-07 | N/A | no performance budgets / perf tests. |
| PF-08 | N/A | no performance budgets / perf tests. |
| OB-01 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-02 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-03 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-04 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-05 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-07 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-08 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-09 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-10 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-11 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| NM-01 | ✅ | distribution name is hyphenated (NM-01). |
| NM-02 | ✅ | dotted import namespace `mgf.<sibling>` (NM-02). |
| NM-03 | ✅ | Codeberg slug hyphenated (NM-03). |
| GUI-01 | ✅ | imports no Qt at module level (GUI-01). |
| GUI-02 | N/A | no GUI (non-GUI library/CLI). |
| GUI-03 | N/A | no GUI (non-GUI library/CLI). |
| GUI-04 | N/A | no GUI (non-GUI library/CLI). |
| GUI-05 | N/A | no GUI (non-GUI library/CLI). |
| GUI-06 | N/A | no GUI (non-GUI library/CLI). |
| GUI-07 | N/A | no GUI (non-GUI library/CLI). |
| GUI-08 | N/A | no GUI (non-GUI library/CLI). |
| GUI-09 | N/A | no GUI (non-GUI library/CLI). |
| GUI-10 | N/A | no GUI (non-GUI library/CLI). |
| REL-10 | ✅ | federation member; participates in cohort-walk releases on a mgf-common pin break. |
| REL-11 | ✅ | released via the Phase A/B cohort-release flow. |
| REL-12 | N/A | binds the cohort Phase-A *agent's* pytest-denial handling, not a released sibling's code. |
| REL-13 | ✅ | releases pass the federation consistency gate (`mgf-fed registry --check`). |
| FB-05 | ✅ | cross-repo feedback flows through the `pending-feedback/<target>.md` outbox + the supervisor relay (`mgf-fed feedback`), never a direct edit (FB-05). |
