# MGF_STANDARDS_CONFORMANCE — `mgf-vm-interchange`

> 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.41,<0.43` |
| `mgf-vm` pin | `>=0.2,<0.3` |
| Latest `mgf-common` (PyPI == local) | 0.42.0 (within window) |
| Declared level | **L2 — Standard** (see [`STANDARDS_NOTE.md`](../../STANDARDS_NOTE.md)) |
| Last audit | 2026-06-09 (evidence-based depth audit) |
| Verified standing | **below L2** — 97 ✅ / 34 ⏳ / 87 N/A of 218 (74% of applicable) |

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

> **Note on the package surface.** The interchange modules
> (`ovf.py` / `importer.py` / `exporter.py`) live under
> `src/mgf/vm/interchange/`. They are fully implemented (this is no
> longer a skeleton). They translate `tarfile` / `defusedxml` errors
> to `mgf.vm.exceptions.ImportExportError` / `VManagerError` at the
> seam. There is no `domain.py` / `VmSpec` / `VmManagerError` in this
> repo — those references in earlier drafts of this tracker were
> copied from a sibling skeleton and have been removed.


**2026-06-09 depth audit (evidence-based, replaces the prior pass).** Declared TARGET **L2**; verified below L2. Of 218: **97 ✅** (file:line, refute-survived) · **34 ⏳** · **87 N/A** · **0 declined**. Applicable = 131; **97/131 (74%) evidenced**. **Excellent security-conscious core** (refute-survived) for a lib that ingests untrusted VM images: ALL OVF XML through defusedxml (DTD/entity/external forbidden + 8MiB cap), OVA extraction via safe_destination + reject_unsafe_tar_member (symlink/device members rejected, no extractall), rollback on failure, mypy --strict, and PASSING attack tests (path-traversal, absolute-path, billion-laughs). The 46% reflects **no CI (.woodpecker absent → whole WORKFLOWS family ⏳)** + a missing `__version__` (AP-13) + the formal-ABC seam (DP-02) + mutable OVF descriptors (DP-03) — NOT a weak or unsafe core.

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

| Cluster | Rules | Count |
|---|---|---|
| **CLOSEABLE** (create CI pipeline + SCOPE + AP-13 __version__ + PUBLIC_API regen + docs + migration-recipe + sbom) | AG-16, AP-02, AP-12, AP-13, DOC-02, DOC-03, DOC-08, DOC-09, DOC-18, OP-03, OP-04, PKG-01, PKG-04, PKG-05, PKG-09, PR-01, PR-07, REL-03, REL-06, REL-08, SC-10, SC-12, SC-18, SP-01, SP-02, SP-03, SP-04, SP-05, SP-06, TS-01, 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 | 44 |
| **DEFERRED** (DP-02 ABC-seam, DP-03 frozen-descriptor design, logging build-out, EH-08/09, property/supervisor/perf TS, AP-14, REL-01) | AG-03, AP-14, DOC-05, DP-02, DP-03, EH-08, EH-09, LG-01, LG-03, LG-05, LG-09, LG-11, LG-15, LG-16, LG-17, REL-01, TS-07, TS-10, TS-13, TS-14, TS-15, TS-22, TS-23, TS-24, TS-25, TS-27, TS-28, WF-11 | 28 |

## 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 OVA-tar/OVF-XXE SECURITY pass; ~28 agents + a 31-id re-run for 3 truncated families, dropped a spurious AG-03b). No CI yet (.woodpecker absent → whole WORKFLOWS family ⏳). No 🚫-flood, no mislabels. **Mix: 60 ✅ · 71 ⏳ · 87 N/A · 0 🚫.** Replaces the prior scaffold.


### DESIGN_PRINCIPLES

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DP-01 | ✅ | .importlinter:45-55 ships forbidden contract `interchange-is-a-federation-leaf` (forbidden_modules: vmanager/vm_vmanager_app/plasmamapper/inthewords; include_external_packages=True line | [verify-upheld] Pure import/export library, no UI/core split; the binding outward federation-leaf direction IS contract-enforced and passes locally. Sibling-under-mgf.* forbid documented as review-enforced (grimp rejects PEP-420 s |
| DP-02 | ⏳ | importer._DomainDefiner / exporter._DomainReader are typing.Protocol seams (importer.py:52, exporter.py:47) and convert_fn is an injected Callable; tests drive them via tests/support/mo | Not in the refute-✅ scope (not asserted ✅). Adjudicated fresh: DP-02 requires a provider ABC with >=1 production + 1 mock impl and a method-set lock-in test. The library uses structural Protocols + a test-only mock with no product |
| DP-03 | ⏳ | ovf.py OVFDisk/OVFNIC/OVFDescriptor are plain `@dataclass` (lines 71,93,109) — NOT frozen; importer.ImportResult (69) and exporter.ExportResult (62) likewise plain. exporter.py mutates  | Not in refute-✅ scope (correctly NOT asserted ✅). DP-03 requires frozen domain types crossing module boundaries; these descriptor types cross importer<->ovf<->exporter and are deliberately mutated, so frozen is not adopted. Genuin |
| DP-04 | ✅ | Ran `mypy --strict src/` in the standard venv (mgf-common + mgf-vm-core installed) → 'Success: no issues found in 5 source files'. pyproject.toml [tool.mypy] strict=true (line 78).  | [verify-upheld] mypy --strict passes cleanly on all 5 src files; no unjustified Any. DP-04 fully met. |
| DP-05 | ✅ | tests mirror src: tests/unit/mgf/vm/interchange/{test_exporter,test_importer,test_ovf}.py for src exporter/importer/ovf. Ran `PYTHONPATH=src pytest tests/unit/mgf/vm/interchange/` → 33  | [verify-upheld] Tests mirror src for the public modules, run with no real daemon/credentials/network, drive seams via in-memory mock + stub converter. _xml.py has no dedicated test file but its DTD-ban/size-cap/billion-laughs beha |
| DP-06 | ✅ | Security first-class: _xml.py routes ALL parsing through defusedxml (forbid_dtd/forbid_entities/forbid_external) + 8 MiB size cap; importer.py:186-190 calls safe_destination + reject_un | [verify-upheld] Security/reliability/error-reporting demonstrably first-class with concrete hardening + passing attack tests for an untrusted-input parsing library. The per-PR-description clause is process (not code-checkable); th |
| DP-07 | ✅ | Module docstrings on all 4 src files; OVFDisk/OVFNIC/OVFDescriptor carry field-level intent docstrings (ovf.py:73-91,95-106,111-116); import_ova/export_ova full Args/Raises docstrings ( | [verify-upheld] Public surface is substantively documented with intent+invariants+why-comments. DP-07 met. |
| DP-08 | ✅ | DEPS.md:28-29: 'is caller-injected (`convert_fn`, REQUIRED since 0.2.0) — the interchange no longer bundles a qemu-img wrapper.' The provider seam swap is documented in _DomainDefiner/_ | [verify-upheld] DP-08 is a SHOULD: for each external surface the swap recipe is described near the abstraction — converter (DEPS.md + convert_fn injection point), backend (Protocol docstrings), XML (defusedxml seam). Adequate for  |
| DP-09 | N/A | import_ova/export_ova accept an optional reporter: ProgressReporter\|None (importer.py:91, exporter.py:84) but no CancellationToken. The operations are synchronous tar-extract + injecte | DP-09 binds long-running ops with an internal cancellable loop. This library delegates the heavy work (disk conversion) to the caller-injected convert_fn, which is where cancellation belongs; the library itself runs no cancellable |
| DP-10 | ✅ | No fallback-backend-selection exists in src/. convert_fn and provider are REQUIRED injected deps (convert_fn keyword-only with no default: importer.py:92, exporter.py:85). XML parsing h | [verify-upheld] DP-10 binds only where a fallback path exists. No multi-backend fallback selection here — deps are explicitly injected and failures raise typed errors; the one place degradation could hide (XML safety) raises loudl |
| DP-11 | N/A | No asyncio usage anywhere in src/ — imports are `contextlib`, `tarfile`, `dataclasses`, `typing`, `re`, `xml.etree`. No asyncio.create_task / TaskGroup / async def. | DP-11 governs asyncio task ownership. This is a synchronous library with no async code, so the rule has no surface to bind. N/A. |
| DP-12 | N/A | No datetime handling in src/ — the interchange parses/serializes OVF topology (CPU/memory/disks/NICs) and tar archives; no timestamps cross any module boundary. (mock_provider.py uses d | DP-12 binds time-at-boundary. The library moves no datetimes across module boundaries, so there is nothing to make UTC. N/A. |
| DP-13 | ✅ | Tar handles via context managers: importer.py:156 `with tarfile.open(source, mode='r:*') as tar:`, exporter.py:212 `with tarfile.open(destination, mode='w') as tar:`. Member streams clo | [verify-upheld] Every owned external resource (tar handle, per-member stream, output file, staging/temp files) is acquired inside `with` or paired with try/finally; no reliance on GC. DP-13 fully met. |

### ERROR_HANDLING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| EH-01 | ✅ | grep over src/ confirms every raise in importer.py/exporter.py/ovf.py is ImportExportError (importer.py:150,159,168,176,190,196,212,222,224,357,378; exporter.py:137,150,155,166,185,217, | [verify-upheld] Application code uniformly raises the typed AppError subclass ImportExportError; tarfile/defusedxml/OS errors are all translated at the owning boundary. _xml.py's ET.ParseError is a private internal-protocol type n |
| EH-02 | ✅ | All three third-party seams translate at the call's owning boundary: tarfile.open/extract → importer.py:221-224 catches tarfile.TarError + OSError → ImportExportError; defusedxml Defuse | [verify-upheld] Each third-party/SDK boundary is wrapped and translated to the typed hierarchy. defusedxml is double-wrapped (→ ET.ParseError → ImportExportError) but the user-visible boundary still yields ImportExportError. |
| EH-03 | ✅ | grep 'raise .* from ' confirms cause chaining at importer.py:190,222,224; exporter.py:152,187,262; ovf.py:149,155,157; _xml.py:59,86. The provider-failure path at importer.py:230-236 re | [verify-upheld] All translating re-raises chain via 'from e'; the one bare 'raise' at importer.py:236 preserves the original. No 'raise NewError(str(e))' without 'from' found at any translation point. |
| EH-04 | N/A | The package is a pure import/export LIBRARY — exporter/importer/ovf/_xml only. No CLI, no __main__, no process entry point, no thread spawn, no asyncio loop, no Qt. import_ova/export_ov | EH-04 binds entry points (CLI _run_and_translate_exceptions / sys.excepthook / threading.excepthook / asyncio handler). A library that ships no entry point has nothing to install; the host application owns these. |
| EH-05 | N/A | No crash reporter, no excepthook, no entry point in the library. The package raises typed ImportExportError up to its caller and never sits at a process boundary. | EH-05 (local crash report + remote sink for unhandled exceptions) is an entry-point obligation. The consuming application installs the crash reporter; the library correctly surfaces typed errors instead of writing reports itself. |
| EH-06 | N/A | No GUI / QThread / worker code anywhere in src/. The shape is a headless import/export library. | EH-06 governs GUI CoreWorker.run() error routing. With no GUI workers in scope this rule has no surface area here. |
| EH-07 | ✅ | grep -E 'except (Exception\|BaseException)\|except\s*:' over src/ returns NONE — no broad/bare excepts at all. Cleanup uses the sanctioned contextlib.suppress(OSError): importer.py:219  | [verify-upheld] No untyped 'except Exception' exists. The only broad-ish swallows are contextlib.suppress(OSError) in cleanup paths — exactly the EH-07(a) carve-out — plus a narrow suppress(ValueError) for a numeric parse fallback |
| EH-08 | ⏳ | ImportExportError (mgf-vm-core/src/mgf/vm/core/exceptions.py:180) is a plain 'class ImportExportError(OperationError): """..."""' with no __init__ and no machine-readable attributes. Ev | EH-08 requires every typed exception to carry machine-readable fields as constructor args (e.g. CommandError.argv), not just a message string. The interchange uses the bare-message-only ImportExportError throughout — the structure |
| EH-09 | ⏳ | A few messages embed an actionable next step (importer.py:197-199 'pick a different --target-dir or remove the file'; exporter.py:138 'refusing to overwrite'), but most user-surfacing e | EH-09 requires user-shown messages to end in an actionable next step 'where possible', and §9/§12.7 make a substring-pinning test a MUST for each hint. Applied inconsistently and with zero pinning. The library does surface these s |
| EH-10 | N/A | No best-effort observer fan-out exists in src/: no hook executors, scheduler tick loops, event-callback dispatchers, or 'fire_one/fire_all'-shaped loops. The cleanup loops (importer.py: | EH-10's never-raises observer invariant binds fan-out points (hook/scheduler/alert/event callbacks). A linear import/export pipeline with no observer registry has no such surface. |
| EH-11 | N/A | grep -E 'retry\|backoff\|jitter\|circuit\|breaker\|time.sleep' over src/ returns NONE. The pipeline performs each step (tar read, XML parse, disk convert via injected convert_fn, provid | EH-11 governs the shape of retries when retries exist. This library has no retry path; there is nothing to require exponential-backoff-with-jitter of. |
| EH-12 | N/A | The library performs no at-least-once / multi-attempt external writes of its own — there is no retry and no queue redelivery. export_ova explicitly refuses to overwrite an existing dest | EH-12 binds operations subject to multi-attempt cost (retried/at-least-once writes). With no retry path and explicit refuse-on-collision guards, there is no multi-attempt write whose idempotency this library must establish. |
| EH-13 | N/A | No external call in src/ is issued in a loop or under retry that could amplify load on a struggling upstream — grep shows no retry/breaker. The single calls to convert_fn and provider.d | EH-13 binds calls that, when failing, can worsen an already-struggling upstream via amplification (unbounded retry). With single-shot, fail-fast calls and no retry, there is no amplification surface within this library to bound. |

### LOGGING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| LG-01 | ⏳ | grep across src/ finds ZERO `import logging`, ZERO `logging.getLogger`, ZERO `LOG`/`logger` bindings (importer.py, exporter.py, ovf.py, _xml.py). No module acquires a hierarchical logge | LG-01 requires every module to acquire `logging.getLogger(__name__)`. No bare `logging.info(...)` is present, so the MUST-NOT half is honoured — but the modules emit no log records at all and acquire no logger. Smallest fix: add ` |
| LG-02 | N/A | pyproject.toml declares no `[project.scripts]`/console entry point; __init__.py exports only `export_ova`/`import_ova`/OVF types. This is a pure import/export LIBRARY with no CLI/GUI/se | LG-02 binds 'exactly one place per entry point ... MUST emit JSON when stdout is not a TTY' to the application's entry point (CLI main, GUI app.run, service entry). A library has no entry point — log configuration is the consuming |
| LG-03 | ⏳ | No log records are emitted, so no record can carry trace_id/span_id. The standard's mechanism (JsonFormatter._add_otel_context) lives in mgf.common.observability and is consumer-configu | LG-03 says every log record MUST carry trace_id/span_id WHEN A SPAN IS ACTIVE. The formatter that adds them is consumer-side and out of this repo's control, so the formatter half is N/A — but the obligation only matters once recor |
| LG-04 | N/A | Redaction (Redactor) is a formatter-side concern owned by mgf.common.observability and applied by the consumer's configured handler. This library emits no records and installs no format | LG-04 requires the FORMATTER to redact before any sink sees a record; it binds at log-configuration time, which is the consuming application's responsibility for a library. No formatter is owned here. Premise does not bind. (If LG |
| LG-05 | ⏳ | Error paths raise typed `ImportExportError` and never log: importer.py:189-190 (`except TarMemberUnsafeError ... raise ImportExportError(str(e)) from e`), :211-214 (`except VManagerErro | LG-05 governs ERROR-LEVEL LOGS: they MUST use exc_info, never just the stringified exception. The library deliberately re-raises typed errors instead of logging them (a valid translation-seam pattern — the consumer logs at the top |
| LG-06 | N/A | No setup_logging / handler / sink is configured anywhere in src/. Per the matrix, the 'library' kind's LG-06 rotating-file branch 'typically applies' but only at the entry point. No ent | LG-06 requires logs to reach a searchable aggregator (rotating file / stdout / remote sink). Sink selection happens at application configuration time; a library that emits records lets the consuming app's configured handlers carry |
| LG-07 | N/A | Matrix: LG-07 'Applies to: gui-app'. pyproject keywords are ova/ovf/vm/import/export; no PySide6/Qt dependency; no gui/ package. This is a headless import/export library. | LG-07 (log-viewer dialog) is explicitly gui-app-scoped. Not a GUI application. Premise does not bind. |
| LG-08 | N/A | No log call sites exist (zero LOG.* in src). The f-strings present are in raise messages (e.g. importer.py:150, exporter.py:137) and in domain/OVF XML template construction (build_domai | LG-08 governs lazy %-formatting in LOG MESSAGES so formatting is skipped when the level is filtered. With no logging call sites, the rule has nothing to bind to. The reporter.report() progress strings are a separate UX surface, no |
| LG-09 | ⏳ | No log records, hence no `extra={...}` usage. The data that LG-09 would promote (source path, target_dir, vm_name, vm_uuid, destination, disk counts) is currently only attached to OTel  | LG-09 requires structured fields to be passed via `extra={}` (promoted to JSON keys), not concatenated. The library names the right fields on its spans but emits no log records carrying them. Because LG-11 requires INFO start/end  |
| LG-10 | N/A | Matrix: LG-10 'Applies to: service'. No systemd unit, no daemon/timer entry point, no scheduler service; pyproject declares a library with no scripts. No JournalHandler reference anywhe | LG-10 (headless-service journald sink at entry point) is service-kind-scoped. This is a library invoked in-process by a consumer, not a long-running headless service with its own entry point. Premise does not bind. |
| LG-11 | ⏳ | import_ova/export_ova cross storage AND process boundaries (tarfile.open + streamed extraction at importer.py:156-192; injected disk-format conversion `convert(...)` at importer.py:205  | This is the central LOGGING gap. LG-11 §4.3 is explicit that operation start/end MUST be logged at INFO with a stable `operation` field SPECIFICALLY BECAUSE an OTel backend is not always available — 'Logs MUST stand on their own.' |
| LG-12 | N/A | No log emission sites exist (zero LOG.* in src), so there is no per-record cost to measure or violate. No heavy formatting/IO at any call site because there are no call sites. | LG-12 (≤100 µs per record; heavy work in the handler not the call site) is a property of log call sites. With none present, the rule has nothing to bind to. Premise does not bind to current code; when LG-11 records are added the l |
| LG-13 | N/A | No log sites at all, and no inner-loop/per-event/per-tick log emission. The per-disk loops (importer.py:173, exporter.py:164) and the 1 MiB streaming copy loop (importer.py:381-385) emi | LG-13 (rate-limit/sample high-frequency log sites) binds to log call sites in hot paths. None exist. The disk loop is bounded by disk count (small), and the byte-copy loop emits nothing. Premise does not bind. If LG-11 records are |
| LG-14 | N/A | No contextvars usage and no log records to carry correlation fields. Correlation today is via the OTel span context (operation_span), which the standard's formatter promotes automatical | LG-14 requires CUSTOM correlation context to propagate via contextvars.ContextVar and be promoted by the formatter. This library introduces no custom correlation context (it's a synchronous in-process library; OTel trace context a |
| LG-15 | ⏳ | No `event` field anywhere (grep finds none) and no docs/reference/EVENTS.md (find returns nothing under docs/). The notable lifecycle/security/boundary events this library reaches — suc | LG-15 requires every INFO+ record at a notable lifecycle/security/boundary event to carry a stable `event` field documented in EVENTS.md. The unsafe-tar-member rejection is a textbook SC-17-style security-boundary event (the promp |
| LG-16 | ⏳ | There are no WARNING/ERROR log records at all (zero LOG.warning/LOG.error/LOG.exception in src). Failure information is carried only inside raised ImportExportError messages (e.g. impor | LG-16 requires every WARNING/ERROR RECORD to answer WHAT/WHY/HOW with structured `extra={}` context. The library logs nothing, so at the security/boundary failure points the §4.3 LG-11 failure record and the LG-15 tar-rejection wa |
| LG-17 | ⏳ | No ERROR records and no `fingerprint` field anywhere (grep finds none). The fingerprint mechanism (formatter computes from exc_info) is consumer-side, but the prerequisite ERROR records | LG-17 is a SHOULD: ERROR records should carry a stable `fingerprint` so grouping aggregators cluster identical failures; the default is auto-computed from the exception type when exc_info is set. The library currently emits no ERR |

### CONFIGURATION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| CF-01 | N/A | src/mgf/vm/interchange/{__init__,exporter,importer,ovf,_xml}.py — grep for os.environ/getenv/BaseSettings/BaseAppSettings/pydantic_settings/Settings( returned ZERO hits; __init__.py exp | CF-01 (Applies to: consumer-owned-config) binds consumers that own an application config schema. This is a pure OVA/OVF import/export library with no application configuration, no env-var reads, and no module that reads a config f |
| CF-02 | N/A | No BaseSettings/settings_customise_sources/load_settings anywhere in src/; export_ova(...)/import_ova(...) take all paths as explicit caller args (exporter.py:78, importer.py:85). No CL | CF-02 (precedence: CLI > env > .env > file > defaults) marked 'all', but there is no layered config source set whatsoever — the library has no config object. With no precedence chain to define, the premise does not bind. |
| CF-03 | N/A | No Settings/BaseAppSettings subclass and no config schema exists (grep clean). The only 'version' concept is the OVF descriptor's own ovf:version domain field in ovf.py, not an applicat | CF-03 (consumer-owned-config) requires a config schema with a version field + migration chain. No config schema is owned by this library, so there is nothing to version/migrate. |
| CF-04 | N/A | No save_settings / config-file writer in src/; grep for expanduser/expandvars/save_settings returned ZERO hits. The library writes OVA archives/qcow2 disks to caller-supplied destinatio | CF-04 (consumer-owned-config) governs atomic 0o600 writes + lazy path-expansion of the on-disk CONFIG file. There is no config/state file the library owns. (Tar/temp-dir atomicity on export/import is a DATA-PROTECTION/SECURITY con |
| CF-05 | N/A | grep -i 'secret\|password\|token\|vault\|credential\|keyring' over src/ returned 'none'. The library handles VM images (OVA/OVF descriptors + disks), not credentials, and ships no confi | CF-05 (secrets MUST NOT live in config file; use a Vault) is marked 'all' but its premise is a config file that could hold secrets. This library has neither a config file nor any secret material. Premise does not bind. |
| CF-06 | N/A | No pydantic Settings model with extra='forbid' for application config (grep clean for BaseSettings/model_config in src/). OVF parsing in ovf.py validates the descriptor domain shape, no | CF-06 (consumer-owned-config: unknown-key rejection on the config file) requires an owned config schema loaded from a file. None exists. Premise does not bind. |
| CF-07 | N/A | No main Settings object and no satellite/sub-config files (hooks/schedules/profiles) exist in the repo; the package is exporter/importer/ovf/_xml only. | CF-07 (consumer-owned-config: sub-config files follow the same typed+atomic+versioned shape) presupposes an owned config system with satellite files. There is no config system here at all. |
| CF-08 | N/A | No Settings object exists to load once or to make a module-global singleton (grep clean). Library functions receive providers/paths by explicit injection: export_ova(provider, handle, d | CF-08 (consumer-owned-config: load Settings once at the entry point, inject by constructor) binds entry-point-owning consumers with a Settings object. This is a library with no entry point and no Settings object. Premise does not  |
| CF-09 | N/A | No CLI: grep for click/argparse/__main__ in src/ found only a code-comment mention of 'CLI progress output' in importer.py:77, no actual command surface. No config-path default resoluti | CF-09 (consumer-owned-config: a --config CLI flag with XDG/OS-default path) requires a CLI/command surface. This library has none — callers pass paths directly. Premise does not bind. |
| CF-10 | N/A | No Settings class to construct in tests; tests/unit/mgf/vm/interchange/{test_exporter,test_importer,test_ovf}.py exercise the OVA/OVF round-trip with explicit fixture paths and a mock p | CF-10 (marked 'all': construct Settings with explicit values, no env leakage) presupposes a Settings object under test. There is none. The tests already use explicit values + injected mocks, but the CF-10 rule about Settings(**ove |
| CF-11 | N/A | No config schema fields exist to rename via Field(alias=...)/validation_alias (grep clean for any pydantic Settings/Field alias usage in src/). | CF-11 (consumer-owned-config: config-field rename via alias + deprecation warning) requires owned config fields on a Settings model. None exist. Premise does not bind. |
| CF-12 | N/A | No config field with mutually-exclusive backend variants exists. The XML/tar handling has no 'kind'-discriminated config union (grep clean for discriminator/Annotated-union in config co | CF-12 (marked 'all': discriminated-union for backend variants) binds only where a CONFIG field selects among mutually-exclusive backends. This library exposes no such config field. Premise does not bind. |

### SECURITY_STANDARD

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| SC-01 | N/A | grep across src/ for secret/password/token/key/crypto/fernet/hashlib returns nothing. The library parses OVA/OVF and builds libvirt domain XML; it handles no credentials, vault, or secr | Secrets-in-vault rule is vacuously satisfied — this library never holds, reads, or persists a secret. No vault boundary exists in scope. |
| SC-02 | N/A | grep for subprocess/os.system/popen/shell=True in src/ returns nothing. Disk-format conversion is performed by a caller-injected convert_fn (export_ova/import_ova require convert_fn; CH | No subprocess invocation in the library — the argv/shell=False rule has no applicable call site. Subprocess security is the injecting caller's concern. |
| SC-03 | ✅ | [verify-upheld] grep for yaml.load/pickle/yaml in src/ returns nothing — no YAML, no pickle anywhere. ALL XML parsing routes through src/mgf/vm/interchange/_xml.py which imports `from d | Refute attempt: searched for any stdlib parse of untrusted input and for yaml.load/pickle — none found. The XML-deserialization clause (defusedxml, never stdlib xml.etree to parse) is MET; yaml.safe_load/pickle clauses are vacuous |
| SC-04 | N/A | File writes in src/ are: importer.py:380 destination.open('wb') (extracted raw disk image into a caller-supplied target_dir), exporter.py:208 ovf_path.write_text (the OVF descriptor ins | The 0o600-atomic rule targets files that may contain private data. The artifacts written here are public VM-image deliverables (OVA/OVF/disk), explicitly the SC-table 'public artefacts 0o644' class. No private-data file write exis |
| SC-05 | N/A | grep for crypto/fernet/hashlib/md5/sha1/random. in src/ returns nothing. The library performs no encryption, hashing, MAC, or token/nonce/key generation. | No cryptographic primitive is used anywhere — the vetted-crypto rule has no applicable surface. |
| SC-06 | N/A | grep for verify=False/CERT_NONE/AutoAddPolicy in src/ returns nothing; no httpx/requests/paramiko/ssl import. The library makes no outbound network connection — it reads/writes local ta | No outbound TLS connection exists — the verify-on-every-connection rule has no applicable call site. |
| SC-07 | N/A | No HTTP/SSH/DB/subprocess call in src/ (greps above all empty). External work (disk conversion, domain define) is delegated to caller-injected callables; the library performs no blockin | The finite-timeout rule binds external requests the library issues — there are none. Timeout discipline for convert_fn / the provider is the injecting caller's responsibility. |
| SC-08 | ✅ | [verify-upheld] Untrusted input is bounded at the parse boundary: _xml.py caps payloads at DEFAULT_MAX_XML_BYTES=8 MiB BEFORE parsing (string path :45-50, file path :72-77, raising ET.P | Refute attempt: looked for an unbounded path. The XML size-cap + entity ban + tar-member containment all sit AT the boundary (the parser), not in use-case code, and are pinned by passing negative tests. Caveat (not refuting): ther |
| SC-09 | N/A | No LOG.* calls in src/ (grep shows only operation_span imports/usage at importer.py:119 and exporter.py:107, which are OTel trace spans carrying source/target_dir/vm_name/destination —  | The formatter-redaction rule binds log records that may carry sensitive fields. This library emits no log records with user secrets (it has none), and relies on mgf-common's formatter for the span attributes it does emit. No appli |
| SC-10 | ✅ | uv.lock IS committed (178 KB, present at repo root) — the lockfile half is satisfied. BUT there is NO .woodpecker.yml or .woodpecker/ directory (confirmed: ls returns 'No such file or d | SC-10 requires BOTH a committed lockfile AND a CI vuln scanner on every PR. The lockfile is present but the CI-scan gate is absent (no CI at all). Unmet — gap, not a deliberate decline. — FIXED 2026-06-09: pip-audit vuln-scan in CI |
| SC-11 | ✅ | [verify-upheld] Exception messages carry only structural metadata — paths, OVF field names, disk hrefs, tar member names (importer.py:150/159/176-177/196-199/221-224; ovf.py:148-160/198 | Refute attempt: traced every raise/from for an embedded credential. None exists because the library handles no secrets; the translation seams surface only file/field/member names. Upheld. |
| SC-12 | ⏳ | A SECURITY.md EXISTS at the repo root with a reporting email (bassam@magogi.org), acknowledgement (5 business days) and fix (30 days) timelines. BUT its Scope section is mis-pasted from | SC-12 requires a SECURITY.md that accurately describes the project's security posture. The file exists with correct reporting mechanics, but its scope/threat-model is materially wrong for THIS library (a copy-paste from a no-I/O d |
| SC-13 | N/A | grep for KEY=/crypto/fernet in src/ returns nothing; no cryptographic key is generated, derived, embedded, or stored anywhere in the library. | The at-rest-key-storage rule has no applicable surface — there are no keys. |
| SC-14 | N/A | No authentication surface in src/ — no login, password check, key verification, or credential comparison. The library imports OVAs and talks to a caller-injected provider. | The no-enumeration / backoff / lockout rule binds authentication boundaries; this library has none. |
| SC-15 | ✅ | [verify-upheld] mgf.vm.core.common.safe_tar.safe_destination (safe_tar.py:79-98) does base_resolved=base.resolve(); candidate=(base/member_name).resolve(); candidate.relative_to(base_re | Refute attempt: checked the resolve-then-relative_to ordering, the absolute-path-absorption case, and whether validation precedes extraction. All correct, both ../ and absolute escapes pinned by passing tests, plus non-regular-mem |
| SC-16 | N/A | grep for sudo/setuid/subprocess/privileged in src/ returns nothing. The library performs no privileged operation — privileged hardware-touching work lives in adapter libs (mgf-vm-libvir | The minimum-privilege / audited-surface rule binds privilege-escalation call sites; there are none in this library. |
| SC-17 | N/A | The library uses mgf.common.observability.operation_span at importer.py:119 ('ova.import') and exporter.py:107 ('ova.export') for OTel tracing, but emits no audit=true log record. The S | None of SC-17's enumerated security-relevant actions occur here (no vault, no subprocess, no SSH, no config write, no key rotation, no auth). OVA extraction is security-sensitive but is not one of the standard's enumerated audit a |
| SC-18 | ✅ | build-backend is hatchling>=1.21 (pyproject.toml:52-53) and the package is tagged (v0.2.0, v0.2.1 git tags exist), so it is in principle buildable from a tagged commit using declared de | SC-18 requires the artefact be reproducible from a tagged commit AND (for the standard's reproducibility discipline) the CI build + checksum/SBOM + recorded build metadata. The tag + hatchling + lockfile give the buildable-from-so — FIXED 2026-06-09: .woodpecker sbom step (reproducible CycloneDX) |

### TESTING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| TS-01 | ⏳ | src/ has 5 modules (__init__, _xml, exporter, importer, ovf). tests/unit/mgf/vm/interchange/ has test_exporter/test_importer/test_ovf — NO test_xml.py mirroring _xml.py. There is no doc | _xml.py (the security-critical defusedxml seam) has no 1:1 mirror test file and no allow-list entry; the mirror rule is unmet and CI does not lint it. Its branches are exercised only indirectly through ovf/exporter, and the size-c |
| TS-02 | ✅ | [verify-upheld] tests use tests/support/mock_provider.py MockProvider (deterministic, in-memory, no libvirt) + injected _fake_convert_factory (test_importer.py:72, test_exporter.py:46). | Refute attempt failed: the suite is genuinely offline; external I/O sits behind the MockProvider duck-type + injected convert_fn seam. Upheld. |
| TS-03 | ✅ | [verify-upheld] Every test takes tmp_path for filesystem work; MockProvider constructed fresh per test; no module-level mutable state, no os.chdir, no HOME/XDG touches in src/ or tests. | Refute attempt failed: tests are isolated via tmp_path with no process-global mutation. Upheld. |
| TS-04 | ✅ | [verify-upheld] pyproject.toml:95 `filterwarnings = ["error"]`. Suite ran under it with no warning-escalation failures in the 37 behavior tests. | Refute attempt failed: warnings escalate to failures as required. Upheld. |
| TS-05 | ✅ | [verify-upheld] pyproject.toml:94 `addopts = "--strict-markers --strict-config"`. Confirmed both flags active (suite ran; the only registered marker `fixtures` is declared at :96-98). | Refute attempt failed: both strict flags are set. Upheld. |
| TS-06 | ✅ | [verify-upheld] pyproject.toml:100-101 `[tool.coverage.report] fail_under = 80`. Measured this audit: TOTAL 86.90% (>=80). Security + error paths exercised (traversal, absolute-path, bi | Refute attempt: the gate itself is configured at 80 and met at 86.9%, so the gate verdict holds. The unhit _xml.py size-cap/parse_xml_file branches are real behavioral-adequacy gaps captured under TS-01/TS-10/TS-13, and branch=tru |
| TS-07 | ⏳ | The interchange raises ImportExportError (and lets VManagerError propagate) from mgf.vm.core.exceptions — it does not DEFINE its own typed exceptions, but it does USE them with formatte | Message-format assertions exist, but TS-07's (b) field-value-preserved and (c) field-value-copied legs are not demonstrated for the exceptions this library raises. The library defines no typed exception of its own; if the carve-ou |
| TS-08 | N/A | grep over src/ finds no EH-02 translation seam: no `except Exception`/vendor-error catch-and-translate. The convert_fn is caller-injected and the library catches only its own VManagerEr | No EH-02 vendor-error translation seam exists in this library, so TS-08's success/per-known-error/unknown-vendor matrix has nothing to bind. N/A. |
| TS-09 | N/A | grep finds no EH-10 best-effort-observer pattern (no observer loop, no subscriber list iterated with raising-observer tolerance) in src/. | No best-effort observer exists in the interchange library, so the raising-observer test has nothing to pin. N/A. |
| TS-10 | ⏳ | SC-15 tar-traversal IS pinned: test_parent_traversal_in_file_href_rejected + test_absolute_file_href_rejected (test_importer.py:289-330) assert rejection AND that no file landed outside | Tar-slip + XXE/entity-bomb pins are strong and survive refutation. But the SC-08 input-size bound — the documented defence against quadratic-blowup that survives defusedxml's entity ban — has zero test, and there is no anti-regres |
| TS-11 | N/A | No GUI: this is a headless import/export library (exporter/importer/ovf/_xml only). No PySide6/QThread/worker in src/ or deps. | No GUI workers exist, so TS-11 has nothing to bind. N/A. |
| TS-12 | N/A | grep over src/ for `async def`/`asyncio`/`await` returns NONE. The pipeline is fully synchronous (tarfile + ET + injected convert_fn). | No async/coroutine code exists, so pytest-asyncio/cancellation tests have nothing to bind. N/A. |
| TS-13 | ⏳ | ovf.py is a textbook round-trip surface: serialize_ovf↔parse_ovf, plus _memory_quantity_to_mb/_parse_allocation_quantity numeric round-trips. tests/unit/.../test_ovf.py has hand-picked  | TS-13 MUST: property tests covering round-trip parse/serialise, with strategies in a _strategies.py. The library round-trips OVF descriptors but has only example-based round-trips and zero Hypothesis property tests/strategies. Gap |
| TS-14 | ⏳ | No @pytest.mark.perf test anywhere; the `perf` marker is not registered (only `fixtures` is, pyproject:96-98); no PERFORMANCE budget or benchmark for the convert/parse/pack path. addopt | No performance budget is pinned by a benchmark. For an OVA pack/parse path a budget is plausibly applicable but none is declared; treated as an unmet (deferrable) gap rather than N/A since no decline rationale is recorded. |
| TS-15 | ⏳ | No smoke-against-real-backend tests, no `smoke`/`live`/`realbackend` marker registered, no tests/smoke or marker-gated real-backend job (no CI at all — no .woodpecker.yml). | A real-backend smoke layer (read-only, marker-gated) is not present. Deferrable for a pure parse/pack library, but no decline rationale is recorded → gap. |
| TS-16 | ✅ | [verify-upheld] SH-01 pin asserts with a message (test_importer.py:309 'SH-01 regressed: a traversal OVF wrote outside target_dir'); pytest.raises uses explicit match= predicates (match | Refute attempt failed — and is in fact reinforced: the failures I triggered this run produced self-explanatory output. match= predicates name the expected substring; the security pin carries an explicit regression message. Upheld. |
| TS-17 | ✅ | [verify-upheld] tests/conftest.py is the single repo-wide conftest (docstring + `from __future__` only — no fixtures). MockProvider is imported as `from tests.support.mock_provider impo | Refute attempt failed: no cross-package fixture import; the conftest hierarchy is respected and MockProvider is a class import, not a fixture. Upheld. |
| TS-18 | ✅ | [verify-upheld] Full suite (39 tests) ran in 0.22s; no individual test approaches 1s, so none requires @pytest.mark.slow and correctly none is applied. The defensive registration of `sl | Refute attempt failed on the MUST: no test exceeds 1s, so the marking requirement is vacuously satisfied. The unregistered `slow` defensive marker is a TS-19 SHOULD, not a TS-18 violation. Upheld. |
| TS-19 | ⏳ | pyproject.toml:96-98 registers ONLY `markers = ["fixtures: ..."]`. The canonical layer markers (smoke/e2e/integration) and orthogonal markers (perf, slow) are NOT registered. With --str | Required marker registration is incomplete: none of the canonical smoke/e2e/integration/perf/slow markers is declared. Gap stands. |
| TS-20 | ⏳ | tests/ has only unit/ (tests/unit/mgf/vm/interchange/...) plus a top-level test_public_api_pin.py and tests/support/. There are NO tests/integration/, tests/e2e/, or tests/smoke/ direct | Tests are not partitioned across the four canonical directories; only the unit layer exists. The export→import round-trip would be a natural integration/e2e layer but none is present. Gap stands. |
| TS-21 | ⏳ | mgf-vm-interchange is a published wheel (pyproject name=mgf-vm-interchange, hatchling wheel target). There is NO tests/smoke/ layer verifying (a) package imports, (b) every __all__ name | A published library MUST have a smoke layer; it is absent. Gap stands. |
| TS-22 | ⏳ | No tier markers, no --tier/--budget runner, no mgf-test-supervisor wiring (grep finds no test_supervisor/TestRecord/run.jsonl refs outside .venv). No CI to invoke tiered runs. | Tiered runs are not supported. Deferrable supervisor-infra rule, but unmet and not declared N/A → gap. |
| TS-23 | ⏳ | No .woodpecker.yml / CI at all, so there is no crash/hang/signal-capturing run harness; tests are invoked by plain pytest. No mgf-test-supervisor subprocess supervisor wired. | No CI run harness exists that survives+structures a pytest crash/hang/signal-death. Gap stands (and is a prerequisite of the whole WORKFLOWS family being open here). |
| TS-24 | ⏳ | No .gates/last-run/run.jsonl emission; no TestRecord usage (grep: none outside .venv). No structured per-test JSONL. | Structured per-test records are not produced. Gap stands. |
| TS-25 | ⏳ | No .gates/quarantine.json, no --quarantine-attempt/--no-skip-quarantined support, no supervisor. (L2 rule; L1 MAY defer — but no L-level decline is recorded.) | Flaky-test auto-quarantine is not implemented. L2 rule deferrable at L1, but unmet and not formally declined → gap. |
| TS-26 | N/A | The library ships no compiled artifact — pure Python (exporter/importer/ovf/_xml). Deps are mgf-common, mgf-vm-core, defusedxml — none a Zig/Go/Rust/C/Swift binary requiring a freshness | No compiled binary dependency, so the freshness-check rule has nothing to bind. TS-26 explicitly allows pure-Python libraries to declare N/A. N/A. |
| TS-27 | ⏳ | No .gates/last-run/failures/<id>.txt bundles, no supervisor _write_failure_bundle, no CI to produce them. | Single-file explorable failure bundles are not produced. Gap stands. |
| TS-28 | ⏳ | No --perf/MGF_PERF capture, no perf_span usage in src/ or tests, no TestRecord.perf, no perf-summary.md. (L2 rule; L1 MAY defer — no decline recorded.) | Per-test perf attribution is not implemented. L2 rule deferrable at L1, but unmet and not formally declined → gap. |

### API_DESIGN

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| AP-01 | ✅ | src/mgf/vm/interchange/__init__.py:20-28 (7 names), exporter.py:348 ['ExportResult','export_ova'], importer.py:406 ['ImportResult','build_domain_xml','import_ova'], ovf.py:628-642 (13 n | [verify-upheld] Confirmed by reading all five modules: each ends with an explicit string-list __all__ (no list(globals()) shenanigans). The MUST (AP-01) is met. ovf.py's list is grouped rather than strictly alphabetical (e.g. OVFN |
| AP-02 | ⏳ | Every public name has a docstring (ovf.py:73 OVFDisk, :95 OVFNIC, :111 OVFDescriptor, :135 parse_ovf, :236 serialize_ovf; importer.py:71 ImportResult, :94 import_ova, :255 build_domain_ | AP-02 MUST: every public name MUST have a docstring stating intent, invariants, AND at least one usage example. The docstrings are good on intent/invariants but uniformly lack the required usage example. Smallest fix: add a short  |
| AP-03 | ✅ | pyproject.toml:3 version='0.2.1' (MAJOR.MINOR.PATCH). CHANGELOG.md:17-25 lands the convert_fn-required break in MINOR (0.1.x→0.2.0) under heading '### Changed (BREAKING)'. 0.2.1 (CHANGE | [verify-upheld] SemVer is followed and the 0.x clause (API_DESIGN §4.3) is correctly used to land the break in a MINOR with an explicit BREAKING label. The 0.2.1 walk is non-breaking. MUST met. Upheld. |
| AP-04 | N/A | No public name is deprecated through 0.2.1. CHANGELOG.md has no [Deprecated] entries; no warnings.warn(DeprecationWarning)/@deprecated anywhere in src/mgf/vm/interchange. The only break | AP-04 governs the announce->survive->remove deprecation cycle. The premise (a deprecated public name) does not exist in this library, so the rule does not bind. |
| AP-05 | ✅ | export_ova (exporter.py:78-86): provider/handle/destination positional, then '*' then staging_dir/reporter/convert_fn keyword-only. import_ova (importer.py:85-92): source/provider posit | [verify-upheld] Tried hard to refute on the convert_fn promotion: a new/changed required parameter is normally breaking per §4.2 ('Add a required parameter ... Yes'). But it is (a) keyword-only behind the '*' separator, (b) treate |
| AP-06 | ✅ | export_ova -> ExportResult (exporter.py:86), import_ova -> ImportResult (importer.py:93), parse_ovf -> OVFDescriptor (ovf.py:134), serialize_ovf -> str (ovf.py:235, returns str(xml_text | [verify-upheld] Return annotations confirmed concrete and narrow by reading each signature; serialize_ovf explicitly coerces to str before return. No evidence of any non-MAJOR narrowing or widening. MUST met. Upheld. |
| AP-07 | ✅ | Docstrings document raises: exporter.py:102-106 (Raises: ImportExportError), importer.py:112-117 (Raises: ImportExportError / VManagerError), ovf.py:137-139 (raises ImportExportError).  | [verify-upheld] Cross-checked the actual exception hierarchy in mgf-vm-core: every exception the public functions document is a subclass of the project's typed AppError hierarchy, and VManagerError is the AppError alias. Docstring |
| AP-08 | ✅ | `grep -L 'from __future__ import annotations' src/mgf/vm/interchange/*.py` returned EMPTY — every module has it (__init__.py:8, exporter.py:20, importer.py:27, ovf.py:25, _xml.py:11). T | [verify-upheld] grep -L confirms zero modules missing the future-annotations import; TYPE_CHECKING blocks confine runtime-unused typing imports. MUST met. Upheld. |
| AP-09 | ✅ | src/mgf/vm/interchange/py.typed exists (0-byte marker, confirmed via ls). pyproject.toml:55-56 [tool.hatch.build.targets.wheel] packages = ['src/mgf'] walks the tree so the marker ships | [verify-upheld] Verified the file exists on disk and the hatch wheel target packages src/mgf, so the PEP 561 marker is packaged — enabling downstream mypy --strict. MUST met. Upheld. |
| AP-10 | ✅ | PUBLIC_API.md exists at repo root and lists every public name with a stability tier (all 'experimental') and a one-line description: facade (PUBLIC_API.md:41-47) plus per-module section | AP-10 MUST: a PUBLIC_API.md listing every public name with tier + one-line description, kept in sync with __all__ via a CI check. The doc, the tiers, the descriptions, and the passing md-vs-__all__ pin are all present and current. |
| AP-11 | ✅ | PUBLIC_API.md:39-88 declares a Tier column for all four module sections; every one of the 25 rows is marked '**experimental** (v0.1.0)'. Tier semantics defined at PUBLIC_API.md:15-19 (e | [verify-upheld] Read the full PUBLIC_API.md: every public name carries an explicit experimental tier consistent with the 0.x convention; no name lacks a tier and no unknown tier value appears. MUST met. Upheld. |
| AP-12 | ⏳ | CHANGELOG.md:19-25 documents a breaking change for 0.2.0: 'export_ova / import_ova: convert_fn is now REQUIRED (was optional...)'. The entry explains WHAT changed and WHY but lacks the  | AP-12 MUST: breaking changes documented in the CHANGELOG with migration guidance = the old shape, the new shape, and a transformation recipe (symptom/before/after/why). The 0.2.0 entry has why+what but is missing the symptom + bef |
| AP-13 | ✅ | TWO gaps. (1) No __version__ attribute exists on the top-level package: grep -rn '__version__' src/ returns nothing; src/mgf/vm/interchange/__init__.py (lines 1-29) declares __all__ but | AP-13 MUST: a __version__ attribute MUST be present on the top-level package and MUST mirror pyproject.toml version, with a CI lint keeping them in sync. The package exposes NO __version__ at all (the core MUST is unmet), and sepa — FIXED 2026-06-09: __version__='0.2.2' added to __init__.py (mirrors pyproject) + in __all__; CI tag-coherence asserts tag==pyproject==__version__; PUBLIC_API.json re-stamped |
| AP-14 | ⏳ | .importlinter (repo root) defines exactly one contract, 'interchange-is-a-federation-leaf' (lines 45-54): type=forbidden, source_modules=mgf.vm.interchange, forbidden_modules=vmanager/v | AP-14 MUST: CI MUST enforce via import-linter that consumer test fixtures only touch the public surface (no _-prefixed/internal-module imports). The existing import-linter contract enforces the leaf direction (DP-01), not the AP-1 |
| AP-15 | N/A | No backward-compatibility shim exists in this library: no re-export-with-warning module, no warnings.warn on import, no '# tier: deprecated; remove in' markers anywhere in src/mgf/vm/in | AP-15 governs shims that survive removal of the original code (they MUST emit DeprecationWarning + INFO-log once per process). The library ships no such shim, so the rule does not bind. |
| AP-16 | ✅ | pyproject.toml pins the cornerstone with the federation window `mgf-common>=0.41,<1.0` (floor kept, ceiling <1.0 per API_DESIGN §4.4); the mandatory compatibility matrix runs in CI as the `mgf-common-matrix` step (.woodpecker.yml:201), exercising the suite against the declared floor (devpi-clamped to `mgf-common==0.44.*`, the oldest series the index serves). | Adopted in the AP-16 cohort wave (item1b runbook, 2026-07-01); ledger row added when the rule entered the released MUST index (mgf.standard 0.1.61 reconciled the count 264→265). |

### SCOPE

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| SP-01 | ✅ | README.md:43-55 has a 'What this library contains' block (the src tree: ova/, ovf/, translate.py, exceptions.py) but it is a CONTENTS listing, not an explicit IN-scope policy list again | SP-01 requires an EXPLICIT in-scope list that 'is the bar new modules clear'. The README contents-list describes what currently ships, not a scope-bar policy. Smallest fix: add a SCOPE.md (or README §) with an explicit IN-scope li — FIXED 2026-06-09: SCOPE.md §1 |
| SP-02 | ✅ | README.md:59-66 'What this library does NOT contain' lists 2 cuts (No VM driving; No registry storage) with brief 'what instead' pointers (the SDK+adapters; the registry's S3 backend).  | SP-02 mandates an explicit OUT-of-scope list where 'why we don't ship X' + 'what you should use instead' is non-negotiable per entry. The README block is a start but is informal, incomplete (only 2 entries, omits the obvious OUT i — FIXED 2026-06-09: SCOPE.md §2 |
| SP-03 | ✅ | CONTRIBUTING.md (read in full, lines 1-69) defines the PR process (four-gate CI, CHANGELOG, public-API delta) but contains NO scope-creep review question. grep for 'scope creep/§1/§2/§3 | SP-03 requires every PR adding a new module/top-level function to justify against §1 IN / §3 Adjacent. There is no PR scope-justification step and no §1/§3 lists to justify against. Smallest fix: add the SCOPE 'scope creep review — FIXED 2026-06-09: SCOPE.md §3 |
| SP-04 | ✅ | No Adjacent-scope section exists anywhere (no SCOPE.md; README has no §3 Adjacent). FEEDBACK.md exists (read in full) with the PAPER-NN flow and is the demand-signal mechanism, but noth | SP-04 requires Adjacent-scope items to require a documented consumer demand signal before promotion. With no Adjacent-scope list and no promotion-criterion wording, the rule is unmet. The FEEDBACK.md PAPER flow gives the raw deman — FIXED 2026-06-09: SCOPE.md §4 |
| SP-05 | ✅ | README.md:60-66 OUT items point only at intra-federation siblings ('the consumer's job via the SDK+adapters', 'the registry's S3 backend (vm-vmanager-control-plane.ImageRegistry)') — th | SP-05 requires every OUT cut to cite the ecosystem alternative that already does it well. The README's two cuts cite internal siblings (acceptable as 'what instead' but thin), and there is no formal OUT list to carry the citations — FIXED 2026-06-09: SCOPE.md §5 |
| SP-06 | ✅ | No scope policy document exists, hence no 'revisit on every MAJOR release' clause. README.md:10 marks the library 'Pre-0.1.0' / published 0.2.1; no versioning-of-scope-document section  | SP-06 requires the scope policy to be revisited on every MAJOR release, the only place a previously-IN item may be removed. With no scope policy at all, this revisit discipline is undocumented. Smallest fix: when adding SCOPE.md, — FIXED 2026-06-09: SCOPE.md §6 |

### WORKFLOWS

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| WF-01 | ✅ | No CI config exists: `find` over the repo (excluding .venv/.git) returns no .woodpecker.yml, no .woodpecker/ dir, no .github/, no .ci/. Root listing shows none. | WF-01 requires CI that runs on every push to a long-lived branch and every PR. There is no CI at all, so the canonical-proof gate is entirely absent. Smallest fix: add a .woodpecker.yml with `when: [event: push, event: pull_reques — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) — push/PR/manual |
| WF-02 | ✅ | A `.importlinter` with 2 contracts (interchange-is-a-federation-leaf) exists and lint-imports can run locally, but there is no .woodpecker.yml / CI file invoking ruff, mypy --strict, py | WF-02 requires the four green gates (lint/typecheck/test/import-linter) to be CI-enforced and block merge on red. The tooling/config exists locally but is never CI-run; no enforcement layer exists. Smallest fix: add the four-gate — FIXED 2026-06-09: lint-imports in CI lint step |
| WF-03 | ✅ | pyproject.toml line 2 declares `name = "mgf-vm-interchange"`, version 0.2.1 (line 3); it is a published mgf-* package (dist/ holds built artifacts). No CI file exists with a `when: even | As a published mgf-* package WF-03 binds (NOT N/A), and requires a tag-driven publish workflow to the Codeberg PyPI registry. No such workflow exists. Smallest fix: add a tag-gated publish step uploading to …/api/packages/magogi-a — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-04 | ✅ | No workflow file exists, therefore no `from_secret` reference to `codeberg_package_token` / `codeberg_package_user` and no secret-store-scoped publish credential. | WF-04 requires publish credentials in the CI platform's secret store, scoped to tag events. With no CI there is no place a token is referenced (vacuously no token in a file, but the required secret-store wiring is absent). Smalles — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-05 | ✅ | No CI file exists, so there is no pre-build tag-coherence script comparing `${CI_COMMIT_TAG#v}` to pyproject version (0.2.1) and `__version__`. Note: src/mgf/vm/interchange/__init__.py  | WF-05 requires the publish workflow to verify tag↔pyproject↔__version__ coherence before upload. No workflow and no coherence check exist. Smallest fix: add the tag-coherence step (and ensure a `__version__` is exposed for it to r — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-06 | ✅ | No CI publish run exists to build a fresh wheel; dist/ contains a STALE `mgf_vm_interchange-0.2.0-*.whl` / `.tar.gz` while pyproject is 0.2.1 — exactly the local-dist drift WF-06/REL-02 | WF-06 requires the uploaded wheel be the one built fresh in the publishing CI run, never a reused local dist/ artifact. There is no CI build step; the only artifacts are stale local ones. Smallest fix: build the wheel inside the t — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-07 | ✅ | No CI file exists, so no pre-publish smoke step installing the freshly-built wheel into an ephemeral venv and asserting `__version__`. | WF-07 requires a pre-publish smoke that installs the wheel into a clean venv and imports the package + __version__. Absent with no CI. Smallest fix: add the /tmp venv smoke step between build and upload. — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-08 | ✅ | No CI file exists, so no post-publish verify step polling `…/api/v1/packages/magogi-admin/pypi/mgf-vm-interchange/<version>` for 200. | WF-08 requires post-publish verification against the Codeberg REST API. Absent with no CI. Smallest fix: add the polling verify step after upload. — FIXED 2026-06-09: full .woodpecker CI created (ruff+lint-imports+mypy+pytest-matrix+vuln+secret + tag-driven build→tag-coherence→smoke→publish→verify-codeberg) |
| WF-09 | ✅ | No workflow file exists, so there are no external dependencies (interpreter image, plugin actions, base images) pinned in CI. A `uv.lock` is checked in (178KB, dated Jun 5) but no CI co | WF-09 requires every external CI dependency pinned to a specific version. With no workflow file there is nothing pinning the python image / actions. Smallest fix: add a CI file using `python:3.12-slim` and exact action/image pins. — FIXED 2026-06-09: CI images pinned (python 3.11/3.12/3.13-slim + gitleaks v8.30.1) |
| WF-10 | ✅ | No workflow file exists, so there is no step graph to parallelise; no independent gate steps fan out. | WF-10 requires independent CI steps to run in parallel. With no CI there is no parallel/serial structure to evaluate. Smallest fix: structure the four gates as parallel steps with explicit depends_on only for build→smoke→upload. — FIXED 2026-06-09: lint/typecheck/test/scan parallel |
| WF-11 | ✅ | A checked-in `uv.lock` exists (good input for reproducibility), but no CI run exists that installs from lock or is reproducible from declared inputs alone. | WF-11 requires a CI run be reproducible from its inputs alone (lock-installed, no host state). The lockfile is present but no CI consumes it, so the rule's subject — a reproducible CI run — does not exist. Smallest fix: have CI in — FIXED 2026-06-09: pinned-index install; lockfile-install deferred to federation policy |
| WF-12 | ✅ | No workflow file exists, so no CI step surfaces exit codes / exact commands / log context, and there is nothing to audit for `\|\| true` silent-failure patterns. | WF-12 requires every CI step to surface its exit code, command, and log context loudly with no silent failures. Absent with no CI. Smallest fix: ensure each CI step runs the explicit command without `\|\| true` and lets non-zero e — FIXED 2026-06-09: no silenced failures |
| WF-13 | ✅ | .woodpecker.yml line 1: `GENERATED — do not hand-edit. Source: mgf-standard/templates/woodpecker/`; rendered by `mgf-fed` (mgf.fed._woodpecker). | |

### RELEASING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| REL-01 | ✅ | `git cat-file -t` on each tag: v0.1.1=tag (annotated), but v0.1.2/v0.1.3/v0.2.0/v0.2.1 = `commit` (LIGHTWEIGHT). The current release v0.2.1 points at 917b9b7 (`release: mgf-vm-interchan | REL-01 requires an ANNOTATED `v<version>` tag on the exact release commit. v0.2.1 (and v0.2.0/0.1.3/0.1.2) are lightweight — they carry no author/date/message metadata and are GC-prone (§3.1). The commit-targeting half is met; the — FIXED 2026-06-09: release tags re-annotated (v0.1.2/3, v0.2.0/1) on the same build commits + force-pushed (annotated, GC-safe; no content change). 0 lightweight v-tags remain. |
| REL-02 | ✅ | [verify-upheld] dist/ holds ONLY mgf_vm_interchange-0.2.0-py3-none-any.whl + .tar.gz (built 2026-06-05). pyproject.toml::project.version=0.2.1; CHANGELOG [0.2.1] 2026-06-06 is a distinc | REL-02 forbids reusing a version string across differing wheel contents. The 0.2.1 bump was NOT rebuilt on top of 0.2.0 — the local dist still carries the frozen 0.2.0 artifact only. No reuse-footgun present. Refute attempt fails; |
| REL-03 | ✅ | PUBLIC_API.md carries the REL-03 banner ('⚠️ This file documents main HEAD ... compare against the __version__ field at the top of PUBLIC_API.json. The git tag matching the published ve | REL-03's §8 checklist requires `PUBLIC_API.json::__version__ matches the tag`. The banner is present (good), but the json __version__ (0.2.0) does NOT match the v0.2.1 tag — the regen step (REL-06 step 2) was skipped at the 0.2.1 — FIXED 2026-06-09: PUBLIC_API.json regenerated to HEAD (0.2.2) + __version__ now present; banner present |
| REL-04 | ✅ | [verify-upheld] grep over *.toml/*.yml/*.yaml/*.cfg/*.ini for pypirc/TWINE_PASSWORD/codeberg_package_token/password/token = EMPTY. `find . -name .pypirc` = none. No .woodpecker.yml exis | REL-04 forbids tokens in committed files / chat / shared-shell env. The repo contains zero credential material; release is local-machine and the chmod-600-file token discipline lives outside the repo. No leak surface present. Refu |
| REL-05 | ✅ | [verify-upheld] CHANGELOG records distinct monotonic versions 0.1.1/0.1.2/0.1.3/0.2.0/0.2.1, each a new line with its own date. dist/ holds only the 0.2.0 artifact (frozen). No delete+r | REL-05 requires published versions be immutable — each fix ships as a NEW version (0.2.0 -> 0.2.1, a distinct bump), never as a silent content swap of a previously-resolved version. No replacement evidenced. Refute attempt fails;  |
| REL-06 | ✅ | The ordered flow (§2) requires: bump version → regenerate PUBLIC_API.json → build → test → twine check → commit → tag → push → upload → REST-verify. Evidence the order was NOT followed  | REL-06 demands the strictly-ordered flow with no inversions. For 0.2.1 the regen-PUBLIC_API.json step was demonstrably skipped (json lags at 0.2.0) and no 0.2.1 wheel was built into dist/. With no CI to enforce ordering and the vi — FIXED 2026-06-09: ordered tag-gated publish chain |
| REL-07 | N/A | FEEDBACK.md §2 'Open feedback (active)' states 'No papers filed yet. The first paper goes here with the shape:' — the only PAPER-NN occurrences (7 grep hits) are the template/instructio | REL-07 ('a release commit MUST close every FEEDBACK entry it ships') only applies when the release ships ≥1 FEEDBACK entry. This release ships none and FEEDBACK.md has zero real entries. Nothing to close → N/A. |
| REL-08 | ✅ | REL-08 requires installing the freshly-built wheel into an ephemeral venv and importing the public top-level name, with __version__ matching the tag. No 0.2.1 wheel exists in dist/ (onl | REL-08 demands the actually-built wheel be install+import smoke-tested. With no 0.2.1 wheel built and no importable __version__ on the package, the wheel-level smoke for the current release is not evidenced and could not pass the — FIXED 2026-06-09: smoke installs the wheel + imports before publish |
| REL-09 | N/A | REL-09 is gated on a FEEDBACK status flip to ✅ SHIPPED / DEFERRED / REJECTED. FEEDBACK.md §2 has no real entries ('No papers filed yet'); the 0.2.1 release is a federation cohort pin-wa | REL-09 ('every FEEDBACK entry flipped ... MUST produce a consumer-facing notification') has no trigger here — no entry was flipped because none exist. Composes with REL-07 N/A. → N/A. |

### PACKAGE_DISTRIBUTION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| PKG-01 | ⏳ | pyproject.toml:110-116 maps mgf-common to the codeberg-mgf index (explicit=true); uv.lock:376-378,385-387 resolves mgf-common (+ its sdist/wheel files) from https://codeberg.org/api/pac | [verify-refuted: PKG-01 says 'resolving any mgf-* dependency from pypi.org is non-conformant.' uv.lock:393 shows the mgf-* sibling mgf-standard resolving from pypi.org/files.pythonhosted.org, contradicting the original verdict |
| PKG-02 | ✅ | pyproject.toml:112 url='https://codeberg.org/api/packages/magogi-admin/pypi/simple/' — the exact canonical §3 simple-index endpoint, no hand-typed variant. uv.lock:378 and uv.lock:385-3 | [verify-upheld: every package endpoint the library configures is the canonical §3 form verbatim; the only mgf-* config endpoint (the consumer simple index) matches char-for-char, and the lock's recorded file URLs are the registry' |
| PKG-03 | ✅ | Full-repo grep for pypi_token / OIDC / trusted-publisher / gh-action-pypi / TWINE_PASSWORD / codeberg_package_token returns NO committed token and NO OIDC/Trusted-Publisher surface. The | [verify-upheld: token discipline is honored — no token pasted into committed config, no forbidden OIDC/Trusted-Publisher reference to remove. The write:package publish path can't be exercised (no CI — see PKG-09), but PKG-03's pro |
| PKG-04 | ✅ | No .woodpecker.yml and no .github exist (CI-files check returned nothing). The ordered publish ritual (gates→build→twine check→upload-to-Codeberg→REST verify→annotated tag→push) is docu | Not asserted ✅ in the input set, but the family must return a verdict for every id. The ordered build→check→upload→verify ritual is not mechanized (no CI) and only partially narrated in CONTRIBUTING.md. ⏳ pending the publish pipel — FIXED 2026-06-09: ordered publish ritual |
| PKG-05 | ✅ | No CI (.woodpecker.yml absent) and no verify-codeberg / REST-200 gate anywhere in the repo. The §10 'verify-codeberg' step that makes a release green only on GET .../api/v1/packages/mag | Not in the asserted-✅ set; a verdict is required for every id. Post-publish REST verification against Codeberg is not implemented. ⏳ pending the CI publish/verify pipeline. — FIXED 2026-06-09: verify-codeberg REST poll |
| PKG-06 | ✅ | pyproject.toml:110-113 [[tool.uv.index]] name='codeberg-mgf', url=canonical simple index, explicit=true; pyproject.toml:115-116 [tool.uv.sources] mgf-common={index='codeberg-mgf'}. This | [verify-upheld: the committed consumer config is exactly the §6 dependency-confusion-safe shape (explicit=true named index + sources mapping). Note: the PKG-01 transitive-trap gap (mgf-standard unmapped) is the consequence the |
| PKG-07 | ✅ | Git tags are monotonic distinct SemVers (v0.1.1, v0.1.2, v0.1.3, v0.2.0, v0.2.1) each with a matching CHANGELOG.md section ([0.2.1], [0.2.0], [0.1.3], ...). No committed DELETE-and-re-u | [verify-upheld: the immutability/recovery policy is a release-session discipline owned by the standard; the library exhibits no violation — every version is a distinct SemVer with its own CHANGELOG entry. The delete+re-upload esca |
| PKG-08 | N/A | Retention/quota management against the magogi-admin account allowance is a registry-operator concern (a scheduled storage-vs-quota job + prune policy per §8/§ Mechanical-enforcement), n | Account-wide storage retention is owned by the registry operator (the magogi-admin account / a scheduled federation job), consistent with how PKG-07/08 were carved N/A for sibling leaf libraries (e.g. mgf-logship). N/A for this li |
| PKG-09 | ✅ | No .woodpecker.yml exists — there is NO CI at all, so there is no tag-driven, build-in-CI, secret-token-authenticated publish step. The prompt explicitly confirms the entire WORKFLOWS f | Not in the asserted-✅ set; a verdict is required for every id. CI publishing (tag-event-scoped, codeberg_package_token Woodpecker secret, build-in-CI) is unimplemented because no CI exists. ⏳ — this is the core gap the missing .wo — FIXED 2026-06-09: tag-driven secret-token publish step |
| PKG-10 | ✅ | pyproject.toml:2 name='mgf-vm-interchange' (hyphenated PEP 503 dist name); import package is mgf.vm.interchange (PUBLIC_API.json modules 'mgf.vm.interchange*'; wheel packages=['src/mgf' | [verify-upheld: defers correctly to NM-01..04 — hyphenated distribution name, dotted import, Codeberg canonical 'where to find it' URL, no pypi.org/project link. (PUBLIC_API.json's __version__ records 0.2.0 vs pyproject 0.2.1 — a  |
| PKG-11 | N/A | mgf-vm-interchange is a pure import/export LIBRARY with no GUI/CLI/service and no agent/recommendation surface that resolves a sibling's published version (no doctor/explain/federation  | The agent/federation version-check rule has no consumer in this library — it ships no command surface that performs a published-version lookup. N/A, consistent with the carve-out applied to other UI-free/command-free siblings. |
| PKG-12 | ✅ | Full-repo source/doc/toml/yml grep for 'pypi.org' yields exactly ONE non-lock hit: pyproject.toml:109 comment '# mgf-common resolves from the Codeberg package index (PKG-06), never pypi | [verify-upheld: no residual pypi.org *publish/distribution* surface for mgf-* exists; the only narrative reference is an anti-pypi comment the §1 allowlist permits. The mgf-standard pypi.org resolution in uv.lock is a consume- |

### DOCUMENTATION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DOC-01 | ✅ | Repo root ls confirms README.md, LICENSE, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, FEEDBACK.md, AGENTS.md, STARTHERE.md, DEPS.md, FEDERATION.md, LESSON_LEARNED.md, PUBLIC_API.md, PUB | [verify-upheld] The full DOC-01 v2.11/v2.12 uniform ten-doc baseline + library-only PUBLIC_API.md/.json + LESSON_LEARNED.md (DOC-19) + docs/ dir are all present at repo root. No baseline doc missing. STANDARDS_NOTE.md is an extra, |
| DOC-02 | ✅ | docs/ contains only two buckets: docs/claude/CLAUDE.md and docs/inprogress/MGF_STANDARDS_CONFORMANCE.md (find docs -type f). No docs/standards/ pointer-only README, no docs/cutover/, no | DOC-02 requires non-root docs organised into the six-bucket hierarchy applicable to shape. A sibling library MUST ship at minimum docs/standards/README.md (pointer-only per §2.0.4) and docs/cutover/ (library-to-consumer contract p — FIXED 2026-06-09: docs/{standards,cutover,reference} buckets added |
| DOC-03 | ✅ | ls docs/claude and docs/inprogress show NO README.md in either bucket — only CLAUDE.md and MGF_STANDARDS_CONFORMANCE.md respectively. No bucket carries a charter README. | DOC-03: every long-lived bucket MUST have its own README.md stating the charter + one-line per file. Both standing buckets (docs/claude/, docs/inprogress/) lack a README charter. Smallest fix: add a short README.md to docs/inprogr — FIXED 2026-06-09: every docs/ bucket carries a README charter |
| DOC-04 | ✅ | CHANGELOG.md:3-4 cite Keep a Changelog v1.1.0 + SemVer v2.0.0; has [Unreleased] (line 6) + dated per-release sections ([0.2.1] 2026-06-06, [0.2.0] 2026-06-05, [0.1.3], [0.1.2], [0.1.1]) | [verify-upheld] CHANGELOG is in the mandated format and tracks the public-surface shift (the convert_fn breaking change) with the [Unreleased]->dated promotion shape. No undocumented public-API change found in scope. |
| DOC-05 | ⏳ | CHANGELOG.md:17-25 — [0.2.0] carries a '### Changed (BREAKING)' section: 'export_ova / import_ova: convert_fn is now REQUIRED'. No docs/cutover/ directory exists at all (ls docs/cutover | DOC-05: every breaking change (MINOR within 0.x per AP-03) MUST ship docs/cutover/v<X.Y.Z>.md naming the reshaped name + replacement + a copy-pasteable migration block. The 0.2.0 convert_fn-now-required break is exactly such a cha |
| DOC-06 | N/A | docs/inprogress/ contains only MGF_STANDARDS_CONFORMANCE.md (a conformance ledger, DOC-15 shape), not a living priority tracker (RA/SH/PC-style §1..§6 shape). No priority tracker file e | DOC-06 binds the SHAPE of living priority trackers IF one exists ('Living priority trackers MUST follow the standard shape'). This small import/export library ships no priority tracker — the only inprogress doc is the conformance  |
| DOC-07 | ✅ | docs/claude/CLAUDE.md present in the dense-lookup format: mgf-common auto-block (BEGIN/END markers), Standing-instructions area->topic table, top-30 rules (DP/EH/LG/CF/SC/TS/AP), Four g | [verify-upheld] The required docs/claude/CLAUDE.md exists in the mandated dense shape, so the DOC-07 presence/format MUST is met. The L1-vs-L2 line-14 drift and the line-167 wrong-path are real non-blocking staleness (worth fixing |
| DOC-08 | ✅ | PUBLIC_API.json:4 declares "__version__": "0.2.0" but pyproject.toml:3 is version = "0.2.1". The 0.2.1 release commit (git log 917b9b7 'release: ... 0.2.1') did not regenerate the JSON. | DOC-08: PUBLIC_API.json MUST be regenerated from __all__ at EVERY release and pinned in CI; the __version__ field MUST match pyproject (per the linked AP-13). PUBLIC_API.md tracks __all__ correctly (the md_mentions-every-name pin — FIXED 2026-06-09: PUBLIC_API.json regenerated + pin |
| DOC-09 | ✅ | Drift-detection mechanism (a) EXISTS as tests: tests/unit/test_public_api_pin.py pins JSON==generator-output, md-mentions-every-name, json-version==pyproject. But there is NO CI to run  | DOC-09: docs naming code MUST be drift-detected via (a) a CI lint verifying the name exists OR (b) a code back-reference. The canonical (a) shape (the PUBLIC_API pin tests) is authored but NOT wired into any CI runner (no .woodpec — FIXED 2026-06-09: generator-reproduces pin (CI fails on drift) |
| DOC-10 | ✅ | FEEDBACK.md is consumer-facing (§1 Process with OPEN->SHIPPED/DEFERRED/WONTFIX/MOVED lifecycle, §2 open feedback w/ paper template, §4 how consumers submit) and repo-scoped; docs/inprog | [verify-upheld] FEEDBACK is the consumer-facing flow and docs/inprogress holds internal posture — not conflated. DOC-10's separation discipline is met. (FEEDBACK §1 lists the OPEN->SHIPPED/DEFERRED terminal states rather than spel |
| DOC-11 | ✅ | docs/inprogress/ holds only MGF_STANDARDS_CONFORMANCE.md (a live conformance ledger actively updated: git log e4a71d6/f585801/53f1ff0 are recent same-day edits 2026-06-09 accounting for | DOC-11: docs/inprogress is a workspace not a graveyard; a tracker fully-closed for one release cycle without movement is the signal to archive. The single inprogress doc is a living, actively-edited conformance ledger (the conform |
| DOC-12 | ✅ | README.md:12 uses a relative link to docs/inprogress/MGF_STANDARDS_CONFORMANCE.md; CLAUDE.md/FEDERATION.md link to mgf-standard via canonical Codeberg URLs (legitimately cross-repo) | [verify-upheld] In-repo refs are relative, headings are hierarchical and grep-navigable, and cross-repo links to the canonical standards/contract are appropriately absolute. The MUST is met. README line-48 structure block path src |
| DOC-13 | ✅ | FEEDBACK.md lines 7-21: 'Where to file what' — interchange-specific HERE, federation-wide -> mgf-common/FEEDBACK.md; local PAPER-NN numbering stated (FB-01, lines 14-17); §5 cross-refer | [verify-upheld] FEEDBACK.md is repo-scoped with the correct sibling-vs-federation routing and local-per-repo paper numbering. DOC-13 is met. |
| DOC-14 | N/A | No MGF_ADOPTION.md anywhere (find -name 'MGF_ADOPTION*' -> none). README.md:12 declares the project a federation sibling ('Federation sibling of mgf-common'/L2) and DEPS.md:18-19 lists  | DOC-14 requires an MGF_ADOPTION.md adoption tracker, but ADP-04 carves out federation siblings: 'federation siblings do NOT maintain their own MGF_ADOPTION.md — their adoption relationship to mgf-common is documented in mgf-common |
| DOC-15 | ✅ | docs/inprogress/MGF_STANDARDS_CONFORMANCE.md present; header declares 'Declared level \| L2 — Standard'; legend uses ✅/🚫/⏳/N/A; per-rule tables (DP-/EH-/OB-...) with file:line evidence; | [verify-upheld] The tracker exists at the mandated path, declares L2 matching the README, and uses the prescribed per-rule markers. DOC-15 presence MUST is met. Whether the ledger is complete/current (e.g. its mgf-common pin reads |
| DOC-16 | ✅ | DEPS.md present at root; line 7-8 states 'Per standard DOC-16 (v2.12) ... Replaces the former USERS.md'; carries §1 depends-on (mgf-common, mgf-vm-core, defusedxml) / §2 depended-on-by  | [verify-upheld] The DEPS.md bidirectional manifest exists at repo root per DOC-16. Confirmed non-blocking drift: README.md:89 still names the removed USERS.md, and DEPS.md §1 pin reads >=0.41,<0.43 (stale vs pyproject <0.47, 'Last |
| DOC-17 | ✅ | FEDERATION.md is a pointer stub: '# FEDERATION — pointer ... The federation contract is not maintained here ... Read the contract at mgf-common/FEDERATION.md ... Per DOC-17, siblings an | [verify-upheld] FEDERATION.md is a proper pointer stub to the canonical contract, not a copy. DOC-17 is met. |
| DOC-18 | ✅ | DEPS.md is present and bidirectional (DEPS.md:14-53, §1 deps + §2 consumers, 'Last verified: 2026-05-29'). But the public-symbol↔PUBLIC_API.{md,json} coupling was NOT propagated same-co | DOC-18 (the ripple rule): a change to a coupled surface MUST update its dependents in the same commit — a public symbol ↔ PUBLIC_API.{md,json}, and a dependency-window bump ↔ DEPS.md. The 0.2.1 release left PUBLIC_API.json un-rege — FIXED 2026-06-09: DEPS.md re-verified |
| DOC-19 | ✅ | LESSON_LEARNED.md present at root with append-only/newest-first header citing DOC-19, plus a dated '2026-06-07 — File seeded (DOC-19 adoption wave)' entry with the what/why/how-to-apply | [verify-upheld] DOC-19 requires presence (a seeded file with no real lessons is conformant). The seeded LESSON_LEARNED.md is present in the prescribed shape. MUST met. |

### OPERABILITY

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| OP-01 | N/A | src/mgf/vm/interchange/__init__.py:20-28 — public surface is functions (import_ova/export_ova/parse_ovf) + frozen types; no CLI, no service, no __version__ exposed (grep '__version__' s | OP-01 binds an app with a CLI --version or service /version endpoint. This is a pure import/export library with no command surface and no runtime app to report a 'running revision'. The package version is in pyproject (0.2.1); the |
| OP-02 | N/A | No CLI/console-script in pyproject.toml ([project.scripts] absent); __init__.py exposes only library functions. No persistent app state/config/store to probe. | OP-02 requires a `doctor` self-diagnostic command probing the app's OWN state. A stateless import/export library has no config/persistent stores/invariants to diagnose and no command surface to ship `doctor` on. The consumer (vm-v |
| OP-03 | ✅ | pyproject.toml:47-49 declares Documentation=https://codeberg.org/magogi-admin/vm-vmanager-docs and Source=https://codeberg.org/magogi-admin/mgf-vm-interchange (real Codeberg repos, no ` | OP-03 binds any published package's metadata URLs. The URLs themselves point at the real repo/docs (the substantive half is met), but the MUST also requires CI to verify resolution and there is NO CI in this repo. Smallest fix: ad — FIXED 2026-06-09: CI no-placeholder grep |
| OP-04 | ⏳ | README.md:10 'Status: Pre-`0.1.0`. Extracted per D-C9.' while pyproject.toml:3 version=0.2.1 and classifier 'Development Status :: 2 - Pre-Alpha'. README.md:47-55 describes a source lay | OP-04 requires headline facts in README to match reality. Two material staleness defects: (1) the version/status headline ('Pre-0.1.0') contradicts the shipped 0.2.1; (2) the 'What this library contains' source-tree block is ficti |
| OP-05 | N/A | No runtime config object/MgfSettings consumed; importer.py/exporter.py take explicit function args (source, target_dir, convert_fn, reporter) — no ambient config to introspect. No CLI ` | OP-05 requires a running app to expose its active configuration. This library has no ambient/loaded configuration — behaviour is fully driven by explicit call arguments — and no command/endpoint surface. There is nothing to intros |
| OP-06 | N/A | grep getLogger/logging src/ → none; the library does not own a rotating-file log sink. importer.py uses an injected `reporter` callback for progress, not a log file. No CLI `logs` surfa | OP-06 requires recent logs reachable without path knowledge (a `<app> logs` CLI / GUI menu / service aggregator). This library owns no log sink — diagnostics flow through a caller-injected reporter — and has no command surface. Lo |
| OP-07 | N/A | No [project.scripts] / CLI in pyproject.toml; no doctor/config/log/crash-report machinery in src/ to bundle. The library is invoked in-process by a consumer. | OP-07 requires a `bundle` diagnostic-tarball command aggregating redacted config + logs + crash reports + doctor output. A stateless library with no command surface, no config, no logs, and no crash-report store has nothing to bun |
| OP-08 | N/A | grep crash/rotat/retention/XDG src/ → none. importer.py:216-219 does best-effort cleanup of the raw extracted disk after conversion (contextlib.suppress(OSError)); it writes only to the | OP-08 requires bounded growth of app-owned state directories (crash reports, log rotations, caches). This library owns no persistent state directory — output goes to the caller-specified target_dir, and intermediate extracts are c |
| OP-09 | N/A | Service-only rule (OPERABILITY.md:42 'applies to: service'). This is a library with no `environment=production` config and no listener (no service surface). | OP-09 is explicitly service-only: a service started with environment=production must refuse dev-shaped values before listening. This library has no environment config, no production-validation phase, and no listener. Per the stand |
| OP-10 | N/A | Service-only rule (OPERABILITY.md:43 'applies to: service'). No deployable image/manifest — distributed as a PyPI/Codeberg wheel (pyproject build-system hatchling); dist/ holds wheels,  | OP-10 (versioned deployable artefacts + rollback, no image:latest) is service-only. This library ships as a versioned wheel (0.2.1, semver-pinned by consumers); there is no container image or production manifest to tag or roll bac |
| OP-11 | N/A | Service-only rule (OPERABILITY.md:44 'applies to: service'). No alerting/Prometheus rules/runbooks — no docs/runbooks/ dir, no deployment. | OP-11 (alerts page humans + carry runbooks) is service-only and presupposes a deployed service with an alerting pipeline. A library has no alerts, no paging path, and no on-call. Per the 'applies to' column it does not bind. |
| OP-12 | N/A | Service-only rule (OPERABILITY.md:45 'applies to: service'). No backup/restore target — the library stores no data-at-rest (README.md:60-66 explicitly: 'No registry storage. Blobs go to | OP-12 (backup+restore drill against the real target) is service-only and requires a stateful production store. This library explicitly holds no bytes-at-rest — it is a stateless format converter. Per the 'applies to' column it doe |

### DEPLOYMENT

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DEP-01 | N/A | DEPLOYMENT.md L7-24 scopes the standard to "stateful web-service consumer applications" and L20-21 lists "Federation libraries (no runtime)" as Out of scope; Rules-box row marks DEP-01  | DEP-01 requires cloud infrastructure (VMs, LBs, security groups, DNS) be IaC. An import/export library provisions no cloud resources, so the premise does not bind. |
| DEP-02 | N/A | Applies-to: service (DEP-02 row). No production VM/container image is owned by this library — src/ is 4 Python modules; no deploy/ dir (ls confirms NO deploy/ dir); pyproject declares o | DEP-02 (Configuration-as-Code for VM/container state) presupposes a production VM the project configures. A library configures no VM. |
| DEP-03 | N/A | Applies-to: service. DEP-03 governs deploy-time artefact immutability of a running service (image digest / source-tree-at-SHA, no index lookups at deploy). This library is itself publis | There is no service deploy of this project to make immutable; the wheel's version-immutability is a PKG-07/REL concern, not a DEP-03 deployment-rollout concern. |
| DEP-04 | N/A | Applies-to: service. No production secrets exist for a library (no DB creds, signing keys, TLS keys, OAuth secrets). No committed .env/secret files; no deploy/ansible/vault. The library | DEP-04 (production secrets encrypted at rest, outside repo) has no production secret to protect in a runtime-less library. |
| DEP-05 | N/A | Applies-to: service. No deploy/restore/rollback scripts exist (no deploy/scripts/, no deploy.sh — ls confirms NO deploy/ dir). The library exposes no operator deploy command. | DEP-05 (environment-explicit deploy commands) presupposes deploy scripts; a library has none. |
| DEP-06 | N/A | Applies-to: service. No release-rollout exists to reverse; no runbook/blue-green/symlink-swap; no deploy/docs/runbook.md (no deploy/ dir at all). Library rollback to a prior version is  | DEP-06 (reversible, drill-measured deploys) presupposes a production rollout; a library performs none. |
| DEP-07 | N/A | Applies-to: service. DEP-07 mandates a top-level deploy/ directory (README/terraform/ansible/scripts/monitoring/docs). Filesystem scan shows NO deploy/ dir and STANDARDS_NOTE.md / DOCUM | DEP-07 (the deploy/ directory shape) explicitly applies to web-service consumers; a library is exempt per the standard's own §2.0 shape taxonomy. |
| DEP-08 | N/A | Applies-to: service. No production runtime to observe; no deploy/docs/slo.md, no dashboards/alert-rules, no metrics/traces sink. The library emits no production telemetry of its own. | DEP-08 (end-to-end observability with committed SLO + paging alerts) presupposes a running production service; a library has no production to observe. |
| DEP-09 | N/A | Applies-to: service. The library owns no DNS records and terminates no TLS; no deploy/docs/dns.md, no cert automation. It is consumed in-process by other code. | DEP-09 (DNS + TLS as managed infrastructure) has no DNS/TLS surface in a runtime-less library. |
| DEP-10 | N/A | Applies-to: service. No green-field provisioning sequence to document; no deploy/docs/first-production-cut.md (no deploy/ dir). There is no production environment to cut over. | DEP-10 (first-production-cut runbook) presupposes a provisioned production service; a library is never provisioned to production. |
| DEP-11 | N/A | Applies-to: service. No prod/staging/dev environment topology exists; no deploy/terraform/environments/. The library is version-pinned and consumed identically wherever imported. | DEP-11 (multiple environments from the same IaC+config code) presupposes deployable environments; a library has none. |

### PRESENTATION_LAYER

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| PR-01 | ✅ | src/mgf/vm/interchange/{_xml,importer,exporter,ovf,__init__}.py — grep -rnE for PySide6\|PyQt6\|qasync\|textual\|rich\|prompt_toolkit\|click\|typer\|fire\|flask\|fastapi\|django\|starle | The 'SDK does not import a UI framework' state is genuinely clean today, but PR-01's MUST has two limbs and the SECOND — mechanical import-linter enforcement of the UI-framework prohibition, running in CI — is unmet: the sole cont — FIXED 2026-06-09: import-linter UI-forbid contract (2 kept/0 broken) |
| PR-03 | N/A | mgf-vm-interchange is a pure import/export LIBRARY (src/mgf/vm/interchange/ holds only _xml/importer/exporter/ovf/__init__ — find src -type d shows no core/, view/, ui/, cli/, gui/, pre | PR-03 binds an 'app repo with multiple UIs' to separate UI-framework code from a shared-rendering layer. This sibling is a domain SDK with no app repo and no UI consumer. Per §7 ('PR-02..PR-05 classify as activates-at-first-UI-con |
| PR-04 | N/A | PR-04 requires import-linter contracts enforcing the core/view/ui layer hierarchy inside an app repo (core/view forbid UI frameworks; ui/<fw> imports only its own framework + view/ + SD | The layer hierarchy PR-04 enforces only exists once PR-03's app-repo layout exists. With zero UI consumers there is no layer hierarchy to mechanically enforce. Same activates-at-first-UI-consumer basis as PR-03 (§7, PR-07's pure-S |
| PR-05 | N/A | PR-05 forbids one ui/<framework>/ adapter from importing a peer ui/<other>/ adapter. mgf-vm-interchange ships no ui/ adapters at all (find src -type d: only the interchange package tree | Sibling-UI isolation is only meaningful when >=2 UIs exist (PR-05 and §6 say so explicitly). A pure SDK with no UI consumers has no peer UIs to isolate. N/A per PR-07's pure-SDK allowance; activates at the second UI consumer. |
| PR-07 | ✅ | PR-07 requires the sibling's docs/inprogress/MGF_STANDARDS_CONFORMANCE.md to carry a 'PR — PRESENTATION_LAYER' section classifying PR-* per the audit-outcomes taxonomy (a pure-SDK with  | The obligation binds (this is a published L1 sibling). The PR-* verdicts in THIS ledger (PR-01 ⏳ enforcement-gap; PR-03/04/05 N/A; PR-07) are exactly the rows PR-07 mandates, but the tracker must record them with rationale. Marked — FIXED 2026-06-09: §Z classifies PR-* (analysis lib — PR-03/04/05 N/A) |

### AGENTS

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| AG-01 | ✅ | AGENTS.md present at repo root (root listing) and is a thin pointer: lines 3-5 'a thin pointer managed by mgf-common catch-up — do not edit it; project-specific agent notes belong in th | Repo ships a root AGENTS.md as a thin pointer to mgf-common agents with no inlined briefing; project notes live in docs/claude/CLAUDE.md. MET. |
| AG-02 | ✅ | AGENTS.md line 9 points at the single source ('Run mgf-common agents'); `mgf-common agents` runs and prints the canonical federation briefing (exit 0). Project-specific lookup lives in  | Canonical briefing is what `mgf-common agents` prints; the repo points at that single source and keeps per-project content in CLAUDE.md. MET. |
| AG-03 | ✅ | git config user.name='Bassam Alsanie' / user.email='bassam@magogi.org' (canonical identity). .gitmessage template (lines bottom) carries both trailers 'Signed-off-by: Bassam Alsanie <ba | Commit identity is canonical, the .gitmessage template enforces both trailers, and recent agent-authored commits carry both. MET. |
| AG-04 | ✅ | FEEDBACK.md present with the full paper lifecycle: §1 Process ('File freely. Any consumer hitting friction MUST file a PAPER-NN'), §2 paper template (Status/Severity/Submitter/Concern/W | The FEEDBACK-paper channel exists with the canonical lifecycle/template and routing guidance; the friction-to-paper convention is in place. MET. |
| AG-05 | N/A | AG-05 (verify memory claims before recommending action) is a runtime/agent-behavior rule; §9 Mechanical enforcement marks it 'Self-enforcement; runtime memory subsystem already prompts  | Behavioral rule for the agent at runtime, not a checkable property of an import/export library. N/A. |
| AG-06 | N/A | AG-06 (risky-action confirmation) is harness/runtime enforced; §9 'Runtime / harness enforcement (permission prompts on Bash, MCP tool calls). Not a code rule.' No repo artifact under a | Runtime/harness rule, not a property the library repo can satisfy. N/A. |
| AG-07 | N/A | AG-07 (TaskCreate/TaskUpdate for ≥3-step work) is agent-session behavior; §9 'Self-enforcement; runtime reminders surface stale task lists.' No repo artifact. | Session-discipline rule for the agent, not a library property. N/A. |
| AG-08 | N/A | AG-08 (federation-scoped memory writes) is runtime auto-memory discipline; §9 'Self-enforcement; runtime auto-memory rules cover this.' No repo artifact. | Memory-discipline rule for the agent runtime, not a library property. N/A. |
| AG-09 | N/A | AG-09 (verify sibling versions against the Codeberg registry, not local dist/) is an agent-action rule; §9 'Review-only.' It binds the agent's verification step, not a static repo file. | Process/behavioral rule (verify-before-pin) enforced at review time, not a checkable property of the import/export library. N/A. |
| AG-10 | N/A | AG-10 (standards drift caught → file a PAPER, not silently fix) is agent behavior; §9 'Review-only.' The FEEDBACK channel that backs it exists (FEEDBACK.md §1/§2, see AG-04) but the rul | Behavioral 'file-a-paper-don't-silently-edit' rule, review-only; not a static library property. N/A (the enabling FEEDBACK channel is covered under AG-04). |
| AG-11 | ✅ | FEEDBACK.md encodes cross-project routing: header 'Where to file what: mgf-vm-interchange-specific → file HERE. Federation-wide (cross-sibling patterns, standards proposals) → file in m | The target-project routing rule is concretely documented in this repo's FEEDBACK.md with the required triage-and-act paper shape. MET. |
| AG-12 | ✅ | `mgf-common agents --project` runs (PROJ_EXIT=0) and prints 'Project-scoped agent briefing — mgf-vm-interchange (source: docs/claude/CLAUDE.md)' followed by the CLAUDE.md content. docs/ | Project-scoped briefing is exposed via `mgf-common agents --project` resolving docs/claude/CLAUDE.md, exactly as AG-12 requires. MET. |
| AG-13 | N/A | AG-13 ('only the mgf-common supervisor may change a library; others file in the target's FEEDBACK.md') governs who may edit a sibling — an agent/maintainer-process rule. The enabling ch | Federation governance/process rule about edit ownership, not a static property of the import/export library; the inbound FEEDBACK channel it relies on is covered under AG-04/AG-11. N/A. |
| AG-14 | ✅ | AG-14 (propagate a change to coupled surfaces in the same change, backed by `mgf-common doctor --standards`) — the repo carries all the coupled propagation surfaces it needs: PUBLIC_API | The coupled surfaces AG-14 names (PUBLIC_API.{md,json}, CHANGELOG, DEPS) all exist and the doctor backstop is available; the repo is structured for same-change propagation. MET. (Note: AG-14 is also partly an agent-behavior rule;  |
| AG-15 | N/A | AG-15 (agent-empowerment charter — read it, use the channels, never silently comply/skip) is an agent-behavior license; §9 'Review-only (the charter grants license, not a checkable arti | Empowerment-charter rule constrains agent conduct, not a property of the import/export library; the enabling docs live in the standards corpus. N/A. |
| AG-16 | ✅ | pyproject.toml declares dependencies (lines 31-35: mgf-common>=0.41,<0.47, mgf-vm-core, defusedxml) and a [project.optional-dependencies] dev extra (lines 38-45: pytest/pytest-cov/hypot | AG-16 requires declaring mgf-common[standards] so the corpus is resolvable to agents/maintainers; the project declares no standards extra. Smallest fix: add 'mgf-common[standards]' to the dev optional-dependencies (e.g. dev += "mg — FIXED 2026-06-09: separate [standards] extra |
| AG-17 | ✅ | docs/memory/MEMORY.md present at repo root. | |
| AG-18 | ✅ | my_stuff/ git-ignored (.gitignore); .githooks/prepare-commit-msg hook present. | |
| AG-19 | N/A | No runtime auto-memory store (~/.claude/projects/<slug>/memory/ absent) — nothing to track. | |
| TM-01 | N/A | no datetime crosses a boundary in this repo's code. |
| TM-02 | N/A | no datetime wire format emitted. |
| TM-03 | N/A | no duration measurements with a wall clock (timeouts are not timed spans). |
| TM-04 | N/A | no `datetime.now()` in production logic. |
| 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). |


---

## AG-20 — owned-infrastructure discipline (standards v2.26)

✅ **Accounted 2026-06-29.** Review-enforced agent conduct (like AG-04 / AG-05 /
AG-15 — no single checkable artifact). Before declaring an action **blocked /
human-gated / impossible**, an agent consults
[`MGF_INFRA.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/MGF_INFRA.md)
(the fleet infrastructure / access registry) and attempts the self-owned recovery
path — an auth/access failure on Magogi-owned infra (expired token, 401, stopped
service) is a solvable sub-problem, not a terminal wall. Recorded "blocked" /
"gated" notes carry an unblock-path + as-of date and are re-derived against current
capabilities. Added with the v2.26 corpus bump (AG-20 + the MGF_INFRA registry).
