# Freeze — MUSEHUB-F7a: HTTP-coverage test spec for overseer-run provenance **Date:** 2026-08-11 **Step:** MUSEHUB-F7a (freeze) → MUSEHUB-F7b (Auto build) **Model:** **Thinking** (this artifact) → **Auto** (F7b builds against it, no redesign) **Repo:** `musehub` (MUSE_HUB workspace) — regime `muse-only`, canonical Muse **Authority:** `docs/MUSEHUB-OVERSEER-HANDOVER.md` § "F7 correction"; proposal `sha256:01e6a0d4…` (changes requested); SD-14; staging deploy remains Tier 3 This document freezes **WHAT** F7b must assert and **HOW** it must assert it. It contains **no implementation** — no test body is written in this session. ## Freeze-contract declaration ```yaml phase: musehub-f7a-http-coverage outputs: - id: f7-http-coverage-spec path: docs/reviews/2026-08-11-f7-http-coverage-freeze.md frozen: true frozen_inputs: [] review_stamp: reviewed_at: '2026-08-11T20:39:13Z' verdict: pass reviewer_mode: agent reviewer_model: thinking-high reviewer_provider: local kit_version: 0.1.0 artifact_digest: sha256:ecf635ee90e6ea4622232a0d414a3e5012056895b5c3b36b8384965ffdb5b420 ``` ## Document conventions | Convention | Reason | | --- | --- | | URL paths are written with a leading `{base}` placeholder — e.g. `{base}/api/overseer-run-provenance/{run_ref}` | `{base}` is the **empty string**. The prefix keeps URL templates from being mis-read as absolute filesystem paths by the C4 heuristic in kit `tools/freeze_reviewer/providers/base.py:15` | | Code citations are repo-relative `path:line` | Freeze-review findings must cite **file+line** (SPEC §6) | | Line numbers marked **(F7)** are as of Muse branch `feat/9a-4-f7-overseer-provenance` @ `e5530c6…` | The F7 route/service/settings do not exist on other branches; F7b carries them forward | ## Problem being frozen Phase 9A-4 F7 **implemented** the enrichment endpoint plus six unit tests (`tests/unit/test_overseer_provenance_f7.py`, F7) on an unmerged Muse feature branch whose proposal has changes requested. F7 is **not merged to `dev`, not deployed, and not live** — `tools/verify/check_consumer_contract.py:97-103` still carries the path as `pending`, and `docs/MUSEHUB-OVERSEER-HANDOVER.md:198` records it as review-blocked. Every one of those six tests calls the service function directly, so **no test crosses the HTTP boundary** — the app's route table is unobserved by the suite. That gap is exactly what the reviewer identified, and it is valid. The reviewer's *proposed remedy* — adding an explicit `app.include_router(...)` — is **rejected on evidence** (see next section). F7b closes the coverage gap and changes **no** registration. ## Verified ground truth (do not re-derive in F7b) Verified 2026-08-11 by running the app on `feat/9a-4-f7-overseer-provenance` (recorded in `docs/MUSEHUB-OVERSEER-HANDOVER.md:35-52`), plus the placement and lifespan facts verified in this session against the working tree: | Fact | Evidence | | --- | --- | | The endpoint **is** already registered at `{base}/api/overseer-run-provenance/{run_ref}` | `app.openapi()` paths matching `overseer` returned exactly that one template | | Registration happens by auto-discovery, not an explicit call | `musehub/api/routes/musehub/__init__.py:70-79` imports every sibling module exposing `router`; the aggregate is mounted with the `/api` prefix at `musehub/main.py:456` (F7) | | `overseer_provenance` is **not** exempted from auto-discovery | `musehub/api/routes/musehub/__init__.py:37-66` (`_DIRECT_REGISTERED`) does not list it | | Wildcard ordering is safe by construction | sort key at `musehub/api/routes/musehub/__init__.py:70-72` pins `repos` last unconditionally; `overseer_provenance` sorts at index 17 and is therefore registered before the wildcard. The total module count is branch-dependent (52 on the F7 branch) and is not load-bearing — the sort key is | | Adding the requested explicit include would **double-register** at root without the `/api` prefix (duplicate `operationId`), which the Scooling adapter rejects by throwing | `tools/verify/check_consumer_contract.py:1-24, 84-104`; consumer pin `src/adapters/museHubRepoTransport.ts:466,481` in the Scooling repo | | The unprefixed path returns **422**, not 404 | the `/{owner}/{repo_slug}` wildcard already claims that namespace | | The route declares the ref segment with the `path` converter and a fixed `operation_id` | `musehub/api/routes/musehub/overseer_provenance.py:21-25` (F7) — OpenAPI renders the template without the converter suffix | | Anonymous requests never touch the DB session | `musehub/auth/request_signing.py:162-171` returns `None` before `db` is used when no `Authorization` header is present and the dependency is optional | | …but the session **is** still declared, so it is still resolved | `musehub/auth/request_signing.py:298-300` declares the session via `Depends(get_db)`; `musehub/auth/dependencies.py:24` aliases `optional_token` to that dependency | | An unoverridden session dependency fails the request | `musehub/db/database.py:145` raises `RuntimeError("Database not initialized. Call init_db() first.")` | | App startup would require Postgres | `musehub/main.py:138-152` — the lifespan handler calls `init_db()` | | Tests at `tests/` root cannot run without Postgres | session-scoped **autouse** fixture `tests/conftest.py:203-247` connects via psycopg2 to the URL at `tests/conftest.py:178-184`. Verified this session with Postgres down: `tests/test_api_contracts.py` → `ERROR … psycopg2.OperationalError: connection … port 5434 failed: Connection refused` | | Tests under `tests/unit/` **can** run without Postgres | `tests/unit/conftest.py:15-18` overrides that fixture with a no-op. Verified this session with Postgres down: `tests/unit/test_phase2_inMemory_walk_dag.py` → **9 passed in 0.09s** | | The posture gate reads the settings singleton at call time | `musehub/services/musehub_overseer_provenance.py:39-44` (F7) compares the settings attribute to the string `enabled` | | The posture field defaults to disabled and is env-aliased | `musehub/config.py:168-171` (F7): default `disabled`, `validation_alias` `MUSEHUB_OVERSEER_PROVENANCE_ENRICHMENT` | ## Branch baseline for F7b (frozen) The F7 route, service, settings field, and existing unit tests are **absent from every other branch** — they exist only on `feat/9a-4-f7-overseer-provenance` @ `e5530c6…`. F7b therefore has three steps, in this order, on a **new** feature branch cut from `origin/dev` (`docs/MUSEHUB-OVERSEER-HANDOVER.md:111`): | # | Step | Notes | | --- | --- | --- | | B1 | Carry the F7 commits forward onto the new branch and resolve the single known conflict, `config.py::Settings` (`docs/MUSEHUB-OVERSEER-HANDOVER.md:117`) | Conflict resolution **must** preserve the field at `musehub/config.py:168-171` (F7) verbatim — default `disabled` and `validation_alias` `MUSEHUB_OVERSEER_PROVENANCE_ENRICHMENT`. Re-check the conflict count; `dev` has moved since it was measured | | B2 | Carry the seven verify-tooling files **verbatim** from `feat/musehub-overseer-governance` | See § "Verify tooling must travel with F7b" below. Copied unmodified; not authored in F7b | | B3 | Add the one new test file specified below | The only net-new file **authored** in F7b | **B1 and B2 must each land as their own Muse commit, separate from B3**, and both commit refs must be recorded in `docs/MUSEHUB-OVERSEER-HANDOVER.md` in the closing governance sync. Without distinct commits there is no diff boundary, and exit criterion 2 becomes a judgment call instead of a mechanical check. **The baseline for criterion 2 is the recorded B2 ref.** ### Verify tooling must travel with F7b Verified this session by enumerating each branch's tree: `tools/verify/` contains three files on `feat/musehub-overseer-governance` and **zero** on `dev`, on `main`, and on `feat/9a-4-f7-overseer-provenance`. A branch cut from `origin/dev` therefore has no verify tooling at all, so exit criterion 5 could not run, and the L1 checkpoints that guard the cross-repo `/api` contract would be absent from the very branch that touches it. **What the carried gates do and do not cover.** Established empirically in review round 7 by adding a duplicate root-level `include_router` and re-running both gates: | Property | Gated? | | --- | --- | | Every declared router module is reachable in the app route table | **Yes** — `tools/verify/check_routes_registered.py:151` | | The templates the Scooling adapter re-derives are served, `/api`-prefixed | **Partly.** The two `required` templates fail the gate when missing (`tools/verify/check_consumer_contract.py:142-148`). The overseer template is `pending` (`:97-103`), so its absence is **reported but does not fail** (`:149-150`, and the checker's own docstring at `:31-33`) — verified by running the gate on a tree without the F7 route: it printed the `PENDING:` line and still exited 0 with `2/3`. Exit criterion 5 covers that endpoint instead, by pinning the `NOTE:` line | | **No second, root-level registration of the same endpoint** | **No.** Both gates exited 0 with byte-identical `ARTIFACT_SHA256` digests after a duplicate root mount was added. The consumer gate is a *presence* test over three hardcoded templates (`:84-104`) and never enumerates extra paths; the routes gate is a *reachability* test (`:151`), and a second mount only adds reachability. FastAPI's duplicate `operationId` raises a warning, not an exception, so `app.openapi()` at `tools/verify/check_consumer_contract.py:112` still succeeds | The assertion set cannot cover uniqueness either: A1 exercises the `/api` path, which keeps returning 200 while a duplicate root mount exists. **Therefore, stated plainly: on the F7b branch there is no mechanical guard against the specific change the external reviewer requested.** The only control is the frozen prohibition in § "Registration is NOT modified". Extending `check_consumer_contract.py` to gate registration uniqueness is real, worthwhile work — and it is **out of scope for F7b**, which may not edit a carried gate (see the prohibition table). It belongs in its own step. What the carry does buy is that all three L1 checkpoints exist and run on this branch at all, which is a precondition for gating uniqueness later. F7b carries all seven files, unmodified, as B2: | File | Role | | --- | --- | | `tools/verify/check_routes_registered.py` | Route-table reachability gate — compares endpoint callable identity, not path strings, and does **not** inspect OpenAPI; deliberately immune to prefix drift (`tools/verify/check_routes_registered.py:11-15`) | | `tools/verify/check_consumer_contract.py` | Asserts the paths the Scooling adapter re-derives, including the `/api` prefix | | `tools/verify/check_app_imports.py` | Import-time gate; both checkers' failure messages point at it, so omitting it would leave a dangling instruction | | `scripts/verify/_python_env.sh` | Shared interpreter resolution, sourced by all three wrappers; fails closed with exit 3 rather than reporting success when no interpreter can import the app (`scripts/verify/_python_env.sh:7-11`, `:41-58`) | | `scripts/verify/verify_routes_registered.sh` | Wrapper — `exec`s the checker so its `ARTIFACT_SHA256` stays the last stdout line | | `scripts/verify/verify_consumer_contract.sh` | Same, for the consumer-contract gate | | `scripts/verify/verify_app_imports.sh` | Same, for the import gate | The wrappers are carried, not just the checkers, because `_python_env.sh` resolves `MUSEHUB_VERIFY_PYTHON` → `.venv/bin/python` → an importing `python3` (`scripts/verify/_python_env.sh:30-39`). That is what makes exit criterion 5 robust rather than dependent on a bare `python` that is not on PATH in this environment. **Carrying F7 forward is not a registration change.** It reproduces the already-reviewed F7 diff — including `musehub/api/routes/musehub/overseer_provenance.py` — byte-for-byte apart from the `Settings` conflict resolution. The prohibitions in the next section and exit criterion 2 are measured against **the post-B2 baseline**, not against `dev`: after B1 and B2, the F7b diff must add exactly one file and nothing else. ## Registration is NOT modified — frozen prohibitions F7b is a **test-only** change. The following are hard prohibitions; violating any of them fails build verification regardless of test results: | Must not | Why | | --- | --- | | Add `app.include_router(...)` for `overseer_provenance` in `musehub/main.py` | Double-registers at root without the `/api` prefix → duplicate `operationId` and a path the Scooling adapter refuses | | Edit `musehub/api/routes/musehub/__init__.py` | Header at line 7 is an explicit **DO NOT EDIT** instruction; auto-discovery already works | | Add `overseer_provenance` to `_DIRECT_REGISTERED` | It is registered *by* auto-discovery; exempting it would unmount it | | Change the route decorator path, the converter, or `operation_id` | `{base}/api/overseer-run-provenance/{run_ref}` is a cross-repo consumer contract | | Edit `tests/conftest.py` or `tests/unit/conftest.py` | The DB-free property comes from the existing no-op override; changing shared conftest scope is out of scope for F7b | | Add or modify a `conftest.py` anywhere | Same reason — no shared-fixture surface change | | Override the auth dependencies themselves (the signed-request dependencies) | That would mask the auth path instead of exercising it; only the DB-session dependency may be overridden | | Edit any of the seven carried verify-tooling files, or relax a gate to make it pass | They are carried verbatim as B2. A failing gate is a finding about the **code**, not licence to weaken the gate — that is the greenwash this freeze exists to prevent. Report and stop | | Touch KD-6b, `python-multipart`, or the vault UI | Separate lane (`docs/MUSEHUB-OVERSEER-HANDOVER.md:141-167`) | | Deploy, push to staging, or merge | Tier 3 — operator only | ## Frozen HOW — mechanism | Item | Frozen decision | | --- | --- | | **New file** | `tests/unit/test_overseer_provenance_f7_http.py` — exactly one new **authored** file (B2 additionally carries seven files verbatim; see § "Branch baseline") | | **Placement rationale** | `tests/unit/` inherits the no-op `_db_schema` override at `tests/unit/conftest.py:15-18`; any other location inherits the Postgres-connecting autouse fixture at `tests/conftest.py:203-247` and the "runs anywhere" property is lost | | **Client** | `fastapi.testclient.TestClient` over the app imported from `musehub.main` | | **Lifespan** | The client must **not** be entered as a context manager. Entering it runs the lifespan at `musehub/main.py:138-152`, which calls `init_db()` and requires Postgres. Plain construction skips the lifespan | | **Server exceptions** | Leave `raise_server_exceptions` at its default. Do **not** set it to `False` — that would convert a genuine unhandled error into an indistinguishable 500 response | | **DB-session override** | `app.dependency_overrides[get_db]`, with `get_db` imported from `musehub.db.database` — the same object the auth dependency depends on. Required because the optional-auth dependency declares it at `musehub/auth/request_signing.py:298-300`; without the override the request fails at `musehub/db/database.py:145` | | **Override shape** | An async-generator override yielding a sentinel (no engine, no connection). Legitimate because anonymous requests return before the session is used (`musehub/auth/request_signing.py:162-171`); all four assertions are anonymous requests | | **Override teardown** | Remove only the single key that was added; do **not** call `dependency_overrides.clear()` — that would stomp overrides owned by other fixtures (e.g. `tests/conftest.py:154-161`) | | **Posture toggle** | `monkeypatch.setattr` on the settings attribute `musehub_overseer_provenance_enrichment_enabled`, reached through the service module — the identical mechanism already used at `tests/unit/test_overseer_provenance_f7.py:33-49` (F7). Set the string `enabled` for A1–A3 and `disabled` for A4 | | **Why not the env var** | The field is env-aliased (`musehub/config.py:168-171`, F7) but the settings object is already instantiated, so setting the environment variable at test time has no effect. Attribute patching is the only correct lever | | **Compile-time flag** | `OVERSEER_PROVENANCE_ENRICHMENT_AUTHORIZED` at `musehub/services/musehub_overseer_provenance.py:16` (F7) stays **untouched** — never patched, never read-modified. Posture is the only variable | | **Postgres** | Not required, not started, not referenced by the new file | | **Fixture ref** | `flow_run:fixture-overseer-001`, seeded in-memory at `musehub/services/musehub_overseer_provenance.py:89-100` (F7). The new file must not seed, mutate, or delete store entries | | **Definition order** | The four cases must be **defined in the file order A1, A3, A2, A4** (pytest runs in definition order; the repo pins `-p no:randomly` at `pyproject.toml:60`). A3's unknown-ref lookup must precede A2's seeded-ref read so that A2 returning the full envelope demonstrates the miss did not mutate or evict store state — that is the data-integrity tier, achieved with no fifth case | ## Frozen WHAT — the four pinned assertions Exactly four test cases. Each issues an **anonymous** GET (no `Authorization` header). | ID | Request | Frozen expectation | | --- | --- | --- | | **A1** | Posture `enabled`; `{base}/api/overseer-run-provenance/flow_run:fixture-overseer-001` | **200**, envelope schema asserted (below) | | **A2** | Posture `enabled`; same ref percent-encoded as `flow_run%3Afixture-overseer-001` | **200**, the returned run ref is **decoded**, and the record equals A1's record exactly | | **A3** | Posture `enabled`; unknown but well-formed ref `flow_run:not-seeded-0001` | **404** | | **A4** | Posture `disabled`; the known-good fixture ref from A1 | **404** — fail-closed | ### A1 — prefixed path returns 200 with the asserted envelope Assert the response body against the serializer at `musehub/services/musehub_overseer_provenance.py:69-84, 111-127` (F7): All thirteen record values are pinned, so the expected record is a **complete** mapping that A2 can reuse verbatim. Ten values come from the seeded fixture at `musehub/services/musehub_overseer_provenance.py:89-100` (F7); the remaining three — `schema`, `contractVersion`, and `untrusted` — are serializer constants injected at `musehub/services/musehub_overseer_provenance.py:69-84` (F7) and are **not** fixture fields. | Field | Frozen value | | --- | --- | | top-level `schema` | `musehub.overseer_run_provenance_envelope/v0` | | `record.schema` | `scooling.overseer_run_provenance/v0` | | `record.contractVersion` | `scooling.overseer_provenance_enrichment/v0` | | `record.runRef` | `flow_run:fixture-overseer-001` | | `record.reviewTrayOutcomeRef` | `outcome:flow_run:fixture-overseer-001` | | `record.outcome` | `pass` | | `record.constitutionVersion` | `0.1.0` | | `record.agentVersionRef` | `agent:fixture-overseer-v0.1.0` | | `record.workerModelFamily` | `family:ollama` | | `record.checkerModelFamily` | `family:llama_cpp` | | `record.externalRef` | JSON `null` — the key stays **present** with a null value | | `record.actorHash` | the character `a` repeated 64 times | | `record.producedAt` | `2026-07-09T12:00:00Z` | | `record.untrusted` | `true` — hardcoded at `musehub/services/musehub_overseer_provenance.py:83` (F7), never derived from the fixture. MuseHub enrichment is always untrusted relative to canonical Knowtation state, so this is `true` for every record and must not be guessed as `false` | | content-free | asserted as an **exact key set**, not a list of absences — see below | The envelope is asserted **over HTTP**, not by calling the service — that is the whole point of F7b. Two exactness requirements make content-freeness mechanically checkable rather than a judgment call: - The **top-level** key set must equal exactly `{schema, record}`. - The **record** key set must equal exactly these thirteen keys, matching `musehub/services/musehub_overseer_provenance.py:69-84` (F7): `schema`, `contractVersion`, `runRef`, `reviewTrayOutcomeRef`, `outcome`, `constitutionVersion`, `agentVersionRef`, `workerModelFamily`, `checkerModelFamily`, `externalRef`, `actorHash`, `producedAt`, `untrusted`. Equality — not "these two content keys are absent" — is what makes the assertion fail if any future change adds prompt text, completion text, or any other request/response body field to the wire. ### A2 — percent-encoded ref returns 200 with a decoded ref The request must be issued with the literal encoded segment `flow_run%3Afixture-overseer-001` (this is the form the Scooling adapter sends). Assert **200** and that `record.runRef` equals the decoded `flow_run:fixture-overseer-001`. The decode happens in the ASGI layer before routing; F7b asserts the observable outcome and must not re-implement or bypass encoding. A2 additionally carries the **data-integrity** burden, because the frozen definition order places it after A3's unknown-ref lookup. A2 must therefore assert the **whole** record — mapping equality against the complete expected record of A1, all thirteen keys and all thirteen values — not merely the run ref. This is what proves the miss in A3 neither evicted nor mutated store state. Value equality is required, not just a 200: the shape validator at `musehub/services/musehub_overseer_provenance.py:47-66` (F7) does not inspect `produced_at` and only length-checks `external_ref`, so a mutation of those fields would still return 200 and would slip past a status-only check. ### A3 — unknown ref returns 404 Use a ref that **satisfies** the ref pattern at `musehub/services/musehub_overseer_provenance.py:18` (F7) but is absent from the store, so the assertion proves a store miss rather than a pattern rejection. Assert **404** — never 200, never 5xx. ### A4 — posture off returns 404 (fail-closed) With the posture set to `disabled` and the **known-good** fixture ref from A1, assert **404**. Two further requirements: - The status must be **404**, not 403 and not 5xx — the gate at `musehub/api/routes/musehub/overseer_provenance.py:16-18` (F7) hides existence rather than advertising a forbidden resource. - The response body must **not** contain the record or any of its fields — no leakage of `runRef`, `outcome`, or `actorHash` when the posture is off. Using the ref that A1 proves exists is essential: it distinguishes "the gate closed the door" from "the row was missing anyway". ## Fail-closed rules (frozen) | Condition | Required behaviour | | --- | --- | | Posture not exactly the string `enabled` | 404 for every ref, including seeded ones | | Ref absent from the store | 404 | | Ref failing the ref pattern | 404 (never 5xx) | | Any 404 | No record fields in the body | | Anonymous request | Allowed for the public fixture row only; F7b must not add credentials, headers, or signed requests | ## Test matrix — seven-tier coverage | Tier | Coverage for this slice | Where | | --- | --- | --- | | unit | Service logic: envelope shape, unknown ref, malformed ref, posture on/off, seed validation | Existing six tests, `tests/unit/test_overseer_provenance_f7.py` (F7) — unchanged by F7b | | integration | **The gap this freeze closes** — A1–A4 across the HTTP boundary through the real app route table, real router registration, and the real optional-auth dependency. Module *reachability* and `/api`-prefixed contract presence are covered alongside it by the gates carried in B2. Registration *uniqueness* is **covered by nothing** on this branch — neither the assertions nor the gates detect a duplicate root mount (§ "Verify tooling must travel with F7b"); it rests on a prohibition, and closing it is a named follow-up | New `tests/unit/test_overseer_provenance_f7_http.py` + `tools/verify/check_consumer_contract.py:130-131` | | e2e | Live staging spot-check of the prefixed path after merge + deploy | **Out of scope — Tier 3.** Tracked in `docs/MUSEHUB-OVERSEER-HANDOVER.md:169-175`; must not be simulated or claimed by F7b | | stress | Not applicable and deliberately omitted: the handler is a read-only in-memory dictionary lookup with no I/O, no connection pool, and no shared mutable state. A loop over the same lookup would assert nothing about production load, whose real limits are the ASGI server and DB pool that this slice does not use | | data-integrity | A GET must not mutate the store: with the frozen definition order A1, A3, A2, A4, the unknown-ref lookup (A3) runs **before** A2's seeded-ref read, and A2 asserts the **full** record — exact key set plus every field value — so a miss that evicted or mutated state fails A2. Record-shape rejection is already covered by the seed-validation unit test | Definition-order row in the mechanism table; the full-record requirement in § A2 | | performance | No timing gate. The four cases run in-process with no DB and no network; the meaningful budget is that the whole new file stays in the sub-second range that `tests/unit/` already exhibits (9 tests in 0.09 s, measured this session). F7b must not add a wall-clock assertion that would flake on loaded CI | | security | A4 proves fail-closed on posture off with no body leakage; anonymous access is exercised through the **real** optional-auth dependency (only the DB session is overridden, never the auth dependencies); no secrets, tokens, or credentials appear in the new file; no path-traversal surface — the ref is pattern-validated server-side | A4 + the override prohibitions above | Every freeze-review finding on this artifact MUST cite **file+line** (SPEC §6). ## Ground-truth edge F7b (Auto) may treat this document as ground truth for the scoped work without re-deriving it: it must not re-litigate the registration question, re-measure the discovery ordering, or redesign the assertion set. Any disagreement with a frozen decision is a **stop-and-report**, not a redesign. This is a side-check freeze under the Check OK path; it does **not** open a new `docs.lanes` baton. ## Out of scope for F7b (named, not silently dropped) | Item | Where it belongs | | --- | --- | | Asserting `/api`-prefixed template presence **inside the new test file** | Covered mechanically by `tools/verify/check_consumer_contract.py:130-131`, carried in B2. The new test file must not duplicate it, and must not grow a fifth case to cover it | | Gating registration **uniqueness** (no second root-level mount) | Covered by **nothing** today — proven in round 7 (§ "Verify tooling must travel with F7b"). Closing it means extending `tools/verify/check_consumer_contract.py` to reject unexpected `paths` entries for the contract endpoints, which F7b may not edit. **Named follow-up step, not silently dropped** | | Flipping the consumer-contract entry from `pending` to `required` | Only after F7 merges to `dev` — `tools/verify/check_consumer_contract.py:97-103` | | Authenticated / private-row behaviour | Deferred in the route's own comment; v0 serves the public fixture row only | | Durable persistence for the enrichment store | Future phase; the in-memory store is the frozen v0 shape | | Replying on the proposal, Muse push, Gabriel re-review, staging deploy | F7b closing steps and Tier 3 operator gates, per `docs/MUSEHUB-OVERSEER-HANDOVER.md:104-139` | ## Exit criteria for F7b (mechanical, no judgment) 1. Exactly one new **authored** file, `tests/unit/test_overseer_provenance_f7_http.py`, containing exactly the four cases A1–A4, defined in the frozen order A1, A3, A2, A4. 2. Measured as the diff between the **recorded B2 commit ref** and the F7b tip, the F7b diff adds exactly that one file and touches **no** file under `musehub/api/routes/`, **no** `musehub/main.py`, and **no** `conftest.py` — governance docs excepted. 3. With Postgres **not running**, both files pass: `.venv/bin/python -m pytest tests/unit/test_overseer_provenance_f7_http.py tests/unit/test_overseer_provenance_f7.py -q` 4. The six pre-existing F7 unit tests still pass unmodified. 5. All three carried gates exit 0, invoked through their wrappers so interpreter resolution is the tooling's own concern. The four `scripts/verify/` files are mode `100644` in the Muse tree with **no execute bit**, so they must be invoked via `bash` — a bare `scripts/verify/…` path exits 126 (permission denied), which is not a pass: `bash scripts/verify/verify_app_imports.sh`, `bash scripts/verify/verify_routes_registered.sh`, `bash scripts/verify/verify_consumer_contract.sh`. Do **not** `chmod +x` them — the prohibition table forbids modifying a carried file, and criterion 6 pins mode as well as content. After B1 the consumer-contract gate must take its **served-but-pending** branch, printing the `NOTE: … is now served but still marked pending` line at `tools/verify/check_consumer_contract.py:135-139` — **not** the `PENDING: … not yet merged to dev` line at `:150`, which is the pre-B1 output and does not satisfy this criterion. Exit 3 means no interpreter could import the app (`scripts/verify/_python_env.sh:41-58`) and is **not** a pass. Exit 2 means the app failed to import **under the resolved interpreter** (`tools/verify/check_routes_registered.py:113-116`, `tools/verify/check_consumer_contract.py:113-116`); because `_python_env.sh:36-37` selects `.venv/bin/python` on the executable bit alone without applying the `_verify_can_import` guard it uses for the `python3` fallback at `:38`, a stale `.venv` also produces exit 2. Confirm with `${PY} -c 'import musehub.main'` before reporting exit 2 as a code defect. 6. The seven files carried in B2 are byte-identical to their `feat/musehub-overseer-governance` versions — verifiable by comparing each against `muse cat --at feat/musehub-overseer-governance ` — **and unchanged in mode**, which `muse cat` cannot show: confirm the four `scripts/verify/` files are still `100644` via `muse ls-tree -r feat/musehub-overseer-governance` compared against the F7b tree. Content comparison alone would pass a `chmod +x`. 7. `/build-verification-review` reaches **`pass`** before any DONE claim — green tests alone are not sufficient. ## Tier authority | Action | Tier | | --- | --- | | Feature-branch Muse commit of this freeze + governance docs | **1** — do without asking | | F7b test implementation on a feature branch | **1** | | Merge to Muse `main` / `dev`, push to staging, deploy, posture flip in any live environment | **3** — operator authorization required | Escalation categories that stop the review loop: `security`, `irreversible`, `real_money`, `gates_tier3`. ## Review record | Round | Reviewer | Verdict | Resolution | | --- | --- | --- | --- | | 0 | `ok check-ok --dry-run` (mechanical checklist, C1–C8) | pass | 0 findings | | 1 | Freeze-review loop (independent thinking reviewer) | findings | **F1** BLOCKER — no branch baseline named, making exit criteria 2 and 3 jointly unsatisfiable → added § "Branch baseline for F7b" (B1/B2) and re-scoped criterion 2 to the post-B1 baseline. **F2** MAJOR — data-integrity ordering contradicted the case order → pinned definition order A1, A3, A2, A4 and rewrote the data-integrity row. **F3** MAJOR — "shipped" over-claimed an unmerged branch → restated as implemented-but-not-merged with contract/handover citations. **F4** MINOR — bare `python` not on PATH → exit criterion 5 now uses `.venv/bin/python`. **F5** MINOR — content-free assertion unenumerable → replaced with exact thirteen-key record key set. Also tightened three citation ranges the reviewer flagged as loose (`:39-44`, `:69-84, 111-127`, `:89-100`) | | 2 | Freeze-review loop (same independent reviewer, re-read from disk) | findings | **R2-F1** MAJOR — the data-integrity burden moved to A2 but A2 still asserted only status and run ref → § A2 now requires the full record (exact key set **and** every field value), citing the validator's blind spots at `musehub/services/musehub_overseer_provenance.py:47-66` (F7); assertion table and data-integrity rows updated. **R2-F2** MINOR — the post-B1 baseline was not identifiable as a ref → B1 must land as its own Muse commit whose ref is recorded in the handover, and criterion 2 is now the diff between that ref and the F7b tip. Also replaced the branch-dependent "index 51 of 52" with the load-bearing sort-key fact the reviewer could confirm | | 3 | Freeze-review loop (same independent reviewer, re-read from disk) | findings | **R3-F1** MAJOR — § A2 scoped value equality to "every field value pinned in A1's table", but A1 pinned only six of thirteen values, excluding `producedAt` and `externalRef` — the two the rationale exists to protect → A1's table now pins **all thirteen** values, and § A2 now requires mapping equality against that complete expected record | | 4 | Freeze-review loop (same independent reviewer, re-read from disk) | findings | **R4-F1** MAJOR — a regression from round 3: re-sourcing A1's table "from the seeded fixture" silently dropped `record.untrusted`, which is a serializer constant at `musehub/services/musehub_overseer_provenance.py:83` (F7) and not a fixture field, leaving twelve values pinned while three statements claimed thirteen → row restored with value `true` and an explicit note that it must not be guessed as `false`; the table preamble now names both value sources (fixture `:89-100` for ten, serializer constants `:69-84` for `schema`, `contractVersion`, `untrusted`) | | 5 | Freeze-review loop (same independent reviewer, re-read from disk) | **pass** | 0 findings. Reviewer independently AST-parsed `_serialize_record` on the F7 branch (13 keys, 13 pinned rows, no missing, no extra, identical order), executed the F7 service and matched all thirteen values plus the top-level schema against live output, and confirmed the ten-fixture / three-serializer-constant split. All nine findings from rounds 1–4 resolved with no new regression; A1–A4 each judged unambiguously buildable from this document alone. **F7b (Auto) is cleared to start** | | 6 | Post-`pass` amendment (operator-authorized scope change, not a review finding) | amended — re-review required | **A1** BLOCKER that rounds 1–5 all missed: exit criterion 5 invoked `tools/verify/` scripts that exist on **no** branch F7b would be cut from (enumerated this session: 3 files on `feat/musehub-overseer-governance`, 0 on `dev`, `main`, and `feat/9a-4-f7-overseer-provenance`), so the criterion was unexecutable — and the out-of-scope table used those same absent scripts to justify not asserting registration uniqueness, leaving the reviewer's exact requested change ungated on the F7b branch. Operator chose to carry the tooling rather than defer it → new § "Verify tooling must travel with F7b" (seven files, verbatim, as commit **B2**); step order renumbered B1/B2/B3; criterion 2's baseline moved to the recorded **B2** ref; criterion 5 rewritten to invoke the shell wrappers, whose `_python_env.sh` also supersedes finding F4's `.venv` workaround; new criterion 6 pins byte-identity of the carried files; new prohibition against editing or relaxing a carried gate; integration and out-of-scope rows updated. Assertion set A1–A4 **unchanged** | | 7 | Freeze-review loop (independent reviewer, amendment scope) | findings | **A2-F1** BLOCKER — the amendment's central justification was false. The reviewer added a duplicate root-level `include_router` and re-ran both carried gates: **both exited 0 with byte-identical `ARTIFACT_SHA256` digests**, because the consumer gate is a presence test over three hardcoded templates and the routes gate is a reachability test, and FastAPI's duplicate `operationId` is a warning rather than an exception (`tools/verify/check_consumer_contract.py:112`, `:130-131`; `tools/verify/check_routes_registered.py:151`). Carrying the tooling does **not** gate registration uniqueness → rationale, test matrix, and out-of-scope rows rewritten to state actual coverage, with the absence of any uniqueness guard recorded explicitly and named as follow-up work. **A2-F2** MAJOR — the four `scripts/verify/` files are mode `100644`; criterion 5's bare-path invocation exits 126 → invocations now prefixed with `bash`, with `chmod +x` explicitly forbidden. **A2-F3** MAJOR — line 83 still said "two steps" above the three-row table, letting an Auto session skip B2 entirely → corrected. **A2-F4** MINOR — mechanism table still claimed "exactly one new file" unqualified → "authored". **A2-F5** MINOR — criterion 5 cited the `ContractPath` declaration instead of the served-but-pending report branch → now cites `:135-139` and pins the expected `NOTE:` string against the pre-B1 `PENDING:` line at `:150`. **A2-F6** MINOR — exit 2 attributed solely to app-import failure, but `_python_env.sh:36-37` selects `.venv/bin/python` on the executable bit without the `_verify_can_import` guard it applies to the `python3` fallback, so a stale venv also yields 2 → qualified. Reviewer independently confirmed the branch enumeration, the seven-file closure (stdlib + `musehub.main` only, with no dependency on kit config, a route manifest, or a policy file, and an empty `UNMOUNTED_ALLOWLIST`), that `musehub/api/routes/**` and `main.py` are byte-identical between `dev` and the governance branch, and that A1 survives a duplicate mount (238 → 265 paths, none removed) | | 8 | Freeze-review loop (same independent reviewer, amendment fixes) | findings | All six round-7 findings confirmed fixed: every one of the eleven cited line numbers re-derived independently at `feat/musehub-overseer-governance`, and criterion 5's three `bash` invocations executed verbatim (all exit 0, and the current-tree output is the `PENDING:` variant the criterion correctly rejects as pre-B1). Four MINOR inaccuracies remained in the newly written coverage text, all now corrected: **R8-F1** the coverage table claimed `/api` template presence was gated for all three templates, but the overseer template is `pending`, so its absence is reported without failing (`tools/verify/check_consumer_contract.py:142-148` vs `:149-150`) — proven by running the gate on a tree lacking the F7 route, which exited 0 with `2/3` → row changed to **Partly** with the mechanism named. **R8-F2** `check_routes_registered.py` was twice described as an OpenAPI/template gate, but it contains no reference to OpenAPI and its docstring calls identity comparison "immune to prefix drift" (`:11-15`) → mislabel removed from both the carried-file table and the out-of-scope row. **R8-F3** "the two L1 checkpoints" where three are carried → corrected. **R8-F4** criterion 6's `muse cat` comparison would pass a `chmod +x` because Muse emits content only → criterion 6 now also pins mode via `muse ls-tree`, and the `chmod` prohibition no longer rests on a check that could not detect it. No BLOCKER, no MAJOR; assertion set, mechanism table, prohibitions, fail-closed rules, and tier authority confirmed untouched and internally consistent |