iterate-plugin 3.8.0 → 3.9.1
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 +10 -7
- package/README.zh-CN.md +4 -3
- package/dist/file-lock.js +29 -15
- package/dist/git-scope.js +25 -10
- package/dist/index.js +9 -4
- package/dist/report-render.js +462 -0
- package/dist/session-record.js +268 -0
- package/dist/skill-prompt.js +2 -1
- package/dist/tools/report.js +239 -0
- package/dist/tools/transcript.js +217 -9
- package/dist/transcript.js +18 -0
- package/lib/client.js +460 -13
- package/package.json +17 -10
- package/src/client/i18n.ts +6 -0
- package/src/client/index.ts +44 -0
- package/src/file-lock.ts +47 -9
- package/src/git-scope.ts +29 -15
- package/src/index.ts +9 -4
- package/src/report-render.ts +589 -0
- package/src/session-record.ts +355 -0
- package/src/skill-prompt.ts +2 -1
- package/src/tools/report.ts +244 -0
- package/src/tools/transcript.ts +226 -9
- package/src/transcript.ts +18 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/report-render.ts — pure renderers for the portable iterate run report.
|
|
3
|
+
*
|
|
4
|
+
* ONE data model, TWO consumers that must never disagree:
|
|
5
|
+
* - the `iterate_report` tool (server side), and
|
|
6
|
+
* - the observatory's "导出报告 / 导出 SARIF" buttons (client side),
|
|
7
|
+
* both call {@link renderRunReportMarkdown} / {@link renderRunReportSarif} on
|
|
8
|
+
* the same normalized {@link RunReportData}.
|
|
9
|
+
*
|
|
10
|
+
* This module is deliberately dependency-free and DOM-free: it is compiled to
|
|
11
|
+
* `dist/` for the server AND inlined into `lib/client.js` by esbuild. It must
|
|
12
|
+
* import NOTHING — importing `lib/parse.js` would drag a file outside `src/`
|
|
13
|
+
* into the server's tsc program and scatter the emit into `dist/src/**`. Every
|
|
14
|
+
* extraction is tolerant: a hand-edited manifest can only ever produce an empty
|
|
15
|
+
* section, never a thrown export.
|
|
16
|
+
*
|
|
17
|
+
* Formats:
|
|
18
|
+
* - **markdown** — a PR-pasteable run report (header, quality gate, findings,
|
|
19
|
+
* fixes, validations, defense events, experience bank).
|
|
20
|
+
* - **sarif** — SARIF 2.1.0 for CI / GitHub Code Scanning (ruleId =
|
|
21
|
+
* review dimension, severity → error | warning | note, file+line
|
|
22
|
+
* locations, suggested fix attached).
|
|
23
|
+
*/
|
|
24
|
+
/** Report formats accepted by the tool and the export buttons. */
|
|
25
|
+
export const REPORT_FORMATS = ['markdown', 'sarif'];
|
|
26
|
+
/** Severity ordering (lowest index = most severe). */
|
|
27
|
+
const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
28
|
+
/** Human labels for the report, keyed by language. */
|
|
29
|
+
const REPORT_LABELS = {
|
|
30
|
+
en: {
|
|
31
|
+
title: 'iterate run report',
|
|
32
|
+
project: 'Project',
|
|
33
|
+
goal: 'Goal',
|
|
34
|
+
mode: 'Mode',
|
|
35
|
+
status: 'Status',
|
|
36
|
+
round: 'Round',
|
|
37
|
+
updated: 'Updated',
|
|
38
|
+
converged: 'converged',
|
|
39
|
+
running: 'running',
|
|
40
|
+
stopped: 'stopped',
|
|
41
|
+
qualityGate: 'Quality gate',
|
|
42
|
+
overall: 'Overall',
|
|
43
|
+
verification: 'Verification',
|
|
44
|
+
dimension: 'Dimension',
|
|
45
|
+
qstatus: 'Status',
|
|
46
|
+
score: 'Score',
|
|
47
|
+
findingsCount: 'Findings',
|
|
48
|
+
fixed: 'Fixed',
|
|
49
|
+
convergence: 'Convergence',
|
|
50
|
+
findings: 'Findings',
|
|
51
|
+
severity: 'Severity',
|
|
52
|
+
location: 'Location',
|
|
53
|
+
summary: 'Summary',
|
|
54
|
+
defensiveType: 'Type',
|
|
55
|
+
none: '_none_',
|
|
56
|
+
fixes: 'Fixes',
|
|
57
|
+
file: 'File',
|
|
58
|
+
delta: 'Δ',
|
|
59
|
+
result: 'Result',
|
|
60
|
+
applied: 'applied',
|
|
61
|
+
failed: 'failed',
|
|
62
|
+
validations: 'Validations',
|
|
63
|
+
command: 'Command',
|
|
64
|
+
exit: 'Exit',
|
|
65
|
+
allowed: 'Allowed',
|
|
66
|
+
yes: 'yes',
|
|
67
|
+
no: 'no',
|
|
68
|
+
defenseEvents: 'Defense events',
|
|
69
|
+
experienceBank: 'Experience bank',
|
|
70
|
+
hits: 'hits',
|
|
71
|
+
entries: 'entries',
|
|
72
|
+
generatedAt: 'Generated at',
|
|
73
|
+
notRecorded: 'not recorded',
|
|
74
|
+
},
|
|
75
|
+
zh: {
|
|
76
|
+
title: 'iterate 运行报告',
|
|
77
|
+
project: '项目',
|
|
78
|
+
goal: '目标',
|
|
79
|
+
mode: '模式',
|
|
80
|
+
status: '状态',
|
|
81
|
+
round: '轮次',
|
|
82
|
+
updated: '更新时间',
|
|
83
|
+
converged: '已收敛',
|
|
84
|
+
running: '运行中',
|
|
85
|
+
stopped: '已结束',
|
|
86
|
+
qualityGate: '质量门禁',
|
|
87
|
+
overall: '总体',
|
|
88
|
+
verification: '验证',
|
|
89
|
+
dimension: '维度',
|
|
90
|
+
qstatus: '状态',
|
|
91
|
+
score: '得分',
|
|
92
|
+
findingsCount: '发现数',
|
|
93
|
+
fixed: '已修复',
|
|
94
|
+
convergence: '收敛率',
|
|
95
|
+
findings: '发现',
|
|
96
|
+
severity: '严重度',
|
|
97
|
+
location: '位置',
|
|
98
|
+
summary: '摘要',
|
|
99
|
+
defensiveType: '类型',
|
|
100
|
+
none: '无',
|
|
101
|
+
fixes: '修复',
|
|
102
|
+
file: '文件',
|
|
103
|
+
delta: '变更',
|
|
104
|
+
result: '结果',
|
|
105
|
+
applied: '成功',
|
|
106
|
+
failed: '失败',
|
|
107
|
+
validations: '验证',
|
|
108
|
+
command: '命令',
|
|
109
|
+
exit: '退出码',
|
|
110
|
+
allowed: '已授权',
|
|
111
|
+
yes: '是',
|
|
112
|
+
no: '否',
|
|
113
|
+
defenseEvents: '防御事件',
|
|
114
|
+
experienceBank: '经验银行',
|
|
115
|
+
hits: '命中',
|
|
116
|
+
entries: '条目',
|
|
117
|
+
generatedAt: '生成时间',
|
|
118
|
+
notRecorded: '未记录',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
function reportLabels(lang) {
|
|
122
|
+
return lang === 'en' ? REPORT_LABELS.en : REPORT_LABELS.zh;
|
|
123
|
+
}
|
|
124
|
+
/** Proxy-safe object read (never lets a hostile getter trap throw). */
|
|
125
|
+
function get(source, key) {
|
|
126
|
+
if (!source || typeof source !== 'object')
|
|
127
|
+
return undefined;
|
|
128
|
+
try {
|
|
129
|
+
return source[key];
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function asString(value) {
|
|
136
|
+
return typeof value === 'string' ? value : '';
|
|
137
|
+
}
|
|
138
|
+
function asNumber(value) {
|
|
139
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
140
|
+
}
|
|
141
|
+
/** Non-negative integer (floors fractions); 0 for anything else. */
|
|
142
|
+
function asInt(value) {
|
|
143
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
144
|
+
}
|
|
145
|
+
function asArray(value) {
|
|
146
|
+
return Array.isArray(value) ? value : [];
|
|
147
|
+
}
|
|
148
|
+
function asObject(value) {
|
|
149
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
150
|
+
? value
|
|
151
|
+
: null;
|
|
152
|
+
}
|
|
153
|
+
/** Escape a value for a Markdown table cell / inline text. */
|
|
154
|
+
function mdCell(value) {
|
|
155
|
+
const s = value === null || value === undefined ? '' : String(value);
|
|
156
|
+
return s.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
|
|
157
|
+
}
|
|
158
|
+
/** Severity of a finding, defaulting to the least-severe bucket. */
|
|
159
|
+
function findingSeverity(f) {
|
|
160
|
+
const s = get(f, 'severity');
|
|
161
|
+
return typeof s === 'string' && SEVERITY_ORDER.includes(s) ? s : 'low';
|
|
162
|
+
}
|
|
163
|
+
/** Repo-relative POSIX uri for SARIF artifact locations. */
|
|
164
|
+
function sarifUri(file) {
|
|
165
|
+
const s = typeof file === 'string' ? file.trim() : '';
|
|
166
|
+
if (!s)
|
|
167
|
+
return '';
|
|
168
|
+
return s.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
169
|
+
}
|
|
170
|
+
/** SARIF result level for an iterate severity. */
|
|
171
|
+
export function sarifLevel(severity) {
|
|
172
|
+
if (severity === 'critical' || severity === 'high')
|
|
173
|
+
return 'error';
|
|
174
|
+
if (severity === 'medium')
|
|
175
|
+
return 'warning';
|
|
176
|
+
return 'note';
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Normalize a manifest (+ the pulled disk artifacts) into ONE shape. Pure;
|
|
180
|
+
* tolerates junk at every field.
|
|
181
|
+
*/
|
|
182
|
+
export function buildRunReportData(manifest, extra) {
|
|
183
|
+
const src = asObject(manifest) ?? {};
|
|
184
|
+
const ex = asObject(extra);
|
|
185
|
+
const qg = asObject(get(ex, 'qualityGate'));
|
|
186
|
+
const bank = asObject(get(ex, 'experienceBank'));
|
|
187
|
+
const defense = asObject(get(ex, 'defenseEvents'));
|
|
188
|
+
const findings = asArray(get(src, 'findings')).map((f) => ({
|
|
189
|
+
dimension: asString(get(f, 'dimension')),
|
|
190
|
+
file: asString(get(f, 'file')),
|
|
191
|
+
line: asInt(get(f, 'line')),
|
|
192
|
+
severity: findingSeverity(f),
|
|
193
|
+
summary: asString(get(f, 'summary')),
|
|
194
|
+
failureScenario: asString(get(f, 'failure_scenario')),
|
|
195
|
+
suggestedFix: asString(get(f, 'suggested_fix')),
|
|
196
|
+
isAtomic: get(f, 'is_atomic') === true,
|
|
197
|
+
acknowledged: get(f, 'acknowledged') === true,
|
|
198
|
+
}));
|
|
199
|
+
findings.sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) -
|
|
200
|
+
SEVERITY_ORDER.indexOf(b.severity));
|
|
201
|
+
const fixes = asArray(get(src, 'fixes')).map((f) => ({
|
|
202
|
+
round: asNumber(get(f, 'round')),
|
|
203
|
+
file: asString(get(f, 'file')),
|
|
204
|
+
summary: asString(get(f, 'summary')),
|
|
205
|
+
linesAdded: asNumber(get(f, 'linesAdded')),
|
|
206
|
+
linesRemoved: asNumber(get(f, 'linesRemoved')),
|
|
207
|
+
success: get(f, 'success') !== false,
|
|
208
|
+
}));
|
|
209
|
+
const validations = asArray(get(src, 'validations')).map((v) => ({
|
|
210
|
+
round: asNumber(get(v, 'round')),
|
|
211
|
+
command: asString(get(v, 'command')),
|
|
212
|
+
exitCode: typeof get(v, 'exitCode') === 'number' ? asInt(get(v, 'exitCode')) : null,
|
|
213
|
+
allowed: get(v, 'allowed') === true,
|
|
214
|
+
rejectReason: asString(get(v, 'rejectReason')),
|
|
215
|
+
}));
|
|
216
|
+
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
217
|
+
for (const f of findings) {
|
|
218
|
+
const key = f.severity;
|
|
219
|
+
if (key in severityCounts)
|
|
220
|
+
severityCounts[key] += 1;
|
|
221
|
+
}
|
|
222
|
+
const mode = get(src, 'mode');
|
|
223
|
+
const taskMode = get(src, 'taskMode');
|
|
224
|
+
const stoppedReason = get(src, 'stoppedReason');
|
|
225
|
+
return {
|
|
226
|
+
project: asString(get(src, 'project')),
|
|
227
|
+
goal: asString(get(src, 'goal')),
|
|
228
|
+
mode: mode === 'normal' || mode === 'dry-run' ? mode : null,
|
|
229
|
+
taskMode: taskMode === 'code' || taskMode === 'iterate' ? taskMode : null,
|
|
230
|
+
round: asNumber(get(src, 'round')),
|
|
231
|
+
maxRounds: asNumber(get(src, 'maxRounds')),
|
|
232
|
+
// An ABSENT `active` means "no run / unknown", not "running": a missing or
|
|
233
|
+
// empty transcript used to render a live, pulsing run at round 0. The
|
|
234
|
+
// transcript builder always writes an explicit boolean, so a strict `true`
|
|
235
|
+
// only ever loses the phantom run, never a real one.
|
|
236
|
+
active: get(src, 'active') === true,
|
|
237
|
+
stoppedReason: typeof stoppedReason === 'string' && stoppedReason ? stoppedReason : null,
|
|
238
|
+
updatedAt: asString(get(src, 'updatedAt')),
|
|
239
|
+
convergence: asArray(get(src, 'convergence')).filter((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0),
|
|
240
|
+
findings,
|
|
241
|
+
severityCounts,
|
|
242
|
+
fixes,
|
|
243
|
+
validations,
|
|
244
|
+
qualityGate: qg,
|
|
245
|
+
experienceBank: bank,
|
|
246
|
+
defenseEvents: defense,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** One-line human status for the report header. */
|
|
250
|
+
function reportStatus(data, labels) {
|
|
251
|
+
if (data.active === true)
|
|
252
|
+
return labels.running;
|
|
253
|
+
if (data.stoppedReason)
|
|
254
|
+
return `${labels.stopped} · ${data.stoppedReason}`;
|
|
255
|
+
return labels.stopped;
|
|
256
|
+
}
|
|
257
|
+
/** Render a PR-pasteable Markdown run report. */
|
|
258
|
+
export function renderRunReportMarkdown(manifest, extra, opts) {
|
|
259
|
+
const data = buildRunReportData(manifest, extra);
|
|
260
|
+
const o = opts ?? {};
|
|
261
|
+
const labels = reportLabels(o.lang === 'en' ? 'en' : 'zh');
|
|
262
|
+
const when = o.now && o.now ? o.now : new Date().toISOString();
|
|
263
|
+
const lines = [];
|
|
264
|
+
lines.push(`# ${labels.title}`);
|
|
265
|
+
lines.push('');
|
|
266
|
+
lines.push(`- **${labels.project}**: \`${mdCell(data.project || labels.notRecorded)}\``);
|
|
267
|
+
if (data.goal)
|
|
268
|
+
lines.push(`- **${labels.goal}**: ${mdCell(data.goal)}`);
|
|
269
|
+
lines.push(`- **${labels.mode}**: ${mdCell(data.mode || labels.notRecorded)}${data.taskMode ? ` · task_mode: ${data.taskMode}` : ''}`);
|
|
270
|
+
const roundText = data.maxRounds > 0 ? `${data.round}/${data.maxRounds}` : String(data.round);
|
|
271
|
+
lines.push(`- **${labels.round}**: ${mdCell(roundText)} · **${labels.status}**: ${mdCell(reportStatus(data, labels))}`);
|
|
272
|
+
lines.push(`- **${labels.updated}**: ${mdCell(data.updatedAt || when)}`);
|
|
273
|
+
lines.push(`- **${labels.generatedAt}**: ${mdCell(when)}`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
// ── Quality gate ──
|
|
276
|
+
const qg = data.qualityGate;
|
|
277
|
+
if (qg) {
|
|
278
|
+
lines.push(`## ${labels.qualityGate}`);
|
|
279
|
+
lines.push('');
|
|
280
|
+
const status = typeof qg.overallStatus === 'string' ? qg.overallStatus.toUpperCase() : 'PENDING';
|
|
281
|
+
lines.push(`- **${labels.overall}**: ${mdCell(status)} (${labels.score} ${asNumber(qg.overallScore)})`);
|
|
282
|
+
lines.push(`- **${labels.verification}**: ${asNumber(qg.passedChecks)}/${asNumber(qg.totalChecks)} (${asNumber(qg.verificationPassRate)}%)`);
|
|
283
|
+
if (typeof qg.failReason === 'string' && qg.failReason) {
|
|
284
|
+
lines.push(`- ${labels.status}: ${mdCell(qg.failReason)}`);
|
|
285
|
+
}
|
|
286
|
+
lines.push('');
|
|
287
|
+
const dims = asArray(qg.dimensions);
|
|
288
|
+
if (dims.length > 0) {
|
|
289
|
+
lines.push(`| ${labels.dimension} | ${labels.qstatus} | ${labels.score} | ${labels.findingsCount} | ${labels.fixed} | ${labels.convergence} |`);
|
|
290
|
+
lines.push('| --- | --- | --- | --- | --- | --- |');
|
|
291
|
+
for (const d of dims) {
|
|
292
|
+
lines.push(`| ${mdCell(get(d, 'dimension'))} | ${mdCell(get(d, 'status'))} | ${mdCell(get(d, 'score'))} | ${mdCell(get(d, 'findingsCount'))} | ${mdCell(get(d, 'fixedCount'))} | ${mdCell(get(d, 'convergenceRate'))}% |`);
|
|
293
|
+
}
|
|
294
|
+
lines.push('');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ── Findings ──
|
|
298
|
+
lines.push(`## ${labels.findings} (${data.findings.length})`);
|
|
299
|
+
lines.push('');
|
|
300
|
+
if (data.findings.length === 0) {
|
|
301
|
+
lines.push(labels.none);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
lines.push(`| ${labels.severity} | ${labels.dimension} | ${labels.location} | ${labels.summary} |`);
|
|
305
|
+
lines.push('| --- | --- | --- | --- |');
|
|
306
|
+
for (const f of data.findings) {
|
|
307
|
+
const loc = f.file ? `${f.file}${f.line > 0 ? `:${f.line}` : ''}` : '';
|
|
308
|
+
lines.push(`| ${mdCell(f.severity)} | ${mdCell(f.dimension)} | ${mdCell(loc)} | ${mdCell(f.summary)} |`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
lines.push('');
|
|
312
|
+
// ── Fixes ──
|
|
313
|
+
lines.push(`## ${labels.fixes} (${data.fixes.length})`);
|
|
314
|
+
lines.push('');
|
|
315
|
+
if (data.fixes.length === 0) {
|
|
316
|
+
lines.push(labels.none);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
lines.push(`| # | ${labels.file} | ${labels.delta} | ${labels.result} | ${labels.summary} |`);
|
|
320
|
+
lines.push('| --- | --- | --- | --- | --- |');
|
|
321
|
+
for (const f of data.fixes) {
|
|
322
|
+
lines.push(`| ${mdCell(f.round)} | ${mdCell(f.file)} | ${mdCell(`+${f.linesAdded}/-${f.linesRemoved}`)} | ${f.success ? labels.applied : labels.failed} | ${mdCell(f.summary)} |`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
lines.push('');
|
|
326
|
+
// ── Validations ──
|
|
327
|
+
if (data.validations.length > 0) {
|
|
328
|
+
lines.push(`## ${labels.validations}`);
|
|
329
|
+
lines.push('');
|
|
330
|
+
lines.push(`| # | ${labels.command} | ${labels.exit} | ${labels.allowed} |`);
|
|
331
|
+
lines.push('| --- | --- | --- | --- |');
|
|
332
|
+
for (const v of data.validations) {
|
|
333
|
+
lines.push(`| ${mdCell(v.round)} | \`${mdCell(v.command)}\` | ${v.exitCode === null ? '—' : mdCell(v.exitCode)} | ${v.allowed ? labels.yes : labels.no} |`);
|
|
334
|
+
}
|
|
335
|
+
lines.push('');
|
|
336
|
+
}
|
|
337
|
+
// ── Defense events ──
|
|
338
|
+
const deEvents = asArray(get(data.defenseEvents, 'events'));
|
|
339
|
+
if (deEvents.length > 0) {
|
|
340
|
+
lines.push(`## ${labels.defenseEvents} (${deEvents.length})`);
|
|
341
|
+
lines.push('');
|
|
342
|
+
lines.push(`| ${labels.severity} | ${labels.defensiveType} | ${labels.round} | ${labels.summary} |`);
|
|
343
|
+
lines.push('| --- | --- | --- | --- |');
|
|
344
|
+
for (const e of deEvents) {
|
|
345
|
+
lines.push(`| ${mdCell(get(e, 'severity'))} | ${mdCell(get(e, 'type'))} | ${mdCell(get(e, 'round'))} | ${mdCell(get(e, 'description') || get(e, 'outcome'))} |`);
|
|
346
|
+
}
|
|
347
|
+
lines.push('');
|
|
348
|
+
}
|
|
349
|
+
// ── Experience bank ──
|
|
350
|
+
const entries = asArray(get(data.experienceBank, 'entries'));
|
|
351
|
+
if (entries.length > 0) {
|
|
352
|
+
const totalHits = asNumber(get(data.experienceBank, 'totalHits'));
|
|
353
|
+
lines.push(`## ${labels.experienceBank} (${entries.length} ${labels.entries}, ${totalHits} ${labels.hits})`);
|
|
354
|
+
lines.push('');
|
|
355
|
+
for (const e of entries) {
|
|
356
|
+
lines.push(`- **[${mdCell(get(e, 'id'))}]** ${mdCell(get(e, 'pattern'))} — ${mdCell(get(e, 'description'))} (${labels.hits}: ${mdCell(get(e, 'hitCount'))})`);
|
|
357
|
+
}
|
|
358
|
+
lines.push('');
|
|
359
|
+
}
|
|
360
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
361
|
+
}
|
|
362
|
+
/** Render a SARIF 2.1.0 document from the same normalized data. */
|
|
363
|
+
export function renderRunReportSarif(manifest, extra, opts) {
|
|
364
|
+
const data = buildRunReportData(manifest, extra);
|
|
365
|
+
const o = opts ?? {};
|
|
366
|
+
const rules = [];
|
|
367
|
+
const seenRules = new Set();
|
|
368
|
+
for (const f of data.findings) {
|
|
369
|
+
const id = f.dimension || 'review';
|
|
370
|
+
if (seenRules.has(id))
|
|
371
|
+
continue;
|
|
372
|
+
seenRules.add(id);
|
|
373
|
+
rules.push({
|
|
374
|
+
id,
|
|
375
|
+
name: id,
|
|
376
|
+
shortDescription: { text: `${id} review dimension` },
|
|
377
|
+
defaultConfiguration: { level: 'warning' },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const results = data.findings.map((f) => {
|
|
381
|
+
const result = {
|
|
382
|
+
ruleId: f.dimension || 'review',
|
|
383
|
+
level: sarifLevel(f.severity),
|
|
384
|
+
message: { text: f.summary || `${f.severity} finding` },
|
|
385
|
+
properties: {
|
|
386
|
+
severity: f.severity,
|
|
387
|
+
atomic: f.isAtomic,
|
|
388
|
+
acknowledged: f.acknowledged,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
const uri = sarifUri(f.file);
|
|
392
|
+
if (uri) {
|
|
393
|
+
result.locations = [
|
|
394
|
+
{
|
|
395
|
+
physicalLocation: {
|
|
396
|
+
artifactLocation: { uri },
|
|
397
|
+
...(f.line > 0 ? { region: { startLine: f.line } } : {}),
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
if (f.suggestedFix)
|
|
403
|
+
result.fixes = [{ description: { text: f.suggestedFix } }];
|
|
404
|
+
return result;
|
|
405
|
+
});
|
|
406
|
+
// `exitCode: null` is the documented "unknown / no exit code" value, not a
|
|
407
|
+
// failure: only a concrete non-zero code makes the run unsuccessful.
|
|
408
|
+
const failedValidations = data.validations.filter((v) => v.exitCode !== null && v.exitCode !== 0).length;
|
|
409
|
+
return {
|
|
410
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
411
|
+
version: '2.1.0',
|
|
412
|
+
runs: [
|
|
413
|
+
{
|
|
414
|
+
tool: {
|
|
415
|
+
driver: {
|
|
416
|
+
name: 'iterate-plugin',
|
|
417
|
+
informationUri: o.informationUri && o.informationUri
|
|
418
|
+
? o.informationUri
|
|
419
|
+
: 'https://github.com/jingzhao-l/iterate-plugin',
|
|
420
|
+
...(o.toolVersion ? { version: o.toolVersion } : {}),
|
|
421
|
+
rules,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
invocations: [
|
|
425
|
+
{
|
|
426
|
+
executionSuccessful: failedValidations === 0 && data.qualityGate?.overallStatus !== 'fail',
|
|
427
|
+
...(data.updatedAt ? { startTimeUtc: data.updatedAt } : {}),
|
|
428
|
+
properties: {
|
|
429
|
+
mode: data.mode,
|
|
430
|
+
taskMode: data.taskMode,
|
|
431
|
+
round: data.round,
|
|
432
|
+
maxRounds: data.maxRounds,
|
|
433
|
+
stoppedReason: data.stoppedReason,
|
|
434
|
+
qualityStatus: data.qualityGate ? (data.qualityGate.overallStatus ?? null) : null,
|
|
435
|
+
convergence: data.convergence,
|
|
436
|
+
findingsBySeverity: data.severityCounts,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
results,
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Render a report in the requested format. Unknown formats fall back to
|
|
447
|
+
* Markdown (the tool's default).
|
|
448
|
+
*/
|
|
449
|
+
export function renderRunReport(format, manifest, extra, opts) {
|
|
450
|
+
if (format === 'sarif')
|
|
451
|
+
return renderRunReportSarif(manifest, extra, opts);
|
|
452
|
+
return renderRunReportMarkdown(manifest, extra, opts);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Deterministic download filename for a report, e.g.
|
|
456
|
+
* `iterate-report-2026-10-18T01-02-03-000Z.md`.
|
|
457
|
+
*/
|
|
458
|
+
export function runReportFilename(format, opts) {
|
|
459
|
+
const ext = format === 'sarif' ? 'sarif' : 'md';
|
|
460
|
+
const stamp = opts?.now && opts.now ? opts.now.replace(/[:.]/g, '-') : new Date().toISOString().replace(/[:.]/g, '-');
|
|
461
|
+
return `iterate-report-${stamp}.${ext}`;
|
|
462
|
+
}
|