# Unified Finding Schema

> Single source of truth for all review agent output formats. Every review agent prompt MUST reference this document.

---

## Table of Contents

1. [Finding Schema (JSON)](#1-finding-schema-json)
2. [Field Definitions](#2-field-definitions)
3. [Findings File Format (Markdown)](#3-findings-file-format-markdown)
4. [Review Directory Structure](#4-review-directory-structure)
5. [Review ID Generation](#5-review-id-generation)
6. [metadata.json Schema](#6-metadatajson-schema)
7. [review.json Schema (Final Output)](#7-reviewjson-schema-final-output)
8. [Examples: Good and Bad Findings](#8-examples-good-and-bad-findings)
9. [pr-state.json Schema](#9-pr-statejson-schema)

---

## 1. Finding Schema (JSON)

Every finding produced by a review agent MUST conform to this schema:

```json
{
  "id": "SEC-001",
  "severity": "critical|high|medium|low",
  "category": "security|quality|logic|codebase-fit|test-integrity|performance|auto-checks",
  "title": "Short description (max 80 chars)",
  "description": "Detailed explanation of the issue",
  "impact": "Real-world consequences if not fixed",
  "file": "relative/path/to/file.ts",
  "line": 42,
  "end_line": 45,
  "evidence": "REQUIRED - actual code snippet copy-pasted from the file",
  "evidence_context": {
    "line_range_examined": [22, 62],
    "verification_method": "direct_code_inspection|cross_file_trace",
    "checked_for_handling_elsewhere": true,
    "search_description": "Grepped for try/catch in callers..."
  },
  "suggested_fix": "Specific code change or guidance",
  "fixable": true,
  "flagged_by": ["security", "logic"],
  "validation_status": "pending|confirmed_valid|dismissed_false_positive|dismissed_evidence_not_literal|dismissed_multi_file_evidence_missing|dismissed_cross_model_falsification|evidence_ambiguous|author_judgment_required",
  "references": ["https://owasp.org/..."],
  "regression_from_fix": false,
  "regression_from_fix_round": null
}
```

---

## 2. Field Definitions

### Required Fields

Every finding MUST include all of the following. A finding missing any required field is invalid and will be rejected during synthesis.

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. Format: `{CATEGORY_PREFIX}-{NNN}`. See [ID Prefixes](#id-prefixes) below. |
| `severity` | `enum` | One of: `critical`, `high`, `medium`, `low`. See [Severity Definitions](#severity-definitions). |
| `category` | `enum` | One of: `security`, `quality`, `logic`, `codebase-fit`, `test-integrity`, `performance`, `auto-checks`. |
| `title` | `string` | Short description, max 80 characters. Must be specific enough to act on without reading the full description. |
| `description` | `string` | Detailed explanation of what the issue is and why it matters. No filler -- every sentence must add information. |
| `impact` | `string` | Real-world consequences if this issue is not fixed. Must describe a concrete scenario, not a vague risk statement. |
| `file` | `string` | Relative path from project root to the affected file. Always use forward slashes. |
| `line` | `number` | The primary line number where the issue occurs. |
| `evidence` | `string` | **Actual code snippet copy-pasted from the file.** Not a paraphrase, not a summary, not "the function at line X". The literal code. |
| `evidence_context` | `object` | Context about how the evidence was gathered. See [Evidence Context](#evidence-context). |
| `suggested_fix` | `string` | A specific, actionable code change or clear guidance. Must be concrete enough that another agent can implement it without further investigation. |
| `fixable` | `boolean` | Whether this can be auto-fixed by an agent (`true`) or requires human judgment (`false`). |

### Optional Fields

These fields are set during synthesis, validation, or cross-agent correlation. Review agents MAY include them, but they are not required during initial finding creation.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `end_line` | `number` | -- | End line of the affected range. Set when the issue spans multiple lines. |
| `flagged_by` | `string[]` | -- | List of agent names that independently flagged this issue. Set during synthesis when deduplicating. |
| `validation_status` | `enum` | `"pending"` | Set during the validation phase. One of: `pending`, `confirmed_valid`, `dismissed_false_positive` (general), `dismissed_evidence_not_literal` (R6 — evidence not a literal substring of cited file range), `dismissed_multi_file_evidence_missing` (R6 — references multiple files but lacks `cross_file_evidence`), `dismissed_cross_model_falsification` (R6 — external-mode FP gate rebutted by codex), `evidence_ambiguous` (validator can't tell from the evidence whether the claim holds — invites more investigation), `author_judgment_required` (genuine design disagreement — only the PR author can settle). `needs_human_review` has been split into the last two and is no longer emitted. |
| `cross_file_evidence` | `array` | -- | **R6.** Required when title or description references more than one file. Each entry: `{file: string, snippet: string, line_range: [number, number]}`. `snippet` must be a literal substring of `file`. Missing or incomplete cross-file evidence triggers `dismissed_multi_file_evidence_missing`. |
| `fp_gate_result` | `object` | -- | **R6.** Populated after the cross-model FP gate runs (external mode, HIGH/MEDIUM only). Shape: `{verdict: 'confirm'\|'falsify', reasoning: string, evidence_quote: string, severity_assessment: 'plausible'\|'over-stated'\|'under-stated'}`. On `falsify` the finding's `validation_status` flips to `dismissed_cross_model_falsification`. |
| `references` | `string[]` | -- | URLs to relevant documentation, OWASP entries, style guides, etc. |
| `regression_from_fix` | `boolean` | `false` | **R5 carve-out.** True when this finding identifies a CRITICAL regression introduced by the author's fix commits between the previous round and this round. Setting this bypasses the verification-phase (round >= 3) new-finding suppression rule. Only valid on critical findings — medium/low regressions are silently suppressed. |
| `regression_from_fix_round` | `number` | `null` | Which prior round's fix commits introduced the regression. Must be set when `regression_from_fix: true`. |

### ID Prefixes

Each category has a standard prefix for finding IDs:

| Category | Prefix | Example |
|----------|--------|---------|
| `security` | `SEC` | `SEC-001` |
| `quality` | `QUA` | `QUA-001` |
| `logic` | `LOG` | `LOG-001` |
| `codebase-fit` | `FIT` | `FIT-001` |
| `test-integrity` | `TST` | `TST-001` |
| `performance` | `PRF` | `PRF-001` |
| `auto-checks` | `CHK` | `CHK-001` |

Each agent numbers its findings sequentially starting from 001.

### Severity Definitions

| Severity | Criteria | Action |
|----------|----------|--------|
| **critical** | Security vulnerability, data loss, crash, or corruption in production. Affects all users or exposes sensitive data. | Must fix before merge. Blocks approval. |
| **high** | Significant bug, race condition, logic error, or missing error handling that causes incorrect behavior under normal usage. | Should fix before merge. Strongly recommended. |
| **medium** | Code smell, missing edge case, suboptimal pattern, inconsistency with project conventions, or maintainability concern. | Fix recommended. May be deferred with justification. |
| **low** | Style issue, naming improvement, minor refactor opportunity, documentation gap. No functional impact. | Fix if convenient. Will not block merge. |

### Evidence Context

The `evidence_context` object documents how the reviewer verified the finding. This prevents false positives from agents that "assume" something is wrong without checking.

| Field | Type | Description |
|-------|------|-------------|
| `line_range_examined` | `[number, number]` | The range of lines the agent actually read when investigating. Must extend beyond the immediate issue line to include surrounding context. |
| `verification_method` | `enum` | `direct_code_inspection` -- Found by reading the file directly. `cross_file_trace` -- Found by tracing a call chain or data flow across files. |
| `checked_for_handling_elsewhere` | `boolean` | Whether the agent checked if the issue is already handled elsewhere (e.g., a missing null check that is actually caught by a parent caller). |
| `search_description` | `string` | What the agent searched for to verify the finding. E.g., "Grepped for `catch` blocks in all callers of `parseConfig`". |

### The Evidence Rule

**NO EVIDENCE = NO FINDING.**

A finding without a literal code snippet in the `evidence` field is automatically rejected. This is the single most important rule in the review system.

Agents MUST NOT:
- Describe code from memory ("the function appears to...")
- Reference code without quoting it ("see line 42")
- Use confidence language as a substitute for evidence ("likely vulnerable", "probably missing")
- Submit findings based on general knowledge without verifying against the actual code

Agents MUST:
- Copy-paste the exact code from the file using the Read tool
- Include enough surrounding context to understand the issue
- Verify the issue exists in the current version of the code (not a stale diff)

---

## 3. Findings File Format (Markdown)

Each review agent writes its findings to `.aperant/pr-reviews/{review-id}/findings/{agent-name}.md` using this exact format:

````markdown
# {Agent Name} Review Findings

**Review ID:** {review-id}
**Files Reviewed:** {count}
**Findings:** {count}

---

## Finding: {id} -- {title}
- **Severity**: {severity}
- **Category**: {category}
- **File**: `{file}:{line}`
- **Fixable**: {yes|no}
- **Impact**: {impact}

### Description
{description}

### Evidence
```
{actual code snippet, copy-pasted from the file}
```

### Evidence Context
- **Lines examined**: {start}-{end}
- **Verification method**: {verification_method}
- **Checked elsewhere**: {yes|no}
- **Search**: {search_description}

### Suggested Fix
{suggested_fix}

---
````

Notes:
- Use `--` (double dash) in headings instead of `---` (which renders as an `<hr>` in some parsers)
- Each finding is separated by a horizontal rule (`---`)
- The agent name in the filename uses lowercase kebab-case: `security.md`, `codebase-fit.md`, `test-integrity.md`
- If an agent finds zero issues, it still writes the file with a "No findings" note and the file/finding counts set to 0

---

## 4. Review Directory Structure

Every review creates a self-contained directory under `.aperant/pr-reviews/`:

```
.aperant/pr-reviews/{review-id}/
  metadata.json          # Review metadata, status, stats
  diff.patch             # The diff being reviewed (captured at review start)
  triggers.json          # What triggered the review and target info
  toolchain.json         # Detected project toolchain (linter, formatter, test runner)
  findings/
    security.md          # Security agent findings
    quality.md           # Quality agent findings
    logic.md             # Logic agent findings
    codebase-fit.md      # Codebase-fit agent findings
    test-integrity.md    # Test-integrity agent findings
    performance.md       # Performance agent findings
    validated.md         # Post-validation consolidated findings
  iterations/
    1/
      fixes-applied.md   # What was fixed in this iteration
      diff.patch          # Diff of fixes applied
      findings/           # Re-review findings after fixes
    2/
      ...
  review.json            # Final structured output (written on completion)
  self-review.md         # Agent self-assessment of review quality
```

### File Descriptions

| File | Written By | When |
|------|-----------|------|
| `metadata.json` | Orchestrator | Review start, updated throughout |
| `diff.patch` | Orchestrator | Review start |
| `triggers.json` | Orchestrator | Review start |
| `toolchain.json` | Orchestrator | Review start (auto-detected) |
| `findings/{agent}.md` | Each review agent | During parallel review phase |
| `findings/validated.md` | Validation agent | After synthesis + validation |
| `iterations/{n}/*` | Fix + re-review agents | During iteration loops |
| `review.json` | Orchestrator | On review completion |
| `self-review.md` | Synthesis agent | After final consolidation |

---

## 5. Review ID Generation

Format: `{target}-{YYMMDD}-{4-char-hash}`

The hash is derived from a combination of the timestamp and target ref to ensure uniqueness within a day.

### Target Types and Examples

| Target | Format | Example |
|--------|--------|---------|
| Pull request | `pr-{number}` | `pr-123-260413-a8f2` |
| Branch comparison | `{branch-name}` | `feat-auth-260413-b3c1` |
| Branch vs main | `main-compare` | `main-compare-260413-c7d4` |
| Staged changes | `staged` | `staged-260413-e1a9` |
| Specific commits | `commits` | `commits-260413-f5b0` |

### Hash Generation

```typescript
function generateReviewId(target: string, date: Date): string {
  const dateStr = date.toISOString().slice(2, 10).replace(/-/g, '');
  const hash = crypto
    .createHash('sha256')
    .update(`${target}-${date.toISOString()}`)
    .digest('hex')
    .slice(0, 4);
  return `${target}-${dateStr}-${hash}`;
}
```

---

## 6. metadata.json Schema

Created at review start. Updated as the review progresses.

```json
{
  "review_id": "pr-123-260413-a8f2",
  "target": {
    "type": "pr|branch|staged|commits",
    "ref": "123"
  },
  "created_at": "2026-04-13T14:30:00.000Z",
  "status": "in-progress|completed|needs-human-review|failed",
  "iterations_completed": 0,
  "max_iterations": 4,
  "toolchain": "biome 2.x + vitest + tsc",
  "trigger_count": 3,
  "stats": {
    "total": 0,
    "critical": 0,
    "high": 0,
    "medium": 0,
    "low": 0
  }
}
```

### Field Details

| Field | Description |
|-------|-------------|
| `review_id` | Matches the directory name. Generated per [Review ID Generation](#5-review-id-generation). |
| `target.type` | What is being reviewed: `pr` (pull request), `branch` (branch vs base), `staged` (staged changes), `commits` (specific commit range). |
| `target.ref` | The reference -- PR number, branch name, or commit SHA range. |
| `created_at` | ISO 8601 timestamp of review start. |
| `status` | Current review status. Transitions: `in-progress` -> `completed` or `needs-human-review` or `failed`. |
| `iterations_completed` | How many fix-and-re-review cycles have been run. |
| `max_iterations` | Upper bound on iterations (default 4). Prevents infinite fix loops. |
| `toolchain` | Auto-detected project toolchain summary (linter, formatter, test runner). |
| `trigger_count` | Number of files or changes that triggered the review. |
| `stats` | Running count of findings by severity. Updated after each phase. |
| `fixer_verdicts` | **R-audit.** Array of hallucination-gate entries — one per fixer-audit invocation (Phase 6 Step 4b). See [Fixer Audit Fields](#fixer-audit-fields) below. Empty until the first fixer returns. |
| `fixer_hallucinations_total` | **R-audit.** Running count of entries in `fixer_verdicts` where `verdict === "hallucinated"`. Surfaced in the Phase 8 final report as the `Fixer Hallucinations:` line. |

### Fixer Audit Fields

Appended to `metadata.json.fixer_verdicts[]` and `iterations/{N}/audit.json.fixer_verdicts[]` by `apt-tools pr-review audit-fixer` after every fix-agent return (Phase 6 Step 4b). Terminology mirrors `validation_status` — each entry is a deterministic, fail-closed verdict on one fixer invocation.

```json
{
  "fixer": "fixer-3",
  "iteration": 1,
  "verdict": "hallucinated",
  "reason": "fixer claimed FIXED=2 but worktree has zero edits matching the assigned files",
  "claimed": {
    "fixed": 2,
    "failed": 0,
    "skipped": 0,
    "verification": "PASS"
  },
  "observed": {
    "worktree_changed_files": [],
    "claimed_files": [
      "apps/desktop/src/main/services/__tests__/version-manager.test.ts",
      "apps/desktop/src/main/ipc-handlers/__tests__/settings-handlers.test.ts"
    ],
    "matched_files": [],
    "unmatched_claimed_files": [
      "apps/desktop/src/main/services/__tests__/version-manager.test.ts",
      "apps/desktop/src/main/ipc-handlers/__tests__/settings-handlers.test.ts"
    ]
  },
  "report_path": ".aperant/pr-reviews/pr-23/iterations/1/fixes-applied-group3.md",
  "timestamp": "2026-04-20T14:58:35.000Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `fixer` | `enum` | One of `fixer-1 | fixer-2 | fixer-3 | self-reviewer`. Identifies which agent returned this status. |
| `iteration` | `number` | Phase 6 iteration index when the audit ran (1-based). |
| `verdict` | `enum` | One of `verified | hallucinated | skipped`. `verified` = claims match the git state; `hallucinated` = claims don't match (fail-closed on parse/IO errors); `skipped` = fixer legitimately had nothing to fix. |
| `reason` | `string | null` | Human-readable explanation when `verdict != "verified"`. Null on verified verdicts. |
| `claimed` | `object | null` | Parsed status line (`fixed`, `failed`, `skipped`, `verification`). Null when the status line failed to parse. |
| `observed` | `object` | Git-state cross-check outputs — `claimed_files` from the fix report, `worktree_changed_files` from `git diff --cached ∪ --name-only ∪ ls-files --others`, and the set-intersection result (`matched_files` + `unmatched_claimed_files`). |
| `report_path` | `string | null` | Absolute path to the `fixes-applied-group{N}.md` (or `self-review.md`) the audit read from. Null when the report was missing — that's itself a hallucinated verdict via the fail-closed path. |
| `timestamp` | `ISO-8601` | When the audit ran. |

---

## 7. review.json Schema (Final Output)

Written when the review completes. This is the structured output consumed by `/apt:ship`, PR comments, and dashboards.

```json
{
  "review_id": "pr-123-260413-a8f2",
  "assessment": "approve|request-changes|comment",
  "summary": "1-2 sentence human-readable assessment of the changes",
  "scores": {
    "security": 9,
    "quality": 8,
    "logic": 8,
    "architecture": 9,
    "performance": 8
  },
  "findings": [
    {
      "id": "SEC-001",
      "severity": "high",
      "category": "security",
      "title": "Unsanitized user input passed to SQL query",
      "description": "...",
      "impact": "...",
      "file": "packages/core/src/storage/libsql-backend.ts",
      "line": 142,
      "evidence": "...",
      "evidence_context": { "...": "..." },
      "suggested_fix": "...",
      "fixable": true,
      "flagged_by": ["security", "quality"],
      "validation_status": "confirmed_valid"
    }
  ],
  "stats": {
    "files_reviewed": 12,
    "findings_total": 5,
    "critical": 0,
    "high": 1,
    "medium": 3,
    "low": 1,
    "false_positives_dismissed": 2
  },
  "iterations": {
    "total": 2,
    "findings_per_iteration": [5, 1]
  },
  "created_at": "2026-04-13T14:30:00.000Z",
  "completed_at": "2026-04-13T14:35:22.000Z"
}
```

### Assessment Rules

| Assessment | Condition |
|-----------|-----------|
| `approve` | Zero critical or high findings remaining after all iterations. |
| `request-changes` | One or more critical or high findings remain that could not be auto-fixed. |
| `comment` | Only medium/low findings remain, or all findings were auto-fixed. Informational review. |

### Score Ranges

All scores are integers from 1-10:

| Score | Meaning |
|-------|---------|
| 9-10 | Excellent. No issues or only trivial improvements possible. |
| 7-8 | Good. Minor issues that don't affect correctness. |
| 5-6 | Acceptable. Some issues worth addressing. |
| 3-4 | Concerning. Multiple significant issues found. |
| 1-2 | Poor. Critical issues that must be addressed. |

---

## 8. Examples: Good and Bad Findings

### Good Finding 1: Concrete Security Issue

```json
{
  "id": "SEC-001",
  "severity": "critical",
  "category": "security",
  "title": "API token stored in localStorage without encryption",
  "description": "The GitHub integration stores the raw OAuth access token in localStorage via window.localStorage.setItem('gh_token', token). localStorage is accessible to any JavaScript running on the page, including injected scripts from browser extensions or XSS attacks.",
  "impact": "If the application has an XSS vulnerability (or a malicious browser extension is installed), an attacker can steal the user's GitHub token and gain full repository access with whatever scopes were granted.",
  "file": "packages/core/src/integrations/github/auth.ts",
  "line": 47,
  "evidence": "window.localStorage.setItem('gh_token', response.access_token);",
  "evidence_context": {
    "line_range_examined": [30, 65],
    "verification_method": "direct_code_inspection",
    "checked_for_handling_elsewhere": true,
    "search_description": "Grepped for 'gh_token' and 'encrypt' across the codebase. No encryption wrapper found. Also checked for Content-Security-Policy headers that might mitigate XSS -- none found."
  },
  "suggested_fix": "Use the TokenStore abstraction from packages/core/src/storage/token-store.ts which encrypts at rest. Replace the direct localStorage call with: tokenStore.set('github', response.access_token)",
  "fixable": true
}
```

**Why this is good:** Exact code quoted. Impact describes a real attack scenario. The agent verified no encryption exists elsewhere. The fix references an existing project abstraction.

### Good Finding 2: Logic Error with Cross-File Trace

```json
{
  "id": "LOG-001",
  "severity": "high",
  "category": "logic",
  "title": "Race condition between settings save and read on desktop",
  "description": "saveSettings() writes to libSQL asynchronously but getSettings() reads from an in-memory cache that is only updated after the write completes. If getSettings() is called between the write initiation and the cache update, it returns stale data. This window is approximately 50-200ms based on typical libSQL write latency.",
  "impact": "Users who change a setting and immediately navigate to a page that reads that setting will see the old value. Particularly visible with theme changes where the old theme flashes briefly.",
  "file": "packages/core/src/services/settings-service.ts",
  "line": 89,
  "end_line": 102,
  "evidence": "async saveSettings(key: string, value: unknown): Promise<void> {\n  await this.backend.execute(\n    'UPDATE app_settings SET value = ? WHERE key = ?',\n    [JSON.stringify(value), key]\n  );\n  // Cache is updated here, after the await\n  this.cache.set(key, value);\n}",
  "evidence_context": {
    "line_range_examined": [75, 120],
    "verification_method": "cross_file_trace",
    "checked_for_handling_elsewhere": true,
    "search_description": "Traced all callers of saveSettings in the desktop app. Found 3 call sites that immediately read settings after save. Checked for optimistic cache updates -- none found."
  },
  "suggested_fix": "Update the cache optimistically before the await: move this.cache.set(key, value) to before the await, and add a rollback in the catch block if the write fails.",
  "fixable": true
}
```

**Why this is good:** Describes the exact timing window. Evidence shows the actual code with the ordering problem. Cross-file verification confirmed callers hit the race. Fix is specific and handles the rollback case.

### Good Finding 3: Medium-Severity Codebase Fit Issue

```json
{
  "id": "FIT-001",
  "severity": "medium",
  "category": "codebase-fit",
  "title": "New utility uses .js extension in relative imports",
  "description": "The new file date-utils.ts imports from ./format-helpers.js. The project convention (documented in CLAUDE.md) requires extensionless relative imports. The .js extension pattern breaks Turbopack/Next.js when resolving .ts source directly via transpilePackages.",
  "impact": "The web app build will fail in development mode when Turbopack tries to resolve the import. Desktop build may still work since electron-vite handles it differently.",
  "file": "packages/core/src/utils/date-utils.ts",
  "line": 3,
  "evidence": "import { formatRelative } from './format-helpers.js';",
  "evidence_context": {
    "line_range_examined": [1, 10],
    "verification_method": "direct_code_inspection",
    "checked_for_handling_elsewhere": false,
    "search_description": "Checked CLAUDE.md conventions section for import rules."
  },
  "suggested_fix": "Remove the .js extension: import { formatRelative } from './format-helpers'",
  "fixable": true
}
```

**Why this is good:** References a specific project convention. Impact explains which build system breaks and why. Fix is a one-line change.

---

### Bad Finding 1: No Evidence (REJECTED)

```json
{
  "id": "SEC-002",
  "severity": "high",
  "category": "security",
  "title": "Potential SQL injection vulnerability",
  "description": "The database query function might be vulnerable to SQL injection if user input is not properly sanitized.",
  "impact": "An attacker could potentially execute arbitrary SQL commands.",
  "file": "packages/core/src/storage/libsql-backend.ts",
  "line": 50,
  "evidence": "The query function appears to concatenate user input directly into the SQL string.",
  "evidence_context": {
    "line_range_examined": [48, 55],
    "verification_method": "direct_code_inspection",
    "checked_for_handling_elsewhere": false,
    "search_description": ""
  },
  "suggested_fix": "Use parameterized queries instead of string concatenation.",
  "fixable": true
}
```

**Why this is rejected:**
- The `evidence` field describes code instead of quoting it ("appears to concatenate"). No actual code snippet was provided.
- The `description` uses hedging language ("might be vulnerable") -- the agent did not verify whether the vulnerability actually exists.
- `checked_for_handling_elsewhere` is `false` -- the agent did not check if parameterized queries are already used.
- The `search_description` is empty -- no verification was performed.
- The impact is generic boilerplate, not specific to this codebase.

### Bad Finding 2: Vague Description, Confidence-Based (REJECTED)

```json
{
  "id": "QUA-003",
  "severity": "medium",
  "category": "quality",
  "title": "Error handling could be improved",
  "description": "Several functions in this file don't have proper error handling. This is a common source of bugs in production applications.",
  "impact": "Errors may go unhandled and cause unexpected behavior.",
  "file": "packages/core/src/services/sync-service.ts",
  "line": 1,
  "evidence": "Various functions throughout the file",
  "evidence_context": {
    "line_range_examined": [1, 200],
    "verification_method": "direct_code_inspection",
    "checked_for_handling_elsewhere": false,
    "search_description": "Scanned the file for try/catch blocks"
  },
  "suggested_fix": "Add try/catch blocks to async functions and handle errors appropriately.",
  "fixable": false
}
```

**Why this is rejected:**
- The `evidence` field is "Various functions throughout the file" -- not a code snippet. This finding quotes zero lines of code.
- `line: 1` is meaningless -- it points at the top of the file, not a specific issue.
- "Several functions" and "could be improved" are vague. Which functions? What specific error paths?
- The title is generic enough to apply to any file in any project.
- The suggested fix is boilerplate advice, not a specific change.
- A valid version of this finding would identify ONE specific function, quote its code, show the unhandled error path, and suggest the exact try/catch structure.

### Bad Finding 3: Assumed Issue Not Verified (REJECTED)

```json
{
  "id": "PRF-001",
  "severity": "high",
  "category": "performance",
  "title": "N+1 query pattern in settings loader",
  "description": "Based on the function name loadAllSettings, this likely loads settings one at a time in a loop, causing N+1 database queries.",
  "impact": "Page load times will increase linearly with the number of settings.",
  "file": "packages/core/src/services/settings-service.ts",
  "line": 25,
  "evidence": "async loadAllSettings(): Promise<Settings>",
  "evidence_context": {
    "line_range_examined": [25, 25],
    "verification_method": "direct_code_inspection",
    "checked_for_handling_elsewhere": false,
    "search_description": ""
  },
  "suggested_fix": "Use a single SELECT query to load all settings at once.",
  "fixable": true
}
```

**Why this is rejected:**
- The agent read only the function signature (line 25 to line 25) and guessed at the implementation based on the function name.
- "Based on the function name... this likely loads" is assumption-based reasoning, not evidence.
- The evidence is just a function signature -- the actual function body was never examined.
- `line_range_examined` of `[25, 25]` proves the agent read exactly one line.
- A valid version would read the full function body, quote the loop that makes individual queries, and show the SQL being executed per iteration.

---

## 9. pr-state.json Schema

The canonical PR-level state record for /apt:pr-review. Lives at
`.aperant/pr-reviews/pr-{N}/pr-state.json` (the **live working copy** — git-ignored
because it flips on every lock/round mutation). When a round closes, a durable
snapshot is written to `.aperant/pr-reviews/pr-{N}/rounds/{R}/pr-state-snapshot.json`
which IS committed (visible to collaborators who inspect round history).

### Purpose

- Replaces the ad-hoc `pr-{N}-{date}-{hash}` review-id scheme for GitHub PR reviews.
- Carries round budget, head-SHA pinning, authorship mode, and the PR-level lock
  used to serialize concurrent `/apt:pr-review --pr N` invocations.
- Authorship is read once and persisted so re-invocations don't re-ask `gh`.

### Fields

```jsonc
{
  "pr_number": 23,
  "head_ref": "feature/personas",      // null until Phase -1 populates it
  "base_ref": "main",
  "head_sha_by_round": {                // pins the reviewed commit per round
    "1": "abc1234...",                  //   → solves the force-push race: every
    "2": "def5678..."                   //     diff in round R uses SHA[R].
  },
  "authorship": "owned | external | unknown",
  "authorship_signal": "authorAssociation=CONTRIBUTOR, fork=true",  // human-readable
  "rounds_completed": 2,                // number of rounds that reached start-round
  "max_rounds": 4,                      // default 4; clamped to 1..10
  "round_phase": "discovery | verification",  // rounds 1-2 = discovery, 3+ = verification
  "lock": {                             // null when idle
    "owner": "agent-abc-pid12345",
    "acquired_at": "2026-04-15T12:34:56.000Z"
  },
  "comment_timeline": [                 // one entry per round that posted a comment
    {
      "round": 1,
      "url": "https://github.com/...#issuecomment-123",
      "posted_at": "2026-04-13T...",
      "body_hash": "sha256-of-body-text",
      "review_id": "round-1-external-comment-abc"
    }
  ],
  "disputes": [                         // author pushed back on a finding
    {
      "finding_id": "FIT-001",
      "round": 1,
      "status": "author_contested",
      "note": "Author points at CLAUDE.md which says X is intentional."
    }
  ],
  "superseded_findings": ["FIT-001", "FIT-003"],  // FPs dismissed post-comment
  "created_at": "2026-04-13T...",
  "updated_at": "2026-04-15T..."
}
```

### Field Definitions

- **`pr_number`** (number, required): GitHub PR number. Keys the folder.
- **`head_ref`** (string | null): PR head branch ref. Updated via
  `apt-tools pr-review init-pr --head-ref`.
- **`base_ref`** (string | null): PR base branch ref.
- **`head_sha_by_round`** (map<round, SHA>): head commit SHA fetched at the start
  of each round. All `git diff` / evidence reads within a round use this SHA so a
  mid-round force-push cannot invalidate cited evidence.
- **`authorship`** (enum): One of `owned | external | unknown`. See `authorship_mode`
  decision tree in SKILL.md Phase -1. Defaults to `unknown` before detection.
- **`authorship_signal`** (string): Human-readable explanation of why
  `authorship` took its value (e.g., `authorAssociation=MEMBER, same-repo branch`).
- **`rounds_completed`** (number): Count of rounds that successfully ran
  `apt-tools pr-review start-round`.
- **`max_rounds`** (number): Upper bound, default 4, range 1..10. Configurable via
  `config.json:pr_review.max_rounds` (R5).
- **`round_phase`** (enum): `discovery` for rounds 1..2, `verification` for rounds
  3+. Computed at start-round time.
- **`lock`** (object | null): PR-level lock field set by `acquire-lock`. Contains
  `owner` and `acquired_at`. Stale locks (older than `--ttl-ms`, default 30 min)
  may be stolen by a fresh acquire.
- **`comment_timeline`** (array): Record of every external-mode comment that was
  posted to GitHub from this PR's review rounds (Phase 6b).
- **`disputes`** (array): Author push-back tracking (R3 in-scope schema; automation
  is deferred to a follow-up task).
- **`superseded_findings`** (array<string>): IDs of findings that were dismissed
  post-comment — used to avoid re-raising known-FPs on later rounds.

### Lifecycle

1. **init-pr** — creates pr-state.json with `rounds_completed: 0`, `authorship: "unknown"`, `lock: null`.
2. **acquire-lock** — sets `lock` if none held (or if held lock is stale).
3. **detect-authorship** — writes `authorship` and `authorship_signal`.
4. **start-round** — increments `rounds_completed`, writes `head_sha_by_round[R]`,
   flips `round_phase` when `R > DISCOVERY_ROUNDS` (= 2).
5. **close-round** — writes durable snapshot to
   `rounds/{R}/pr-state-snapshot.json`, clears `lock`.
6. **release-lock** — clears `lock` without closing the round.

### Gitignore layering

All `.aperant/pr-reviews/**` is **gitignored**. The framework treats every
file under this tree as local coordination — including `pr-state.json` and
the per-round snapshots. See `packages/framework/docs/artifact-lifecycle.md
→ "PR-review artifacts are local-only"` for the full rationale (PID leak in
`pr-state.json`, snapshot fields reconstructible from GitHub PR API,
empirical never-honored COMMITTABLE classification).

For richer cross-machine traceability (which AI flagged what across rounds,
drift over time, model A vs model B), use Aperant the app — that's where
AI-performance telemetry belongs, not in every consumer's git repo.

`apt-tools pr-review close-round` auto-prunes `.aperant/pr-reviews/pr-{N}/`
on MERGED/CLOSED so the local working tree stays clean across reviews.
