# Proof Verification Template — verification.json

JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json` — the structured proof verification record.

**Output format is JSON.** Do not write VERIFICATION.md. This file is the single source of truth.

---

## Full Schema

```json
{
  "meta": {
    "status": "testing | partial | complete | diagnosed",
    "app": "{app-name}",
    "run": "{run-id}",
    "started": "{ISO timestamp}",
    "updated": "{ISO timestamp}",
    "spec": "{spec.md path or task description}",
    "verdict": "pending | approved | approved_with_notes | rejected",
    "tools": ["electron", "puppeteer", "cdp_video"],
    "selected_driver": {
      "driverId": "electron",
      "version": "0.1.0",
      "manifest_sha256": "abc123...",
      "trust_tier": "first_party_signed",
      "sandbox_profile": "workspace_write"
    },
    "fallback_attempts": [
      { "driver_id": "browser", "reason": "missing required capabilities: dump_state" }
    ]
  },

  "automated_checks": {
    "tests": {
      "status": "pass | fail",
      "count": 0,
      "output": "{first 50 lines of test output}"
    },
    "typecheck": {
      "status": "pass | fail",
      "output": "{first 20 lines}"
    },
    "lint": {
      "status": "pass | fail",
      "output": "{first 20 lines}"
    }
  },

  "coverage": {
    "registry_file": ".aperant/features/{area}.json",
    "registered_count": 0,
    "tested_count": 0,
    "skipped_count": 0,
    "percent": 0,
    "gate_result": "pass | warn | block",
    "by_surface": {
      "electron": { "tested": 0, "total": 0 },
      "web": { "tested": 0, "total": 0 },
      "cli": { "tested": 0, "total": 0 },
      "api": { "tested": 0, "total": 0 }
    }
  },

  "tests": [
    {
      "id": "TEST-01",
      "registry_id": "terminals-grid-auto-tile",
      "name": "Three Terminal Grid Auto-Tile",
      "outcome_id": "O3",
      "surface": "electron",
      "transport": "mcp",
      "precondition": "2 terminals active",
      "action": "Press Cmd+T",
      "expected": "Grid reflows to 2+1 layout",
      "actual": "Grid correctly tiled to 2+1",
      "evidence": "electron/screenshots/03-grid.png",
      "analysis": "Screenshot shows three terminal panes. Top row has 2 equal-width panes, bottom row has 1 centered pane. Grid lines are clean, no overlap or gap artifacts. Each pane shows an active shell prompt with correct path.",
      "result": "pass | fail | blocked | skip",
      "severity": null
    },
    {
      "id": "TEST-02",
      "registry_id": "settings-theme-picker",
      "name": "Theme picker — cross-surface",
      "outcome_id": "O5",
      "surface": "web",
      "transport": "skill",
      "precondition": "App loaded in web shell",
      "action": "Click 'Dark mode' in the theme picker",
      "expected": "Background flips to dark; persists across reload",
      "actual": "Dark mode applied, persisted across reload",
      "evidence": "web/screenshots/05-dark-mode.png",
      "analysis": "Screenshot shows app body with #111 background, all text legible on dark surface. After reload, dark mode is still active — localStorage entry confirmed.",
      "result": "pass",
      "severity": null
    }
  ],

  "skipped": [
    {
      "registry_id": "terminals-settings-scrollback-slider",
      "name": "Scrollback buffer slider",
      "reason": "unsatisfied_optional_capability",
      "detail": "Selected driver does not implement the IDL verb required for slider interaction"
    }
  ],

  "dimensions": {
    "completeness": {
      "score": 7,
      "rationale": "18/22 spec requirements have passing tests. 4 blocked by platform."
    },
    "correctness": {
      "score": 8,
      "rationale": "All tested features behave as expected. No blockers or majors."
    },
    "code_quality": {
      "score": 9,
      "rationale": "Zero lint errors, zero type errors."
    },
    "test_coverage": {
      "score": 6,
      "rationale": "Unit tests cover core logic but no integration tests for new grid layout."
    }
  },

  "gaps": [
    {
      "truth": "Terminal should display file listing after ls command",
      "status": "failed",
      "reason": "Terminal shows blank output — PTY not forwarding stdout",
      "severity": "blocker | major | minor | cosmetic",
      "feature": "terminals-pty-output",
      "test_id": "TEST-05",
      "evidence": "screenshots/05-ls-output.png",
      "root_cause": "",
      "fix_plan": ""
    }
  ],

  "video": {
    "captured": true,
    "file": "recording.mp4",
    "duration_seconds": 230,
    "fps": 2
  },

  "model_verdicts": [
    {
      "model": "gemini-2.5-pro",
      "provider": "google",
      "role": "visual_qa",
      "verdict": "approved",
      "confidence": 85
    }
  ],

  "summary": {
    "features_tested": 18,
    "features_total": 22,
    "tests_passed": 15,
    "tests_failed": 2,
    "tests_blocked": 1,
    "blockers_found": 0,
    "screenshots_captured": 18,
    "screenshots_analyzed": 18,
    "outcome": "Terminal input works, grid rendering works, settings panel functional. Two minor visual glitches in font preview."
  }
}
```

---

## Field Descriptions

### meta
| Field | Type | Rule | Description |
|-------|------|------|-------------|
| `status` | string | OVERWRITE | Progression: `testing` → `partial` → `complete` → `diagnosed` |
| `app` | string | IMMUTABLE | Application name from package.json |
| `run` | string | IMMUTABLE | Run ID (HHMMSS format) |
| `started` | string | IMMUTABLE | ISO timestamp when verification started |
| `updated` | string | OVERWRITE | ISO timestamp, updated on every write |
| `spec` | string | IMMUTABLE | Path to spec or task description |
| `verdict` | string | OVERWRITE | Derived from dimension scores after all tests |
| `tools` | array | IMMUTABLE | MCP tools / auxiliary capture tools detected as available |
| `selected_driver` | object | IMMUTABLE | The verify-proof driver chosen by the resolver per ID-02. Fields: `driverId`, `version`, `manifest_sha256`, `trust_tier`, `sandbox_profile`. |
| `fallback_attempts` | array | IMMUTABLE | Drivers the resolver REJECTED before selecting. Each entry has `driver_id` + `reason` (missing capabilities, stability filter, hash drift, policy denial). Audit trail for coverage-loss decisions. |

### automated_checks
Each check has `status` (pass/fail) and `output` (captured terminal output). These are populated in step 3 before any manual testing begins.

### coverage
| Field | Type | Description |
|-------|------|-------------|
| `registry_file` | string | Path to the feature registry used |
| `registered_count` | number | Total sub-features in the registry |
| `tested_count` | number | Number of tests written (each with a registry_id) |
| `skipped_count` | number | Number of entries in the skipped array |
| `percent` | number | `tested_count / registered_count * 100` |
| `gate_result` | string | `block` if < 50%, `warn` if < 80%, `pass` if >= 80% |
| `by_surface` | object | v0.8.6 — per-surface rollup. Each surface key (`electron`, `web`, `cli`, `api`) maps to `{tested, total}` so reviewers can tell which surface fell short. |

### tests
Each test entry tracks one sub-feature verification. Fields:

| Field | Type | Rule | Description |
|-------|------|------|-------------|
| `id` | string | IMMUTABLE | Sequential ID: TEST-01, TEST-02, etc. |
| `registry_id` | string | IMMUTABLE | Must match a sub-feature `id` from `.aperant/features/{area}.json` |
| `name` | string | IMMUTABLE | Human-readable test name |
| `outcome_id` | string | IMMUTABLE | v0.8.6 — positional ID from `## User Outcomes` (e.g. `O1`, `O2`). Synthesized by `parseUserOutcomes`; passed through verbatim by `resolveDriversForOutcomes`. Optional for legacy specs without `## User Outcomes`. |
| `surface` | string | IMMUTABLE | v0.8.6 — one of `electron | web | cli | api`. Drives the per-surface screenshot path and the `coverage.by_surface` rollup. Cross-surface outcomes (`[electron+web]`) produce TWO test entries with the same `outcome_id` and different `surface` values. |
| `transport` | string | OVERWRITE | v0.8.6 — `skill | mcp | binary | http`. Populated by the workflow runner from the dispatcher's directive (per Step 7a of `workflows/verify-proof.md`). For browser-driver web outcomes on Skill-capable hosts the value is `"skill"`; on legacy MCP hosts it falls back to `"mcp"`. |
| `precondition` | string | IMMUTABLE | What must be true before testing |
| `action` | string | IMMUTABLE | Specific action to perform |
| `expected` | string | IMMUTABLE | Specific observable result expected |
| `actual` | string | OVERWRITE | What actually happened (filled after testing) |
| `evidence` | string | OVERWRITE | Screenshot filename or video timestamp. v0.8.6 — paths are PER-SURFACE: `{surface}/screenshots/{outcome-id}.png`. |
| `analysis` | string | OVERWRITE | Adversarial analysis of evidence — describe what you SEE, not what you expect |
| `result` | string | OVERWRITE | `pass`, `fail`, `blocked`, or `skip` |
| `severity` | string/null | OVERWRITE | `blocker`, `major`, `minor`, `cosmetic`, or `null` if pass |

### skipped (MANDATORY)
Every registered sub-feature that does NOT have a test MUST appear here. Silence about untested features is not acceptable.

| Field | Type | Description |
|-------|------|-------------|
| `registry_id` | string | The sub-feature `id` from the feature registry |
| `name` | string | Human-readable name of the skipped feature |
| `reason` | string | One of: `time_constraint`, `unsatisfied_optional_capability` (preferred), `depends_on_other`, `not_applicable`. **Legacy alias:** `blocked_by_platform` is read by the schema reader as an alias for `unsatisfied_optional_capability` and emits a deprecation warning. New writes MUST use the new reason. |
| `detail` | string | Brief explanation of why this feature was skipped |

> **Fail-closed, not a skip.** If a REQUIRED IDL capability has no implementing driver in the resolver chain, the resolver throws `unsatisfied_required_capability` (per ID-02) — aborts the run. That entry does NOT land in `skipped[]`; coverage loss is loud, deliberate, and audited via `meta.fallback_attempts[]`.

### dimensions
Four QA dimensions, each with `score` (1-10) and `rationale` (string). Scores are evidence-based, never assumed.

**Scoring caps:**
- Any blocker in the area → max score 3
- Any major in the area → max score 5
- A dimension cannot score above 5 if any blocker exists

**Verdict derivation:**
- ALL dimensions >= 7 → `approved`
- ALL dimensions >= 5 → `approved_with_notes`
- ANY dimension < 5 → `rejected`

### gaps
Issues discovered during testing. Feeds into the fix planning pipeline.

| Field | Type | Description |
|-------|------|-------------|
| `truth` | string | Expected behavior |
| `status` | string | `failed` |
| `reason` | string | What actually happened |
| `severity` | string | `blocker`, `major`, `minor`, `cosmetic` |
| `feature` | string | Feature ID from registry |
| `test_id` | string | Which test found this |
| `evidence` | string | Screenshot or video reference |
| `root_cause` | string | Filled after diagnosis |
| `fix_plan` | string | Filled after diagnosis |

### video
Video capture metadata. `captured` is false if CDP was not available.

### model_verdicts
Only populated if multi-model verification is enabled. Array of model verdict objects.

### summary
Overwritten after all tests complete. Provides at-a-glance counts and a one-line human-readable outcome.

---

## Coverage Gate Rules

These rules are enforced BEFORE test execution begins (step 4f of the workflow):

1. **Count registered sub-features** from `.aperant/features/{area}.json` → `registered_count`
2. **Count tests written** (each must have a `registry_id`) → `tested_count`
3. **Calculate coverage**: `tested_count / registered_count * 100`

| Coverage | Gate Result | Action |
|----------|-------------|--------|
| < 50% | `block` | **STOP.** Add more tests. Do not proceed to execution. |
| 50-79% | `warn` | List untested features. Proceed with warning. |
| >= 80% | `pass` | Proceed normally. |

4. **Every untested feature** must appear in the `skipped` array with a valid reason.
5. `tested_count + skipped_count` must equal `registered_count`. No features may be silently omitted.

---

## Test Writing Rules

**Good test — action-based:**
```json
{
  "action": "Type 'ls -la' into Terminal 1 and press Enter",
  "expected": "File listing appears showing current directory contents with permissions"
}
```

**Bad test — appearance-based:**
```json
{
  "action": "Look at the terminal",
  "expected": "Terminal is visible"
}
```

**Good test — interaction-based:**
```json
{
  "action": "Click 'New Terminal' button in the toolbar, then click 'New Terminal' again",
  "expected": "Two terminal panels appear in a 2-column grid layout, each with an active shell prompt"
}
```

**Bad test — static:**
```json
{
  "action": "Navigate to the terminal page",
  "expected": "Terminal page loads"
}
```

---

## Lifecycle

**Creation:** When /apt:verify-proof starts
- Load feature registry and enumerate sub-features
- Generate action-based tests, each with a `registry_id`
- Enforce coverage gate
- Initialize verification.json with `status: "testing"`, all test results as `null`

**During testing:**
- Execute each test action (type, click, submit)
- Capture evidence immediately after action
- Run adversarial analysis on evidence
- Record actual behavior and verdict in the test entry
- Update summary counts
- If issue found: append to gaps array
- Write verification.json to disk after each test

**On completion:**
- `status` → `"complete"`
- Score dimensions based on evidence
- Derive verdict from scores
- Generate HTML proof report from verification.json
- Commit verification.json

**Resume after context reset:**
1. Read `meta.status` → know overall state
2. Find first test with `result: null` → continue from there
3. `summary` shows progress
