# Finding citation + comment-precision — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: use /ship (fresh subagent per task + two-stage review) or /executing-plans. Steps use checkbox (`- [ ]`). Work on a `.worktrees/<branch>` worktree via the `using-git-worktrees` skill (branch `feat/finding-citation`) — NEVER the main checkout, NEVER a sibling/tmp dir.

**Goal:** Remove the #1 false-positive cluster (comment-vs-code blindness) via a MEASURED band-1 prompt fix, and capture the citation it produces into a navigable emit — without building the deferred band-3 verifier.

**Architecture:** Spec SoT = `docs/specs/2026-06-19-finding-citation-and-comment-precision-design.md`. Two shipped tiers: (1) a scoped prompt rule (flag only executing code; quote the line at/nearest the defect — absence bugs stay first-class), (2) a citation-spine that captures the quoted snippet, resolves the REAL target line by locating the snippet in the file (the LLM's line is bundle-offset-wrong in bundle mode), and wires `line` + a dedicated `code` field into `build_emit_dict` (today hard-coded `line=0, symbol=""`). The band-3 tokenizer-verifier is DEFERRED (delete-test: prompt recovers the catch at n=1).

**Tech Stack:** Python stdlib + pytest (spine, deterministic). `gate.py` LLM leg via `claude -p` (recall validation only).

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1, Task 2 | `domains/security/detectors/baseline/baseline.prompt.txt` ‖ `orchestrator/gate.py` + `tests/test_gate.py` + `tests/test_gate_emit.py` | ✅ no file overlap |
| 2 | Task 3 | `docs/validation/2026-06-19-citation-prompt-recall.md` (new) | single task (LLM ship-gate, needs Task 1) |
| 3 | Task 4 | `docs/validation/2026-06-19-live-fire-precision-3repos.md`, `CLAUDE.md` | single task (docs, needs Task 3 green) |

## File Structure
- `domains/security/detectors/baseline/baseline.prompt.txt` (MODIFY) — Task 1, the prompt rule.
- `orchestrator/gate.py` (MODIFY) — Task 2, `parse_findings` + `union_rolls` + a `_resolve_line` helper + `build_emit_dict`.
- `tests/test_gate.py`, `tests/test_gate_emit.py` (MODIFY) — Task 2, spine unit tests.
- `docs/validation/2026-06-19-citation-prompt-recall.md` (CREATE) — Task 3, the recall-no-regression ship-gate record.
- Live-fire doc + `CLAUDE.md` (MODIFY) — Task 4, measured-state fold-in.

---

### Task 1: The scoped prompt rule

**Wave:** 1 · **Blocks:** Task 3 · **Blocked by:** —

**Files:** Modify: `domains/security/detectors/baseline/baseline.prompt.txt`

- [ ] **Step 1: Edit field 2/3 of the OUTPUT CONTRACT** (currently line 72, "2. **Location** — `file:line` or `function name`"). Replace that single field with two:
```
2. **Location** — `file:line` (mandatory, exact).
3. **Code** — quote the executable line AT or NEAREST the defect, verbatim from the file. For a MISSING check/guard/validation/idempotency, quote the unguarded sink/query/call/return where the check is ABSENT. One line or a short span.
```
Renumber the existing Severity → 4, Trigger/Exploit → 5, Fix → 6.

- [ ] **Step 2: Add the executes-only rule** to "Rules for the output" (after the line "Prefer one precise finding…"). Paste VERBATIM (this is the MEASURED wording — do not strengthen into a suppression rule):
```
- Flag only code that EXECUTES. A defect that exists SOLELY inside commented-out, illustrative, or otherwise non-executing code is NOT a finding — do not report it. (Scoped to comment/illustration trivia ONLY; declarative config, schema, and IaC that take effect at deploy/runtime ARE live code and remain in scope.)
- An absence-class defect (missing authorization/IDOR, missing idempotency, missing validation, fail-open) IS a real finding — quote the line where the guard should be. Quoting a substring is required for anchoring; never withhold an absence finding for lack of a defective substring.
```

- [ ] **Step 3: Do NOT commit yet.** This prompt ships only if Task 3's recall-no-regression is green. Leave staged on the worktree.

---

### Task 2: The citation-spine (deterministic, TDD)

**Wave:** 1 · **Blocks:** — · **Blocked by:** —

**Files:** Modify: `orchestrator/gate.py`, `tests/test_gate.py`, `tests/test_gate_emit.py`

- [ ] **Step 1: Write the failing parse test** in `tests/test_gate.py` (path-load `gate.py` per the existing convention in that file):
```python
def test_parse_findings_captures_snippet_and_line_hint():
    text = (
        "**1. Missing ownership check on refund**\n"
        "2. Location — refund.ts:42\n"
        "3. Code — `await db.refund(orderId)`\n"
        "4. Severity — high\n"
    )
    rows = gate.parse_findings(text)
    assert len(rows) == 1
    title, sev, body, snippet, line_hint = rows[0]
    assert snippet == "await db.refund(orderId)"
    assert line_hint == 42
```
Run: `python3 -m pytest tests/test_gate.py::test_parse_findings_captures_snippet_and_line_hint -v` → FAIL (tuple is 3-wide).

- [ ] **Step 2: Widen `parse_findings`** to return `(title, sev, body, snippet, line_hint)`. Parse from `body`: `snippet` = first backtick-quoted span on a `Code`/`Code —` line (fallback: any backtick span); `line_hint` = integer after `file:` in the `Location` line (fallback `0`). Missing → `("", 0)`. Update the SOLE consumer `union_rolls` (gate.py:114) `for title, sev, _ in …` → `for title, sev, _, snippet, line_hint in …`. Run the test → PASS.

- [ ] **Step 3: Carry snippet/line onto the group.** In `union_rolls`, when creating a group (gate.py:124) add `"snippet": snippet, "line_hint": line_hint`; on merge, fill them only if currently empty (first non-empty wins). Run `python3 -m pytest tests/test_gate.py -q` → PASS (no regression).

- [ ] **Step 4: Write the failing line-resolution + emit test** in `tests/test_gate_emit.py`:
```python
def test_emit_resolves_real_line_from_snippet_not_hint(tmp_path):
    # snippet is the PRIMARY anchor: the real line comes from locating the snippet in the
    # target file, NOT the LLM's line_hint (bundle-offset-wrong in bundle mode).
    target = os.path.join(str(tmp_path), "refund.ts")
    open(target, "w").write("line one\nline two\nawait db.refund(orderId)\nline four\n")
    groups = [{"title": "Missing ownership check", "sev": "high", "rolls": {0, 1, 2},
               "snippet": "await db.refund(orderId)", "line_hint": 99}]
    d = gate.build_emit_dict(target, "baseline", ["S5"], 3, groups, [], {}, False)
    f = d["findings"][0]
    assert f["line"] == 3, f"must resolve from snippet (line 3), not hint 99; got {f['line']}"
    assert f["code"] == "await db.refund(orderId)"
    assert f["symbol"] == "", "snippet must NOT be stuffed into symbol"
```
Run → FAIL (build_emit_dict emits line=0, no `code`).

- [ ] **Step 5: Add `_resolve_line` + wire `build_emit_dict`.** Add a helper:
```python
def _resolve_line(target, snippet, line_hint):
    """Real target line = where the verbatim snippet appears in the target FILE (1-based, first match).
    The LLM's line_hint is bundle-offset-wrong in bundle mode -> snippet is primary, hint is fallback."""
    if snippet:
        try:
            for i, ln in enumerate(open(target, encoding="utf-8", errors="replace"), 1):
                if snippet in ln:
                    return i, True
        except OSError:
            pass
    return (line_hint or 0), False
```
In `build_emit_dict`, for each group replace `line=0, symbol=""` (gate.py:191) with `line=<resolved>, symbol=""`, then set `f["code"] = g.get("snippet", "")` and `f["anchor"] = "resolved" if resolved else "unverified"`. Run the test → PASS.

- [ ] **Step 6: Full suite** — `python3 -m pytest -q`. Expected: prior count + 2, all pass (the 3 existing `build_emit_dict` tests use snippet-less groups → `line=0`, backward-compatible).

- [ ] **Step 7: Commit** (the spine is independent of the prompt ship-gate):
```bash
git add orchestrator/gate.py tests/test_gate.py tests/test_gate_emit.py
git commit -m "feat(gate): citation-spine — capture quoted snippet, resolve real line from file, emit navigable findings"
```

---

### Task 3: Recall-no-regression — the prompt SHIP GATE (LLM, k=3)

**Wave:** 2 · **Blocks:** Task 4 · **Blocked by:** Task 1

**Files:** Create: `docs/validation/2026-06-19-citation-prompt-recall.md`

- [ ] **Step 1: Verify the corpus has absence-class cells.** Inspect the 17-cell recall set (`docs/validation/2026-06-18-baseline-generalization-recall.md` + the corpus cells). Confirm ≥1 absence-class cell (missing auth/IDOR, missing idempotency, fail-open). If NONE exists, STOP — the recall check is blind to the class the prompt rule endangers; harvest + add one real absence-class cell (RAW git fix commit, `command git --no-pager show`) to the recall set BEFORE proceeding. Record which cells are absence-class.

- [ ] **Step 2: Run k=3 right-reason recall** with the patched prompt over the 17-cell corpus (clean config, refresh creds; the same harness used for `2026-06-18-baseline-generalization-recall.md`). Record per-cell rolls.

- [ ] **Step 3: Assert no regression.** Bar: 10 cells 3/3, 7 cells 2/3; safe.ts discriminator 17/17 (canonical NOT re-raised on the fixed file). Absence-class cells MUST hold their prior roll counts. ANY drop, especially on an absence-class cell → the prompt wording suppressed a real catch. STOP, fix Task 1 wording, re-measure. NEVER ship on precision alone.

- [ ] **Step 4: Precision-hold (n≥3).** Re-run the 2 zync stub files (`apps/zync-api/src/integrations/payment-gateways/{cardcom,stripe}.ts`) at k=3, 3 times; confirm the comment-construct FPs stay ≈0 as a RATE (the A/B was n=1). READ-ONLY on zync.

- [ ] **Step 5: Write the record** `docs/validation/2026-06-19-citation-prompt-recall.md` — the per-cell recall table, the absence-class cells named, the precision-hold rate, and the GREEN/RED ship verdict. If GREEN, commit the prompt:
```bash
git add domains/security/detectors/baseline/baseline.prompt.txt
git commit -m "feat(baseline): scoped executes-only rule + mandatory code-anchor — comment-FP cluster eliminated (recall no-regression k=3)"
```

---

### Task 4: Fold measured state into doc-truth

**Wave:** 3 · **Blocks:** — · **Blocked by:** Task 3 (GREEN)

**Files:** Modify: `docs/validation/2026-06-19-live-fire-precision-3repos.md`, `CLAUDE.md`

- [ ] **Step 1:** Append to the live-fire doc: defect #1 (comment-blindness) CLOSED by the band-1 prompt fix (cite the recall record + the n≥3 precision rate); the band-3 verifier DEFERRED per delete-test; the residual executable-stub over-flagging + defect #2 remain open (named non-goals). Record the 3 live-repo robust-fix branches (trance `36606dfe`/`c85d533f`, zync `04b1fb3`, platform `0587629`/`2179bde`/`542e980`) as the NEEDS-HUMAN/out-of-scope-real resolutions (isolated, unpushed).

- [ ] **Step 2:** Update `CLAUDE.md` "Measured state": note the comment-FP cluster is closed by the prompt fix + the navigable-emit citation-spine; verifier deferred.

- [ ] **Step 3: Commit:**
```bash
git add docs/validation/2026-06-19-live-fire-precision-3repos.md CLAUDE.md
git commit -m "docs: comment-FP cluster CLOSED (band-1 prompt fix + citation-spine); verifier deferred (delete-test)"
```

---

## Self-Review
- **Spec coverage:** Tier 1 prompt → Task 1; Tier 2 spine → Task 2; Tier 3 verifier DEFERRED → not built (recorded in Task 4); validation (recall-no-regression + absence-class + precision-hold + spine units) → Task 3 + Task 2. All spec sections map.
- **Placeholder scan:** none — all code/commands concrete.
- **Type consistency:** `parse_findings` → 5-tuple `(title, sev, body, snippet, line_hint)`, single consumer `union_rolls` updated (Step 2); group dict gains `snippet`/`line_hint` (Step 3) read by `build_emit_dict`/`_resolve_line` (Step 5); `build_emit_dict` signature UNCHANGED (internals only) → 3 existing emit tests stay green (Step 6). `_resolve_line(target, snippet, line_hint) -> (int, bool)` consistent across Steps 5.
- **Wave check:** Wave 1 Task 1 (prompt.txt) ‖ Task 2 (gate.py + 2 test files) — zero file overlap ✅. Task 3 needs Task 1's prompt in place → Wave 2. Task 4 needs Task 3 green → Wave 3. No same-wave file overlap.
- **Ship-gate ordering:** prompt commit is GATED behind Task 3 (Task 1 Step 3 holds it); the deterministic spine commits independently (Task 2 Step 7). Correct — a recall regression blocks only the prompt, not the spine.
