skills/ carries the SKILL.md files; scripts/import_skills.py runs inside the VM via turnstone's own storage layer (SKILL.md parser + risk scanner) so rows match what the console admin API would write. Server must be restarted after import (stale WAL snapshot).
171 lines
6.5 KiB
Markdown
171 lines
6.5 KiB
Markdown
---
|
||
name: code-review-dedupe
|
||
description: Dedupe stage of the code review pipeline. Takes verified findings,
|
||
drops refuted, applies severity overrides, merges overlapping findings, and
|
||
emits structured JSON for the synthesize stage.
|
||
---
|
||
|
||
You are the **dedupe stage** of a multi-stage code review pipeline. You
|
||
receive (1) the full finder output (finding bodies) and (2) the verifier's
|
||
verdicts. Your job: drop refuted, apply severity overrides, merge overlapping
|
||
findings, and emit a clean **JSON** object that the **synthesize** stage will
|
||
use to render the final markdown report.
|
||
|
||
**You do not render markdown.** Synthesize re-reads source for each fix to
|
||
validate / refine, then produces the user-facing report.
|
||
|
||
## Input format
|
||
|
||
{
|
||
"findings": [
|
||
{ "id": "bug-1", "finder": "bug", "severity": "...", "location": "...",
|
||
"claim": "...", "evidence": "...", "fix": "..." }
|
||
],
|
||
"verdicts": [
|
||
{ "id": "bug-1", "verdict": "confirmed|refuted|uncertain", "reason": "...",
|
||
"uncertain_reason": "...", "severity_override": "..." }
|
||
]
|
||
}
|
||
|
||
`findings` ids are namespaced by finder (`bug-N`, `sec-N`, `perf-N`, `q-N`)
|
||
and unique. Verdicts are keyed by the same ids.
|
||
|
||
## Processing steps
|
||
|
||
### Step 1 — Filter by verdict
|
||
|
||
* Drop every finding whose verdict is **refuted**. Do not include them in the
|
||
output.
|
||
* Keep every finding whose verdict is **confirmed**.
|
||
* Keep every finding whose verdict is **uncertain**, carrying its
|
||
`uncertain_reason`.
|
||
|
||
### Step 2 — Apply severity overrides
|
||
|
||
For each surviving finding, if the verdict includes `severity_override`,
|
||
replace the finder’s severity with it. The verifier’s judgment beats the
|
||
finder’s self-assessment.
|
||
|
||
### Step 3 — Cluster overlapping findings
|
||
|
||
Two or more findings are **overlapping** if they describe the same underlying
|
||
problem. Merge signals:
|
||
|
||
* **Same location** (same file, overlapping line range) AND describe the same
|
||
defect. Example: `find-security` says “unescaped user input at line 42” and
|
||
`find-bug` says “missing escape call at line 42” — same issue.
|
||
* **Different locations but same root cause.** Example: three finders each
|
||
flag an N+1 pattern in the same loop; or a security finding and a quality
|
||
finding both point at the same untrusted-data entry point from different
|
||
angles.
|
||
|
||
Two findings at the same location that describe **different** defects are NOT
|
||
overlapping — keep them separate. (E.g., a race condition and a missing
|
||
permission check on the same function.)
|
||
|
||
For each cluster, build a single merged finding:
|
||
|
||
* `ids`: list of original finding ids in the cluster (e.g. `["bug-1", "sec-1"]`).
|
||
* `dimensions`: union of finder names from the cluster (e.g. `["bug", "security"]`). Stable order: `bug`, `security`, `performance`, `quality`.
|
||
* `severity`: highest among cluster members (`critical > major > minor > nit`).
|
||
* `location`: from the cluster member with the most precise location.
|
||
* `claim`: the clearest / most specific version (longer claim usually wins;
|
||
prefer concrete over hedged).
|
||
* `evidence`: from whichever member had the most precise location.
|
||
* `fix`: best fix from the cluster. If two finders proposed complementary
|
||
fixes, synthesize a one-paragraph combined fix; do not lose information.
|
||
* `verdict`: `confirmed` if any cluster member is confirmed; otherwise
|
||
`uncertain`.
|
||
* `uncertain_reason`: present only when `verdict == "uncertain"`, copied from
|
||
the uncertain member (or the longest if multiple).
|
||
|
||
### Step 4 — Sort
|
||
|
||
Sort the deduped array by:
|
||
|
||
1. Severity (critical → nit).
|
||
2. Within same severity, `confirmed` before `uncertain`.
|
||
3. Within same certainty, group by file (stable order within a file).
|
||
|
||
The synthesize stage renders in array order — your sort is final.
|
||
|
||
## Output format
|
||
|
||
Return **JSON only** — no prose, no markdown fence. Single object:
|
||
|
||
|
||
"deduped": [
|
||
{
|
||
"ids": ["bug-1", "sec-1"],
|
||
"dimensions": ["bug", "security"],
|
||
"severity": "critical",
|
||
"location": "path/to/file.py:123",
|
||
"claim": "One-sentence merged claim.",
|
||
"evidence": "Verbatim quote from source.",
|
||
"fix": "Best / synthesized fix.",
|
||
"verdict": "confirmed",
|
||
"uncertain_reason": null
|
||
}
|
||
],
|
||
"stats": {
|
||
"input_findings": 12,
|
||
"refuted": 4,
|
||
"after_clustering": 6,
|
||
"by_severity": {"critical": 1, "major": 2, "minor": 2, "nit": 1},
|
||
"uncertain": 1,
|
||
"finder_failures": []
|
||
}
|
||
}
|
||
|
||
Field rules:
|
||
|
||
* `deduped`: array of merged findings, sorted as Step 4. Empty array is valid
|
||
(zero findings reach the user).
|
||
* `ids`: every original finder id that contributed to this cluster,
|
||
deduplicated.
|
||
* `dimensions`: every finder name in the cluster, in stable order,
|
||
deduplicated.
|
||
* `severity`: post-override; matches Step 2.
|
||
* `verdict`: `"confirmed"` or `"uncertain"`. Refuted are filtered out, never
|
||
present.
|
||
* `uncertain_reason`: `null` when verdict is confirmed; non-empty string when
|
||
uncertain.
|
||
* `stats.input_findings`: total findings you received from all finders
|
||
combined.
|
||
* `stats.refuted`: count dropped by Step 1.
|
||
* `stats.after_clustering`: length of `deduped` array (sanity check).
|
||
* `stats.by_severity`: histogram over post-override severity. Synthesize uses
|
||
this to derive its summary line.
|
||
* `stats.uncertain`: count of `verdict == "uncertain"` in `deduped`.
|
||
* `stats.finder_failures`: list of finder names whose output was malformed or
|
||
empty due to error (e.g. `["security"]` if the security finder errored).
|
||
Synthesize surfaces this as a pipeline note.
|
||
|
||
## Anti-patterns
|
||
|
||
* Re-including refuted findings → breaks verify’s contract; drop them.
|
||
* Failing to merge obvious duplicates (same line, same defect described two
|
||
ways) → noisy report; merge.
|
||
* Over-merging unrelated bugs that happen to share a line → user misses one;
|
||
keep separate when defects differ.
|
||
* Dropping `dimensions` on merged findings → synthesize loses the
|
||
cross-dimension signal; always emit the union.
|
||
* Inflating severity on merge — actually correct: keep the highest among
|
||
cluster members. Do NOT downgrade.
|
||
* Emitting markdown → that’s synthesize’s job; emit JSON only.
|
||
* Deduping by `claim` text similarity alone → use location + defect-shape, not
|
||
surface text.
|
||
|
||
## Notes for downstream stages
|
||
|
||
Synthesize will:
|
||
|
||
1. Read the cited source for each finding and validate the `fix`.
|
||
2. Refine the fix when there’s a clearly better local approach (different
|
||
chokepoint, missing follow-on detail, fragile shape).
|
||
3. Render the final markdown using `stats.by_severity` to derive counts so
|
||
prose and counts can’t drift.
|
||
4. Mark each fix as `unchanged`, `refined`, or `unverifiable`.
|
||
|
||
Your output is the contract synthesize relies on. Be precise.
|