iterate-plugin 2.7.3 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -229,6 +229,19 @@ All tests pass:
229
229
  - **212 unit tests green**, type-check clean
230
230
  - Coverage: dedupe, filter, sort, multi-round convergence, meta-review audit, path safety, timeout clamping, config read/write + rollback, triage merge, diff computation, checkpoint validation, fix registry, history read + filter, prune cleanup report + dry-run semantics, UI pure functions (select-all key, runtime status guide).
231
231
 
232
+ ## ⚠️ Disclaimer
233
+
234
+ This project is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement.
235
+
236
+ **Automated code review and fixing carries inherent risk.** All changes produced in normal mode are generated by AI models and may introduce bugs, regressions, or unintended behavior. Before merging, you should:
237
+
238
+ - Review every diff before applying it to your main branch or pushing.
239
+ - Make sure your project is under git control and can be rolled back (`git restore`, revert, or restore from backup).
240
+ - Run your project's own test suite and build checks after each round of fixes.
241
+ - Never run this on secrets, credentials, `.env`, or files that must not be modified — configure `protected_paths` accordingly.
242
+
243
+ Users are solely responsible for the code that is generated, modified, or committed as a result of using this project. By using it, you acknowledge that neither the maintainers nor contributors are liable for any loss, damage, or legal consequences arising from its use.
244
+
232
245
  ## License
233
246
 
234
247
  MIT
package/README.zh-CN.md CHANGED
@@ -229,6 +229,19 @@ npm test
229
229
  - **212 个单元测试全绿**,类型检查通过
230
230
  - 覆盖:去重、过滤、排序、多轮收敛、meta-review 审计、路径安全、超时钳制、配置读写与回滚、triage 合并、diff 计算、checkpoint 校验、修复注册表、历史读取与过滤、prune 清理报告与 dry-run 语义、UI 纯函数(select-all 键、运行时状态指引)等
231
231
 
232
+ ## ⚠️ 免责声明
233
+
234
+ 本项目按「现状」(AS IS)提供,不附带任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性及不侵权性的担保。
235
+
236
+ **自动化的代码审查与修复存在固有风险。** normal 模式下产生的改动均由 AI 模型生成,可能引入缺陷、回归或非预期行为。在合并改动前,你应当:
237
+
238
+ - 在应用到主分支或推送前,逐条 review 每一处 diff。
239
+ - 确保项目处于 git 版本控制之下,并可随时回滚(`git restore`、revert 或从备份恢复)。
240
+ - 在每轮修复后运行项目自身的测试与构建检查。
241
+ - 切勿在密钥、凭证、`.env` 或任何不允许修改的文件上运行本项目;请在配置中设置 `protected_paths` 予以保护。
242
+
243
+ 使用者需为本项目使用过程中所产生、修改或提交的代码负全部责任。使用本项目即表示你同意:维护者与贡献者不对因使用本项目而导致的任何损失、损害或法律后果承担责任。
244
+
232
245
  ## License
233
246
 
234
247
  MIT
@@ -48,7 +48,7 @@ export function defaultConfig() {
48
48
  auto_merge: false,
49
49
  },
50
50
  validation: { command_whitelist: [], commands: {} },
51
- reviewer: { output_schema_validation: true },
51
+ reviewer: { output_schema_validation: true, evidence_validation: true },
52
52
  };
53
53
  }
54
54
  /**
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Deterministic code-evidence verification for review findings.
3
+ *
4
+ * Mirror of `iterate_harness/iterate/evidence.py` for the iterate-plugin.
5
+ *
6
+ * The iterate review loop requires that reviewer subagent findings ANCHOR to
7
+ * real code instead of speculating. This module enforces it:
8
+ *
9
+ * - a finding's `file` must resolve to an existing file under the project root
10
+ * (traversal-safe), otherwise evidence is poisoned (`file_not_found`);
11
+ * - a finding with an explicit line must reference a line that actually exists
12
+ * in that file (`line_out_of_range`);
13
+ * - a whole-file finding (line 0 / undefined) must still reference an existing
14
+ * file, so even structural findings cannot point at nothing;
15
+ * - `readVerified` is a best-effort, NON-gating hint: the plugin's reviewers are
16
+ * subagents whose reads are not aggregated here, so it is only set when a
17
+ * read set is explicitly provided and never fails the audit.
18
+ *
19
+ * Gate rule (user preference): ANY localizable finding with poisoned evidence
20
+ * flips the whole audit to `passed: false`, so the meta-review forces revision.
21
+ *
22
+ * The pure math (`countLines`, `verifyLineBounds`) is separated from the
23
+ * filesystem half (`verifyFinding`) to stay unit-testable without touching disk.
24
+ */
25
+ import { existsSync, readFileSync } from 'node:fs';
26
+ import { resolve, sep } from 'node:path';
27
+ /** Sentinel for whole-file findings (line 0 or omitted means the whole file). */
28
+ export const WHOLE_FILE_LINE = 0;
29
+ /** Number of physical lines in `text`. A trailing newline does not add a line. */
30
+ export function countLines(text) {
31
+ if (text === '')
32
+ return 0;
33
+ const parts = text.split(/\r\n|\r|\n/);
34
+ // A trailing newline leaves an empty final element that is NOT a line
35
+ // (mirrors Python `str.splitlines()` used by the harness).
36
+ if (parts[parts.length - 1] === '')
37
+ return parts.length - 1;
38
+ return parts.length;
39
+ }
40
+ /** Resolve `root/rel` and reject any path escaping `root` (returns null). */
41
+ export function resolveWithin(root, rel) {
42
+ const resolved = resolve(root, rel);
43
+ const rootResolved = resolve(root);
44
+ if (resolved === rootResolved)
45
+ return resolved;
46
+ const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
47
+ if (!resolved.startsWith(prefix))
48
+ return null;
49
+ return resolved;
50
+ }
51
+ /**
52
+ * Pure check that `line` (if anchored) exists in `text`.
53
+ * Whole-file findings (undefined/0) are always bounds-valid.
54
+ */
55
+ export function verifyLineBounds(line, text) {
56
+ const lineTotal = countLines(text);
57
+ if (line === undefined || line === null || line === WHOLE_FILE_LINE) {
58
+ return { inBounds: true, lineTotal };
59
+ }
60
+ if (line < 1)
61
+ return { inBounds: false, lineTotal };
62
+ return { inBounds: line <= lineTotal, lineTotal };
63
+ }
64
+ /** Verify a single finding's location against the real filesystem. */
65
+ export function verifyFinding(root, input, opts = {}) {
66
+ const relFile = input.file ?? '';
67
+ const line = typeof input.line === 'number' ? input.line : null;
68
+ const resolved = resolveWithin(root, relFile);
69
+ if (resolved === null || !existsSync(resolved)) {
70
+ return {
71
+ file: relFile,
72
+ line,
73
+ lineTotal: null,
74
+ resolvedPath: resolved,
75
+ verified: false,
76
+ error: 'file_not_found',
77
+ };
78
+ }
79
+ let text;
80
+ try {
81
+ text = readFileSync(resolved, 'utf-8');
82
+ }
83
+ catch {
84
+ return {
85
+ file: relFile,
86
+ line,
87
+ lineTotal: null,
88
+ resolvedPath: resolved,
89
+ verified: false,
90
+ error: 'file_not_found',
91
+ };
92
+ }
93
+ const { inBounds, lineTotal } = verifyLineBounds(line, text);
94
+ if (!inBounds) {
95
+ return {
96
+ file: relFile,
97
+ line,
98
+ lineTotal,
99
+ resolvedPath: resolved,
100
+ verified: false,
101
+ error: 'line_out_of_range',
102
+ };
103
+ }
104
+ const outcome = {
105
+ file: relFile,
106
+ line,
107
+ lineTotal,
108
+ resolvedPath: resolved,
109
+ verified: true,
110
+ };
111
+ if (opts.readSet !== undefined) {
112
+ outcome.readVerified = opts.readSet.has(resolved);
113
+ }
114
+ return outcome;
115
+ }
116
+ /** Attest every finding in a list. */
117
+ export function verifyFindings(root, findings, opts = {}) {
118
+ const results = findings.map((f) => verifyFinding(root, f, opts));
119
+ return { checked: results.length, results };
120
+ }
121
+ /** `passed` is true only when no real existence failure exists (read is a hint). */
122
+ export function evidencePassed(audit) {
123
+ return audit.results.every((r) => r.error === undefined);
124
+ }
125
+ /** Violating (non-grounded) results. */
126
+ export function evidenceViolations(audit) {
127
+ return audit.results.filter((r) => r.error !== undefined);
128
+ }
129
+ /** Serialize an audit for tool payloads (pure). */
130
+ export function evidenceToPlain(audit) {
131
+ const computable = audit.results.filter((r) => r.readVerified !== undefined);
132
+ const readRatio = computable.length === 0
133
+ ? null
134
+ : Number((computable.filter((r) => r.readVerified === true).length / computable.length).toFixed(3));
135
+ return {
136
+ checked: audit.checked,
137
+ passed: evidencePassed(audit),
138
+ violations: audit.results
139
+ .filter((r) => r.error !== undefined)
140
+ .map((r) => ({ file: r.file, line: r.line, lineTotal: r.lineTotal, verified: r.verified, error: r.error })),
141
+ readVerifiedRatio: readRatio,
142
+ };
143
+ }
@@ -157,9 +157,37 @@ export function metaReviewReport(report) {
157
157
  /**
158
158
  * Build the final review report: pair the source report with its meta-review
159
159
  * verdict and a rolled-up summary. Pure and deterministic.
160
+ *
161
+ * `evidence` (an EvidenceAudit produced against the real repo) is the hard
162
+ * code-evidence gate: every finding whose file/line does not resolve to
163
+ * existing code is emitted as a critical EVIDENCE_VIOLATION and flips the
164
+ * verdict to `needs_revision`. The audit itself reads the filesystem; this
165
+ * function only folds the (pure, precomputed) result in.
160
166
  */
161
- export function buildFinalReviewReport(report) {
167
+ export function buildFinalReviewReport(report, opts = {}) {
162
168
  const meta = metaReviewReport(report);
169
+ const evidence = opts.evidence ?? null;
170
+ if (evidence !== null) {
171
+ meta.checksRun += 1;
172
+ if (evidence.results.some((r) => r.error !== undefined)) {
173
+ for (const violation of evidence.results) {
174
+ if (violation.error === undefined)
175
+ continue;
176
+ const detail = violation.error === 'line_out_of_range'
177
+ ? `${violation.line} is beyond this file's ${violation.lineTotal} lines`
178
+ : `${violation.file} does not exist at all (verifiable read required)`;
179
+ meta.issues.push({
180
+ code: 'EVIDENCE_VIOLATION',
181
+ severity: 'critical',
182
+ summary: `Finding references non-existent code: ${violation.file}` +
183
+ (violation.line ? `:${violation.line}` : ''),
184
+ detail: detail + '. Review results must anchor to real, read code.',
185
+ });
186
+ }
187
+ meta.passed = false;
188
+ meta.verdict = 'revise';
189
+ }
190
+ }
163
191
  const summary = report?.summary ?? {};
164
192
  const verdict = meta.passed ? 'approved' : 'needs_revision';
165
193
  return {
package/dist/review.js CHANGED
@@ -290,9 +290,17 @@ export function reviewerTaskPrompt(input) {
290
290
  else {
291
291
  parts.push('This is round 1 — report every issue you find in this dimension.');
292
292
  }
293
+ parts.push('EVIDENCE RULE (mandatory): read every file you report on with the ' +
294
+ 'read_file tool BEFORE judging it. NEVER report a location you did not ' +
295
+ 'actually read — speculation about code you never inspected is a ' +
296
+ 'disqualifying failure, and fabricated line numbers are treated as ' +
297
+ 'poisoned evidence. Anchor every finding to real code.');
293
298
  parts.push(`Return a JSON object: {"findings": [...]}.`, `Each finding: dimension (must be "${input.dimension}"), file (relative path), ` +
294
- 'line (optional integer), severity (critical/high/medium/low), summary (one line), ' +
295
- 'failure_scenario (how/when it fails, specific evidence), suggested_fix (the concrete fix), ' +
299
+ 'line (REQUIRED positive integer the exact line you READ for an ' +
300
+ 'anchored, line-targeted issue; use 0 for whole-file/module-level ' +
301
+ 'issues), severity (critical/high/medium/low), summary (one line), ' +
302
+ 'failure_scenario (how/when it fails, backed by the code you actually ' +
303
+ 'read), suggested_fix (the concrete fix), ' +
296
304
  `is_atomic (true if the fix is <= ${input.maxLines} lines within a SINGLE file/function, else false).`, `Write summaries and details in ${input.outputLanguage}.`);
297
305
  return parts.join('\n');
298
306
  }
@@ -121,10 +121,11 @@ return {
121
121
 
122
122
  Key rules for dry-run:
123
123
  - **NEVER call a fixer / never edit files / never create branches or worktree.** Reviewers read only.
124
+ - **Every reviewer MUST actually read each file it reports on (read_file) BEFORE judging it, and anchor every finding to a real location. Fabricated file paths or invented line numbers are poisoned evidence and fail the run.** Subagents never report on code they didn't inspect.
124
125
  - Each round feeds the already-known findings to reviewers so they hunt NEW issues only → that is what drives convergence.
125
126
  - Stop when a round reports 0 new findings (converged) or maxReviewRounds is reached.
126
127
  - The report (with per-round convergence stats + suggested fix priorities) is the deliverable.
127
- - **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The \`finalReport.verdict\` is \`approved\` only when the report passes every check; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
128
+ - **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The meta-review ALSO runs the hard code-evidence gate (default on): every finding's file/line is validated against real files on disk, so any fabricated location surfaces as a critical \`EVIDENCE_VIOLATION\` and flips the verdict to \`needs_revision\`. The \`finalReport.verdict\` is \`approved\` only when the report passes every check AND every finding anchors to real, read code; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
128
129
  - Only a single \`report\` entry may be appended to the decision log; nothing else is written.
129
130
 
130
131
  ### Normal-mode workflow (autonomous closed loop)
@@ -328,11 +329,12 @@ Key rules for normal mode:
328
329
  - Close with \`iterate_status\` metrics and surface the convergence indicators (fixed count, remaining architectural count, abort reason) in the final summary.
329
330
 
330
331
  ### Finding schema (for reviewer agents)
331
- { "dimension": string, "file": string (relative path), "line": number (optional),
332
+ { "dimension": string, "file": string (relative path), "line": number (REQUIRED for line-targeted issues — the exact line you READ; use 0 for whole-file/module-level issues),
332
333
  "severity": "critical" | "high" | "medium" | "low", "summary": string (one line),
333
334
  "failure_scenario": string (how/when it fails), "suggested_fix": string (the concrete fix),
334
335
  "is_atomic": boolean (true if fix ≤ max_lines within a single file/function) }
335
336
  Atomic = is_atomic true (single file, single function, ≤ config.atomic.max_lines lines change). Architectural = everything else.
337
+ Every finding MUST reference a file the reviewer actually read (read_file) and a real location — never speculate about code that was never inspected. Fabricated paths/lines are poisoned evidence and fail the meta-review evidence gate.
336
338
 
337
339
  ### Workflow meta
338
340
  Always pass \`meta: { name: "iterate", description: "Autonomous iterate loop" }\`.
@@ -2,6 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { loadEffectiveConfig, resolveProjectRoot } from "../config-loader.js";
3
3
  import { buildReviewPlan, buildReviewReport } from "../review.js";
4
4
  import { buildFinalReviewReport, metaReviewReport } from "../meta-review.js";
5
+ import { evidenceToPlain, verifyFindings } from "../evidence.js";
5
6
  /** Default round cap when neither the arg nor config provides one. */
6
7
  const DEFAULT_MAX_REVIEW_ROUNDS = 3;
7
8
  /**
@@ -82,6 +83,7 @@ export function registerReviewTool(ctx) {
82
83
  found: { type: 'boolean' },
83
84
  plan: { type: 'json' },
84
85
  report: { type: 'json' },
86
+ evidence: { type: 'json' },
85
87
  finalReport: { type: 'json' },
86
88
  error: { type: 'string' },
87
89
  },
@@ -147,12 +149,19 @@ export function registerReviewTool(ctx) {
147
149
  };
148
150
  }
149
151
  const audit = metaReviewReport(source);
150
- const finalReport = buildFinalReviewReport(source);
152
+ // Hard code-evidence gate (default on): every finding's file/line is
153
+ // validated against real files on disk before folding into the final
154
+ // verdict. Disable via config `reviewer.evidence_validation: false`.
155
+ const evidenceEnabled = config.reviewer?.evidence_validation !== false;
156
+ const findings = Array.isArray(source.findings) ? source.findings : [];
157
+ const evidence = evidenceEnabled ? verifyFindings(projectRoot, findings) : null;
158
+ const finalReport = buildFinalReviewReport(source, { evidence });
151
159
  return {
152
160
  operation: 'meta-review',
153
161
  mode,
154
162
  found: true,
155
163
  report: audit,
164
+ evidence: evidence ? evidenceToPlain(evidence) : null,
156
165
  finalReport: finalReport,
157
166
  };
158
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.7.3",
3
+ "version": "2.8.0",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus a dry-run pure-review mode with multi-round convergence and a meta-review that audits the report and emits a final review report.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@ export function defaultConfig(): IterateConfig {
49
49
  auto_merge: false,
50
50
  },
51
51
  validation: { command_whitelist: [], commands: {} },
52
- reviewer: { output_schema_validation: true },
52
+ reviewer: { output_schema_validation: true, evidence_validation: true },
53
53
  }
54
54
  }
55
55
 
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Deterministic code-evidence verification for review findings.
3
+ *
4
+ * Mirror of `iterate_harness/iterate/evidence.py` for the iterate-plugin.
5
+ *
6
+ * The iterate review loop requires that reviewer subagent findings ANCHOR to
7
+ * real code instead of speculating. This module enforces it:
8
+ *
9
+ * - a finding's `file` must resolve to an existing file under the project root
10
+ * (traversal-safe), otherwise evidence is poisoned (`file_not_found`);
11
+ * - a finding with an explicit line must reference a line that actually exists
12
+ * in that file (`line_out_of_range`);
13
+ * - a whole-file finding (line 0 / undefined) must still reference an existing
14
+ * file, so even structural findings cannot point at nothing;
15
+ * - `readVerified` is a best-effort, NON-gating hint: the plugin's reviewers are
16
+ * subagents whose reads are not aggregated here, so it is only set when a
17
+ * read set is explicitly provided and never fails the audit.
18
+ *
19
+ * Gate rule (user preference): ANY localizable finding with poisoned evidence
20
+ * flips the whole audit to `passed: false`, so the meta-review forces revision.
21
+ *
22
+ * The pure math (`countLines`, `verifyLineBounds`) is separated from the
23
+ * filesystem half (`verifyFinding`) to stay unit-testable without touching disk.
24
+ */
25
+
26
+ import { existsSync, readFileSync } from 'node:fs'
27
+ import { resolve, sep } from 'node:path'
28
+ import type { ReviewFinding } from './types.ts'
29
+
30
+ /** Sentinel for whole-file findings (line 0 or omitted means the whole file). */
31
+ export const WHOLE_FILE_LINE = 0
32
+
33
+ export type EvidenceError = 'file_not_found' | 'line_out_of_range'
34
+
35
+ /** Per-finding attestation result. */
36
+ export interface FindingEvidence {
37
+ file: string
38
+ line: number | null
39
+ lineTotal: number | null
40
+ resolvedPath: string | null
41
+ verified: boolean
42
+ error?: EvidenceError
43
+ /** True/False only when a read-set is supplied; undefined = not checkable. */
44
+ readVerified?: boolean
45
+ }
46
+
47
+ /** Aggregate attestation over a findings list. */
48
+ export interface EvidenceAudit {
49
+ checked: number
50
+ results: FindingEvidence[]
51
+ }
52
+
53
+ /** A single finding object that exposes `file` / `line` (for verification). */
54
+ interface Locatable {
55
+ file?: string
56
+ line?: number
57
+ }
58
+
59
+ /** Number of physical lines in `text`. A trailing newline does not add a line. */
60
+ export function countLines(text: string): number {
61
+ if (text === '') return 0
62
+ const parts = text.split(/\r\n|\r|\n/)
63
+ // A trailing newline leaves an empty final element that is NOT a line
64
+ // (mirrors Python `str.splitlines()` used by the harness).
65
+ if (parts[parts.length - 1] === '') return parts.length - 1
66
+ return parts.length
67
+ }
68
+
69
+ /** Resolve `root/rel` and reject any path escaping `root` (returns null). */
70
+ export function resolveWithin(root: string, rel: string): string | null {
71
+ const resolved = resolve(root, rel)
72
+ const rootResolved = resolve(root)
73
+ if (resolved === rootResolved) return resolved
74
+ const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep
75
+ if (!resolved.startsWith(prefix)) return null
76
+ return resolved
77
+ }
78
+
79
+ /**
80
+ * Pure check that `line` (if anchored) exists in `text`.
81
+ * Whole-file findings (undefined/0) are always bounds-valid.
82
+ */
83
+ export function verifyLineBounds(
84
+ line: number | null | undefined,
85
+ text: string,
86
+ ): { inBounds: boolean; lineTotal: number } {
87
+ const lineTotal = countLines(text)
88
+ if (line === undefined || line === null || line === WHOLE_FILE_LINE) {
89
+ return { inBounds: true, lineTotal }
90
+ }
91
+ if (line < 1) return { inBounds: false, lineTotal }
92
+ return { inBounds: line <= lineTotal, lineTotal }
93
+ }
94
+
95
+ /** Verify a single finding's location against the real filesystem. */
96
+ export function verifyFinding(
97
+ root: string,
98
+ input: Locatable,
99
+ opts: { readSet?: Set<string> } = {},
100
+ ): FindingEvidence {
101
+ const relFile = input.file ?? ''
102
+ const line = typeof input.line === 'number' ? input.line : null
103
+ const resolved = resolveWithin(root, relFile)
104
+
105
+ if (resolved === null || !existsSync(resolved)) {
106
+ return {
107
+ file: relFile,
108
+ line,
109
+ lineTotal: null,
110
+ resolvedPath: resolved,
111
+ verified: false,
112
+ error: 'file_not_found',
113
+ }
114
+ }
115
+
116
+ let text: string
117
+ try {
118
+ text = readFileSync(resolved, 'utf-8')
119
+ } catch {
120
+ return {
121
+ file: relFile,
122
+ line,
123
+ lineTotal: null,
124
+ resolvedPath: resolved,
125
+ verified: false,
126
+ error: 'file_not_found',
127
+ }
128
+ }
129
+
130
+ const { inBounds, lineTotal } = verifyLineBounds(line, text)
131
+ if (!inBounds) {
132
+ return {
133
+ file: relFile,
134
+ line,
135
+ lineTotal,
136
+ resolvedPath: resolved,
137
+ verified: false,
138
+ error: 'line_out_of_range',
139
+ }
140
+ }
141
+
142
+ const outcome: FindingEvidence = {
143
+ file: relFile,
144
+ line,
145
+ lineTotal,
146
+ resolvedPath: resolved,
147
+ verified: true,
148
+ }
149
+ if (opts.readSet !== undefined) {
150
+ outcome.readVerified = opts.readSet.has(resolved)
151
+ }
152
+ return outcome
153
+ }
154
+
155
+ /** Attest every finding in a list. */
156
+ export function verifyFindings(
157
+ root: string,
158
+ findings: Locatable[],
159
+ opts: { readSet?: Set<string> } = {},
160
+ ): EvidenceAudit {
161
+ const results = findings.map((f) => verifyFinding(root, f, opts))
162
+ return { checked: results.length, results }
163
+ }
164
+
165
+ /** `passed` is true only when no real existence failure exists (read is a hint). */
166
+ export function evidencePassed(audit: EvidenceAudit): boolean {
167
+ return audit.results.every((r) => r.error === undefined)
168
+ }
169
+
170
+ /** Violating (non-grounded) results. */
171
+ export function evidenceViolations(audit: EvidenceAudit): FindingEvidence[] {
172
+ return audit.results.filter((r) => r.error !== undefined)
173
+ }
174
+
175
+ /** Serialize an audit for tool payloads (pure). */
176
+ export function evidenceToPlain(audit: EvidenceAudit): Record<string, unknown> {
177
+ const computable = audit.results.filter((r) => r.readVerified !== undefined)
178
+ const readRatio =
179
+ computable.length === 0
180
+ ? null
181
+ : Number(
182
+ (
183
+ computable.filter((r) => r.readVerified === true).length / computable.length
184
+ ).toFixed(3),
185
+ )
186
+ return {
187
+ checked: audit.checked,
188
+ passed: evidencePassed(audit),
189
+ violations: audit.results
190
+ .filter((r) => r.error !== undefined)
191
+ .map((r) => ({ file: r.file, line: r.line, lineTotal: r.lineTotal, verified: r.verified, error: r.error })),
192
+ readVerifiedRatio: readRatio,
193
+ }
194
+ }
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { ReviewFinding, ReviewReport } from './types.ts'
18
+ import type { EvidenceAudit } from './evidence.ts'
18
19
  import { sortFindings } from './review.ts'
19
20
 
20
21
  /** A single defect found while auditing a review report. */
@@ -278,9 +279,41 @@ export function metaReviewReport(report: ReviewReport): MetaReviewResult {
278
279
  /**
279
280
  * Build the final review report: pair the source report with its meta-review
280
281
  * verdict and a rolled-up summary. Pure and deterministic.
282
+ *
283
+ * `evidence` (an EvidenceAudit produced against the real repo) is the hard
284
+ * code-evidence gate: every finding whose file/line does not resolve to
285
+ * existing code is emitted as a critical EVIDENCE_VIOLATION and flips the
286
+ * verdict to `needs_revision`. The audit itself reads the filesystem; this
287
+ * function only folds the (pure, precomputed) result in.
281
288
  */
282
- export function buildFinalReviewReport(report: ReviewReport): FinalReviewReport {
289
+ export function buildFinalReviewReport(
290
+ report: ReviewReport,
291
+ opts: { evidence?: EvidenceAudit | null } = {},
292
+ ): FinalReviewReport {
283
293
  const meta = metaReviewReport(report)
294
+ const evidence = opts.evidence ?? null
295
+ if (evidence !== null) {
296
+ meta.checksRun += 1
297
+ if (evidence.results.some((r) => r.error !== undefined)) {
298
+ for (const violation of evidence.results) {
299
+ if (violation.error === undefined) continue
300
+ const detail =
301
+ violation.error === 'line_out_of_range'
302
+ ? `${violation.line} is beyond this file's ${violation.lineTotal} lines`
303
+ : `${violation.file} does not exist at all (verifiable read required)`
304
+ meta.issues.push({
305
+ code: 'EVIDENCE_VIOLATION',
306
+ severity: 'critical',
307
+ summary:
308
+ `Finding references non-existent code: ${violation.file}` +
309
+ (violation.line ? `:${violation.line}` : ''),
310
+ detail: detail + '. Review results must anchor to real, read code.',
311
+ })
312
+ }
313
+ meta.passed = false
314
+ meta.verdict = 'revise'
315
+ }
316
+ }
284
317
  const summary = report?.summary ?? {}
285
318
  const verdict: FinalReviewVerdict = meta.passed ? 'approved' : 'needs_revision'
286
319
  return {
package/src/review.ts CHANGED
@@ -351,11 +351,21 @@ export function reviewerTaskPrompt(input: {
351
351
  } else {
352
352
  parts.push('This is round 1 — report every issue you find in this dimension.')
353
353
  }
354
+ parts.push(
355
+ 'EVIDENCE RULE (mandatory): read every file you report on with the ' +
356
+ 'read_file tool BEFORE judging it. NEVER report a location you did not ' +
357
+ 'actually read — speculation about code you never inspected is a ' +
358
+ 'disqualifying failure, and fabricated line numbers are treated as ' +
359
+ 'poisoned evidence. Anchor every finding to real code.',
360
+ )
354
361
  parts.push(
355
362
  `Return a JSON object: {"findings": [...]}.`,
356
363
  `Each finding: dimension (must be "${input.dimension}"), file (relative path), ` +
357
- 'line (optional integer), severity (critical/high/medium/low), summary (one line), ' +
358
- 'failure_scenario (how/when it fails, specific evidence), suggested_fix (the concrete fix), ' +
364
+ 'line (REQUIRED positive integer the exact line you READ for an ' +
365
+ 'anchored, line-targeted issue; use 0 for whole-file/module-level ' +
366
+ 'issues), severity (critical/high/medium/low), summary (one line), ' +
367
+ 'failure_scenario (how/when it fails, backed by the code you actually ' +
368
+ 'read), suggested_fix (the concrete fix), ' +
359
369
  `is_atomic (true if the fix is <= ${input.maxLines} lines within a SINGLE file/function, else false).`,
360
370
  `Write summaries and details in ${input.outputLanguage}.`,
361
371
  )
@@ -122,10 +122,11 @@ return {
122
122
 
123
123
  Key rules for dry-run:
124
124
  - **NEVER call a fixer / never edit files / never create branches or worktree.** Reviewers read only.
125
+ - **Every reviewer MUST actually read each file it reports on (read_file) BEFORE judging it, and anchor every finding to a real location. Fabricated file paths or invented line numbers are poisoned evidence and fail the run.** Subagents never report on code they didn't inspect.
125
126
  - Each round feeds the already-known findings to reviewers so they hunt NEW issues only → that is what drives convergence.
126
127
  - Stop when a round reports 0 new findings (converged) or maxReviewRounds is reached.
127
128
  - The report (with per-round convergence stats + suggested fix priorities) is the deliverable.
128
- - **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The \`finalReport.verdict\` is \`approved\` only when the report passes every check; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
129
+ - **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The meta-review ALSO runs the hard code-evidence gate (default on): every finding's file/line is validated against real files on disk, so any fabricated location surfaces as a critical \`EVIDENCE_VIOLATION\` and flips the verdict to \`needs_revision\`. The \`finalReport.verdict\` is \`approved\` only when the report passes every check AND every finding anchors to real, read code; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
129
130
  - Only a single \`report\` entry may be appended to the decision log; nothing else is written.
130
131
 
131
132
  ### Normal-mode workflow (autonomous closed loop)
@@ -329,11 +330,12 @@ Key rules for normal mode:
329
330
  - Close with \`iterate_status\` metrics and surface the convergence indicators (fixed count, remaining architectural count, abort reason) in the final summary.
330
331
 
331
332
  ### Finding schema (for reviewer agents)
332
- { "dimension": string, "file": string (relative path), "line": number (optional),
333
+ { "dimension": string, "file": string (relative path), "line": number (REQUIRED for line-targeted issues — the exact line you READ; use 0 for whole-file/module-level issues),
333
334
  "severity": "critical" | "high" | "medium" | "low", "summary": string (one line),
334
335
  "failure_scenario": string (how/when it fails), "suggested_fix": string (the concrete fix),
335
336
  "is_atomic": boolean (true if fix ≤ max_lines within a single file/function) }
336
337
  Atomic = is_atomic true (single file, single function, ≤ config.atomic.max_lines lines change). Architectural = everything else.
338
+ Every finding MUST reference a file the reviewer actually read (read_file) and a real location — never speculate about code that was never inspected. Fabricated paths/lines are poisoned evidence and fail the meta-review evidence gate.
337
339
 
338
340
  ### Workflow meta
339
341
  Always pass \`meta: { name: "iterate", description: "Autonomous iterate loop" }\`.
@@ -3,6 +3,7 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
3
3
  import { loadEffectiveConfig, resolveProjectRoot } from '../config-loader.ts'
4
4
  import { buildReviewPlan, buildReviewReport } from '../review.ts'
5
5
  import { buildFinalReviewReport, metaReviewReport } from '../meta-review.ts'
6
+ import { evidenceToPlain, verifyFindings } from '../evidence.ts'
6
7
  import type { KnownIntentional, ReviewFinding, ReviewReport, ReviewRound } from '../types.ts'
7
8
 
8
9
  /** Default round cap when neither the arg nor config provides one. */
@@ -94,6 +95,7 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
94
95
  found: { type: 'boolean' },
95
96
  plan: { type: 'json' },
96
97
  report: { type: 'json' },
98
+ evidence: { type: 'json' },
97
99
  finalReport: { type: 'json' },
98
100
  error: { type: 'string' },
99
101
  },
@@ -165,12 +167,19 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
165
167
  }
166
168
  }
167
169
  const audit = metaReviewReport(source)
168
- const finalReport = buildFinalReviewReport(source)
170
+ // Hard code-evidence gate (default on): every finding's file/line is
171
+ // validated against real files on disk before folding into the final
172
+ // verdict. Disable via config `reviewer.evidence_validation: false`.
173
+ const evidenceEnabled = config.reviewer?.evidence_validation !== false
174
+ const findings: ReviewFinding[] = Array.isArray(source.findings) ? source.findings : []
175
+ const evidence = evidenceEnabled ? verifyFindings(projectRoot, findings) : null
176
+ const finalReport = buildFinalReviewReport(source, { evidence })
169
177
  return {
170
178
  operation: 'meta-review',
171
179
  mode,
172
180
  found: true,
173
181
  report: audit as unknown as JsonValue,
182
+ evidence: evidence ? (evidenceToPlain(evidence) as unknown as JsonValue) : null,
174
183
  finalReport: finalReport as unknown as JsonValue,
175
184
  }
176
185
  }
package/src/types.ts CHANGED
@@ -16,7 +16,7 @@ export interface IterateConfig {
16
16
  command_whitelist: string[]
17
17
  commands: Record<string, string[]>
18
18
  }
19
- reviewer: { output_schema_validation: boolean }
19
+ reviewer: { output_schema_validation: boolean; evidence_validation: boolean }
20
20
  onboarding?: Record<string, unknown>
21
21
  personalization?: Record<string, unknown>
22
22
  }