# MGF_STANDARDS_CONFORMANCE — `mgf-vm-forensics`

> 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` (latest released: 0.42.0) |
| `mgf-vm` pin | `>=0.2,<0.3` (transitional umbrella; latest 0.2.1) |
| Declared level | **L2 — Standard** (see [`STANDARDS_NOTE.md`](../../STANDARDS_NOTE.md)) |
| Library version | `0.1.0` (unreleased) — full forensics surface ported (20 src modules, 242 tests) |
| Last audit | 2026-06-09 (evidence-based depth audit) |
| Verified standing | **below L2** — 117 ✅ / 26 ⏳ / 75 N/A of 218 (82% of applicable) |

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


**2026-06-09 depth audit (evidence-based, replaces the prior pass).** Declared TARGET **L2**; verified below L2. Of 218: **117 ✅** (file:line, refute-survived) · **26 ⏳** · **75 N/A** · **0 declined**. Applicable = 143; **117/143 (82%) evidenced**. **Strong CORE** (refute-survived): frozen domain types + copy-on-write Case (DP-03), mypy --strict on 20 files (DP-04), fakes-based hermetic tests (DP-05), and a genuinely security-conscious **untrusted-pcap path** — per-packet parse errors caught into a bounded list, sample caps to bound memory, bounds-checked manual TLS parsing, atomic 0o600 writes, `with`-managed handles, and privileged ops (`sudo -n` qemu-nbd/tcpdump) confined to the capture layer + documented (SC-16 ✅). The 51% reflects **no CI pipeline (.woodpecker absent → the whole WORKFLOWS family ⏳)** + the formal-ABC-vs-callable-injection seam choice (DP-02) + a small SC-15 path-hardening fix — NOT a weak core.

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

| Cluster | Rules | Count |
|---|---|---|
| **CLOSEABLE** (create CI pipeline + SCOPE + public-API + docs + SC-15 path-fix + sbom) | AG-16, AP-10, AP-13, DOC-02, DOC-03, DOC-08, DOC-09, DOC-18, DP-01, OP-03, OP-04, PKG-03, PKG-04, PKG-05, PKG-08, PKG-09, PKG-12, PR-01, PR-07, REL-03, REL-06, REL-08, SC-10, SC-15, SC-18, SP-01, SP-02, SP-03, SP-04, SP-05, SP-06, TS-01, TS-06, TS-18, TS-19, TS-20, TS-21, WF-01, WF-02, WF-03, WF-04, WF-05, WF-06, WF-07, WF-08, WF-09, WF-10, WF-12 | 48 |
| **DEFERRED** (DP-02 ABC-seam design, DP-08/09, logging build-out, SC-08/17, property/supervisor/perf TS, CF-06, AP-14) | AP-14, CF-06, DOC-05, DP-02, DP-08, DP-09, LG-11, LG-15, LG-16, REL-01, SC-08, SC-17, TS-07, TS-10, TS-13, TS-22, TS-23, TS-24, TS-25, TS-27, TS-28, WF-11 | 22 |

## 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 untrusted-pcap/archive SECURITY pass; ~32 agents + a 19-id re-run). No CI yet (.woodpecker absent → the whole WORKFLOWS family is ⏳). No 🚫-flood, no mislabels. **Mix: 73 ✅ · 70 ⏳ · 75 N/A · 0 🚫.** Replaces the prior scaffold.


### DESIGN_PRINCIPLES

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DP-01 | ✅ | .importlinter exists with two contracts (forensics-is-a-leaf forbidden + forensics-layers downward, root_packages=mgf, include_external_packages=True). No .woodpecker.yml and no .woodpe | DP-01 MUST: 'Imports MUST flow downward only. CI lints this with import-linter.' The contracts are authored correctly but the mechanical enforcement is absent because there is no CI. Per the prompt's WORKFLOWS carve-out the whole — FIXED 2026-06-09: lint-imports now CI-enforced (the existing .importlinter contracts gain a mechanical gate) |
| DP-02 | ⏳ | Third-party I/O is injected as callables, not formal ABCs: capture/screenshot.py `screenshot_fn: ScreenshotFn`, capture/memory.py `dump_fn: DumpMemoryFn`, capture/filesystem.py `mount_d | DP-02 MUST: every third-party I/O dep MUST sit behind an abstract base class with >=1 production + >=1 mock impl, locked by an EXPECTED_METHODS test, and CI must lint that the vendor SDK is imported only inside the provider impl.  |
| DP-03 | ✅ | [verify-upheld] All inter-module domain types are frozen: case.py:83 `@dataclass(frozen=True) class Artifact`, :104 `class Case`; analysis/network.py:55/65/79/88/96/106 (TalkerPair, Pro | Tried to refute by hunting for a bare dict/str crossing a boundary as identity or a non-frozen domain record — none found. Every domain type passed between modules is a frozen dataclass or StrEnum, and immutable updates return cop |
| DP-04 | ✅ | [verify-upheld] `.venv/bin/mypy src` → `Success: no issues found in 20 source files` (ran in the project's own venv). pyproject [tool.mypy] strict=true, warn_unused_ignores=true, with a | Tried to refute by re-running mypy and hunting unjustified Any — mypy --strict is clean on all 20 files, and every Any sits on the unstubbed-dpkt boundary which is documented. Meets the DP-04 MUST. Upheld. |
| DP-05 | ✅ | [verify-upheld] tests/unit/mgf/vm/forensics/ mirrors the src path; 15 test modules cover the source modules (test_analysis_network/_filesystem/_ioc, test_capture_network/_filesystem/_me | Tried to refute by grepping for live network/daemon/subprocess in tests — every external side effect is faked via the injected popen_fn / fake callables, dpkt runs over in-repo fixture bytes. Layout is grouped (one test file per s |
| DP-06 | ✅ | SECURITY.md present at repo root; security is treated as central in code: per-packet parse failures caught + bounded error list (analysis/network.py:175-182), bounded sample caps to kee | DP-06 is a review-only MUST about PR descriptions addressing security/reliability/performance/error-reporting on new surface. The code consistently reflects all four concerns as first-class (esp. the security-central untrusted-pca |
| DP-07 | ✅ | Public functions/classes carry intent+invariant docstrings: archive_case (archive.py:40, full Parameters/Side-effects), analyze_pcap (network.py:138, streaming + sample caps), iter_pack | Sampled the public surface across analysis/, capture/, archive, case, ioc — every public class and function has a docstring stating intent and invariants, none are the forbidden 'returns the result' shape. Meets the DP-07 MUST. |
| DP-08 | ⏳ | No ARCHITECTURE.md and no §5-style replaceability/swap-recipe table found in README.md or any root doc (grep for swap/replaceab/seam/inject/provider in README.md returned nothing; docs  | DP-08's MUST clause: 'the doc near it MUST describe the swap recipe' and 'the table MUST live in the project's ARCHITECTURE.md'. The per-fn injection points are docstring-documented (partial credit) but the required swap-recipe ta |
| DP-09 | ⏳ | No CancellationToken / cancel parameter anywhere: grep for Cancellation/cancel/token in src found only prose in live.py (docstring 'until cancelled' at :277 and a 'Cancellation' header  | DP-09 MUST: long-running ops take an explicit CancellationToken-like parameter and a cancellation MUST raise a typed exception, not bare KeyboardInterrupt. The live-tail streamers are genuinely long-running and lack an explicit to |
| DP-10 | ✅ | The one backend-selection fallback — optional YARA — is NOT silent: ioc.py:269-272 catches ImportError and `LOG.info('YARA not installed; skipping IOC scan. Install via ...')` then retu | DP-10 MUST: if a fallback path exists the active backend MUST be queryable and SHOULD be logged. The only backend-selection fallback (yara present/absent) is explicitly logged at INFO with a remediation hint — observable, not sile |
| DP-11 | N/A | No asyncio anywhere in src/: grep for asyncio/create_task/async def/await returned zero hits. The library is fully synchronous (streaming generators + subprocess.Popen, not coroutines). | DP-11 governs asyncio.create_task ownership. There is no async code in this library, so the rule has no surface to bind. N/A by shape, not a gap. |
| DP-12 | ✅ | [verify-upheld] Every datetime is tz-aware UTC at boundaries: case.py:116 `datetime.now(UTC)`, render.py:90 `datetime.now(UTC)`, manifest.py UTC, capture/{memory,screenshot,filesystem,n | Tried to refute by grepping for any naive datetime construction or utcnow — none exist. All datetimes are UTC-aware at boundaries, ISO-8601 serialised, and DTZ lint is enabled. Meets DP-12/TM rules. Upheld. |
| DP-13 | ✅ | [verify-upheld] Files via `with`: network.py:499 `with pcap_path.open('rb')`, ioc.py:108/216, manifest.py:246, capture/{memory:67,screenshot:66,filesystem:477}, render.py:99 `with os.fd | Tried hard to refute on the security-sensitive paths: confirmed the pcap reader closes via the with-block on generator GC/abandon; confirmed the foreground tcpdump subprocess is escalated SIGTERM→SIGKILL in finally; confirmed the  |

### ERROR_HANDLING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| EH-01 | ✅ | ~30 raises in src/mgf/vm/forensics/ all use OperationError (store.py:102-239, archive.py:72, capture/_nbd.py:105-160, capture/filesystem.py:144-467, capture/network.py:152-380, live.py: | [verify-upheld] Every domain error is an AppError subclass via the mgf.common hierarchy. The single ValueError is the conventional Python bad-argument signal for caller misuse (a programmer-error precondition), NOT a third-party e |
| EH-02 | ✅ | Third-party translated at owning boundaries: analysis/filesystem.py:77/102 (OSError/json.JSONDecodeError->OperationError), store.py:143/158 (OSError/yaml.YAMLError/KeyError/TypeError/Va | [verify-upheld] Ruthless check on the untrusted-input boundary: dpkt parsing is the highest-risk third-party call. _process_packet's dpkt.Ethernet/IP/TCP/DNS calls are wrapped by the per-packet try/except (network.py:162-175) and  |
| EH-03 | ✅ | 16 `from e` on translated re-raises: capture/network.py:198/310/312, store.py:143/158, live.py:395/402, manifest.py:180, capture/_nbd.py:105/160, analysis/filesystem.py:77/102, capture/ | [verify-upheld] Spot-checked the boundary: every `raise X from e` sits inside an `except ... as e:` (e.g. network.py:305-314 both branches chain `from e`; analysis/filesystem.py:77/102 chain the OSError/JSONDecodeError). Fresh rai |
| EH-04 | N/A | mgf-vm-forensics is an analysis LIBRARY: src/mgf/vm/forensics/ ships analyzers/capture/case-types consumed by the vmanager app/tooling. No __main__.py, no Click group, no sys.excepthook | EH-04 binds 'every ENTRY POINT' (CLI/process/threads/asyncio/Qt). A library has no entry point; the consuming app owns the top-level handler. Per the documented carve-out (no GUI/CLI/service of its own), EH-04 does not apply. N/A. |
| EH-05 | N/A | No crash reporter in src/ (no observability/crash_reporter, no report_now usage). The library raises typed OperationError up to its caller; the consuming vmanager app owns crash-report  | EH-05 (local crash report + remote sink on unhandled exception) is an entry-point/process concern. A consumed library has no process to crash-report for; it propagates typed errors to the owning app, which installs the reporter (m |
| EH-06 | N/A | No GUI/Qt code in src/mgf/vm/forensics/ (no QThread/CoreWorker/PySide6; capture/screenshot.py is a capture helper, not a UI). The library is headless; the vmanager GUI wraps these funct | EH-06 governs GUI worker threads (QThread.run catching every exception, error(str) signal). This library has no GUI workers. The docstrings (e.g. archive.py) note the GUI worker reuses the code path but the worker itself lives in  |
| EH-07 | ✅ | Exactly two broad `except Exception` (analysis/network.py:175, :312) plus suppress() cleanup (live.py:414/419/425 contextlib.suppress(OSError,ProcessLookupError)/(TimeoutExpired,OSError | [verify-upheld] Both broad catches are EH-07(b) best-effort observer loops over UNTRUSTED packet bytes that log/count/continue; cleanup suppress() are EH-07(a) finally/suppress paths. Minor deviation: EH-07(b) wants the invariant  |
| EH-08 | ✅ | Typed errors with non-trivial state carry machine-readable fields: CommandError (raised from mgf.vm.core.common.shell.run_command, used in _nbd.py) carries argv/returncode/stderr per th | [verify-upheld] EH-08 requires machine-readable fields on exceptions that HAVE non-trivial state ('e.g. CommandError.argv, SpecValidationError.errors'). The boundary errors that own structured state (subprocess) go through run_com |
| EH-09 | ✅ | User-facing messages end in actionable steps: capture/filesystem.py:140 'Stop the VM first (`vmanager vm stop {vm_name}`)...', :218 'Run `vmanager forensics capture filesystem-baseline` | [verify-upheld] Both halves of EH-09 hold: messages carry what+why+where+how-to-fix, AND §9's anti-rot guard (each hint pinned by a STABLE SUBSTRING, e.g. match='--purge', not the whole message) is present across multiple test fil |
| EH-10 | ✅ | Best-effort observers honour never-raises: analysis/network.py:162-182 per-packet loop catches every exception, records to errors[] bounded to 50, continues; failures observable via Net | [verify-upheld] The observer-shaped code (per-packet analyzer, terminate cleanup, per-target YARA) catches broadly, surfaces via errors-tuple / suppress() / WARNING log, and continues — the EH-10 never-raises invariant. _terminate |
| EH-11 | N/A | grep for retry/backoff/jitter found only: _nbd.py:260 a single inline fallback ('If noload was rejected, fall through and retry plain ro') — one alternative attempt, not a retry LOOP; a | EH-11 governs 'Retries' (exponential backoff + full jitter + finite cap). This library performs no multi-attempt retry against any external system — captures are one-shot subprocess spawns; the _nbd.py mount has a single mode-fall |
| EH-12 | N/A | _nbd.py:90 ('Idempotent. modprobe nbd...') and network.py:250/331 (capture start/stop idempotent: missing PID file is fine) document idempotency where it matters. No write/send to an ex | EH-12 binds 'operations against external systems with multi-attempt cost (writes, sends)' paired with retries. There is no retried external write/send; the local operations that COULD run twice (modprobe, capture stop) are already |
| EH-13 | N/A | No external network/RPC call that could amplify load on a struggling upstream: the library shells out to LOCAL tools (tcpdump, qemu-nbd, modprobe) and reads/writes LOCAL files. No HTTP/ | EH-13 governs upstream-amplifying load (unbounded retries against an overloaded remote service). This library makes no calls to a shared external service that could be deepened into an outage — all I/O is local subprocess + local  |

### LOGGING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| LG-01 | ✅ | All logging modules acquire a module-scope logger via an explicit canonical hierarchical name: render.py:41, store.py:53, live.py:66, archive.py:30, capture/_nbd.py:60, capture/network. | [verify-upheld] LG-01 §2.1 explicitly permits an explicit hierarchical string literal in lieu of __name__. Every logger is module-scope, canonical `mgf.vm.forensics.*`, no bare-root calls, no per-call allocation. Logger-less pure- |
| LG-02 | N/A | No entry point of its own: grep for setup_logging/basicConfig/click.group/def main/app.run in src/ = NONE. pyproject describes a 'Pure-Python analysis library' consumed by the vmanager  | LG-02 binds 'exactly one place per entry point (CLI main, GUI app.run, service entry)'. A consumed analysis library has no entry point and configures nothing — the consuming app owns single-point setup + JSON-when-not-TTY. Carve-o |
| LG-03 | N/A | grep for trace_id/span_id/opentelemetry/otel in src/ = NONE. The library installs no formatter; the standard formatter (mgf.common.observability.JsonFormatter) attaches OTel context aut | LG-03 says 'the standard formatter adds these automatically'. The library does not own the formatter or any handler; it emits LogRecords into the consumer's configured hierarchy, where trace context is attached. Nothing for the li |
| LG-04 | N/A | No formatter/redactor owned in src/ (no JsonFormatter/Redactor refs). Logged fields are non-secret primitives only: str(path), hex(magic), pid, size_bytes, fstype, str(error). No passwo | LG-04 places redaction in the formatter 'before any sink sees them' — a consumer-side setup concern. The library neither owns a formatter nor logs any secret-bearing field, so there is nothing to redact at this layer. N/A for the  |
| LG-05 | ✅ | Every live-exception site that names a failure attaches the exception via exc_info=True: render.py:72, store.py:191, capture/network.py:274, capture/network.py:329. grep for the `LOG.er | [verify-upheld] Ruthlessly checked the security/subprocess swallow sites: _nbd.py:174/264/283 (qemu-nbd/mount/umount) and ioc.py:319 (YARA) pass error=str(e) in extra WITHOUT exc_info — but these are recovered WARNINGs at a subpro |
| LG-06 | N/A | No sink/handler wiring in src/ (no RotatingFileHandler/StreamHandler/basicConfig). The library emits into the logging hierarchy and inherits the consuming app's handlers/aggregator. | LG-06 (rotating-file / stdout / remote sink) is satisfied by the consumer's setup_logging; a consumed library configures no sinks and must not. Per the per-kind matrix, 'library' takes LG-06's rotating-file branch — but that branc |
| LG-07 | N/A | grep for PySide/QDialog/QtWidgets in src/ = no GUI. The package is a headless analysis library. | LG-07 is gui-app-only per the per-kind matrix. No GUI surface exists. N/A. |
| LG-08 | ✅ | All parameterised sites use lazy %-args: capture/network.py:153 ('reclaiming stale PID file at %s', pid_path), :284 ('...pid %d is alive...', pid), ioc.py:276/291/320. grep for f-string | [verify-upheld] No eager f-string logging anywhere; every variable is either a lazy %-arg or promoted to extra={}. (The free-form f-strings at live.py:121/128/130 are display-string assembly for the live-stats panel return value,  |
| LG-09 | ✅ | Structured facts are uniformly promoted via extra={} (not concatenated): archive.py:89 (case_name/target_path/size_bytes), render.py:71/111, store.py:190 (path), capture/filesystem.py:3 | [verify-upheld] Facts go to extra={} for the formatter to surface as top-level keys; lazy %-templates stay stable. No buried key=value concatenation in messages. Reserved-name collision check: no extra key uses a reserved LogRecor |
| LG-10 | N/A | grep for systemd/JournalHandler/service entry in src/ = NONE. No daemon/timer/background-task entry point. | LG-10 is service-kind-only (headless entry point configures journald). The library has no entry point of any kind. N/A. |
| LG-11 | ⏳ | grep for the `operation` extra field across src/ = ZERO. Functions that clearly cross storage/process boundaries and exceed 100ms log no start/end pair: analyze_pcap (render.py:63), cas | LG-11 is a MUST for any function ≥100 ms OR crossing a process/network/storage boundary, precisely so flows are reconstructable from logs without the trace backend. dpkt pcap parsing, tar archive creation, qemu-nbd mounts, full-tr |
| LG-12 | ✅ | All ~15 log call sites across the library use cheap emission: lazy %-args + extra={} dicts, never f-strings (grep for LOG.<lvl>(f" / f' returned zero hits), never json.dumps or large .j | LG-12 binds 'all' kinds and requires cheap emission (≤100µs/record) with heavy formatting/IO pushed into the handler. The library satisfies this: no eager f-string formatting, no serialization, no large traversals at call sites, a |
| LG-13 | ✅ | The only genuine per-event/inner-loop log candidate is live.py:164 (per-pcap-chunk parsing) — but it is one-shot: it sets counter.disabled=True + counter.pcap_header_seen=True and the f | [verify-upheld] LG-13 targets high-frequency sites that could flood every sink under a runaway condition. The one per-chunk path is explicitly latched off after a single record; remaining WARNINGs are per-failure and naturally bou |
| LG-14 | N/A | grep for contextvars/ContextVar/correlation in src/ = NONE. The library is synchronous, single-pass analysis code with no async tasks or correlation-owning boundary; correlation context | LG-14 binds custom correlation context to contextvars at the boundary that OWNS the correlation (CLI command, HTTP handler, scheduler tick). A consumed library owns no such boundary and introduces no correlation fields. N/A at thi |
| LG-15 | ⏳ | Only 2 of the boundary/security/lifecycle log sites carry a stable `event` field — capture/network.py:158 + :291, both 'network_capture.stale_pid_file'. The remaining notable-boundary e | LG-15: every INFO+ record at a notable lifecycle/security/boundary event MUST carry a stable event field AND that name MUST be documented in docs/reference/EVENTS.md ('an event name without an EVENTS.md entry is a defect — the reg |
| LG-16 | ⏳ | All WARNINGs satisfy WHAT (past-tense declarative — 'qemu-nbd --disconnect failed', 'mount failed; skipping partition', 'capture PID file unreadable; treating as stopped') and WHY (stru | LG-16 is a MUST at WARNING+ and requires WHAT + WHY + HOW (an actionable hint in the message OR a hint field whenever remediation is known). Several warnings have a clear known remediation (e.g. unreadable PID file, mount failure  |
| LG-17 | N/A | There are ZERO ERROR-level records in the codebase (all failure sites are recovered LOG.warning). grep for `fingerprint` = NONE. No grouping backend (Sentry/Datadog/OTLP-APM) is in scop | LG-17 is a SHOULD that applies to ERROR records flowing into a grouping aggregator, with an explicit coverage carve-out when no grouping backend is in scope. With no ERROR sites and no owned egress to a grouping backend, there is  |

### CONFIGURATION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| CF-01 | N/A | grep over src/ for BaseSettings/BaseAppSettings/pydantic_settings/Settings = 0 hits; grep for os.environ/getenv in src/ = 0 hits. The package is a pure analysis library (analysis/, capt | CF-01 (Applies to: consumer-owned-config) governs a typed pydantic-settings Settings object. This library has none and reads no env vars; the rule's subject does not exist here. N/A by shape (not a gap). |
| CF-02 | N/A | No Settings object, no .env, no CLI flags, no env-var reads in src/ (grep = 0). Layered precedence has nothing to layer. | CF-02 precedence applies to a config object built from layered sources. The library has no such object; precedence is vacuous. N/A by shape. |
| CF-03 | ✅ | [verify-upheld] case.py:39 CASE_SCHEMA_VERSION=1 + case.py:124 schema_version field; store.py:147-153 raises OperationError when raw schema_version != CASE_SCHEMA_VERSION (refuses unkno | Refute attempt: tried to find an owned on-disk format with no version field or a silent-accept-on-mismatch path. Every persisted artifact (case.yaml, manifest.json, baseline.json, diff.json) carries schema_version and fails loud ( |
| CF-04 | ✅ | [verify-upheld] store.py:_save_yaml (263-280): tempfile.mkstemp(dir=target.parent) → os.fdopen write → tmp.chmod(0o600) → tmp.replace(target), with tmp.unlink(missing_ok=True) on OSErro | Refute attempt: looked for (a) a global-temp path breaking os.replace atomicity — not present (mkstemp dir=target.parent); (b) missing 0o600 — present at store.py:275; (c) eager module-load expanduser — none (both defaults are wra |
| CF-05 | N/A | grep for password/secret/token/vault/keyring/api_key in src/ surfaces only sudo password-PROMPT text in _nbd.py/live.py (operational descriptions of qemu-nbd needing root), no stored cr | CF-05 requires secrets to live in a Vault, not config. This library stores no secrets in any owned file — it persists forensic metadata and SHA-256 hashes only. There is nothing to route through a vault. N/A by shape. |
| CF-06 | ⏳ | manifest.json / baseline.json / diff.json use frozen pydantic BaseModel with ConfigDict(extra='forbid') (manifest.py:72/82, filesystem.py:72/83/100) → unknown keys raise. BUT case.yaml  | CF-06 (consumer-owned-config) requires loading an unknown key from an owned format to RAISE (typo protection). The JSON satellite formats meet this via extra='forbid', but the primary owned format case.yaml does not: _from_dict ac |
| CF-07 | ✅ | [verify-upheld] manifest.json: Manifest frozen extra='forbid' BaseModel (manifest.py:79-86) with schema_version, saved/loaded via VersionedFileStore (atomic + version-checked + unknown- | Refute attempt: the most common §4 gap is a satellite store using plain write_text (non-atomic). I checked every save path: manifest/baseline/diff go through VersionedFileStore (temp+rename), and case.yaml uses store.py's explicit |
| CF-08 | N/A | No Settings object exists (grep = 0). CaseStore (store.py:84-96) is constructed with an explicit root and injected by callers; there is no module-global singleton config (no module-leve | CF-08 (loaded-once-at-entry-point, no module singletons) applies to a Settings object. There is none. The library's stateful object (CaseStore) already follows constructor injection (root passed in). N/A by shape, with the injecti |
| CF-09 | N/A | No CLI in src/ (grep for click/argparse/--config = 0). This is a library consumed by the vmanager app/tooling; the --config flag is the consuming app's responsibility. | CF-09 (consumer-owned-config) requires a --config CLI flag on an application that loads config. This library ships no CLI and no Settings; the rule's subject is absent. N/A by shape (library, not app). |
| CF-10 | ✅ | [verify-upheld] tests build state under tmp_path with explicit values: test_render.py:31 CaseStore(root=tmp_path/'forensics'); test_archive.py:33 CaseStore(root=tmp_path/'forensics'); t | Refute attempt: I went after the rationale's 'grep = 0' assertion and it is factually wrong — there are two setenv('HOME') calls. But CF-10's MUST is 'no env-var leakage / explicit isolated construction', and those setenv calls st |
| CF-11 | N/A | No Settings object with renameable config fields (grep = 0). The owned formats are versioned via schema_version (CF-03); a field rename in case.yaml/manifest.json would be handled by th | CF-11 (rename via Field(alias=)/validation_alias) is a pydantic-Settings facility for a config schema. This library has no Settings schema and no field rename in flight; its on-disk formats use schema-version migration (CF-03) for |
| CF-12 | N/A | No config field with multiple mutually-exclusive backend variants. ArtifactKind (case.py:65-80) is a StrEnum discriminator on the Artifact data record, not a config-backend selector, an | CF-12 requires discriminated unions only where a config field has mutually-exclusive backend variants. The library has no such config field (no vault/cache/queue backend choice in any owned config). §14.2 ('when NOT to use' — only |

### SECURITY_STANDARD

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| SC-01 | N/A | grep across src/ for secret material, vault usage, tokens, passwords: none. The library handles no authentication secrets, API keys, or credentials. .gitleaks.toml present (3129B SECURI | A forensic-analysis library that parses evidence and writes case metadata holds no secret material — no passwords, API keys, OAuth tokens, or signing keys. The vault boundary (SC-01) has no surface here. Structurally N/A for this  |
| SC-02 | ✅ | [verify-upheld] grep -rn 'shell=True\|os.system\|os.popen' src/ → NONE. Every subprocess site uses an explicit argv list: live.py:382-389 _spawn(argv,...) with default shell=False; capt | Upheld. All subprocess invocation uses argv lists with the default shell=False. No string-concatenated commands, no os.system, no user-template f-strings building a command. The bpf filter (network.py:177) is appended as a single  |
| SC-03 | ✅ | [verify-upheld] store.py:140 yaml.safe_load, :269 yaml.safe_dump. json.loads at network.py:507, timeline.py:182, filesystem.py:74. grep -rn 'yaml.load(' src/ → NONE; grep -rn 'pickle' s | Upheld. No yaml.load, no pickle anywhere. Deserialization of the case.yaml (which could be attacker-modified inside a case dir) is safe_load only — no !!python/object RCE path. JSON loads are paired with schema-version + isinstanc |
| SC-04 | ✅ | [verify-upheld] store.py:260-280 _save_yaml: tempfile.mkstemp(dir=target.parent) + os.fdopen + yaml.safe_dump + tmp.chmod(0o600) + tmp.replace(target), with tmp.unlink on OSError. rende | Upheld. Files carrying private case data (case.yaml, reports, pid/meta) are written atomically (temp+chmod+rename) at 0o600; case dirs at 0o700. Matches the canonical SC-04 / §5.1 pattern, and crucially chmods the temp file BEFORE |
| SC-05 | ✅ | [verify-upheld] Only crypto is integrity hashing: ioc.py:211-213 hashlib.md5(usedforsecurity=False)/sha1(usedforsecurity=False)/sha256(); manifest.py:245, screenshot.py:65, memory.py:66 | Upheld. No DIY crypto, no PRNG misuse. MD5/SHA-1 appear only as forensic artifact fingerprints (IOC matching against threat-intel feeds that publish MD5/SHA-1) and are correctly flagged usedforsecurity=False — §6.3 permits documen |
| SC-06 | N/A | grep -rn 'verify=False\|CERT_NONE\|AutoAddPolicy' src/ → NONE. No httpx/requests/aiohttp/paramiko/ssl import anywhere in src/. The library makes no outbound TLS connections — it reads l | No outbound network client of any kind. There is no TLS connection to verify or disable. SC-06's surface is empty for an offline forensic-analysis library. |
| SC-07 | ✅ | [verify-upheld] grep 'timeout' src/ → only live.py:417 proc.wait(timeout=2.0) and :426 proc.wait(timeout=1.0) (SIGTERM→SIGKILL escalation in _terminate). stop_network_capture polls with | Upheld. There is no 'blocks forever' external-request path. The only blocking waits (process termination) carry explicit finite timeouts with a kill escalation; the capture-stop settle loop is bounded. Long-running detached captur |
| SC-08 | ⏳ | [verify-refuted: unbounded-memory paths on a crafted pcap]. Streaming (iter_packets:499 'with open' + yield from reader), per-packet try/except isolation (network.py:162-182), and froze | Refuted on the specific claim 'no unbounded memory on a 5GB or crafted pcap.' The boundary IS typed/streamed/exception-isolated and the sampled lists are genuinely capped, but talkers (per-unique-pair) and the errors list (the 50- |
| SC-09 | N/A | No Redactor / setup_logging / formatter is constructed in src/. LOG.info/warning calls pass non-secret extras: store.py:189 path, network.py:153-160 capture_id/event, archive.py:87-93 c | SC-09 (redact sensitive fields before the log sink) presupposes the application handles secret-bearing fields. This library logs only non-secret forensic metadata and constructs no formatter/redactor of its own (it inherits mgf-co |
| SC-10 | ✅ | uv.lock IS committed (179184 bytes, present at repo root). DEPS.md documents dpkt pin (SH-05/SH-06 tight upper bound). BUT there is NO .woodpecker.yml (ls → No such file or directory) a | Half-met: the lockfile clause is satisfied (uv.lock committed), but SC-10's CI-vuln-scan-on-every-PR clause is unmet because the project has no CI pipeline. Per the carve-out brief, the entire publish/CI chain is ⏳ (not N/A) for t — FIXED 2026-06-09: pip-audit vuln-scan in CI |
| SC-11 | ✅ | [verify-upheld] Exception messages carry only non-secret context: store.py:142 (path, exc), :149 (path, schema_version), :104 (case name), archive.py:71 (target_path), live.py:391 (bina | Upheld. Because the library handles no secrets (see SC-01 N/A), the SC-11 leak surface is structurally empty — tracebacks and exception messages are built from filesystem paths, case names, counts, and parser-error strings only. T |
| SC-12 | ✅ | [verify-upheld] SECURITY.md present at repo root (3129 bytes). Contains a 'Reporting a vulnerability' section (private email bassam@magogi.org with subject prefix), acknowledgement targ | Upheld. A root SECURITY.md exists with the security contact, embargo/disclosure timelines, and a scope/threat-model section. It is materially richer than the §11.1 template minimum (it customises the threat model to this library's |
| SC-13 | N/A | No cryptographic keys are generated, derived, or stored anywhere in src/ (no KDF, no PBKDF2/Argon2, no Fernet, no key files, grep for KEY= literals: none — only hashlib digests). The li | SC-13 (keys at rest derived via KDF / kept in keyring) has no surface: the library holds no cryptographic keys at all. The only crypto is integrity hashing (SC-05). Structurally N/A. |
| SC-14 | N/A | No authentication path exists — grep for auth/login/password verification: none. The library parses evidence and manages local case directories; it never authenticates a person or servi | SC-14 (no auth-failure enumeration, rate-limit/backoff) presupposes an authentication boundary. This library has none — there is no user/password/key check to make uniform or to rate-limit. Structurally N/A. |
| SC-15 | ✅ | archive.py:84 only WRITES tarballs (tarfile.open(target_path, mode="w:gz") + tar.add) — there is NO archive/zip extraction path anywhere in src/ (grep for extractall/extractfile/namelis | The premise binds — this library constructs FS paths (case dirs, mount sub-paths) from external input. No untrusted-archive extraction exists (so the worst traversal vector is absent), and the store name check does in practice rej — FIXED 2026-06-09: CaseStore.case_dir now resolve()+is_relative_to containment-checks the case path (defence-in-depth beyond the string check) — released 0.1.6 |
| SC-16 | ✅ | Privileged ops DO exist (the prompt's "likely N/A" is falsified by grep): capture/_nbd.py prefixes `sudo -n` for `modprobe nbd` (_nbd.py:96), `qemu-nbd --connect`/`--disconnect` (_nbd.p | The premise binds (real sudo/privileged escalation present). SC-16 requires privileged ops to be (a) documented per call site, (b) justified by comment, (c) excluded from layers that don't need them. (a)+(b): every privileged bina |
| SC-17 | ⏳ | grep -rn 'audit' src/ → NO match anywhere. Security-relevant actions ARE performed but emit no audit=true record: subprocess spawn against user-controlled argv (network.py:186 tcpdump w | Unmet. SC-17 requires a structured INFO record with audit=true for every security-relevant action — and this library performs several (user-controlled-argv subprocess invocation, sudo privilege escalation, case-file writes) yet em |
| SC-18 | ✅ | Published as mgf-vm-forensics 0.1.5 with uv.lock committed and a dist/ dir present, so a tagged-commit build from declared deps is plausible. BUT there is NO .woodpecker.yml / CI — noth | Unmet via the CI clause: SC-18 (releases reproducible from a tagged commit, build metadata recorded, verified in CI per the §15 matrix) cannot be satisfied without the publish pipeline this project lacks. Lockfile + pyproject make — FIXED 2026-06-09: .woodpecker sbom step (reproducible CycloneDX) |

### TESTING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| TS-01 | ⏳ | tests/ mirrors src/ at tests/unit/mgf/vm/forensics/ but the mirror is INCOMPLETE: src has analysis/network.py, analysis/filesystem.py, analysis/ioc.py, analysis/timeline.py, capture/{fi | The 1:1 mirror is only approximate (combined test files) and the MUST also requires a CI mirror-lint with a documented allow-list, which is absent because there is no CI. Gap, not a decline. |
| TS-02 | ✅ | [verify-upheld] `.venv/bin/python -m pytest -q` ran fully offline in 0.63s. Network tests build in-memory pcap bytes with dpkt and read from tmp_path (test_analysis_network.py); the onl | Refute attempt failed: the suite is genuinely hermetic; dpkt parses in-memory bytes, all external I/O goes through injected fakes. |
| TS-03 | ✅ | [verify-upheld] tmp_path used across 12 test files; CaseStore constructed per-test with root=tmp_path. The only process-global mutations found are guarded: test_analysis_ioc.py:28 and t | Refute attempt failed: every global mutation is save/restored (monkeypatch or explicit try/finally); no order-dependent shared state. (pytest-randomly not installed, but the MUST is isolation, which holds.) |
| TS-04 | ✅ | [verify-upheld] pyproject.toml line 103: `filterwarnings = ["error"]`. Suite passes (modulo the 2 acknowledged PUBLIC_API pins) under the escalation, so no unhandled deprecation/Resourc | Set exactly as the MUST requires; verified at the source line. |
| TS-05 | ✅ | [verify-upheld] pyproject.toml line 102: `addopts = "--strict-markers --strict-config"`. Both flags present. The single custom marker `fixtures` is declared under [tool.pytest.ini_optio | Both flags present and the only used marker is registered; refute attempt failed. |
| TS-06 | ✅ | pyproject.toml line 109: `fail_under = 80` is set under [tool.coverage.report], but there is NO CI to enforce it (no .woodpecker.yml), the suite is not run with --cov-fail-under in any  | The MUST is the gate firing on every CI job; with no CI the floor is never enforced. Gap. — FIXED 2026-06-09: CI pytest --cov=mgf.vm.forensics (fail_under=80) |
| TS-07 | ⏳ | 42 pytest.raises sites exist and OperationError messages are asserted (e.g. test_archive.py:72 match='refusing to overwrite', test_capture_network.py:174 match='apt install tcpdump'). B | Message/raise paths are tested but the full (a)/(b)/(c) per-exception contract is not pinned. Partial; recorded as gap. |
| TS-08 | N/A | No EH-02 translation seam (third-party-error → typed-error catch-all) found in the forensics library. Network analyzer wraps dpkt parse errors with bare `except Exception` / `except (In | No translation seam present, so the rule has nothing to pin here. |
| TS-09 | N/A | No EH-10 'best-effort observer' loop (a callback/observer list iterated where one raising entry must not abort the loop) found in src. The packet-skip loops are dpkt-parse-error toleran | No best-effort-observer construct exists in this library. |
| TS-10 | ⏳ | SECURITY is central (parses untrusted pcap via dpkt; archive.py). Defensive coverage exists — test_analysis_network.py has test_empty_pcap_returns_zero_packets and test_non_http_payload | The security-relevant rules that ARE mechanically pinnable here (malformed-pcap robustness) lack a negative-case pin test. Gap. |
| TS-11 | N/A | No GUI/Qt worker code anywhere in src (no PySide6/QThread/pytest-qt). This is a headless analysis library consumed by the vmanager app. | No GUI workers exist; rule does not apply. |
| TS-12 | N/A | No async code in src (no `async def`/`await`/asyncio); pyproject has no pytest-asyncio/anyio and no asyncio_mode. Capture/analysis are synchronous. | No coroutines to test; rule does not apply. |
| TS-13 | ⏳ | hypothesis>=6.100 is a declared dev dependency in pyproject, but grep found NO @given/@settings/@example usage and no _strategies.py module anywhere in tests/. The library has clear rou | Round-tripping code exists and Hypothesis is even installed, yet no property tests cover it. Gap. |
| TS-14 | N/A | No PERFORMANCE.md budget is owned by this library and no @pytest.mark.perf test exists; `perf` is not even a registered marker. No per-record/per-op latency budget is asserted anywhere  | No performance budget is in scope for this library, so there is no benchmark to require. (If a budget is later declared it would become ⏳.) |
| TS-15 | N/A | No real-backend smoke layer and no live/realbackend marker; the only registered marker is `fixtures` (replay against pre-recorded pcap files under tests/fixtures/, which is hermetic, no | No real-backend smoke surface exists for this analysis library; rule does not apply. |
| TS-16 | ✅ | [verify-upheld] Ambiguous asserts carry context strings: test_capture_network.py:438 `assert events, "expected a WARNING about the stale PID file"` and :477 `"expected a stale-PID-file  | Refute attempt failed: failure messages are self-explanatory — context strings where bare asserts would be ambiguous, named-field equality elsewhere. |
| TS-17 | ✅ | [verify-upheld] A single tests/conftest.py at the root is the only conftest in the tree (no per-directory conftests). Per-file fixtures live in their file (e.g. `store` fixture in test_ | Refute attempt failed: fixtures are discoverable via the conftest hierarchy and never imported across packages, matching the MUST. |
| TS-18 | ⏳ | No test is marked @pytest.mark.slow and `slow` is NOT among the registered markers (only `fixtures` is). The suite runs in 0.63s so no test currently exceeds 1s, but TS-19 explicitly sa | No current slow test, but the defensive `slow` registration the standard calls for is absent. Minor gap. |
| TS-19 | ⏳ | pyproject markers = ["fixtures: ..."] — only the project-specific `fixtures` marker is registered. The standard's defensive registrations are missing: no `slow` marker (TS-18/TS-19 SHOU | Existing marker is registered, but the SHOULD-register `slow` (and the layer markers if layers were added) are absent. Partial. |
| TS-20 | ⏳ | tests/ contains only `unit/` (tests/unit/mgf/vm/forensics/). There is no tests/integration/, tests/e2e/, or tests/smoke/ directory. All 233 tests live in the unit layer; cross-module fl | The four-directory partition is not realized; only the unit layer exists. Gap (a published lib with cross-module chains would benefit from integration/e2e). |
| TS-21 | ⏳ | This IS a published library (mgf-vm-forensics 0.1.5) with an __all__ in src/mgf/vm/forensics/__init__.py (line 71) and a PUBLIC_API pin test, so the smoke-layer requirement APPLIES. But | Published wheel surface with no smoke layer; rule applies and is unmet. Gap. |
| TS-22 | ⏳ | No tiered-run support: no dependency on mgf-test-supervisor, no --tier/--budget runner, no tier marker overlay. Tests run only via plain `pytest`. As a published federation library it i | The federation tiering contract is absent. Gap (deferred-for-cause candidate — typically gated on CI + supervisor adoption). |
| TS-23 | ⏳ | No .woodpecker.yml at all, so there is NO CI run that survives/structures a pytest crash/hang/signal-death. No mgf-test-supervisor dependency, no subprocess supervisor, no .gates/last-r | No CI and no supervisor mechanism: the crash/hang/signal-survival contract is entirely unmet. Gap. |
| TS-24 | ⏳ | No structured per-test records: no .gates/last-run/run.jsonl, no use of mgf.common.testing.TestRecord, no JSONL emission. No supervisor wired to produce them. | Structured per-test JSONL records are not produced. Gap (depends on supervisor/CI adoption). |
| TS-25 | ⏳ | No flaky-test auto-quarantine: no .gates/quarantine.json, no --quarantine-attempt support, no supervisor. (TS-25 is L2; L1 projects MAY defer — but this lib has not recorded a defer rat | Quarantine machinery absent and no recorded L1 defer; open gap. |
| TS-26 | N/A | The package is pure-Python (src/mgf/vm/forensics/*.py only); it ships no compiled artifact (no Zig/Go/Rust/C/Swift). Its runtime deps (mgf-common, mgf-vm-core, dpkt) are Python packages | No built-binary dependency to freshness-check; rule explicitly N/A for pure-Python libs. |
| TS-27 | ⏳ | No single-file failure bundles: no .gates/last-run/failures/<id>.txt, no supervisor _write_failure_bundle, no DIAGNOSING_HANGS.md. Failures surface only as raw pytest output (as seen in | The single-file explorable failure bundle is not produced (no supervisor/CI). Gap. |
| TS-28 | ⏳ | No per-test perf attribution: no --perf/MGF_PERF path, no perf_span usage, no per-test cProfile artifacts, no TestRecord.perf. (TS-28 is L2; L1 MAY defer — but no defer rationale is rec | Perf-attribution machinery absent and no recorded L1 defer; open gap (commonly deferred-for-cause as perf-infra). |

### API_DESIGN

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| AP-01 | ✅ | src/mgf/vm/forensics/__init__.py:71-106 explicit string-only __all__; every module/package boundary declares __all__ (verified by grep: __init__.py, case.py:30, store.py:46, manifest.py | [verify-upheld] Tried to refute on the 'sorted alphabetically' clause: the lists are NOT casefold-sorted, but they ARE deterministically default-sorted (ASCII codepoint order), which is exactly what Python's sorted() and the stand |
| AP-02 | ✅ | case.py:84-93 Artifact / 104-106 Case docstrings state intent + frozen/hashed-at-write + threat-model note; network.py:138-143 analyze_pcap documents the memory-bounding sample caps; st | [verify-upheld] §14 enforcement matrix scopes AP-02 to discipline + review + PUBLIC_API.md coverage (pydocstyle D intentionally off). Usage code examples are thin, but the docstrings are substantive (intent + invariants), matching |
| AP-03 | ✅ | pyproject.toml:3 version='0.1.5'; CHANGELOG.md follows Keep-a-Changelog + SemVer header; 0.1.1→0.1.5 history shows pin-walk MINOR/PATCH discipline; the 0.1.2 capture-signature change (d | [verify-upheld] SemVer in the 0.x window; the breaking capture-signature reshape landed in MINOR (0.1.2), which §4.3 explicitly permits pre-1.0. Met. (Note: a version-drift issue exists in PUBLIC_API.json/.md but that bears on AP- |
| AP-04 | N/A | grep across src/ finds zero DeprecationWarning / PendingDeprecationWarning / @deprecated / warnings.warn; CHANGELOG has no [Deprecated] or [Removed] entries. No public name has been dep | Adjudicated fresh (not in the ✅ batch). The deprecation-cycle MUST binds only once a deprecation exists; none have shipped (the 0.1.2 reshape was a direct 0.x break, not a deprecation). N/A until the first deprecation — mirrors th |
| AP-05 | ✅ | network.py:130-137 analyze_pcap(pcap_path, *, top_n_talkers=20, sample_dns=200, sample_sni=200, sample_http=200) — every tunable is keyword-only behind the * separator with a default; a | [verify-upheld] Public signatures use the AP-05-safe keyword-only-with-default shape and have been stable across the 0.1.3/0.1.4/0.1.5 line (those releases were pin-walks, no signature change). The only signature reshape (0.1.2) i |
| AP-06 | ✅ | analyze_pcap -> NetworkAnalysis (frozen dataclass, network.py:107); iter_packets is annotated Iterator[tuple[float, bytes]] (network.py:490) — a narrow contract, NOT a loose Iterable; b | [verify-upheld] Refute attempt: the prompt's claim cited 'network.py:505-507' for iter_packets, but the public return annotation is at line 490 (Iterator[tuple[float, bytes]]) — narrowly pinned to a concrete tuple, not widened to  |
| AP-07 | ✅ | store.py:32 imports OperationError from mgf.vm.core.exceptions; raises it at 102/105/125/138/143/146/153/158/167/208/215/239; store.py:134 docstring documents 'Raises :class:`OperationE | [verify-upheld] Ruthless SC check: public failure modes raise the project's typed OperationError; the untrusted-pcap per-packet parse path swallows dpkt exceptions into a typed, bounded(-ish) error list rather than crashing the an |
| AP-08 | ✅ | Loop over all src/**/*.py with grep -L 'from __future__ import annotations' printed NOTHING (every module has it; __init__.py:21, case.py:15, network.py:21, store.py:20, archive.py:10,  | [verify-upheld] Mechanical FA100/FA102 ruff gate passes clean and the future-annotations grep finds zero missing modules. The §14 enforcement mechanism is satisfied. Met. |
| AP-09 | ✅ | src/mgf/vm/forensics/py.typed exists; pyproject.toml [tool.hatch.build.targets.wheel] packages=['src/mgf'] walks the tree so the one-byte marker ships in the wheel; pyproject [tool.mypy | [verify-upheld] PEP 561 marker present at the package leaf and included by the hatchling tree-walk (same mechanism mgf-common relies on). Met. |
| AP-10 | ✅ | PUBLIC_API.md exists at repo root + PUBLIC_API.json companion + tools/generate_public_api_json.py generator + tests/unit/.../test_public_api_pin.py CI pin (the §2.4/§2.5 shape is fully  | Adjudicated fresh (not in the ✅ batch). The AP-10 MUST is 'the list MUST stay in sync with __all__ (CI check)'. The infrastructure is exemplary, but the contract doc is CURRENTLY OUT OF SYNC and its own sync pins are RED — PUBLIC_ — FIXED 2026-06-09: PUBLIC_API.md + generated PUBLIC_API.json (10 modules/100 names) + pin |
| AP-11 | ✅ | PUBLIC_API.md:16-23 declares the three tiers (Stable/Experimental/Internal) and states 'Everything below is Experimental at 0.1.0' — a blanket experimental tier consistent with the 0.x  | [verify-upheld] Tiers ARE declared, uniformly Experimental (the standard's prescribed 0.x starting tier). The stale `0.1.0` version header in PUBLIC_API.md is the same staleness defect captured under AP-10 (the doc wasn't regenera |
| AP-12 | ✅ | CHANGELOG.md present, Keep-a-Changelog + SemVer header, [Unreleased] block + per-release entries 0.1.1-0.1.5 with Added/Changed/Internal/Fixed/Security-style sections; the 0.1.2 capture | [verify-upheld] CHANGELOG follows the required format. The one breaking entry (0.1.2) carries the old/new shape prose. The four-piece migration recipe (explicit symptom + code before/after + why) is thin on the exact TypeError sym |
| AP-13 | ✅ | __version__ = '0.1.5' present at src/mgf/vm/forensics/__init__.py:23 AND mirrors pyproject.toml:3 version='0.1.5' — the dunder/pyproject pair IS in sync. HOWEVER, there is NO tests/unit | Adjudicated fresh (not in the ✅ batch). AP-13 has two halves: (1) __version__ present + mirrors pyproject — MET; (2) 'CI MUST lint that the two stay in sync' — NOT met (no pyproject↔__init__ sync test exists, and no CI exists to r — FIXED 2026-06-09: __version__ mirrors pyproject (0.1.6); CI tag-coherence asserts tag==pyproject==__version__ |
| AP-14 | ⏳ | .importlinter exists at repo root with two real contracts: 'forensics does not import any consumer application' (type=forbidden, line 31) + 'forensics internal layering points downward' | Adjudicated fresh (not in the ✅ batch). AP-14 §14 mechanism is 'CI MUST enforce via import-linter that consumer test fixtures only touch the public surface.' The contracts EXIST and are runnable locally (CHANGELOG shows lint-impor |
| AP-15 | N/A | No backward-compatibility shim modules exist: grep finds zero warnings.warn / DeprecationWarning / re-export-shim modules in src/. The 'transitional note' in PUBLIC_API.md:142-150 descr | Adjudicated fresh (not in the ✅ batch). AP-15 binds only when a back-compat shim survives removal of original code; this library ships none (no deprecated names → no shims). N/A — mirrors mgf-common's shim discussion where shims a |
| 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 | ✅ | No SCOPE.md exists (find -iname '*scope*' returns nothing under repo, docs/ has only claude/CLAUDE.md + inprogress/MGF_STANDARDS_CONFORMANCE.md). The closest artifact is README.md:48-65 | SP-01 requires an explicit published IN-scope list that serves as the bar new modules must clear. The README contents section names what currently ships but does not state scope criteria; there is no SCOPE.md. Smallest fix: add do — FIXED 2026-06-09: SCOPE.md §1 |
| SP-02 | ✅ | README.md:72-78 'What this library does NOT contain' lists two cuts (No VM driving → mgf-vm-libvirt/mgf-vm-firecracker; No GUI/CLI → consumers own it). This gives a partial OUT gesture  | SP-02 requires an explicit OUT-of-scope list where 'why we don't ship X' + 'what to use instead' are non-negotiable per entry. The README has only two bullets and the GUI/CLI bullet gives a 'what instead' (consumers) but the VM-dr — FIXED 2026-06-09: SCOPE.md §2 |
| SP-03 | ✅ | CONTRIBUTING.md 'Process' (lines 32-49) lists: file feedback, branch+four-gate CI, CHANGELOG entry, PUBLIC_API delta, reviewer. grep for 'scope\|§1\|§2\|§3\|adjacent\|reject' in CONTRIB | SP-03 requires every PR adding a new module or top-level function to justify against §1/§3 (and OUT PRs be rejected with an ecosystem pointer). The contributor process has no scope-creep gate. Smallest fix: add a CONTRIBUTING.md s — FIXED 2026-06-09: SCOPE.md §3 |
| SP-04 | ✅ | No §3 Adjacent-scope list exists (no SCOPE.md), and no documented promotion criterion tied to a consumer-demand signal. FEEDBACK.md exists (PAPER-NN flow, §2 'No papers filed yet') so a | SP-04 requires Adjacent-scope items to require a documented consumer-demand signal before promotion. Without a §3 list and a stated promotion criterion, the rule is unmet. Smallest fix: SCOPE.md §3 Adjacent list (e.g. memory-foren — FIXED 2026-06-09: SCOPE.md §4 |
| SP-05 | ✅ | README.md:74-77 the two OUT bullets: 'No VM driving — that's mgf-vm-libvirt / mgf-vm-firecracker' DOES cite siblings; 'No GUI/CLI — consumers ... own the command orchestration' cites 'c | SP-05 requires every OUT cut to name the ecosystem alternative. Partial citation exists for the VM-driving cut, but there is no structured OUT list and the GUI/CLI cut points only at 'consumers' generically. Smallest fix: as part — FIXED 2026-06-09: SCOPE.md §5 |
| SP-06 | ✅ | pyproject.toml:3 version = "0.1.5" — pre-1.0, no MAJOR boundary crossed yet, so no revisit was *due*. But SP-06 requires the policy to BE revisitable on every MAJOR release, which presu | SP-06 requires scope policy be revisited on every MAJOR release. At 0.1.5 no revisit is overdue, but the prerequisite — a documented scope policy with a versioning/revisit section — is absent, so the rule cannot be satisfied. Smal — FIXED 2026-06-09: SCOPE.md §6 |

### WORKFLOWS

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| WF-01 | ✅ | No CI config exists anywhere: `find` over the repo returns no `.woodpecker.yml`, no `.woodpecker/`, no `.github/`, no `.gitlab-ci.yml`, no Jenkinsfile. The only `lint-imports` reference | WF-01 MUST: every project has CI on every push to a long-lived branch and every PR. This is a published mgf-* library (mgf-vm-forensics 0.1.5) with no CI at all — every check today is a 'ship from local laptop' path, exactly what — 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) — runs on push/PR/manual |
| WF-02 | ✅ | No `.woodpecker.yml`. The four gates exist as tooling (ruff, mypy --strict, pytest, `.importlinter` at repo root with `forensics-is-a-leaf` + `forensics-layers` contracts) and are run m | WF-02 MUST: lint/typecheck/test/import-linter are CI-enforced and block merge on red. The gates are configured but enforcement is by human discipline, not CI/branch-protection. Smallest fix: add the four-step gate job to a workflo — FIXED 2026-06-09: lint-imports in the CI lint step |
| WF-03 | ✅ | pyproject.toml:2 `name = "mgf-vm-forensics"`, version 0.1.5 — a published mgf-* package. No workflow file exists, so there is no tag-driven publish step targeting the Codeberg registry. | WF-03 MUST: an mgf-* library has a tag-driven publish workflow targeting the Codeberg registry (`v<X.Y.Z>` tag triggers; non-tag pushes never publish). None exists. Smallest fix: add a `when: event: tag` / `ref: refs/tags/v*` publ — 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 → no `from_secret` reference to `codeberg_package_token` and no CI secret store wired. `.gitleaks.toml` exists at repo root (secret-scan config present) but there is no | WF-04 MUST: publish credentials live in the CI platform's secret store (`codeberg_package_token`, `write:package`), scoped to tag events. Since publishing currently happens off-CI (stale dist/ wheel), the token model isn't establi — 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 workflow file → no tag-coherence check runs in CI. Versions are currently coherent (src/mgf/vm/forensics/__init__.py:23 `__version__ = "0.1.5"` matches pyproject.toml:3 `version = "0 | WF-05 MUST: the publish workflow verifies `v<X.Y.Z>` tag == pyproject version == `__version__` before upload, aborting on mismatch. The coherence is fine today but unenforced. Smallest fix: add the tag-coherence shell step (per §4 — 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 build step. dist/ contains a stale hand-built wheel `mgf_vm_forensics-0.1.4-py3-none-any.whl` / `.tar.gz` (dated Jun 5) while the project is at 0.1.5 — concrete evidence that publ | WF-06 MUST: the uploaded wheel is the one built in the CI publish run; never reuse an earlier/dev-machine wheel. The presence of an out-of-date dist/ wheel is exactly the REL-02 drift this rule guards against. Smallest fix: build — 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 workflow file → no pre-publish smoke step. There is no ephemeral-venv install-the-wheel + import + `__version__`-assert anywhere (only manual gates referenced in README/docs). | WF-07 MUST: pre-publish smoke installs the freshly-built wheel into an ephemeral venv and imports the top-level package + `__version__`. Absent. Smallest fix: add the §3.2 smoke step (`python -m venv`, `pip install dist/*.whl`, as — 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 workflow file → no post-publish verification polling the Codeberg REST API `…/api/v1/packages/magogi-admin/pypi/mgf-vm-forensics/<version>` for 200. | WF-08 MUST: post-publish verify confirms the artifact appeared in the Codeberg registry via the Forgejo REST API (not pypi.org). Absent. Smallest fix: add the PKG-05 polling step after upload, failing if the polling window expires — 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 image/plugin pins to inspect. uv.lock IS present and checked in (179KB, repo root), satisfying the dependency-pinning leg, but the interpreter-i | WF-09 MUST: workflow files pin every external dependency (interpreter image, plugin actions, base-image digests). With no workflow file the rule is unsatisfiable; the lockfile half is done but the CI-image/plugin half is missing. — FIXED 2026-06-09: all CI images pinned (python 3.11/3.12/3.13-slim + gitleaks v8.30.1) |
| WF-10 | ✅ | No workflow file → no step graph, so no parallelisation of the four gates / matrix axes exists. | WF-10 MUST: independent CI steps run in parallel; only genuine dependencies (build→smoke→upload) are sequential. Unsatisfiable without a workflow. Smallest fix: in the new workflow, run lint/typecheck/test/import-linter as paralle — FIXED 2026-06-09: lint/typecheck/test/scan run in parallel |
| WF-11 | ✅ | Partial: uv.lock is checked in (repo root) which supports lockfile-based reproducible installs, but there is no CI run to be reproducible — no workflow declares its env/inputs. Cannot c | WF-11 MUST: a CI run is reproducible from its inputs alone. The lockfile substrate is in place, but with no workflow there is no reproducible CI run to assess/enforce. Smallest fix: have the new workflow install from `uv.lock` (uv — FIXED 2026-06-09: editable install with pinned index; lockfile-install deferred to federation policy |
| WF-12 | ✅ | No workflow file → no CI steps exist to surface exit codes / commands / log context, and no `\|\| true` patterns to audit (there is nothing to audit). | WF-12 MUST: every CI step surfaces its exit code, exact command, and log context on failure; no silent failures. Unsatisfiable without any CI. Smallest fix: author the workflow so each step is a discrete loud command with no error — FIXED 2026-06-09: no silenced failures (pip-audit --strict) |
| WF-13 | ✅ | `.woodpecker.yml` header: `# GENERATED — do not hand-edit. Source: mgf-standard/templates/woodpecker/`. | app-doctor ✓ WF-13 |

### RELEASING

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| REL-01 | ✅ | git cat-file -t v0.1.5 → commit (lightweight), not tag; v0.1.2/v0.1.3/v0.1.4/v0.1.5 all lightweight, only v0.1.1 is annotated. v0.1.5 commit e31b98f is an ancestor of HEAD e3f6c13 (HEAD | REL-01 requires every release to have an ANNOTATED v<version> tag on the build commit. The release commit is correctly identified, but v0.1.5 (and v0.1.2-4) are lightweight tags lacking author/date/message metadata and prone to GC — FIXED 2026-06-09: release tags re-annotated (v0.1.2-v0.1.5) on the same build commits + force-pushed (annotated, GC-safe; no content change). 0 lightweight v-tags remain. |
| REL-02 | ✅ | dist/ holds ONLY mgf_vm_forensics-0.1.4-py3-none-any.whl and -0.1.4.tar.gz (no second version, no overwritten pair). dist/ is gitignored (.gitignore line 11 'dist/', plus dist/.gitignor | [verify-upheld] REL-02 forbids reusing a version string across different wheel contents. The dist cache holds a single, lower version (0.1.4); the source tree is bumped to 0.1.5 ahead of any build; there is no overwritten or dupli |
| REL-03 | ✅ | PUBLIC_API.md:9-22 shows Version `0.1.0` and carries NO REL-03/PAPER-12 banner (grep for reflect\|ahead of\|pip show\|published\|registry\|master HEAD found no banner text, only symbol- | REL-03 requires (a) PUBLIC_API.md/.json reflect master HEAD with a banner telling consumers to verify via pip show + PUBLIC_API.json::__version__, and (b) the JSON __version__ track HEAD. Both fail: no banner, and the JSON __versi — FIXED 2026-06-09: PUBLIC_API.json regenerated to HEAD (0.1.6) + REL-03 master-HEAD banner on PUBLIC_API.md |
| REL-04 | ✅ | `grep -niE 'password\|token\|pypirc\|twine'` over pyproject.toml returns NOTHING. `git ls-files \| grep -iE 'pypirc\|\.env\|token'` returns nothing (no committed credential file). .giti | [verify-upheld] REL-04 is a storage-discipline negative invariant: tokens MUST NOT be in the repo, chat, or shared-shell env / workflow file. No token is committed anywhere, none appears in pyproject or any workflow, and gitleaks  |
| REL-05 | ✅ | Versions increment monotonically (CHANGELOG 0.1.1→0.1.2→0.1.3→0.1.4→0.1.5, distinct dated entries with distinct content). Tags v0.1.1..v0.1.5 all exist (v0.1.5 → e31b98f, the 0.1.5 bump | [verify-upheld] REL-05 immutability is by-policy on Codeberg, observable maintainer-side as: new-version-per-fix, no recorded delete-and-republish, no duplicate dist artifacts. All three hold here. The live-registry query (REST 20 |
| REL-06 | ✅ | No .woodpecker.yml (find/ls confirm absent) — no CI publish chain. dist/ contains only mgf_vm_forensics-0.1.4-py3-none-any.whl + .tar.gz (built Jun 5), no 0.1.5 artifacts; PUBLIC_API.js | REL-06 mandates the strictly-ordered flow bump→regen JSON→build→test→twine check→commit→tag→push→upload→REST verify. For 0.1.5 the regen-JSON and build-for-released-version steps are missing (JSON+dist frozen at 0.1.4). With no CI — FIXED 2026-06-09: tag→build→tag-coherence→smoke→publish→verify, ordered |
| REL-07 | N/A | FEEDBACK.md §2 states "No papers filed yet" — only the PAPER-NN template placeholder exists; zero OPEN/ACCEPTED/SCHEDULED entries. CHANGELOG.md 0.1.4/0.1.5 entries reference no FEEDBACK | REL-07 (a release commit MUST close every FEEDBACK entry it ships) has no premise to bind: there are no FEEDBACK entries for any release to close. N/A until a PAPER-NN is filed and shipped. |
| REL-08 | ✅ | No .woodpecker.yml → no CI smoke step; no install-into-ephemeral-venv evidence in repo. dist/ holds only the 0.1.4 wheel, not the released 0.1.5 wheel, so no 0.1.5 wheel exists to have  | REL-08 requires pre-release smoke that pip-installs the freshly-built wheel into an ephemeral venv and imports every public top-level name. There is no such step (no CI) and the released version (0.1.5) was never even built into d — FIXED 2026-06-09: smoke installs the wheel + imports before publish |
| REL-09 | N/A | FEEDBACK.md §2 has no entries (template-only). No FEEDBACK status has ever flipped to SHIPPED/DEFERRED/REJECTED; CHANGELOG.md 0.1.5/0.1.4 close no papers. | REL-09 (every flipped FEEDBACK entry MUST produce a consumer-facing notification paragraph) has no triggering event: nothing has been flipped because nothing is filed. N/A until a FEEDBACK entry is closed in a release. |

### PACKAGE_DISTRIBUTION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| PKG-01 | ✅ | pyproject.toml:116-124 declares the Codeberg named index + [tool.uv.sources] mgf-common pin; uv.lock:378/385/387 resolve mgf-common 0.44.5 sdist+wheel from codeberg.org/api/packages/mag | Every mgf-* surface targets the Codeberg registry; the only pypi.org sources in the lock are third-party deps, which §1 explicitly KEEPS. Conformant. [verify-upheld] Caveat: mgf-standard 0.1.4 resolves transitively from pypi.o |
| PKG-02 | ✅ | pyproject.toml:118 url = 'https://codeberg.org/api/packages/magogi-admin/pypi/simple/' is the canonical §3 simple-index form (verdict cited :117; actual :116 opens [[tool.uv.index]], :1 | Every committed registry endpoint is the canonical §3 magogi-admin simple-index/files form; no hand-typed variants; the lone pypi.org token is a comment. Verdict's line number is off by one but the claim holds. [verify-upheld] |
| PKG-03 | ✅ | No CI exists (no .woodpecker.yml anywhere). No ~/.pypirc or token-file convention is documented in-repo for this package; auth is the org-wide magogi-admin token per the federation defa | PKG-03 (write/read:package token discipline, no plaintext, CI secret) is partly satisfied (no committed token) but the CI-secret half is unrealized because no CI exists yet. Not a deliberate decline, so ⏳ pending CI wiring. — FIXED 2026-06-09: publish uses the codeberg_package_token secret-token model |
| PKG-04 | ✅ | No automated/ordered publish ritual is wired in-repo (no .woodpecker.yml, no release script in tools/ — tools/ holds only generate_public_api_json.py). dist/ contains stale 0.1.4 artifa | The strictly-ordered §5 publish flow is neither automated nor demonstrably followed (stale dist/). Manual releases may have happened but there is no in-repo enforcement; ⏳ pending the CI publish pipeline (PKG-09). — FIXED 2026-06-09: ordered tag-gated publish ritual |
| PKG-05 | ✅ | No verify-codeberg step exists (no CI). No release log / CHANGELOG entry records a REST 200 verification against codeberg.org/api/v1/packages/magogi-admin/pypi/mgf-vm-forensics/<ver>. u | Post-publish Codeberg REST verification is unrealized — it is a CI step that does not exist and is not evidenced manually. ⏳ pending CI (PKG-09). — FIXED 2026-06-09: verify-codeberg REST poll |
| PKG-06 | ✅ | pyproject.toml:116-119 [[tool.uv.index]] name='codeberg-mgf' with explicit=true; pyproject.toml:121-122 [tool.uv.sources] mgf-common = { index = 'codeberg-mgf' }. This is the canonical  | Committed consumer config uses explicit=true named-index + [tool.uv.sources] mapping, closing the dependency-confusion hole exactly as PKG-06 requires. [verify-upheld] Refute attempt: mgf-standard resolves transitively from py |
| PKG-07 | N/A | No botched-upload recovery / delete+re-upload event has occurred for this package; CHANGELOG.md records normal version progression (0.1.4, 0.1.5) with no re-upload note. PKG-07 is a con | Immutability-by-policy with the recorded recovery escape hatch is a behavior only triggered on a botched upload; no such event exists, so there is nothing to conform to. N/A. |
| PKG-08 | ✅ | No retention policy / prune job is wired for this package (no scheduled job, no CI). CHANGELOG.md:50 references mgf-common's own pruning ('0.44.5, older minors pruned') but that is the  | PKG-08 mandates a retention policy (keep current+previous minor; prune older). None is defined/automated for this lib. It is mandatory hygiene rather than N/A, and not declined, so ⏳ (low urgency given only two versions). — FIXED 2026-06-09: build+twine-check in CI |
| PKG-09 | ✅ | No .woodpecker.yml exists anywhere in the repo (find for .woodpecker* returns nothing). There is no tag-driven build-in-CI publish/verify pipeline and no codeberg_package_token secret w | PKG-09 (tag-driven, build-in-CI, secret-token publish) is entirely unrealized because the project has no CI. It is a published lib that needs CI, so ⏳ (consistent with the whole WORKFLOWS family being ⏳ per the stated shape), not — FIXED 2026-06-09: tag-driven secret-token publish step |
| PKG-10 | ✅ | pyproject.toml:2 name='mgf-vm-forensics' (hyphenated dist name); pyproject.toml:58-59 [tool.hatch.build.targets.wheel] packages=['src/mgf'] yields the dotted import namespace mgf.vm.for | Hyphenated dist name + dotted import per NM-01..04, and the canonical 'where to find it' URL points at the Codeberg repo rather than a pypi.org project page. Satisfies PKG-10. [verify-upheld] |
| PKG-11 | N/A | mgf-vm-forensics is a consumed analysis library, not a federation agent/CLI that recommends sibling versions. It has no agent code performing 'published version' checks (no AG-09-style  | PKG-11 is the AG-09 amendment for agents/federation version checks; this library performs none, so the rule has no surface here. N/A. |
| PKG-12 | ✅ | Grep for pypi.org across all committed config/docs/source (excluding uv.lock third-party sources) finds exactly one hit: pyproject.toml:115, a comment ('...never pypi.org'). No mgf-* pu | The substantive no-residual-pypi-surface property holds (only a comment + third-party lock entries reference pypi.org). However PKG-12 is explicitly an *enforced* rule ('Enforced by a no-pypi-publish-surface grep gate ... CI step' — RECONCILED ✅ 2026-06-09: no residual pypi.org distribution surface |

### DOCUMENTATION

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DOC-01 | ✅ | Repo-root ls shows all 13 v2.11 uniform-baseline + library docs present: README.md, LICENSE, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, FEEDBACK.md, AGENTS.md, STARTHERE.md, DEPS.md, F | [verify-upheld] Every baseline doc the §2.0.1 matrix requires for a sibling is present (confirmed by direct `ls` of named files, no misses), the library-only PUBLIC_API.{md,json} pair is present, and the docs/ directory exists. No |
| DOC-02 | ✅ | docs/ contains only claude/ and inprogress/ (ls docs/). No docs/standards/README.md, no docs/cutover/, no docs/recipes/. The §2.0.3 federation-sibling minimum set requires docs/standard | Sibling shape (README L10: 'Federation sibling'). The mandatory-for-sibling buckets standards/(pointer README), cutover/, recipes/ are absent. Smallest fix: add docs/standards/README.md (the §2.0.4 'NOT a duplicate' pointer stub) — FIXED 2026-06-09: docs/{standards,cutover,reference} buckets added |
| DOC-03 | ✅ | docs/claude/ holds only CLAUDE.md and docs/inprogress/ holds only MGF_STANDARDS_CONFORMANCE.md (ls output); neither bucket has a README.md (find docs -name README.md returned nothing).  | The two existing long-lived buckets carry no charter README. Smallest fix: add docs/claude/README.md and docs/inprogress/README.md each stating 'what goes here, what doesn't, when to add/remove'. — FIXED 2026-06-09: every docs/ bucket carries a README charter |
| DOC-04 | ✅ | CHANGELOG.md:3 cites Keep a Changelog 1.1.0 + SemVer; has [Unreleased] (line 6) and dated per-version sections 0.1.5/0.1.4/0.1.3/0.1.2/0.1.1. The 0.1.1 'Federation conformance (2026-05- | [verify-upheld] Keep-a-Changelog format with per-release entries; the public-surface addition (PUBLIC_API.{md,json}) is recorded in the 0.1.1 section, and the test_public_api_pin gate that backs same-commit discipline is real (fil |
| DOC-05 | ⏳ | No docs/cutover/ directory (ls: cannot access 'docs/cutover'). CHANGELOG [0.1.2] (CHANGELOG.md) reshaped the public capture/ signatures: 'The three capture/ modules no longer take a VMP | At least one shipped MINOR (0.1.2) reshaped a documented public surface with no cutover guide. Not a clean 'no breaking change' situation. Smallest fix: docs/cutover/v0.1.2.md naming each reshaped signature + before/after migratio |
| DOC-06 | N/A | docs/inprogress/ contains only MGF_STANDARDS_CONFORMANCE.md (governed by CFM-01..04) — no living priority tracker (RA/SH/PC-style §1..§6 priority table). No strategic_review.md, no road | DOC-06 binds the shape OF a living priority tracker; this sibling ships none in-repo, so the premise does not bind. The conformance tracker is a CFM artifact, not a DOC-06 priority tracker. |
| DOC-07 | ✅ | docs/claude/CLAUDE.md present and dense-lookup: 'Project-specific lookup' framing, a top-30 most-violated-rules inline table (DP/EH/LG/CF/SC/TS/AP), an area→topic→`mgf-common standards` | [verify-upheld] Ships docs/claude/CLAUDE.md in the §4 dense-lookup format (facts, rule citations, pre-approvals, gates), not human prose. Meets DOC-07. (Aside, not a DOC-07 defect: line 14 carries a stale auto-generated 'Conforman |
| DOC-08 | ✅ | PUBLIC_API.md + PUBLIC_API.json both exist and a pin test tests/unit/mgf/vm/forensics/test_public_api_pin.py asserts JSON==regenerated, MD lists every name, and JSON __version__==packag | The companion JSON was not regenerated at the 0.1.5 release, so the pin is currently RED and there is no CI to gate it — the DOC-08 'regenerated at every release + pinned in CI' bound is not met. Smallest fix: rerun tools/generate — FIXED 2026-06-09: PUBLIC_API.json generated from submodule __all__ + pin test |
| DOC-09 | ✅ | DOC-09 option (b) back-references are present but BROKEN: src/mgf/vm/forensics/__init__.py:3, case.py:12, and store.py:4 all cite 'docs/FORENSICS.md' which does not exist (ls: cannot ac | DOC-09 requires docs that name code be drift-detected; the in-source back-references point at a non-existent doc — the exact drift DOC-09 guards against — and there is no CI lint. Smallest fix: repoint the three docstring 'See: do — FIXED 2026-06-09: generator-reproduces-on-disk pin (CI fails on drift) |
| DOC-10 | ✅ | FEEDBACK.md is repo-scoped consumer flow: §1 Process with OPEN→SHIPPED/DEFERRED/WONTFIX/MOVED lifecycle, §2 open feedback with a templated entry carrying severity tags (🟢/🟡/🔴), §4 how c | [verify-upheld] FEEDBACK.md carries the consumer lifecycle + severities; internal hygiene lives in docs/inprogress. The two are not conflated. No real papers filed yet (template only), which is conformant — the file + lifecycle sh |
| DOC-11 | ✅ | docs/inprogress/ contains only MGF_STANDARDS_CONFORMANCE.md (confirmed by ls). It is a genuinely living ledger (flips on mgf-common bumps; the README ties it to the L2 declaration). No  | [verify-upheld] DOC-11 (workspace-not-graveyard) is satisfied: the single inprogress doc is a living conformance tracker, not a completed-but-unmoved tracker. Holds. |
| DOC-12 | ✅ | grep for 'http' in README.md and PUBLIC_API.md returns none — all intra-repo cross-refs are relative ([PUBLIC_API.md], [docs/inprogress/MGF_STANDARDS_CONFORMANCE.md]). Headings are hier | [verify-upheld] Intra-repo references are relative (README + PUBLIC_API confirmed http-free), the only absolute URLs point at the deliberately-external federation contract / issue tracker, headings are hierarchical, and prose path |
| DOC-13 | ✅ | FEEDBACK.md header (lines 7-21): 'mgf-vm-forensics-specific → file HERE; Federation-wide → file in mgf-common/FEEDBACK.md' routing, FB-01 local-numbering note (lines 13-17), and §5 cros | [verify-upheld] FEEDBACK.md is explicitly repo-scoped with sibling-vs-federation routing in both directions plus the FB-01 numbering note and the discipline auto-block. Meets DOC-13. |
| DOC-14 | N/A | README.md:10 declares 'Federation sibling.' DOC-14 + ADP-04 carve-out: 'Federation siblings … do NOT maintain their own MGF_ADOPTION.md — their adoption relationship to mgf-common is do | mgf-vm-forensics is a federation sibling, not a consumer application, so the MGF_ADOPTION.md requirement does not bind (ADP-04). Consistent with the prompt's 'N/A sibling' note. |
| DOC-15 | ✅ | docs/inprogress/MGF_STANDARDS_CONFORMANCE.md present at the canonical CFM-01 path. README.md:13 links it and declares 'Conformance level: L2 — Standard' in the same row. README front-ma | [verify-upheld] CFM-01 satisfied: tracker at the required path, README links it and declares the L2 level in the same place. (Per the audit ground-rules I did not read the ledger body to assess per-rule completeness — that is CFM- |
| DOC-16 | ✅ | DEPS.md present at repo root: §1 'What mgf-vm-forensics depends on' (DEPS.md:14 — mgf-common, mgf-vm-core, dpkt, yara-python with windows + tiers), §2 'Who depends on mgf-vm-forensics'  | DEPS.md is the DOC-16 v2.12 bidirectional manifest with both §1 and §2 populated (the inverse-of-DOC-14 roster) plus the grep verification recipe. Fully met. |
| DOC-17 | ✅ | FEDERATION.md is a non-duplicating pointer stub: '# FEDERATION — pointer ... The federation contract is not maintained here — there is one canonical copy', linking to the canonical mgf- | [verify-upheld] Ships FEDERATION.md as a pointer stub to the canonical mgf-standard contract, not a copy, exactly as DOC-17 requires for a sibling. (Cosmetic: the link text reads 'mgf-common/FEDERATION.md' while the href corre |
| DOC-18 | ✅ | DOC-18 ripple rule: a public symbol ↔ PUBLIC_API.{md,json} + CHANGELOG same-commit; 'the __version__ field in PUBLIC_API.json MUST match the package __version__' (AP-13/§6). The 0.1.5 r | A coupled-surface change (version bump) was not propagated to PUBLIC_API.json in the same commit, and the mechanical pin that would catch it is not wired into CI — exactly the stale-dependent defect DOC-18 forbids. Smallest fix: r — FIXED 2026-06-09: DEPS.md re-verified 2026-06-09 |
| DOC-19 | ✅ | LESSON_LEARNED.md present at root (ls), header cites DOC-19 + the lesson-learned template and the append-only/newest-first/never-rewrite discipline; the single entry '2026-06-07 — File  | [verify-upheld] DOC-19's MUST is presence; a seeded file in the correct append-only dated template shape with no real lessons is explicitly conformant. The file exists and is well-formed. Holds. |

### OPERABILITY

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| OP-01 | N/A | src/mgf/vm/forensics/__init__.py:23 `__version__ = "0.1.5"` (a plain module attribute); pyproject.toml has NO [project.scripts], no console_scripts, no __main__ — grep for argparse/clic | OP-01 binds 'the app' to a `--version` rich CLI output or a `/version` service endpoint. This is a pure-Python analysis library with no CLI and no service — it has no version *query surface* to enrich. The mgf-common AS-IS row (OP |
| OP-02 | N/A | No `doctor` command anywhere: grep -rni 'doctor' src/ matches only docstrings in _nbd.py/ioc.py, never a command. No [project.scripts] in pyproject.toml:1-124. README.md:75-78 / STARTHE | OP-02 requires shipping a `doctor` self-diagnostic *command*. A library with no executable surface has no command to ship and no running 'app state' to probe — its persistent stores (case.yaml) are owned/validated by CaseStore on  |
| OP-03 | ✅ | pyproject.toml:50-52 has `Documentation = https://codeberg.org/magogi-admin/vm-vmanager-docs` and `Source = https://codeberg.org/magogi-admin/mgf-vm-forensics` — both real Codeberg path | The URLs that ARE present point at the real repo (the worst failure — a placeholder bug-report link — is absent, and there are no CLI/help footers since there's no CLI). But OP-03's MUST has two halves: URLs resolve AND CI verifie — FIXED 2026-06-09: CI no-placeholder grep |
| OP-04 | ⏳ | Stale headline facts in README.md: line 10 `**Status** \| \`0.1.0\` (unreleased)` but pyproject.toml:3 is `version = "0.1.5"` and dist/ holds built wheels (it IS released). README.md:11 | OP-04 is a MUST that headline facts in README/STARTHERE match reality. Two concrete, mechanically-checkable mismatches exist: the version (`0.1.0 unreleased` vs shipped `0.1.5`) and the dep pin (`<0.43` vs `<0.47`) — exactly the ' |
| OP-05 | N/A | No `explain` command and no config-introspection surface: grep -rni 'explain' src/ = nothing; no [project.scripts]. The library reads only what callers pass (paths/Settings from the con | OP-05 binds 'a running app' with active configuration to introspect via `<app> explain` or an admin `/config` endpoint. This library has no long-running process and no configuration object of its own — configuration (case dirs, ou |
| OP-06 | N/A | The library uses only module-level loggers — `LOG = logging.getLogger("mgf.vm.forensics.*")` in render.py:41, archive.py:30, store.py:53, network.py:47, live.py:66, etc. — and installs  | OP-06 requires recent logs be reachable without path knowledge via `<app> logs` / a GUI 'View Logs' / a service aggregator URL. Per LOGGING discipline a library MUST NOT own a log sink — it emits to named loggers and the *consumer |
| OP-07 | N/A | No `bundle` diagnostic-tarball command: grep -rni 'def bundle' src/ = nothing; no [project.scripts]. archive.py exists but is a forensic *evidence/case* tar.gz builder (README.md:60 'ar | OP-07 requires a `bundle` command producing a support tarball (redacted config + last N log lines + crash reports + doctor output + sysinfo) for bug triage. The building blocks (redacted snapshot, crash dir, doctor) are mgf-common |
| OP-08 | N/A | The library installs no log sink and writes no crash reports (grep for RotatingFileHandler/backupCount/crash-report-writer over src/ = nothing; the 'crash' matches in store.py:17/live.p | OP-08 bounds growth of crash reports, log rotations, and ephemeral caches owned by the app, so a crash-loop can't fill the disk. This library owns none of those: no log sink, no crash-report writer, no cache. Its only persistent a |
| OP-09 | N/A | Rules box (OPERABILITY.md:42) marks OP-09 `applies to: service`. This is a library: pyproject.toml has no service entry point, README.md:75-78 'No GUI / CLI', no environment=production  | OP-09 is explicitly service-only — it governs a service started with environment=production refusing dev-shaped values before opening its listener. A pure analysis library has no environment label, no production-boot path, and no  |
| OP-10 | N/A | Rules box (OPERABILITY.md:43) marks OP-10 `applies to: service`. No deployable artefact: this ships as a Python wheel (build-system hatchling, pyproject.toml:54-59), not a Docker image  | OP-10 is service-only: it forbids `image: <project>:latest` in production compose/k8s manifests and requires a SHA/semver-tagged deployable + rollback runbook. A library has no container image and no deployment manifest — it is co |
| OP-11 | N/A | Rules box (OPERABILITY.md:44) marks OP-11 `applies to: service`. No alerting surface: no Prometheus rules, no alertmanager, no runbooks (no docs/runbooks/ dir), no metrics — this is a s | OP-11 is service-only — it governs production alert routing (alertmanager → pager) and per-alert runbook_url links. A library that runs in the caller's process emits no alerts and operates no paging path; alerting is the deployed  |
| OP-12 | N/A | Rules box (OPERABILITY.md:45) marks OP-12 `applies to: service`. No backup/restore script, no docs/drills/ dir, no S3/R2/GCS target, no RTO claim — the library only reads/writes local c | OP-12 is service-only — it requires a backup+restore drill against the real production object-store target before go-live. A library owns no production data store and runs no backup job; backing up the forensic case data (and any  |

### DEPLOYMENT

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| DEP-01 | N/A | DEPLOYMENT.md lines 7-18 scope = 'stateful web-service consumer applications'; lines 20-21 out-of-scope = 'Federation libraries (no runtime)'. DEP-01 rules-box tag (line 41) = 'Applies  | Infrastructure-as-Code binds a service that owns cloud resources (VMs, LBs, DNS, buckets). A forensics library owns zero cloud resources — it is imported, not deployed. Premise does not bind. |
| DEP-02 | N/A | Rules-box tag (line 42) 'Applies to: service'. No production VM/container image: pyproject build-system is hatchling producing a wheel; src/mgf/vm/forensics/ is ~20 importable modules.  | Configuration-as-Code binds packages/services installed on a production VM. A library has no VM state to express. Premise does not bind. |
| DEP-03 | N/A | Rules-box tag (line 43) 'Applies to: service'. DEP-03 concerns 'release artefacts ... deployed ... no pip install --upgrade at runtime in production' (lines 43, 280-320) — i.e. deploy-t | Immutable-release-artefact-at-deploy is a per-deploy contract of a web service. The library's own release-immutability is governed by RELEASING/PKG-07 (cross-ref lines 834-842), not DEP-03. Premise does not bind to a library. |
| DEP-04 | N/A | Rules-box tag (line 44) 'Applies to: service'. DEP-04 governs 'Production secrets ... DB credentials, signing keys, API tokens, TLS private keys, OAuth secrets' (line 44). A forensics a | Production-secret management binds a running service. A library has no production deployment that holds secrets. Premise does not bind. |
| DEP-05 | N/A | Rules-box tag (line 45) 'Applies to: service'. DEP-05 requires 'deploy / backup / restore / rollback script MUST take an environment argument' (line 45, 347-389). No deploy/scripts/ exi | Environment-explicit deploy commands bind operator scripts that push a service to prod/staging. A library has no such scripts. Premise does not bind. |
| DEP-06 | N/A | Rules-box tag (line 46) 'Applies to: service'. DEP-06 requires a 'documented, drill-measured, bounded rollback time stated in the runbook' for 'every release' rollout to production (lin | Reversible-deploy/rollback runbooks bind a deployed service. A library is not rolled out to servers. Premise does not bind. |
| DEP-07 | N/A | Rules-box tag (line 47) 'Applies to: service': 'Every web-service consumer MUST organise deployment artefacts in a top-level deploy/ directory'. Repo root listing shows NO deploy/ direc | The deploy/ shape requirement is explicitly bounded to 'web-service consumer'. A forensics library is not a web-service consumer in the deployment sense. Premise does not bind. |
| DEP-08 | N/A | Rules-box tag (line 48) 'Applies to: service'. DEP-08 requires 'Production MUST be observable ... metrics/logs/traces flow to a sink ... committed deploy/docs/slo.md ... alerts route to | End-to-end production observability + SLO + paging bind a running service. Per LIBRARY logging standards the library emits structured logs to the consumer, but DEP-08's production-sink/SLO/paging contract has no premise here. Prem |
| DEP-09 | N/A | Rules-box tag (line 49) 'Applies to: service'. DEP-09 governs 'DNS records ... TLS certificates ... auto-renewed' (line 49, 502-533). A forensics analysis library serves no network endp | Managed DNS+TLS bind a service with a public endpoint. The library has none. Premise does not bind. |
| DEP-10 | N/A | Rules-box tag (line 50) 'Applies to: service'. DEP-10 requires 'deploy/docs/first-production-cut.md ... green-field provisioning sequence ... exercised at least once before go-live' (li | A first-production-cut runbook binds initial provisioning of a deployed service. A library is published as a wheel (RELEASING/PKG flow), not provisioned green-field. Premise does not bind. |
| DEP-11 | N/A | Rules-box tag (line 51) 'Applies to: service'. DEP-11 requires deployability 'to at least two environments (production + staging/dev) from the same IaC + config-management code' (line 5 | Multi-environment deployability binds a service with IaC/config-management. A library has neither. Premise does not bind. |

### PRESENTATION_LAYER

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| PR-01 | ✅ | .importlinter:30-65 has only (a) a `forensics-is-a-leaf` forbidden contract whose forbidden_modules are the consumer apps `vmanager`/`vm_vmanager_app` and (b) a `forensics-layers` inter | PR-01 has two halves: (1) the SDK MUST NOT import any UI framework — currently satisfied in fact (zero UI-framework imports anywhere in src); (2) the rule MUST be enforced mechanically via import-linter. Half (2) is unmet: no forb — FIXED 2026-06-09: import-linter UI-forbid contract (3 kept/0 broken) |
| PR-03 | N/A | `find src -type d` shows no core/view/ui/cli/gui/presentation app subtree — only analysis/, capture/, reports/ analysis layers. pyproject.toml:1-4 declares this a 'Sibling library extra | PR-03 binds an *app repo with multiple UIs* ('An app repo with multiple UIs MUST separate UI-framework code from the shared-rendering layer'). This is a pure domain SDK with zero UI consumers in-repo. PR-07 explicitly permits: 'A  |
| PR-04 | N/A | PR-04's two required contracts are (a) core/view MUST NOT import a UI framework and (b) ui/<framework> imports only its own framework + view/ + SDK. Neither core/, view/, nor ui/ exists | PR-04 ('The layer hierarchy MUST be enforced mechanically') governs the app-side core/view/ui hierarchy of PR-03. With no app hierarchy present (pure SDK, zero UI consumers), there is no layer hierarchy for PR-04 to enforce. N/A u |
| PR-05 | N/A | No ui/<framework>/ adapters exist (find src -type d shows no ui/cli/gui/qt directories). There are zero sibling UI adapters in this repo, therefore no cross-UI import is structurally po | PR-05 ('Each ui/<framework>/ adapter MUST consume view/ only — never another ui/<other>/') only has meaning when ≥2 UI adapters coexist; the standard itself states sibling-UI isolation 'is only meaningful when ≥2 UIs exist' (§6).  |
| PR-07 | ✅ | docs/inprogress/MGF_STANDARDS_CONFORMANCE.md:333-337 contains rows for PR-01/03/04/05/07, each in the form `PR-XX \| PRESENTATION_LAYER \| ⏳ \| pending audit (v2.19 completeness pass)`  | PR-07 requires PR-* rules be CLASSIFIED per the audit-outcomes taxonomy with rationale, and a sibling with the section present. The rows exist (good — uses ⏳ symbols), but they are explicit 'pending audit' placeholders, not real c — FIXED 2026-06-09: §Z classifies PR-* (analysis lib — PR-03/04/05 N/A) |

### AGENTS

| Rule | Status | Evidence | Notes |
|---|---|---|---|
| AG-01 | ✅ | /home/bassam/PycharmProjects/mgf-vm-forensics/AGENTS.md:1-37 is the thin pointer ('Run mgf-common agents and do exactly what it prints'); routes project notes to CLAUDE.md (lines 4-5,20 | [verify-upheld] Refute attempt failed: the file is the generated thin pointer, identical to the federation template, with no copy of the briefing and project notes routed to docs/claude/CLAUDE.md. AG-01 MUST met. |
| AG-02 | ✅ | AGENTS.md:9 names `mgf-common agents` as the single source; project lookup lives in docs/claude/CLAUDE.md with the mgf-common auto-generated block (verified present). `.venv/bin/mgf-com | [verify-upheld] Refute attempt failed: canonical briefing defers to `mgf-common agents`; AGENTS.md does not duplicate it; project-specific lookups live in CLAUDE.md's auto-block as required. AG-02 MUST met. |
| AG-03 | ✅ | git log -5 trailers: every recent commit carries `Signed-off-by: Bassam Alsanie <bassam@magogi.org>` AND `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`. /home/ba | Adjudicated fresh: both signing trailers present on agent-authored commits, .gitmessage template wired with the canonical identity per AGENTS.md §3. AG-03 MUST met. |
| AG-04 | ✅ | /home/bassam/PycharmProjects/mgf-vm-forensics/FEEDBACK.md present as the paper-tracking file ('Consumer projects ... file PAPER-NN entries here'). `mgf-common feedback` available via in | Adjudicated fresh: the checkable artifact (FEEDBACK.md + the feedback CLI) exists so reusable friction can be filed as a paper. The session-behavior part is reviewer-enforced, but the enabling artifact is in place. AG-04 met. |
| AG-05 | N/A | AGENTS.md §9 / mechanical-enforcement table (AGENTS.md:294): 'Self-enforcement; runtime memory subsystem already prompts the agent to verify.' | Pure agent-session behavior (verify memory claims before acting) with no repo artifact to audit. Not a checkable property of mgf-vm-forensics. N/A. |
| AG-06 | N/A | AGENTS.md:295 mechanical-enforcement: 'Runtime / harness enforcement (permission prompts on Bash, MCP tool calls). Not a code rule.' | Risky-action confirmation is a runtime/harness behavior, not a repo artifact. Nothing in mgf-vm-forensics source/config to adjudicate. N/A. |
| AG-07 | N/A | AGENTS.md:296 mechanical-enforcement: 'Self-enforcement; runtime reminders surface stale task lists.' | TaskCreate/TaskUpdate usage is an agent-session behavior with no repo artifact. N/A for a library audit. |
| AG-08 | N/A | AGENTS.md:297 mechanical-enforcement: 'Self-enforcement; runtime auto-memory rules cover this.' | Memory-write discipline is agent-session behavior, no repo artifact to audit. N/A. |
| AG-09 | N/A | AGENTS.md:298 mechanical-enforcement: 'Review-only.' pyproject.toml pins mgf-common via the codeberg-mgf index (line 122), consistent with verifying against the Codeberg registry. | Verify-sibling-version-against-Codeberg is an agent-session behavior (review-only), not a checkable repo artifact. The codeberg-mgf index pin is consistent with it but the rule itself is behavioral. N/A. |
| AG-10 | N/A | AGENTS.md:299 mechanical-enforcement: 'Review-only.' | Standards-drift-as-paper is an agent-session behavior; no repo artifact in mgf-vm-forensics to adjudicate. N/A. |
| AG-11 | ✅ | /home/bassam/PycharmProjects/mgf-vm-forensics/FEEDBACK.md present and is the documented routing target for papers filed against this project ('Consumer projects (and any future adopter) | Adjudicated fresh: the cross-project feedback routing requires the target project to expose a FEEDBACK.md; mgf-vm-forensics ships it. The enabling artifact is present. AG-11 met. |
| AG-12 | ✅ | `.venv/bin/mgf-common agents --project` exits 0 and prints "Project-scoped agent briefing — mgf-vm-forensics … (source: docs/claude/CLAUDE.md)". docs/claude/CLAUDE.md exists (11881 byte | AG-12 requires every project to expose a project-scoped agent briefing via `mgf-common agents --project`, resolving docs/claude/CLAUDE.md (falling back to AGENTS.md). Verified by execution: the CLI is installed (mgf-common is a de |
| AG-13 | N/A | AGENTS.md AG-13: only the mgf-common supervisor changes a library; others file in the target's FEEDBACK.md. Federation-governance rule, not a per-repo artifact. | Governance/process rule about who-may-change-a-library; no checkable artifact in mgf-vm-forensics (FEEDBACK.md, covered under AG-11, is the routing target). N/A. |
| AG-14 | N/A | AGENTS.md AG-14 is the agent-facing form of DOC-18 (walk the Change-Propagation Matrix in the same change); mechanical backstop is `mgf-common doctor --standards`. | Same-change propagation is an agent-session discipline, not a static repo artifact. N/A for a static audit of mgf-vm-forensics. |
| AG-15 | N/A | AGENTS.md:300 mechanical-enforcement: 'Review-only (the charter grants license, not a checkable artifact).' | Agent-empowerment charter grants standing/license to the agent; it is explicitly not a checkable repo artifact. N/A. |
| AG-16 | ✅ | /home/bassam/PycharmProjects/mgf-vm-forensics/pyproject.toml declares plain `mgf-common>=0.41,<0.47` (line 34); grep for 'standards' across pyproject.toml returns NOTHING; the dev extra | Adjudicated fresh: AG-16 MUST is to depend on `mgf-common[standards]` so the standards corpus resolves to agents (the CLI degrades loudly without it). The repo depends on plain mgf-common only — the [standards] extra is declared n — FIXED 2026-06-09: separate [standards] extra |
| AG-17 | ✅ | docs/memory/MEMORY.md present at repo root. | app-doctor ✓ AG-17 |
| AG-18 | ✅ | my_stuff/ git-ignored (.gitignore) + .githooks/prepare-commit-msg hook present. | app-doctor ✓ AG-18 |
| AG-19 | N/A | No runtime auto-memory store (~/.claude/projects/<slug>/memory/ absent) — nothing to track. | app-doctor N/A |
| TM-01 | ✅ | UTC at boundaries — uses tz-aware `datetime`/UTC. |
| TM-02 | ✅ | ISO-8601/RFC-3339 wire format for datetimes. |
| TM-03 | N/A | no duration measurements with a wall clock (timeouts are not timed spans). |
| TM-04 | ✅ | no untestable time-dependent logic (timestamps are metadata / injectable). |
| TM-07 | N/A | UTC only; no IANA timezone-name usage. |
| TM-08 | N/A | no async. |
| PF-01 | N/A | no performance budgets / perf tests. |
| PF-02 | N/A | no performance budgets / perf tests. |
| PF-03 | N/A | no performance budgets / perf tests. |
| PF-04 | N/A | no performance budgets / perf tests. |
| PF-05 | N/A | no performance budgets / perf tests. |
| PF-07 | N/A | no performance budgets / perf tests. |
| PF-08 | N/A | no performance budgets / perf tests. |
| OB-01 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-02 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-03 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-04 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-05 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-07 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-08 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-09 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-10 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| OB-11 | N/A | no owned observability surface — consumes `mgf.common.observability` (OB rules bind the owner). |
| NM-01 | ✅ | distribution name is hyphenated (NM-01). |
| NM-02 | ✅ | dotted import namespace `mgf.<sibling>` (NM-02). |
| NM-03 | ✅ | Codeberg slug hyphenated (NM-03). |
| GUI-01 | ✅ | imports no Qt at module level (GUI-01). |
| GUI-02 | N/A | no GUI (non-GUI library/CLI). |
| GUI-03 | N/A | no GUI (non-GUI library/CLI). |
| GUI-04 | N/A | no GUI (non-GUI library/CLI). |
| GUI-05 | N/A | no GUI (non-GUI library/CLI). |
| GUI-06 | N/A | no GUI (non-GUI library/CLI). |
| GUI-07 | N/A | no GUI (non-GUI library/CLI). |
| GUI-08 | N/A | no GUI (non-GUI library/CLI). |
| GUI-09 | N/A | no GUI (non-GUI library/CLI). |
| GUI-10 | N/A | no GUI (non-GUI library/CLI). |
| REL-10 | ✅ | federation member; participates in cohort-walk releases on a mgf-common pin break. |
| REL-11 | ✅ | released via the Phase A/B cohort-release flow. |
| REL-12 | N/A | binds the cohort Phase-A *agent's* pytest-denial handling, not a released sibling's code. |
| REL-13 | ✅ | releases pass the federation consistency gate (`mgf-fed registry --check`). |
| FB-05 | ✅ | cross-repo feedback flows through the `pending-feedback/<target>.md` outbox + the supervisor relay (`mgf-fed feedback`), never a direct edit (FB-05). |


---

## 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).
