iterate-plugin 2.5.0 → 2.7.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 +83 -15
- package/dist/config-loader.js +171 -0
- package/dist/config-write.js +174 -0
- package/dist/index.js +58 -0
- package/dist/meta-review.js +181 -0
- package/dist/paths.js +32 -0
- package/dist/review.js +328 -0
- package/dist/skill-prompt.js +337 -0
- package/dist/tools/checkpoint.js +260 -0
- package/dist/tools/config.js +134 -0
- package/dist/tools/context.js +160 -0
- package/dist/tools/decision-log.js +162 -0
- package/dist/tools/fix.js +553 -0
- package/dist/tools/history.js +138 -0
- package/dist/tools/prune.js +268 -0
- package/dist/tools/review.js +159 -0
- package/dist/tools/triage.js +333 -0
- package/dist/tools/validate.js +164 -0
- package/dist/types.js +1 -0
- package/lib/client.js +84 -4
- package/lib/parse.js +68 -0
- package/package.json +7 -3
- package/src/index.ts +4 -0
- package/src/meta-review.ts +14 -1
- package/src/review.ts +33 -5
- package/src/tools/fix.ts +11 -0
- package/src/tools/history.ts +162 -0
- package/src/tools/prune.ts +313 -0
- package/src/tools/triage.ts +7 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/prune.ts — runtime artifact cleanup for the iterate loop.
|
|
3
|
+
*
|
|
4
|
+
* iterate_prune — inspect or remove stale runtime artifacts (.iterate/).
|
|
5
|
+
* Defaults to dry-run (report-only); set `dryRun: false` to
|
|
6
|
+
* actually delete.
|
|
7
|
+
*
|
|
8
|
+
* Artifacts managed:
|
|
9
|
+
* - Decision-log entries older than `retainDays` (default 30, via since).
|
|
10
|
+
* - Stale checkpoint files (checkpoint.json).
|
|
11
|
+
* - Fix backups left over from old rounds (backups whose fix-id no longer
|
|
12
|
+
* appears in the registry).
|
|
13
|
+
* - Empty fix rounds (rounds with 0 records).
|
|
14
|
+
*
|
|
15
|
+
* Security model:
|
|
16
|
+
* - Only operates under the resolved project `.iterate/` directory.
|
|
17
|
+
* - dryRun=true by default — the caller must explicitly opt into deletion.
|
|
18
|
+
* - Each deletion is logged to the decision log (when not dry-run).
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
23
|
+
import { resolveProjectRoot } from "../config-loader.js";
|
|
24
|
+
import { readDecisionEntries, appendDecisionEntry } from "./decision-log.js";
|
|
25
|
+
import { readRegistry, removeRecord, recomputeRoundCounts } from "./fix.js";
|
|
26
|
+
import { iterateDir, fixesDir, checkpointPath, fixRegistryPath } from "../paths.js";
|
|
27
|
+
/** Default retention for decision-log entries (in days). */
|
|
28
|
+
const DEFAULT_RETAIN_DAYS = 30;
|
|
29
|
+
const MIN_RETAIN_DAYS = 1;
|
|
30
|
+
const MAX_RETAIN_DAYS = 365;
|
|
31
|
+
/** Clamp retainDays to a sane range. */
|
|
32
|
+
export function clampRetainDays(days) {
|
|
33
|
+
if (typeof days !== 'number' || !Number.isInteger(days) || days <= 0) {
|
|
34
|
+
return DEFAULT_RETAIN_DAYS;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(Math.max(days, MIN_RETAIN_DAYS), MAX_RETAIN_DAYS);
|
|
37
|
+
}
|
|
38
|
+
/** Build the cutoff timestamp for a given retainDays. */
|
|
39
|
+
export function cutoffTimestamp(retainDays) {
|
|
40
|
+
const d = new Date();
|
|
41
|
+
d.setDate(d.getDate() - retainDays);
|
|
42
|
+
return d.toISOString();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Inspect the runtime state and report what would be pruned.
|
|
46
|
+
* Pure (no deletions). Returns a structured report.
|
|
47
|
+
*/
|
|
48
|
+
export function inspectPrune(projectRoot, retainDays) {
|
|
49
|
+
const cutoff = cutoffTimestamp(retainDays);
|
|
50
|
+
// 1. Decision-log entries older than retainDays.
|
|
51
|
+
const entries = readDecisionEntries(projectRoot);
|
|
52
|
+
const oldLogEntries = entries.filter((e) => e.timestamp < cutoff).length;
|
|
53
|
+
// 2. Checkpoint presence.
|
|
54
|
+
const hasCheckpoint = existsSync(checkpointPath(projectRoot));
|
|
55
|
+
// 3. Stale fix backups: .bak files whose fix-id prefix is not in the registry.
|
|
56
|
+
const registry = readRegistry(projectRoot);
|
|
57
|
+
const activeIds = new Set();
|
|
58
|
+
for (const r of registry.rounds) {
|
|
59
|
+
for (const rec of r.records) {
|
|
60
|
+
activeIds.add(rec.id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const staleBackups = [];
|
|
64
|
+
const fixDir = fixesDir(projectRoot);
|
|
65
|
+
if (existsSync(fixDir)) {
|
|
66
|
+
for (const entry of readdirSync(fixDir)) {
|
|
67
|
+
if (!entry.endsWith('.bak'))
|
|
68
|
+
continue;
|
|
69
|
+
// Extract the fix-id prefix (up to the first underscore after the id).
|
|
70
|
+
// e.g. "fix-abc123_2026-08-17T00-00-00-000Z.bak" → "fix-abc123"
|
|
71
|
+
const match = entry.match(/^(fix-[a-z0-9]+)_/);
|
|
72
|
+
const id = match?.[1];
|
|
73
|
+
if (id && !activeIds.has(id)) {
|
|
74
|
+
staleBackups.push(entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 4. Empty rounds (rounds with 0 records).
|
|
79
|
+
const emptyRounds = registry.rounds
|
|
80
|
+
.filter((r) => r.records.length === 0)
|
|
81
|
+
.map((r) => r.round);
|
|
82
|
+
return {
|
|
83
|
+
oldLogEntries,
|
|
84
|
+
hasCheckpoint,
|
|
85
|
+
staleBackups,
|
|
86
|
+
emptyRounds,
|
|
87
|
+
totalLogEntries: entries.length,
|
|
88
|
+
registryRounds: registry.rounds.length,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Actually prune the runtime artifacts (only called when dryRun=false).
|
|
93
|
+
* Returns a detailed report of what was deleted.
|
|
94
|
+
*/
|
|
95
|
+
export function executePrune(projectRoot, retainDays, report) {
|
|
96
|
+
const cutoff = cutoffTimestamp(retainDays);
|
|
97
|
+
const result = {
|
|
98
|
+
deletedLogEntries: 0,
|
|
99
|
+
deletedCheckpoint: false,
|
|
100
|
+
deletedBackups: [],
|
|
101
|
+
trimmedEmptyRounds: 0,
|
|
102
|
+
errors: [],
|
|
103
|
+
};
|
|
104
|
+
// 1. Rewrite the decision log, keeping only recent entries.
|
|
105
|
+
try {
|
|
106
|
+
const entries = readDecisionEntries(projectRoot);
|
|
107
|
+
const kept = entries.filter((e) => e.timestamp >= cutoff);
|
|
108
|
+
result.deletedLogEntries = entries.length - kept.length;
|
|
109
|
+
if (result.deletedLogEntries > 0) {
|
|
110
|
+
writeFileSync(join(iterateDir(projectRoot), 'decision-log.jsonl'), kept.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
result.errors.push(`failed to rewrite decision log: ${String(err)}`);
|
|
115
|
+
result.deletedLogEntries = 0;
|
|
116
|
+
}
|
|
117
|
+
// 2. Remove checkpoint.
|
|
118
|
+
if (report.hasCheckpoint) {
|
|
119
|
+
try {
|
|
120
|
+
rmSync(checkpointPath(projectRoot), { force: true });
|
|
121
|
+
result.deletedCheckpoint = true;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
result.errors.push(`failed to remove checkpoint: ${String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 3. Delete stale backups.
|
|
128
|
+
for (const bak of report.staleBackups) {
|
|
129
|
+
try {
|
|
130
|
+
unlinkSync(join(fixesDir(projectRoot), bak));
|
|
131
|
+
result.deletedBackups.push(bak);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
result.errors.push(`failed to delete backup ${bak}: ${String(err)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 4. Trim empty rounds from the registry.
|
|
138
|
+
if (report.emptyRounds.length > 0) {
|
|
139
|
+
try {
|
|
140
|
+
let registry = readRegistry(projectRoot);
|
|
141
|
+
for (const round of report.emptyRounds) {
|
|
142
|
+
for (const rec of [...registry.rounds.find((r) => r.round === round)?.records ?? []]) {
|
|
143
|
+
registry = removeRecord(registry, rec.id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
registry = recomputeRoundCounts(registry);
|
|
147
|
+
writeFileSync(fixRegistryPath(projectRoot), JSON.stringify(registry, null, 2), 'utf-8');
|
|
148
|
+
result.trimmedEmptyRounds = report.emptyRounds.length;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
result.errors.push(`failed to trim empty rounds: ${String(err)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Register the `iterate_prune` tool.
|
|
158
|
+
* Defaults to dry-run: inspects the runtime state and reports what would be
|
|
159
|
+
* cleaned up. Pass `dryRun: false` to actually delete.
|
|
160
|
+
*/
|
|
161
|
+
export function registerPruneTool(ctx) {
|
|
162
|
+
ctx.tools.register(defineTool({
|
|
163
|
+
name: 'iterate_prune',
|
|
164
|
+
description: 'Inspect or clean up old iterate runtime artifacts (.iterate/). ' +
|
|
165
|
+
'Defaults to dry-run (report-only, no deletion). Pass `dryRun: false` to actually prune. ' +
|
|
166
|
+
'Manages: old decision-log entries, stale checkpoints, orphaned fix backups, empty fix rounds. ' +
|
|
167
|
+
'Each deletion is logged to the decision log.',
|
|
168
|
+
parameters: {
|
|
169
|
+
dryRun: {
|
|
170
|
+
type: 'boolean',
|
|
171
|
+
description: 'When true (default), only report what would be pruned without deleting anything.',
|
|
172
|
+
},
|
|
173
|
+
retainDays: {
|
|
174
|
+
type: 'integer',
|
|
175
|
+
description: `Keep entries newer than this many days (default: ${DEFAULT_RETAIN_DAYS}, range: ${MIN_RETAIN_DAYS}-${MAX_RETAIN_DAYS}).`,
|
|
176
|
+
},
|
|
177
|
+
path: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: 'Project root directory (default: current working directory).',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
output: {
|
|
183
|
+
schema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
additionalProperties: false,
|
|
186
|
+
properties: {
|
|
187
|
+
ok: { type: 'boolean', required: true },
|
|
188
|
+
dryRun: { type: 'boolean', required: true },
|
|
189
|
+
retainDays: { type: 'integer' },
|
|
190
|
+
report: { type: 'json' },
|
|
191
|
+
result: { type: 'json' },
|
|
192
|
+
error: { type: 'string' },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
render: (_args, value) => {
|
|
196
|
+
if (!value.ok)
|
|
197
|
+
return [{ type: 'text', text: `prune failed: ${value.error}` }];
|
|
198
|
+
const report = value.report;
|
|
199
|
+
const result = value.result;
|
|
200
|
+
if (value.dryRun) {
|
|
201
|
+
const lines = [
|
|
202
|
+
`[dry-run] prune report (retainDays=${value.retainDays}):`,
|
|
203
|
+
` Decision-log entries to remove: ${report?.oldLogEntries ?? '?'} (of ${report?.totalLogEntries ?? '?'})`,
|
|
204
|
+
` Checkpoint to delete: ${report?.hasCheckpoint ? 'yes' : 'none'}`,
|
|
205
|
+
` Stale backups to delete: ${report?.staleBackups?.length ?? 0}`,
|
|
206
|
+
` Empty rounds to trim: ${report?.emptyRounds?.length ?? 0}`,
|
|
207
|
+
'',
|
|
208
|
+
'Pass dryRun:false to execute the prune.',
|
|
209
|
+
];
|
|
210
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
211
|
+
}
|
|
212
|
+
const lines = [
|
|
213
|
+
`Prune complete (retainDays=${value.retainDays}):`,
|
|
214
|
+
` Deleted ${result?.deletedLogEntries ?? 0} old log entries.`,
|
|
215
|
+
` Checkpoint deleted: ${result?.deletedCheckpoint ? 'yes' : 'no'}`,
|
|
216
|
+
` Deleted ${result?.deletedBackups?.length ?? 0} stale backups.`,
|
|
217
|
+
` Trimmed ${result?.trimmedEmptyRounds ?? 0} empty rounds.`,
|
|
218
|
+
];
|
|
219
|
+
const errs = result?.errors ?? [];
|
|
220
|
+
if (errs.length > 0) {
|
|
221
|
+
lines.push('', ' Warnings:');
|
|
222
|
+
for (const e of errs)
|
|
223
|
+
lines.push(` - ${e}`);
|
|
224
|
+
}
|
|
225
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
async execute(args) {
|
|
229
|
+
const resolved = resolveProjectRoot(args.path);
|
|
230
|
+
if (!resolved.ok)
|
|
231
|
+
return { ok: false, dryRun: true, error: resolved.reason };
|
|
232
|
+
const projectRoot = resolved.root;
|
|
233
|
+
const retainDays = clampRetainDays(args.retainDays);
|
|
234
|
+
const dryRun = args.dryRun !== false;
|
|
235
|
+
const report = inspectPrune(projectRoot, retainDays);
|
|
236
|
+
if (dryRun) {
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
dryRun: true,
|
|
240
|
+
retainDays,
|
|
241
|
+
report: report,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const result = executePrune(projectRoot, retainDays, report);
|
|
245
|
+
// Log the prune to the decision log.
|
|
246
|
+
appendDecisionEntry(projectRoot, {
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
round: 0,
|
|
249
|
+
type: 'decision',
|
|
250
|
+
data: {
|
|
251
|
+
action: 'prune',
|
|
252
|
+
retainDays,
|
|
253
|
+
deletedLogEntries: result.deletedLogEntries,
|
|
254
|
+
deletedCheckpoint: result.deletedCheckpoint,
|
|
255
|
+
deletedBackups: result.deletedBackups.length,
|
|
256
|
+
trimmedEmptyRounds: result.trimmedEmptyRounds,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
dryRun: false,
|
|
262
|
+
retainDays,
|
|
263
|
+
report: report,
|
|
264
|
+
result: result,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { loadEffectiveConfig, resolveProjectRoot } from "../config-loader.js";
|
|
3
|
+
import { buildReviewPlan, buildReviewReport } from "../review.js";
|
|
4
|
+
import { buildFinalReviewReport, metaReviewReport } from "../meta-review.js";
|
|
5
|
+
/** Default round cap when neither the arg nor config provides one. */
|
|
6
|
+
const DEFAULT_MAX_REVIEW_ROUNDS = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Register the `iterate_review` tool.
|
|
9
|
+
*
|
|
10
|
+
* Two operations:
|
|
11
|
+
* - `plan`: deterministic review plan for a mode (normal | dry-run).
|
|
12
|
+
* Returns the goal, scope, per-dimension reviewer prompts,
|
|
13
|
+
* the findings schema, and the max round cap. The orchestrator
|
|
14
|
+
* uses this instead of inventing prompts ad hoc.
|
|
15
|
+
* - `aggregate`: deterministic aggregation of raw per-round findings.
|
|
16
|
+
* Applies known_intentional filtering, cross-round dedupe,
|
|
17
|
+
* severity sort, and convergence stats; returns a ReviewReport.
|
|
18
|
+
* Purely computational — NEVER touches the filesystem.
|
|
19
|
+
*/
|
|
20
|
+
export function registerReviewTool(ctx) {
|
|
21
|
+
ctx.tools.register(defineTool({
|
|
22
|
+
name: 'iterate_review',
|
|
23
|
+
description: 'Deterministic review engine for the iterate workflow. ' +
|
|
24
|
+
'Use `plan` to generate the review plan (dimensions, reviewer prompts, findings schema, round cap) ' +
|
|
25
|
+
'for normal or dry-run mode. Use `aggregate` to merge raw per-round findings into a deduped, ' +
|
|
26
|
+
'severity-sorted report with multi-round convergence statistics, and to audit that report ' +
|
|
27
|
+
'(`meta-review`) producing a final review report. ' +
|
|
28
|
+
'`aggregate`/`meta-review` are purely computational — they never modify any file.',
|
|
29
|
+
parameters: {
|
|
30
|
+
operation: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
required: true,
|
|
33
|
+
description: '"plan" to build the review plan, "aggregate" to merge findings, "meta-review" to audit a report.',
|
|
34
|
+
enum: ['plan', 'aggregate', 'meta-review'],
|
|
35
|
+
},
|
|
36
|
+
mode: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Review mode: "dry-run" (pure review, no fixes) or "normal" (autonomous loop). Default: dry-run.',
|
|
39
|
+
enum: ['dry-run', 'normal'],
|
|
40
|
+
},
|
|
41
|
+
rounds: {
|
|
42
|
+
type: 'json',
|
|
43
|
+
description: 'For `aggregate`: array of per-round findings, e.g. ' +
|
|
44
|
+
'[{"round":1,"findings":[...]},{"round":2,"findings":[...]}]. Each finding: ' +
|
|
45
|
+
'{dimension,file,line?,severity,summary,failure_scenario,suggested_fix,is_atomic}.',
|
|
46
|
+
},
|
|
47
|
+
maxReviewRounds: {
|
|
48
|
+
type: 'integer',
|
|
49
|
+
description: 'Round cap for dry-run convergence. Default: config.max_rounds, else 3.',
|
|
50
|
+
},
|
|
51
|
+
goal: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Optional goal override for `aggregate` (defaults to config goal).',
|
|
54
|
+
},
|
|
55
|
+
knownIntentional: {
|
|
56
|
+
type: 'json',
|
|
57
|
+
description: 'For `aggregate`: known-intentional entries to filter out, e.g. ' +
|
|
58
|
+
'[{"file":"db/queries.py","line":42,"dimension":"security","reason":"..."}]. line=0/omitted = whole file.',
|
|
59
|
+
},
|
|
60
|
+
report: {
|
|
61
|
+
type: 'json',
|
|
62
|
+
description: 'For `meta-review`: the ReviewReport JSON (as returned by `aggregate`) to audit for ' +
|
|
63
|
+
'internal consistency and produce the final review report.',
|
|
64
|
+
},
|
|
65
|
+
path: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Project root directory (default: current working directory).',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
output: {
|
|
71
|
+
schema: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
properties: {
|
|
75
|
+
operation: { type: 'string', required: true },
|
|
76
|
+
mode: { type: 'string' },
|
|
77
|
+
found: { type: 'boolean' },
|
|
78
|
+
plan: { type: 'json' },
|
|
79
|
+
report: { type: 'json' },
|
|
80
|
+
finalReport: { type: 'json' },
|
|
81
|
+
error: { type: 'string' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
render: (_args, value) => [
|
|
85
|
+
{ type: 'text', text: JSON.stringify(value, null, 2) },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
async execute(args) {
|
|
89
|
+
const resolved = resolveProjectRoot(args.path);
|
|
90
|
+
if (!resolved.ok) {
|
|
91
|
+
return { operation: args.operation, error: resolved.reason };
|
|
92
|
+
}
|
|
93
|
+
const projectRoot = resolved.root;
|
|
94
|
+
// Effective config = defaults merged with project overrides. Never
|
|
95
|
+
// null, so `plan`/`aggregate` work even without a config file.
|
|
96
|
+
const { config } = loadEffectiveConfig(projectRoot);
|
|
97
|
+
const mode = args.mode ?? 'dry-run';
|
|
98
|
+
if (args.operation === 'plan') {
|
|
99
|
+
const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS;
|
|
100
|
+
const knownIntentional = config.personalization
|
|
101
|
+
?.known_intentional;
|
|
102
|
+
const plan = buildReviewPlan({ config, mode, maxReviewRounds, knownIntentional });
|
|
103
|
+
return { operation: 'plan', mode, found: true, plan: plan };
|
|
104
|
+
}
|
|
105
|
+
if (args.operation === 'aggregate') {
|
|
106
|
+
const rawRounds = Array.isArray(args.rounds) ? args.rounds : [];
|
|
107
|
+
const rounds = rawRounds
|
|
108
|
+
.map((r) => {
|
|
109
|
+
const rr = r;
|
|
110
|
+
const findings = Array.isArray(rr?.findings) ? rr.findings : [];
|
|
111
|
+
return { round: typeof rr?.round === 'number' ? rr.round : 0, findings };
|
|
112
|
+
})
|
|
113
|
+
.filter((r) => r.round > 0);
|
|
114
|
+
if (rounds.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
operation: 'aggregate',
|
|
117
|
+
mode,
|
|
118
|
+
error: 'rounds must be a non-empty array of {round, findings}.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS;
|
|
122
|
+
const goal = args.goal ?? config.goal ?? '';
|
|
123
|
+
const dimensions = config.dimensions ?? [];
|
|
124
|
+
const report = buildReviewReport({
|
|
125
|
+
mode,
|
|
126
|
+
goal,
|
|
127
|
+
dimensions,
|
|
128
|
+
maxReviewRounds,
|
|
129
|
+
rounds,
|
|
130
|
+
knownIntentional: args.knownIntentional,
|
|
131
|
+
});
|
|
132
|
+
return { operation: 'aggregate', mode, report: report };
|
|
133
|
+
}
|
|
134
|
+
if (args.operation === 'meta-review') {
|
|
135
|
+
const source = args.report;
|
|
136
|
+
if (!source || typeof source !== 'object') {
|
|
137
|
+
return {
|
|
138
|
+
operation: 'meta-review',
|
|
139
|
+
mode,
|
|
140
|
+
error: 'report must be a ReviewReport JSON object (as returned by `aggregate`).',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const audit = metaReviewReport(source);
|
|
144
|
+
const finalReport = buildFinalReviewReport(source);
|
|
145
|
+
return {
|
|
146
|
+
operation: 'meta-review',
|
|
147
|
+
mode,
|
|
148
|
+
found: true,
|
|
149
|
+
report: audit,
|
|
150
|
+
finalReport: finalReport,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
operation: args.operation,
|
|
155
|
+
error: `Unknown operation "${args.operation}". Use "plan", "aggregate", or "meta-review".`,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
}
|