<!--
Implementation profile — verifier sidecar. Lead lazy-loads this file ONCE
at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
-->

# Implementation profile — Verifier sidecar

## Verifier independence

- Every verdict comes from a fresh session with no shared context, never from the session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
- Session isolation is the primary self-review safeguard: each verifier is a separate invocation with its own context window. Reusing the executor's model is acceptable. The model comes from the run's stored assignment.
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.

## Verifier QA duties (independent re-run mandate)

Every verifier acts as a QA gate, not just a diff reviewer. Trusting the executor's reported evidence is forbidden — verifiers MUST reproduce it themselves from the same worktree path the executor used.

### Two-tier command lookup (NO auto-detection)

Verifier obtains the QA command set from exactly two declared sources, in order — there is **no fallback to guessing tools from manifest files**.

1. **Tier 1 — plan validation set (task-specific):** every command listed under the approved plan's `validation` block (pre / mid / post). The plan is the file at this prompt's `**Approved plan:**` anchor, scoped to the stage its `**Stage for this implementation run:**` anchor names; both are generated headers, so a missing one is `contract-violated`, never a value to infer.
2. **Tier 2 — project baseline:** the project's standing QA baseline from the `Project QA Commands` section emitted by `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <task-ref>`.
   ```json
   {
     "qaCommands": {
       "lint":      [{ "label": "cargo clippy", "cmd": "cargo clippy --all-targets -- -D warnings", "language": "rust" }],
       "format":    [{ "label": "cargo fmt",    "cmd": "cargo fmt --check",                          "language": "rust" }],
       "typecheck": [{ "label": "tsc",          "cmd": "pnpm exec tsc --noEmit",                     "language": "ts"   }],
       "test":      [{ "label": "cargo test",   "cmd": "cargo test --workspace --locked",            "language": "rust" }],
       "db-test":   [{ "label": "db integ",     "cmd": "pnpm test:db",                               "language": "ts"   }]
     }
   }
   ```
   `language` is optional; when present, verifier MAY skip categories whose `language` is not represented in this run's diff (recorded as `qa-command skipped: <label> (language=<x> not in diff)`). Absent `language` means "always run".

### Execution rule

Tier 1 commands run verbatim first. Then every Tier 2 entry runs once. Then the Tier 3 stage conformance script (below) runs once. Then the self-mock detector (below) runs once whenever the diff changed a test file. Each command runs in the worktree cwd, and is recorded in the worker result with its exact command line, exit code, and the tail of stdout/stderr. Substituting or paraphrasing a Tier 1 command is forbidden (see Verifier-specific forbidden actions below).

### Tier 3 — stage conformance scripts

Tier 3 still attempts the declared `runCommand` against the permitted test
environment. Classify the manifest entry by `requires`: any `db`, `http`, or
`external` capability makes the entry external-advisory; an empty list or
`io`-only entry stays blocking.

**External QA outcome policy.** Record PASS normally. For FAIL, MISSING, no
result, startup failure, credential/network/service absence, write the honest
sidecar and command evidence with status `ADVISORY`. That result alone
MUST NOT change the overall verifier verdict from PASS to CONCERNS/FAIL, trigger a fix
cycle, or block the stage. Return the exact command, prerequisites, observed
result, and expected result to the report writer as a user-owned follow-up.
Enforcement: `scripts/okstra_ctl/conformance.py` (`decide_conformance_gate`)
performs the core advisory classification/reduction;
`validators/validate-run.py` (`_validate_conformance`) routes that status to
warnings instead of failures; and
`tests/contract/test_okstra_ctl_conformance.py` (`test_external_non_pass_is_advisory`),
`tests/contract/test_validate_run_conformance.py` (`test_external_non_pass_returns_warning_not_failure`),
and `tests/contract/test_final_report_contracts.py` (`test_external_qa_advisory_renders_without_downgrading_accepted_verdict`)
lock the core, gate, and accepted-report behavior respectively.

An `io`-only non-PASS remains BLOCKING. Manifest/schema/source-mutation defects
also remain contract violations.

- **Source.** The conformance manifest is `<task_root>/qa/conformance-manifest.json` (the directory is the `TASK_QA_PATH` token). This run's stage conformance entry is the manifest `entries[]` item whose `stageKey` equals this run's stageKey — `<task-id>-stage-<N>`, where `<N>` is the injected Stage number. Find that one entry; ignore the others (other stages are run by their own implementation runs or by final-verification).
- **Exemption / waiver → do NOT run.** If the entry carries an `exemption` (or a user `waiver`), the verifier does NOT execute the script. It records the fact and the reason (`exemption.reason` / `waiver.reason` + `waiver.acknowledgedBy`) in the Read-only command log AND writes the result sidecar reflecting the skip. An `exemption` passes outright. An external-advisory waiver is reported as `ADVISORY` with `conditional=false`; only an `io`-only blocking waiver is conditional. An empty `requires` list cannot be waived; it is declaration/contract trouble and remains BLOCKING. No script runs in either permitted waiver case.
- **Otherwise run `runCommand` in the worktree cwd.** Execute the entry's `runCommand` verbatim from the worktree cwd. Inject env from `<PROJECT_ROOT>/.okstra/project.json`'s `qaEnv` (replica DB DSN / app base URL / env file — declared in Phase 4e). This is a **replica / test environment only** path — never run it against shared / staging / prod, identical to the DB real-execution gate principle above.
- **Interpret the standard interface.** Parse the process exit code together with stdout: the `QA-RESULT: PASS|FAIL` marker line (if several appear, the last one wins) and the per-requirement `REQ <id>: PASS|FAIL: <reason>` lines. If no `QA-RESULT` marker is emitted, the overall result is `MISSING`; classify it according to the entry's blocking or external-advisory capability policy above.
- **Write the result sidecar (BLOCKING deliverable).** Write `<task_root>/qa/result-<stageKey>.json` as:
  ```json
  {
    "stageKey": "<task-id>-stage-<N>",
    "overall": "PASS",
    "ranAt": "<UTC ISO8601>",
    "requirements": { "<id>": { "status": "PASS", "reason": "<from REQ line>" } }
  }
  ```
  `overall` is exactly one of `PASS` / `FAIL` / `MISSING`. Writing the honest sidecar is mandatory whenever the script runs and on the exemption/waiver skip path. A missing `io`-only sidecar blocks; a missing external-advisory sidecar is reported as `ADVISORY` rather than accepted as hidden evidence.
- **Read-only command log.** Record the `runCommand` exact line + its exit code in the Read-only command log. Tier 3 external non-PASS evidence MUST remain visible with status `ADVISORY`. Unlike Tiers 1·2, a conformance script MAY mutate the **replica datastore** (exercising integrated state is its whole purpose) — but only the `qaEnv` replica target, never a shared/staging/prod store. The `runCommand` itself is still subject to the same source/lockfile mutation deny-list as Tier 2 (`--fix`, `npm install` without `ci`, etc.); a denied token aborts with `contract-violated`.
- **No manifest / no entry for this stage.** If the approved plan declared `Conformance exemption:` for this stage, and the manifest is absent or has no matching `stageKey`, record `conformance: no manifest entry for <stageKey>` and proceed. If the approved plan declared `Conformance tests:` and the script file or matching entry is absent, that is a FAIL — do not treat it as a skip. **Enforced:** `validators/validate-run.py` `_validate_conformance`.

### Self-mock detection (changed test files)

A green suite does not prove a test exercises the unit it names — a test that stubs its own SUT passes forever, including after the real implementation is deleted. The static detector is the machine half of the **Self-mocking** blocking check below, and running it is the verifier's own duty: it is never delegated to the executor and never inferred from the executor's evidence.

- **Trigger.** This run's diff changed at least one **test** file. Enumerate the changed files with `git diff --name-only <base>...HEAD` from the worktree cwd — the same enumeration the static review's Scope rule uses — then keep only the paths the gate itself treats as tests: `*.spec.*`, `*.test.*`, a `test_`-prefixed basename, a `_test.` suffixed basename, or any path segment `test/` or `tests/`. Pass nothing else; non-test files are excluded. Exclude `tests/fixtures/self_mock/**` as well — those are the detector's own deliberately self-mocked fixtures, which `validate-run.py` also excludes from the trigger, so feeding them in would manufacture a `FAIL` the gate then blocks on. No changed test file → no run and no sidecar; the gate is vacuous by design.
- **Run the detector once, in the worktree cwd**, one `--test-file` per changed test file:
  ```bash
  python3 ~/.okstra/lib/validators/detect_self_mock.py \
    --test-file <changed test file> [--test-file <changed test file> ...] \
    --changed-file <changed file> [--changed-file <changed file> ...] \
    --sidecar <task_root>/qa/self-mock-<stage-name>.json \
    --stage-name <stage-name> \
    --waivers <task_root>/qa/self-mock-waivers.json \
    --diff <task_root>/qa/self-mock-<stage-name>.diff \
    --worktree <this stage's worktree root>
  ```
  `--changed-file` / `--diff` / `--worktree` feed **gate B** (the mutation probe) and are separate from `--test-file`, which feeds gate A. `--changed-file` takes **every** path in the stage's diff — production sources included, not only the test files — because each mutation adapter selects its own production sources out of that set; hand it only the test files and every adapter finds nothing to mutate, which records a vacuous PASS while gate B is silently dead. Write `--diff` first with `git diff <base>...HEAD > <task_root>/qa/self-mock-<stage-name>.diff` — the same `<base>` and the same range every other `git diff` in this file uses: it is what scopes surviving mutants to the lines this stage added or modified, and without it gate B reports `unsupported(diff-unavailable)` rather than guessing.
  `<stage-name>` is literally `stage-<N>` for this run's injected Stage number (`stage-3` — not the bare number, not the stageKey), and `<task_root>/qa` is the `TASK_QA_PATH` token, the same directory Tier 3's manifest and `result-*.json` live in. A whole-task run with no stage writes `<task_root>/qa/self-mock.json` and omits `--stage-name`. Any other filename or directory is invisible to the gate and reads exactly like "the detector never ran". Pass `--waivers` **unconditionally**: an absent waiver file is the normal case and the detector treats it as "no waivers", so there is no branch to decide and no file for you to create.
- **Write the result sidecar (BLOCKING deliverable).** The detector writes `<task_root>/qa/self-mock-<stage-name>.json` itself:
  ```json
  {
    "stageName": "stage-<N>",
    "overall": "PASS",
    "ranAt": "<UTC ISO8601>",
    "scannedFiles": ["<test file the detector read>"],
    "skippedFiles": ["<test file it received but could not read>"],
    "changedFiles": ["<every --changed-file path you passed>"],
    "staticDetect": { "status": "PASS", "hits": [], "waived": [], "waiverSource": "<the --waivers path, or null>" },
    "mutation": { "status": "unsupported(stryker:tool-not-declared)", "tool": "stryker", "survived": [], "survivedTotal": 0, "waived": [], "waiverSource": "<the --waivers path, or null>" }
  }
  ```
  `overall` is exactly one of `PASS` / `FAIL`. `scannedFiles` + `skippedFiles` together are the detector's own record of **every** `--test-file` path it received: it read the first list, and could not read the second (extension with no signal set, or no file on disk). Which list a path lands in is the **detector's** decision, never yours — a Go/Ruby/C# test, a JSON fixture under `tests/`, and a test file this stage deleted are all legitimate `skippedFiles` entries and none of them is a defect. The verifier MUST NOT hand-write, edit, or "correct" this file — the detector's own output is the evidence, and a hand-authored sidecar is a `contract-violated` outcome. Its absence is not a passive skip: **Enforced:** `validators/validate-run.py` `_validate_selfmock` fails any report whose §5.7.3 diff summary lists a changed test file while this sidecar is absent, unreadable, malformed, or carries `overall != PASS`.
  **Enforced (coverage, gate B):** the same gate fails the report when a file from §5.7.3 is missing from `changedFiles`, or when that field is absent — that is how "gate B saw this stage" stays distinguishable from "gate B was handed nothing". Pass **every** path in the diff summary to `--changed-file`, production sources included.
  **Enforced (coverage, gate A):** the same gate fails the report when a changed test file from §5.7.3 appears in **neither** `scannedFiles` nor `skippedFiles` — that means you never passed it, and a PASS over a narrower input says nothing about the file left out. So pass **every** file the trigger enumeration kept, in the same repo-relative spelling the diff summary uses, and let the detector sort them. Pre-filtering by language, or dropping a path because the stage deleted the file, is the one way to trip this check.
- **Suspected false positive → report it, never waive it (self-check safety).** The signal set is regex-based, so it will occasionally accuse a test that is not self-mocked. The escape hatch is `<task_root>/qa/self-mock-waivers.json`, a JSON array of `{"file": "<repo-relative path, as in --test-file>", "line": <hit line>, "signal": "<signal name>", "reason": "<why this hit is not a self-mock>", "acknowledgedBy": "<the user who accepted it>"}`. A waived hit moves out of `staticDetect.hits` into `staticDetect.waived` and stops counting toward the verdict, so it is the one input that can talk the gate out of a finding — and the finding is about **the code this run is verifying**, which is why the acknowledgement must come from outside the run.
  **The verifier MUST NOT create, edit, extend, or re-order that file.** It is the user's acknowledgement channel, not yours; writing an entry into it is self-certification of your own finding and is a `contract-violated` outcome exactly like hand-editing the sidecar. You also MUST NOT point `--waivers` at any other file, and MUST NOT copy a waiver entry into the sidecar by hand. **Enforced (source):** the detector records the `--waivers` argument verbatim as `staticDetect.waiverSource`, and `_validate_selfmock` fails any report whose sidecar carries a non-empty `waived` read from anywhere other than this task's `<task_root>/qa/self-mock-waivers.json` — so redirecting the flag at a file you wrote yourself blocks the run instead of clearing it, and every applied waiver is left in the task bundle to review.
  What you do instead: keep the verdict `FAIL`, and record the suspected false positive in your worker result under the hit's citation — the `path:line`, the signal name, why you believe it is not a self-mock, and the exact JSON object the user would add. The user reviews it, adds the entry with their own `acknowledgedBy`, and the next detector run picks it up through `--waivers`. **Enforced:** `validators/validate-run.py` `_validate_selfmock` fails the report when any `staticDetect.waived` entry is missing a non-empty `reason` or a non-empty `acknowledgedBy` — so an unacknowledged or unexplained waiver blocks the run instead of clearing it, and a matching-but-unacknowledged waiver reaches that gate rather than being silently dropped by the detector.
- **Read-only command log.** Record the exact command line, its exit code (`0` = PASS, `1` = FAIL), and the detector's last stdout line `QA-RESULT: PASS|FAIL`, together with every `SELF-MOCK <file>:<line> <signal>` line it printed. When the sidecar's `staticDetect.waived` is non-empty, list each waived hit with its `reason` and `acknowledgedBy` so the report shows what the run was excused from and on whose authority. A `FAIL` sets the verifier verdict to `FAIL` with each hit cited `path:line` + signal name and the recommended fix recorded (delete the stub and exercise the real method, or stub injected collaborators only) — the same verdict machinery as the **Self-mocking** blocking check below, which the detector cites for but does not replace: a self-mock the detector's signal set does not cover is still the verifier's finding to make by reading the diff.
- **Gate B (mutation) runs inside the same detector call.** The detector invokes the mutation probe itself over `--changed-file` and writes the `mutation` block (`status` / `tool` / `survived` / `waived`); your duty is to pass the three flags above, never to author or edit that block by hand. Gate B is a **real gate now** — the `mutation` block is no longer a `pending-phase-2` placeholder, and running the probe over the supported languages in this diff is MANDATORY, which is what the `--changed-file` / `--diff` / `--worktree` flags above accomplish. `status` is `PASS`, `FAIL`, or `unsupported(<reason>)`, and the reason's **class** decides what happens:
  - **Capability gap** (`no-adapter:<lang>`, `tool-not-declared`, `diff-scope-unavailable`, `no-production-sources`, `no-changed-sources`) or **nothing to verify** (`no-mutants-generated`, `diff-adds-no-line`) — gate B legitimately had no tool or nothing to check. Non-blocking; this is the normal case in a repo without mutation tooling.
  - **Integrity / inspection failure** (`diff-incomplete`, `diff-unavailable`, `report-unavailable`, `report-unparsed`, `adapter-malformed-status`, `no-conclusive-mutants`, or any reason not listed above) — **this BLOCKS the run.** It means the stage was never actually inspected: most often a `--diff` you built from a different `<base>` than the `--changed-file` list, or one written before your last edit, so the diff does not cover the changed sources. Rebuild the diff from the same `<base>` and re-run the detector; do not treat it as a skip.
  **Enforced:** `validators/validate-run.py` `_validate_selfmock` blocks on `mutation.status == "FAIL"`, on a missing or malformed `mutation` block, and on any `unsupported(...)` in the integrity/inspection class; the other classes stay excluded from the verdict and are kept in the sidecar for audit.
- **Suspected false-positive MUTANT → report it, never waive it (self-check safety).** A surviving mutant can be a false accusation too — an unreachable branch, a mutation with no observable behaviour. The escape hatch is the SAME `<task_root>/qa/self-mock-waivers.json` gate A uses, so the user manages one file: a static entry is keyed `{"file", "line", "signal", ...}` and a mutation entry `{"file": "<repo-relative path>", "line": <survivor line>, "mutant": "<mutator name as the detector printed it>", "reason": "<why this mutant is not a real gap>", "acknowledgedBy": "<the user who accepted it>"}`. The `mutant` field is what marks it as gate B's; entries without it are gate A's and never clear a mutant. A waived mutant moves out of `mutation.survived` into `mutation.waived`, and once every survivor on a changed line is waived the mutation verdict is `PASS`.
  **The verifier MUST NOT create, edit, extend, or re-order that file** — the same rule as gate A, for the same reason: it is the user's acknowledgement channel, and writing an entry into it is self-certification of your own finding, a `contract-violated` outcome exactly like hand-editing the sidecar. You also MUST NOT point `--waivers` anywhere else. **Enforced (fields + source):** the probe only MATCHES waivers and carries an unacknowledged one straight through, so `_validate_selfmock` fails any report whose `mutation.waived` entry is missing a non-empty `reason` or `acknowledgedBy`, or whose `mutation.waiverSource` is not this task's own `qa/self-mock-waivers.json`.
  What you do instead: keep the verdict `FAIL`, and record the suspected false positive in your worker result under the mutant's citation — the `path:line`, the mutator name, its `Survived`/`NoCoverage` status, why you believe it is not a real coverage gap, and the exact JSON object the user would add. The user reviews it, adds the entry with their own `acknowledgedBy`, and the next detector run picks it up through the same `--waivers` flag. When it is `FAIL`, cite each `MUTANT-SURVIVED <file>:<line> <mutator> (<status>)` line the detector printed: `Survived` means the test ran that line and asserted nothing about it, `NoCoverage` means no test reached it at all — which is what a stubbed subject looks like from the outside.

### Missing-tier handling

If a tier is empty or absent, verifier records the single line `qa-command not configured: <category>` per missing category (`lint` / `format` / `typecheck` / `test`; and `db-test` **only when the diff touches DB/IO/SQL**, where a missing `db-test` is escalated to a blocking finding per the DB real-execution gate below) in the worker result and proceeds — silent omission is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_missing_qa_categories_recorded` for the four unconditional categories; `db-test` is left to the DB gate below because its requirement depends on whether the diff touches DB/IO/SQL. Without the note, "the category passed" and "the category never ran" read identically in the report. Verifier MUST NOT auto-detect or invent a command in this case; the user/operator must declare it in `project.json.qaCommands` or in the plan.

### `cmd` field deny-list (Tier 2 validation)

The runtime AND the verifier MUST reject any `cmd` containing tokens that imply mutation: `--fix`, `--write`, ` -w` (gofmt write), ` -u` (jest snapshot update), `--update-snapshots`, `--snapshot-update`, `--update-goldens`, `INSTA_UPDATE=` (with any value other than `no`), `cargo insta accept`, `npm install` (without `ci`), `cargo update`, `pip install -U`, `pnpm add`, `bun add`. Encountering a denied token aborts the verifier run with `contract-violated` and the operator is asked to re-declare the command in check-only form.

### Discrepancy rule

Tier 3 external-advisory discrepancies are excluded from this promotion: preserve the executor/verifier divergence in the advisory evidence and user-owned follow-up without changing the verdict. For Tier 1, Tier 2, and blocking `io`-only Tier 3, if the verifier's re-run result differs from what the executor reported (a passing test fails on re-run, a clean lint surfaces warnings, an exit code mismatches), the verifier MUST issue verdict `FAIL` with the divergence cited. The Okstra lead MUST NOT silently prefer the executor's evidence over a verifier's reproduced result during synthesis; if it overrides, it MUST cite a concrete reproduction-time reason (flaky-test commit-cited, environment delta documented) — handwaving is not allowed.

### Read-only command log (per verifier)

The worker result MUST contain a `Read-only command log` block listing every command executed during the verifier run with its exact invocation and exit code, in execution order — including the Tier 3 conformance `runCommand` (or the exemption/waiver skip note when no script ran). No source-mutating command may appear in this block; the only permitted mutations are a Tier 3 conformance script writing to its `qaEnv` replica datastore and the self-mock detector writing its own `<task_root>/qa/self-mock-*.json` sidecar — both are artifact-directory writes, both are logged like any other command, and neither touches the worktree source, so the verifier runs them without hesitation. This log is copied into the final report's verifier result section verbatim.

### Verifier evidence is independent of executor evidence

The final report keeps both — executor's `Validation evidence` AND each verifier's `Read-only command log` — so reviewers can compare them line-by-line.

### Static design & test-quality review (gate — runs after the command re-run, before the verdict)

Re-running commands proves the diff *builds and passes*; it does NOT prove the diff is *well-designed*. Lint/test green is necessary but not sufficient — self-mocked tests, interaction-only assertions, and untruthful names all survive a green pipeline. This gate is the filter for exactly those defects, so the executor's design errors are caught here instead of in post-merge PR review. It is a real gate, not a checklist: it enumerates the full diff and a blocking hit forces `FAIL`. This blocking-check taxonomy is the **gate counterpart** of the executor's `Pre-commit diff review sweep` (`_implementation-diff-review.md`) prevention pass — the same defect families, different action (executor fixes in place; verifier fails the verdict). Both lists are inlined on purpose: each sidecar is delivered stand-alone into a flat CLI prompt (lazy-read, not INCLUDE-expanded), so neither can factor the taxonomy into a shared fragment.

- **Scope (no silent sampling).** Enumerate every changed source/test file via `git diff --name-only <base>...HEAD` and review each one. Skipping a changed file silently is a `contract-violated` outcome. If a file's language has no reference and is not covered by the agnostic checks below, record `design-review skipped: <file> (language=<x> no reference)` — never pass it silently.
- **Load the same conventions the executor used via the routed pack.** Use this worker prompt's `**Coding preflight pack:**` anchor header as the absolute path to the installed routed pack. Read `overview.md` first, then `clean-code.md`, then apply the router's three ordered stages: language, framework, architecture. In each stage, iterate every rule, treat a rule as matched when any listed condition is true, and accumulate every matching resource — including `frameworks/node-server.md` for server-side Node work and `architectures/hexagonal.md` for ports-and-adapters / NestJS-hex layouts. Degrade to the agnostic checks below when the resolved pack is unreadable, and record either `coding-conventions: resources=<...>` or `coding-conventions: resource-unavailable → applied <project rules + agnostic principles>`. The verifier does NOT inline language rules — it loads the same situation-specific resources as the executor preflight.
- **Load the project's review rule packs.** Run the project-context projection above and union its `Project Review Rule Packs` entries with exact `SKILL.md` paths cited by the task brief's `Source Material` / `Reporter Confirmations`. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an overlay on this static review, but do NOT dispatch extra reviewer agents unless the task explicitly configured them. Record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited` in the worker result — an unreadable declared pack is a recorded gap, not a skip.
- **Declared architecture style promotes the placement overlay from advisory to binding.** Take `Architecture style` from that projection and record `architecture-style: <hexagonal|layered|none>` in the worker result next to the `coding-conventions:` line. A declared `hexagonal` counts the overlay as loaded even when none of the router's Stage 3 layout signals matched, so the **Hexagonal** blocking check below applies in full, and the concrete-adapter injection listed under Advisory findings is promoted to a blocking finding → verdict `FAIL`, not a `should-fix`. A declared `layered` has no pack resource; its binding invariant is direction — an upper layer may import a lower one, never the reverse — so a changed file whose import list reaches back up a layer, or around a layer boundary, is a blocking placement violation cited `path:line` from that import list. The `layered` half is worker judgement: no machine check reads layer names, so a missed reverse dependency is a missed finding, not a validator failure. A `none` or absent projected style leaves Stage 3 detection-driven and the placement items advisory. **Enforced:** `scripts/okstra_project/resolver.py` `resolve_architecture` reads the same stored field for the planning-side rule in `validators/validate-run.py` `_validate_variation_point_analysis`, and `_validate_verifier_fail_blocks_verdict` keeps the resulting `FAIL` from being dropped during synthesis.
- **Blocking checks (any hit → verdict `FAIL`, cited `path:line` + rule name, recommended fix recorded — the verifier does NOT apply it):**
  - **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. Recommend the shared module location.
  - **Self-mocking:** a test for `Foo` stubs/spies a method on the `Foo` instance under test (`jest.spyOn(sut, ...)`, `spyOn(FooService.prototype, ...)` in `foo.*.spec.*`, `vi.mocked(sut)` + stub). Mocking injected collaborators is fine.
  - **Interaction-only assertion:** a test whose only/primary assertion is `toHaveBeenCalled*` / `toHaveBeenCalledTimes` on an internal helper or a non-side-effecting collaborator, with no assertion on the returned value / resulting state / persisted row / emitted event.
  - **Tautological delegation assertion:** a test asserts the SUT result equals a direct call to the same pure helper/collaborator that the SUT delegates to, instead of asserting an independent literal value or observable state.
  - **Untruthful name:** a read-named function (`get*` / `find*` / `load*`) that writes/inserts/mutates; an adapter or repository name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*` / `findActive*`).
  - **Wrong-result trace (every changed source file):** follow the paths this diff creates or alters to their end — error, partial, concurrent, selection — and name the input or state that produces a wrong result (`clean-code.md` §"Trace what this change can do wrong"). This one is an obligation, not a pattern: the defects that reach production are usually ordinary code with no name on this list. The bar is also the noise filter — a finding names the failing input; an alternative structure, a guard for a state no caller reaches, or a "consider extracting" is an improvement and verdicts `clean`. **The unit is the changed source file**: record `clean` or findings for each one, list every file you excluded with its reason (lockfiles, generated code, pure config), and close the section with `general: <N> files — all verdicted, <M> excluded`.
  - **Business rules in an application service (only when the hexagonal overlay is loaded):** changed service code that decides a business outcome inline — an `if`/`else` chain over domain fields, a business formula in arithmetic, a private method named for a domain concept — instead of calling a domain function (`architectures/hexagonal.md` Rule H6). Orchestration control flow, DTO mapping, and calling a domain predicate are not violations. Blocking when the embedded rule is substantial: money, permissions, a state machine.
  - **Hexagonal (only when the overlay is loaded):** business logic inside a port body; an adapter method that is not pure I/O (post-fetch JS filtering on domain state, domain-rule evaluation); a domain object declared outside the `domain/` boundary; a changed file under the domain folder importing an ORM / DB layer, a framework or its DI decorators, or anything under adapters / infrastructure / services (read the import list — this verdict is mechanical, not a judgment). **The routed pack's project-convention latitude does not reach this cell.** `architectures/hexagonal.md` lets a documented convention resolve conflicts in the project's favour elsewhere in that overlay; here the import list decides, and "matches existing convention" is not an answer to it (see that file's §"How far a project convention reaches"). A run that resolved exactly this finding as project convention returned `clean` and the team's PR review flagged the same import.
  - **gitignored file committed to the branch:** any path in the `git diff --name-only <base>...HEAD` enumeration that `.gitignore` excludes — enumerate them by piping that list through `git check-ignore --stdin --no-index`. A committed ignored file means the executor bulk-added (`git add .`/`-A`) or force-staged (`git add -f`) it, leaking build output, scratch files, or verification artifacts into the eventual PR. This explicitly includes `.okstra/` paths (and `.project-docs/` when the legacy symlink is present): `.okstra/**` is gitignored, so a committed okstra file (qa scripts, conformance results) is always this defect. Cite each path; recommend `git rm --cached <path>` to untrack it while keeping the file on disk. Conformance/qa evidence belongs in the carry sidecar / verifier result, never in git history.
  - **Real-IO test in source tree:** a changed/added test under the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*` — that opens a **real** DB connection / DSN, makes a real `fetch` / `axios` / `http` request, or otherwise hits real external IO without mocking the injected collaborator (a live handle, not a stub/spy). Real-IO tests MUST live under `<task_root>/qa/scripts/` per the executor's *Real-IO test isolation* rule — a live-IO test in source silently breaks the project's CI suite and violates the artifact-home rule. Cite the test file + the real-IO line; recommend moving it to `<task_root>/qa/scripts/` (or declaring it as a Tier 3 conformance script). Mock-only unit tests in source are NOT a hit.
  - **Proxy-based identity decision:** a move / ownership / re-parenting decision taken from a status field or flag while the source and destination identifiers were available and never compared. Cite the condition and the identifiers it should have compared, and show the opposite case the condition also reads true for.
  - **Before-state read after a mutating boundary:** a value recorded as the "before" state (audit column, diagnostic field) that is read off the original object *after* a strategy call, repository write, reload, or intervening `await`, or reconstructed from the post-state. Cite the boundary and the field. Before-values feeding one diagnostic must come from a single pre-boundary snapshot.
  - **Existing-row update outside its owned fields:** a reuse path that writes provenance, ownership, or externally-managed columns with no explicit requirement authorising it, or that loses the create-vs-reuse distinction before the write. The update field set must be an allowlist.
  - **Zero affected rows treated as a verdict:** a conditional write whose zero-row result is reported as success, or as one undifferentiated failure, without re-reading state to separate already-done / conflicting / deleted.
  - **Authoritative type re-declared:** a hand-maintained string union or state-literal list duplicating an enum / type the domain or a dependency already exports. Grep the state literals and the new type name; cite the existing declaration.
  - **Shared fixture default flipped for an exception:** a common fixture's default inverted to exercise one exceptional path, silently changing the meaning of every existing no-argument caller. The exceptional condition belongs in the test that needs it.
  - **Positional mock-argument access:** `mock.calls[<n>][<m>]` (or the framework equivalent) used to inspect arguments instead of an intent-revealing `toHaveBeenCalledWith` / explicit-absence assertion. Detect with `rg 'mock\.calls'` over the changed test files.
  - **Non-separating test data:** two scenarios whose setup values and assertions are identical, so a wrong implementation passes both; or new test tooling added in this diff (mock, state setter, repository branch) that no test calls.
  - **Effect claimed under its own mock:** a test presented as covering an effect whose producing path is replaced by a mock inside that same test. The mock's presence in the harness is not evidence the branch behind it works.
  - **Single-caller abstraction (KISS):** an abstraction layer this diff introduces — a helper module, a strategy / factory / builder, an indirection or wrapper layer, an interface or abstract base — that has exactly one caller after the change, where inlining it at that call site would be simpler. The rule is `overview.md` core principle 2: name the second caller now, or inline. Four exits are legitimate and each must **cite its evidence**: the approved plan declares it (a `testSeams[]` boundary or a variation-point extraction — cite the plan step); the project declares `architecture.style = hexagonal` and this is a port at the domain boundary (a port with one adapter is the normal shape, not a violation); it breaks a real import cycle (name both modules); or its single caller is the public API of a published package. Absent one of those, cite `path:line` and recommend the inlined shape. Do NOT raise this on a function extracted purely to bring a body under the 50-line cap — that is principle 5 doing its job — nor on a pre-existing abstraction this diff merely edits.
  - **Caller-less identifier (YAGNI / orphan):** an identifier this diff leaves with zero callers — either newly added and never called (a speculative parameter, an optional config object, a "future-proof" hook, an exported helper whose only caller is hypothetical), or pre-existing code orphaned because this diff removed its last caller. Grep the identifier across the whole repository rather than the diff alone; a hit inside its own declaration, its own test, or a commented-out line is not a caller. Two exits are legitimate and must be **stated with their evidence**, never assumed: an identifier the approved plan reserves for a named later stage (cite the plan step), and a contract point something other than project code calls (a framework entrypoint, an implemented interface method, a migration hook, the public API of a published package — cite the caller or the registration). Anything else is work no requirement asked for: recommend deleting it, or folding an added parameter back into its single call site. A caller-less identifier is the one YAGNI violation that survives a green pipeline unchanged, which is why it grades here rather than as a recommendation.
- **Advisory findings (recorded as recommendations; verdict MAY still PASS):** function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, weak-but-not-misleading names, priority-between-inputs policy inlined in a service condition instead of a named domain function, an error message asserting a cause the code never observed, a memory / concurrency / batching change with no test pinning the bound it claims, or (hexagonal overlay only) a service dependency this diff adds or modifies that injects a concrete adapter instead of a port — record it with the port sketch; advisory only while the project has not declared `architecture.style = hexagonal`, since that declaration — and only that one — promotes exactly this item to blocking per the declared-style bullet above, while a declared `layered` binds dependency direction instead and leaves this item advisory; an existing convention of concrete injections does not convert this one to `clean`, it is the debt the rule pays down. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
- **Output.** Every finding — blocking or advisory — is a structured item in the verifier's worker result (`path:line`, rule, severity, suggested fix) so it carries into Phase 5.5 convergence and the final report. A blocking hit sets the verifier verdict to `FAIL` with the rule cited, using the same verdict machinery as the Discrepancy rule above. The Okstra lead MUST NOT silently downgrade a cited blocking finding to advisory during synthesis; an override requires a concrete cited reason, exactly as for the Discrepancy rule.

### Fix-run incremental scope (applies when the profile carries a "Fix-Run Carry" block)

A fix run re-verifies a stage whose previous run already passed a full static
sweep and failed only on cited blocking findings. Re-sweeping the whole stage
diff re-buys wall-clock without new information, so the static scope narrows —
the command re-run does not:

- **Command re-run stays full.** Every Tier 1/2/3 command from the plan's
  validation set runs end-to-end exactly as in a first run. QA-RESULT gating
  (`validate-run.py` Tier 3) is unchanged.
- **Static design & test-quality sweep narrows to the fix diff.** Enumerate
  `git diff <prev-head>..HEAD` (the `Previous run HEAD` line of the Fix-Run
  Carry block) instead of the whole stage diff, and re-check each carried
  blocking finding against the current tree. The blocking/advisory taxonomy
  above applies unchanged to that narrowed file set.
- **Carried findings drive the verdict.** Every carried blocking finding MUST
  be re-checked and cited in the verifier result as `resolved` (with the fix
  commit) or `still-failing` (verdict `FAIL`). A new blocking defect inside
  the fix diff also forces `FAIL`. Files untouched since `<prev-head>` are
  out of static-sweep scope — they already passed the previous full sweep.

Enforcement: the carried-finding re-check lands in the verifier result file
(Phase 5.5 convergence consumes it); a fix-run verifier result that cites no
carried findings is a contract violation the lead records via
`okstra error-log append-observed --error-type contract-violation`.

### DB / IO / SQL change — real-execution gate (mock-only acceptance forbidden)

A mocked unit test cannot observe the SQL a query builder actually emits — `count({ col: 'FontFamily.fontFamily' })` passes a mocked suite yet throws `Unknown column` on a real database. For this class of change a green mock-only suite is therefore NOT evidence; only a run against a real (or faithful-replica) datastore is. This gate is the verifier's enforcement of that rule.

**External Tier 3 de-duplication exception.** A DB/IO/SQL surface covered by an in-scope Tier 3 entry whose `requires` include `db`, `http`, or `external` is governed by the External QA outcome policy. Its non-PASS or unavailable result MUST NOT generate a second legacy db-test-not-configured or mock-only blocker solely for that same Tier 3 non-PASS or unavailable result. Tier 1 or Tier 2 failures remain blocking, and DB surfaces without declared external Tier 3 coverage remain blocking.

- **Trigger.** Fires when `git diff <base>...HEAD` touches DB/IO/SQL: ORM / query-builder code (sequelize / typeorm / prisma / knex / raw SQL), `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string.
- **Requirement when fired.** The verifier MUST reproduce a real-DB execution: run the `db-test` tier (Tier 1 = plan `validation` db step; else Tier 2 = `project.json.qaCommands.db-test`) against a **local / replica** datastore (same engine + schema — never shared / staging / prod, consistent with the verifier forbidden-actions list) and record its exact command + exit code. A mock, an in-memory shim that does not parse real SQL, or static reasoning does NOT satisfy this.
- **No `db-test` command available → blocking, not a passive skip.** If neither tier declares a `db-test` command, the verifier records the blocking finding `db-test not configured — DB change unverified (mock-only)` and sets the verdict to `FAIL`; it MUST NOT emit only the passive `qa-command not configured` note and pass. Recommended fix: declare a `db-test` command in `project.json.qaCommands` or the plan's validation set.
- **Mock-only evidence → unverified.** If the diff's only DB coverage is mocked, the verifier labels the DB portion `static-analysis only …, unverified (not executed)` (never `verified`), records it as a blocking finding, and sets `FAIL`. Never downplay the real run as "too heavy / static proof suffices".
- **Surface it at every layer.** The finding is copied verbatim into the verifier result and MUST survive into the final report's `## 6.` and Verdict Card, so the user sees the DB-unverified state continuously — it is the load-bearing reason a downstream `final-verification` cannot reach `accepted` and `release-handoff` cannot push. **Enforced:** `validators/validate-run.py` `_validate_verifier_fail_blocks_verdict` fails a report whose `finalVerdict.verdictToken` is `accepted` / `conditional-accept` while any `implementation.verifierResults[]` row records `verdict: FAIL` — a rejection dropped during synthesis is exactly how rejected work reached `release-handoff`.

## All-verifier-failure policy

If every verifier present in the resolved roster ends with a non-result terminal status (`timeout`, `error`, `not-run`) — i.e. zero independent verdicts were produced — the run MUST end with status `blocked` and route to a follow-up `error-analysis` run. The Okstra lead MUST NOT substitute its own verdict in place of the missing verifier outputs; synthesis requires at least one independent verifier's verdict. If one or more verifiers fail but at least one returns a verdict, the run proceeds with the surviving verdict(s) and the final report MUST explicitly notate which verifiers were unavailable, with the captured error / timeout evidence per failed verifier. Record such a verifier's `verifierResults[].verdict` as `not-run`, never as `FAIL`: `FAIL` is a rejection of the diff and it drives two gates — it blocks a passing verdict and it opens a stage fix cycle — so spending it on a verifier that produced no verdict manufactures a defect that does not exist.

## Verifier-specific forbidden actions (any occurrence → terminal status `contract-violated`)

- running lint / formatter auto-fix modes during a verifier's re-run — `eslint --fix`, `prettier --write`, `ruff check --fix`, `rustfmt` (writes by default; verifiers MUST use `cargo fmt --check` or `rustfmt --check`), `gofmt -w`, `black .` (use `black --check`), `isort .` (use `isort --check-only`), or any equivalent rewrite mode
- updating snapshots / golden fixtures during verification — `jest -u` / `--updateSnapshot`, `pytest --snapshot-update`, `INSTA_UPDATE=*` (any value other than `no`), `cargo insta accept`, `--update-goldens`, or any equivalent "make the test agree with current output" flag
- masking test failure with selection or shell tricks during re-run — `-k <expr>` / `--ignore` / `--deselect` to skip subsets, trailing `|| true`, `set +e` followed by a manually softened comparison, redirecting non-zero exit to success. The plan's listed test command MUST run in full
- substituting the plan's validation commands — verifier MUST run the plan's pre/mid/post validation commands verbatim; replacing them with paraphrased or "equivalent" commands is forbidden. Adding supplementary check-only lint/type-check is allowed and is logged separately in the verifier's Read-only command log
- mutating lockfiles or dependency manifests — `npm install <pkg>`, `npm install` (without lockfile freeze; use `npm ci`), `pnpm add`, `bun add`, `cargo add`, `cargo update`, `pip install -U`, or any dependency install that is not lockfile-frozen (`--locked` / `--frozen-lockfile` / `npm ci` / `pip install --require-hashes`)
- git state mutations — `git add`, `git commit`, `git stash`, `git checkout -- <file>`, `git restore`, `git reset`, `git rebase`, `git merge`, branch creation/deletion, tag creation. Only read-only git queries (`git status`, `git diff`, `git log`, `git show`, `git rev-parse`, `git blame`) are permitted for verifiers
- running integration / end-to-end tests that produce non-local side effects (DB writes against a non-local datastore, external API writes, docker compose against a non-isolated environment) unless that exact command is listed in the approved plan's validation set
- redirecting tool caches or output to paths outside the worktree — e.g. setting `CARGO_TARGET_DIR`, `PYTEST_CACHE_DIR`, `NODE_OPTIONS=--require=<external>`, or any env var that causes the verifier's command to write outside the worktree's normal build artifact paths

## Executor completion self-check (not this role's gate)

- The executor's `Implementation self-check` gate (`prompts/profiles/_implementation-self-check.md`) belongs to the worker that owns the diff, and its body is deliberately not delivered here: it asks for in-place fixes and break-then-restore mutation checks, every one of which this verifier is forbidden to perform. Do not re-derive its items or claim to have run it. What grades the same defects from this side is the blocking taxonomy above, applied to the diff you re-read yourself. When the executor's `Coverage:` / `Self-check coverage:` lines are among the inputs this prompt enumerates, a missing line or one whose file list does not reconcile with the diff is a blocking finding — the gate was skipped or partially run.
