iterate-plugin 3.8.0 → 3.9.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/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- /* iterate-plugin client source-digest: fc3f99c3c9a74c891ce54564ac49e7a336441432dfafe0cb21775ee4afe8f411 */
1
+ /* iterate-plugin client source-digest: 50774433818a03e70b4ac5341a6683c1ccb3d3a5ff93561a29007232a03b3e01 */
2
2
  window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
3
3
  var module = { exports: {} };
4
4
  var exports = module.exports;
@@ -502,18 +502,18 @@ function normalizeTranscript(manifest) {
502
502
  const asStr = (v) => typeof v === "string" ? v : "";
503
503
  const asBool = (v) => v === true;
504
504
  const asCount = (v) => typeof v === "number" ? v : 0;
505
- const asArray = (v) => Array.isArray(v) ? (
505
+ const asArray2 = (v) => Array.isArray(v) ? (
506
506
  /** @type {Array<Record<string, unknown>>} */
507
507
  v
508
508
  ) : [];
509
- const rounds = asArray(safeGet(src, "rounds")).map((r) => ({
509
+ const rounds = asArray2(safeGet(src, "rounds")).map((r) => ({
510
510
  round: asNum(safeGet(r, "round")),
511
- threads: asArray(safeGet(r, "threads")).map((t2) => ({
511
+ threads: asArray2(safeGet(r, "threads")).map((t2) => ({
512
512
  dimension: asStr(safeGet(t2, "dimension")),
513
513
  attempt: asNum(safeGet(t2, "attempt")),
514
- messages: asArray(safeGet(t2, "messages")).map((m) => typeof m === "string" ? m : ""),
515
- readFiles: asArray(safeGet(t2, "readFiles")).map((f) => typeof f === "string" ? f : ""),
516
- findings: asArray(safeGet(t2, "findings")).map((f) => ({ ...f }))
514
+ messages: asArray2(safeGet(t2, "messages")).map((m) => typeof m === "string" ? m : ""),
515
+ readFiles: asArray2(safeGet(t2, "readFiles")).map((f) => typeof f === "string" ? f : ""),
516
+ findings: asArray2(safeGet(t2, "findings")).map((f) => ({ ...f }))
517
517
  }))
518
518
  }));
519
519
  const cp = safeGet(src, "checkpoint");
@@ -538,7 +538,7 @@ function normalizeTranscript(manifest) {
538
538
  // v3.0: harness task_mode (code/iterate), tolerated when absent.
539
539
  taskMode: tm === "code" || tm === "iterate" ? tm : null,
540
540
  goal: asStr(safeGet(src, "goal")),
541
- phases: asArray(safeGet(src, "phases")).map((p) => typeof p === "string" ? p : ""),
541
+ phases: asArray2(safeGet(src, "phases")).map((p) => typeof p === "string" ? p : ""),
542
542
  round: asNum(safeGet(src, "round")),
543
543
  maxRounds: asNum(safeGet(src, "maxRounds")),
544
544
  // v3.5: why a FINISHED run stopped (converged / max_rounds_reached /
@@ -546,17 +546,17 @@ function normalizeTranscript(manifest) {
546
546
  // observatory badge to render the stop reason instead of a bare "已结束".
547
547
  stoppedReason: src.stoppedReason === null || src.stoppedReason === void 0 ? null : asStr(safeGet(src, "stoppedReason")) || null,
548
548
  rounds,
549
- convergence: asArray(safeGet(src, "convergence")).map((n) => asCount(n)),
550
- findings: asArray(safeGet(src, "findings")).map((f) => ({ ...f })),
551
- fixes: asArray(safeGet(src, "fixes")).map((f) => ({ ...f })),
552
- live: asArray(safeGet(src, "live")).map((e) => ({
549
+ convergence: asArray2(safeGet(src, "convergence")).map((n) => asCount(n)),
550
+ findings: asArray2(safeGet(src, "findings")).map((f) => ({ ...f })),
551
+ fixes: asArray2(safeGet(src, "fixes")).map((f) => ({ ...f })),
552
+ live: asArray2(safeGet(src, "live")).map((e) => ({
553
553
  ts: typeof safeGet(e, "ts") === "number" ? String(safeGet(e, "ts")) : asStr(safeGet(e, "ts")),
554
554
  type: asStr(safeGet(e, "type")),
555
555
  tool: asStr(safeGet(e, "tool")),
556
556
  target: asStr(safeGet(e, "target"))
557
557
  })),
558
558
  checkpoint,
559
- timeline: asArray(safeGet(src, "timeline")).map((t2) => ({ ...t2 })),
559
+ timeline: asArray2(safeGet(src, "timeline")).map((t2) => ({ ...t2 })),
560
560
  // #6: per-round validation rows (command / exitCode / allowed), captured
561
561
  // by the workflow and persisted through `iterate_transcript capture`.
562
562
  validations: normalizeValidations(safeGet(src, "validations")),
@@ -1912,6 +1912,404 @@ function latestPhase(phases) {
1912
1912
  return latest;
1913
1913
  }
1914
1914
 
1915
+ // src/report-render.ts
1916
+ var SEVERITY_ORDER2 = ["critical", "high", "medium", "low"];
1917
+ var REPORT_LABELS = {
1918
+ en: {
1919
+ title: "iterate run report",
1920
+ project: "Project",
1921
+ goal: "Goal",
1922
+ mode: "Mode",
1923
+ status: "Status",
1924
+ round: "Round",
1925
+ updated: "Updated",
1926
+ converged: "converged",
1927
+ running: "running",
1928
+ stopped: "stopped",
1929
+ qualityGate: "Quality gate",
1930
+ overall: "Overall",
1931
+ verification: "Verification",
1932
+ dimension: "Dimension",
1933
+ qstatus: "Status",
1934
+ score: "Score",
1935
+ findingsCount: "Findings",
1936
+ fixed: "Fixed",
1937
+ convergence: "Convergence",
1938
+ findings: "Findings",
1939
+ severity: "Severity",
1940
+ location: "Location",
1941
+ summary: "Summary",
1942
+ defensiveType: "Type",
1943
+ none: "_none_",
1944
+ fixes: "Fixes",
1945
+ file: "File",
1946
+ delta: "\u0394",
1947
+ result: "Result",
1948
+ applied: "applied",
1949
+ failed: "failed",
1950
+ validations: "Validations",
1951
+ command: "Command",
1952
+ exit: "Exit",
1953
+ allowed: "Allowed",
1954
+ yes: "yes",
1955
+ no: "no",
1956
+ defenseEvents: "Defense events",
1957
+ experienceBank: "Experience bank",
1958
+ hits: "hits",
1959
+ entries: "entries",
1960
+ generatedAt: "Generated at",
1961
+ notRecorded: "not recorded"
1962
+ },
1963
+ zh: {
1964
+ title: "iterate \u8FD0\u884C\u62A5\u544A",
1965
+ project: "\u9879\u76EE",
1966
+ goal: "\u76EE\u6807",
1967
+ mode: "\u6A21\u5F0F",
1968
+ status: "\u72B6\u6001",
1969
+ round: "\u8F6E\u6B21",
1970
+ updated: "\u66F4\u65B0\u65F6\u95F4",
1971
+ converged: "\u5DF2\u6536\u655B",
1972
+ running: "\u8FD0\u884C\u4E2D",
1973
+ stopped: "\u5DF2\u7ED3\u675F",
1974
+ qualityGate: "\u8D28\u91CF\u95E8\u7981",
1975
+ overall: "\u603B\u4F53",
1976
+ verification: "\u9A8C\u8BC1",
1977
+ dimension: "\u7EF4\u5EA6",
1978
+ qstatus: "\u72B6\u6001",
1979
+ score: "\u5F97\u5206",
1980
+ findingsCount: "\u53D1\u73B0\u6570",
1981
+ fixed: "\u5DF2\u4FEE\u590D",
1982
+ convergence: "\u6536\u655B\u7387",
1983
+ findings: "\u53D1\u73B0",
1984
+ severity: "\u4E25\u91CD\u5EA6",
1985
+ location: "\u4F4D\u7F6E",
1986
+ summary: "\u6458\u8981",
1987
+ defensiveType: "\u7C7B\u578B",
1988
+ none: "\u65E0",
1989
+ fixes: "\u4FEE\u590D",
1990
+ file: "\u6587\u4EF6",
1991
+ delta: "\u53D8\u66F4",
1992
+ result: "\u7ED3\u679C",
1993
+ applied: "\u6210\u529F",
1994
+ failed: "\u5931\u8D25",
1995
+ validations: "\u9A8C\u8BC1",
1996
+ command: "\u547D\u4EE4",
1997
+ exit: "\u9000\u51FA\u7801",
1998
+ allowed: "\u5DF2\u6388\u6743",
1999
+ yes: "\u662F",
2000
+ no: "\u5426",
2001
+ defenseEvents: "\u9632\u5FA1\u4E8B\u4EF6",
2002
+ experienceBank: "\u7ECF\u9A8C\u94F6\u884C",
2003
+ hits: "\u547D\u4E2D",
2004
+ entries: "\u6761\u76EE",
2005
+ generatedAt: "\u751F\u6210\u65F6\u95F4",
2006
+ notRecorded: "\u672A\u8BB0\u5F55"
2007
+ }
2008
+ };
2009
+ function reportLabels(lang) {
2010
+ return lang === "en" ? REPORT_LABELS.en : REPORT_LABELS.zh;
2011
+ }
2012
+ function get(source, key) {
2013
+ if (!source || typeof source !== "object") return void 0;
2014
+ try {
2015
+ return source[key];
2016
+ } catch {
2017
+ return void 0;
2018
+ }
2019
+ }
2020
+ function asString(value) {
2021
+ return typeof value === "string" ? value : "";
2022
+ }
2023
+ function asNumber(value) {
2024
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2025
+ }
2026
+ function asArray(value) {
2027
+ return Array.isArray(value) ? value : [];
2028
+ }
2029
+ function asObject(value) {
2030
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2031
+ }
2032
+ function mdCell(value) {
2033
+ const s = value === null || value === void 0 ? "" : String(value);
2034
+ return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
2035
+ }
2036
+ function findingSeverity(f) {
2037
+ const s = get(f, "severity");
2038
+ return typeof s === "string" && SEVERITY_ORDER2.includes(s) ? s : "low";
2039
+ }
2040
+ function sarifUri(file) {
2041
+ const s = typeof file === "string" ? file.trim() : "";
2042
+ if (!s) return "";
2043
+ return s.replace(/\\/g, "/").replace(/^\.\//, "");
2044
+ }
2045
+ function sarifLevel(severity) {
2046
+ if (severity === "critical" || severity === "high") return "error";
2047
+ if (severity === "medium") return "warning";
2048
+ return "note";
2049
+ }
2050
+ function buildRunReportData(manifest, extra) {
2051
+ const src = asObject(manifest) ?? {};
2052
+ const ex = asObject(extra);
2053
+ const qg = asObject(get(ex, "qualityGate"));
2054
+ const bank = asObject(get(ex, "experienceBank"));
2055
+ const defense = asObject(get(ex, "defenseEvents"));
2056
+ const findings = asArray(get(src, "findings")).map((f) => ({
2057
+ dimension: asString(get(f, "dimension")),
2058
+ file: asString(get(f, "file")),
2059
+ line: asNumber(get(f, "line")),
2060
+ severity: findingSeverity(f),
2061
+ summary: asString(get(f, "summary")),
2062
+ failureScenario: asString(get(f, "failure_scenario")),
2063
+ suggestedFix: asString(get(f, "suggested_fix")),
2064
+ isAtomic: get(f, "is_atomic") === true,
2065
+ acknowledged: get(f, "acknowledged") === true
2066
+ }));
2067
+ findings.sort(
2068
+ (a, b) => SEVERITY_ORDER2.indexOf(a.severity) - SEVERITY_ORDER2.indexOf(b.severity)
2069
+ );
2070
+ const fixes = asArray(get(src, "fixes")).map((f) => ({
2071
+ round: asNumber(get(f, "round")),
2072
+ file: asString(get(f, "file")),
2073
+ summary: asString(get(f, "summary")),
2074
+ linesAdded: asNumber(get(f, "linesAdded")),
2075
+ linesRemoved: asNumber(get(f, "linesRemoved")),
2076
+ success: get(f, "success") !== false
2077
+ }));
2078
+ const validations = asArray(get(src, "validations")).map((v) => ({
2079
+ round: asNumber(get(v, "round")),
2080
+ command: asString(get(v, "command")),
2081
+ exitCode: typeof get(v, "exitCode") === "number" ? asNumber(get(v, "exitCode")) : null,
2082
+ allowed: get(v, "allowed") === true,
2083
+ rejectReason: asString(get(v, "rejectReason"))
2084
+ }));
2085
+ const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
2086
+ for (const f of findings) {
2087
+ const key = f.severity;
2088
+ if (key in severityCounts) severityCounts[key] += 1;
2089
+ }
2090
+ const mode = get(src, "mode");
2091
+ const taskMode = get(src, "taskMode");
2092
+ const stoppedReason = get(src, "stoppedReason");
2093
+ return {
2094
+ project: asString(get(src, "project")),
2095
+ goal: asString(get(src, "goal")),
2096
+ mode: mode === "normal" || mode === "dry-run" ? mode : null,
2097
+ taskMode: taskMode === "code" || taskMode === "iterate" ? taskMode : null,
2098
+ round: asNumber(get(src, "round")),
2099
+ maxRounds: asNumber(get(src, "maxRounds")),
2100
+ active: get(src, "active") !== false,
2101
+ stoppedReason: typeof stoppedReason === "string" && stoppedReason ? stoppedReason : null,
2102
+ updatedAt: asString(get(src, "updatedAt")),
2103
+ convergence: asArray(get(src, "convergence")).filter(
2104
+ (n) => typeof n === "number" && Number.isFinite(n) && n >= 0
2105
+ ),
2106
+ findings,
2107
+ severityCounts,
2108
+ fixes,
2109
+ validations,
2110
+ qualityGate: qg,
2111
+ experienceBank: bank,
2112
+ defenseEvents: defense
2113
+ };
2114
+ }
2115
+ function reportStatus(data, labels) {
2116
+ if (data.active === true) return labels.running;
2117
+ if (data.stoppedReason) return `${labels.stopped} \xB7 ${data.stoppedReason}`;
2118
+ return labels.stopped;
2119
+ }
2120
+ function renderRunReportMarkdown(manifest, extra, opts) {
2121
+ const data = buildRunReportData(manifest, extra);
2122
+ const o = opts ?? {};
2123
+ const labels = reportLabels(o.lang === "en" ? "en" : "zh");
2124
+ const when = o.now && o.now ? o.now : (/* @__PURE__ */ new Date()).toISOString();
2125
+ const lines = [];
2126
+ lines.push(`# ${labels.title}`);
2127
+ lines.push("");
2128
+ lines.push(`- **${labels.project}**: \`${mdCell(data.project || labels.notRecorded)}\``);
2129
+ if (data.goal) lines.push(`- **${labels.goal}**: ${mdCell(data.goal)}`);
2130
+ lines.push(
2131
+ `- **${labels.mode}**: ${mdCell(data.mode || labels.notRecorded)}${data.taskMode ? ` \xB7 task_mode: ${data.taskMode}` : ""}`
2132
+ );
2133
+ const roundText = data.maxRounds > 0 ? `${data.round}/${data.maxRounds}` : String(data.round);
2134
+ lines.push(`- **${labels.round}**: ${mdCell(roundText)} \xB7 **${labels.status}**: ${mdCell(reportStatus(data, labels))}`);
2135
+ lines.push(`- **${labels.updated}**: ${mdCell(data.updatedAt || when)}`);
2136
+ lines.push(`- **${labels.generatedAt}**: ${mdCell(when)}`);
2137
+ lines.push("");
2138
+ const qg = data.qualityGate;
2139
+ if (qg) {
2140
+ lines.push(`## ${labels.qualityGate}`);
2141
+ lines.push("");
2142
+ const status = typeof qg.overallStatus === "string" ? qg.overallStatus.toUpperCase() : "PENDING";
2143
+ lines.push(`- **${labels.overall}**: ${mdCell(status)} (${labels.score} ${asNumber(qg.overallScore)})`);
2144
+ lines.push(
2145
+ `- **${labels.verification}**: ${asNumber(qg.passedChecks)}/${asNumber(qg.totalChecks)} (${asNumber(qg.verificationPassRate)}%)`
2146
+ );
2147
+ if (typeof qg.failReason === "string" && qg.failReason) {
2148
+ lines.push(`- ${labels.status}: ${mdCell(qg.failReason)}`);
2149
+ }
2150
+ lines.push("");
2151
+ const dims = asArray(qg.dimensions);
2152
+ if (dims.length > 0) {
2153
+ lines.push(
2154
+ `| ${labels.dimension} | ${labels.qstatus} | ${labels.score} | ${labels.findingsCount} | ${labels.fixed} | ${labels.convergence} |`
2155
+ );
2156
+ lines.push("| --- | --- | --- | --- | --- | --- |");
2157
+ for (const d of dims) {
2158
+ lines.push(
2159
+ `| ${mdCell(get(d, "dimension"))} | ${mdCell(get(d, "status"))} | ${mdCell(get(d, "score"))} | ${mdCell(get(d, "findingsCount"))} | ${mdCell(get(d, "fixedCount"))} | ${mdCell(get(d, "convergenceRate"))}% |`
2160
+ );
2161
+ }
2162
+ lines.push("");
2163
+ }
2164
+ }
2165
+ lines.push(`## ${labels.findings} (${data.findings.length})`);
2166
+ lines.push("");
2167
+ if (data.findings.length === 0) {
2168
+ lines.push(labels.none);
2169
+ } else {
2170
+ lines.push(`| ${labels.severity} | ${labels.dimension} | ${labels.location} | ${labels.summary} |`);
2171
+ lines.push("| --- | --- | --- | --- |");
2172
+ for (const f of data.findings) {
2173
+ const loc = f.file ? `${f.file}${f.line > 1 ? `:${f.line}` : ""}` : "";
2174
+ lines.push(`| ${mdCell(f.severity)} | ${mdCell(f.dimension)} | ${mdCell(loc)} | ${mdCell(f.summary)} |`);
2175
+ }
2176
+ }
2177
+ lines.push("");
2178
+ lines.push(`## ${labels.fixes} (${data.fixes.length})`);
2179
+ lines.push("");
2180
+ if (data.fixes.length === 0) {
2181
+ lines.push(labels.none);
2182
+ } else {
2183
+ lines.push(`| # | ${labels.file} | ${labels.delta} | ${labels.result} | ${labels.summary} |`);
2184
+ lines.push("| --- | --- | --- | --- | --- |");
2185
+ for (const f of data.fixes) {
2186
+ lines.push(
2187
+ `| ${mdCell(f.round)} | ${mdCell(f.file)} | ${mdCell(`+${f.linesAdded}/-${f.linesRemoved}`)} | ${f.success ? labels.applied : labels.failed} | ${mdCell(f.summary)} |`
2188
+ );
2189
+ }
2190
+ }
2191
+ lines.push("");
2192
+ if (data.validations.length > 0) {
2193
+ lines.push(`## ${labels.validations}`);
2194
+ lines.push("");
2195
+ lines.push(`| # | ${labels.command} | ${labels.exit} | ${labels.allowed} |`);
2196
+ lines.push("| --- | --- | --- | --- |");
2197
+ for (const v of data.validations) {
2198
+ lines.push(
2199
+ `| ${mdCell(v.round)} | \`${mdCell(v.command)}\` | ${v.exitCode === null ? "\u2014" : mdCell(v.exitCode)} | ${v.allowed ? labels.yes : labels.no} |`
2200
+ );
2201
+ }
2202
+ lines.push("");
2203
+ }
2204
+ const deEvents = asArray(get(data.defenseEvents, "events"));
2205
+ if (deEvents.length > 0) {
2206
+ lines.push(`## ${labels.defenseEvents} (${deEvents.length})`);
2207
+ lines.push("");
2208
+ lines.push(`| ${labels.severity} | ${labels.defensiveType} | ${labels.round} | ${labels.summary} |`);
2209
+ lines.push("| --- | --- | --- | --- |");
2210
+ for (const e of deEvents) {
2211
+ lines.push(
2212
+ `| ${mdCell(get(e, "severity"))} | ${mdCell(get(e, "type"))} | ${mdCell(get(e, "round"))} | ${mdCell(get(e, "description") || get(e, "outcome"))} |`
2213
+ );
2214
+ }
2215
+ lines.push("");
2216
+ }
2217
+ const entries = asArray(get(data.experienceBank, "entries"));
2218
+ if (entries.length > 0) {
2219
+ const totalHits = asNumber(get(data.experienceBank, "totalHits"));
2220
+ lines.push(`## ${labels.experienceBank} (${entries.length} ${labels.entries}, ${totalHits} ${labels.hits})`);
2221
+ lines.push("");
2222
+ for (const e of entries) {
2223
+ lines.push(
2224
+ `- **[${mdCell(get(e, "id"))}]** ${mdCell(get(e, "pattern"))} \u2014 ${mdCell(get(e, "description"))} (${labels.hits}: ${mdCell(get(e, "hitCount"))})`
2225
+ );
2226
+ }
2227
+ lines.push("");
2228
+ }
2229
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
2230
+ }
2231
+ function renderRunReportSarif(manifest, extra, opts) {
2232
+ const data = buildRunReportData(manifest, extra);
2233
+ const o = opts ?? {};
2234
+ const rules = [];
2235
+ const seenRules = /* @__PURE__ */ new Set();
2236
+ for (const f of data.findings) {
2237
+ const id = f.dimension || "review";
2238
+ if (seenRules.has(id)) continue;
2239
+ seenRules.add(id);
2240
+ rules.push({
2241
+ id,
2242
+ name: id,
2243
+ shortDescription: { text: `${id} review dimension` },
2244
+ defaultConfiguration: { level: "warning" }
2245
+ });
2246
+ }
2247
+ const results = data.findings.map((f) => {
2248
+ const result = {
2249
+ ruleId: f.dimension || "review",
2250
+ level: sarifLevel(f.severity),
2251
+ message: { text: f.summary || `${f.severity} finding` },
2252
+ properties: {
2253
+ severity: f.severity,
2254
+ atomic: f.isAtomic,
2255
+ acknowledged: f.acknowledged
2256
+ }
2257
+ };
2258
+ const uri = sarifUri(f.file);
2259
+ if (uri) {
2260
+ result.locations = [
2261
+ {
2262
+ physicalLocation: {
2263
+ artifactLocation: { uri },
2264
+ ...f.line > 0 ? { region: { startLine: f.line } } : {}
2265
+ }
2266
+ }
2267
+ ];
2268
+ }
2269
+ if (f.suggestedFix) result.fixes = [{ description: { text: f.suggestedFix } }];
2270
+ return result;
2271
+ });
2272
+ const failedValidations = data.validations.filter((v) => v.exitCode !== 0).length;
2273
+ return {
2274
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
2275
+ version: "2.1.0",
2276
+ runs: [
2277
+ {
2278
+ tool: {
2279
+ driver: {
2280
+ name: "iterate-plugin",
2281
+ informationUri: o.informationUri && o.informationUri ? o.informationUri : "https://github.com/jingzhao-l/iterate-plugin",
2282
+ ...o.toolVersion ? { version: o.toolVersion } : {},
2283
+ rules
2284
+ }
2285
+ },
2286
+ invocations: [
2287
+ {
2288
+ executionSuccessful: failedValidations === 0 && data.qualityGate?.overallStatus !== "fail",
2289
+ ...data.updatedAt ? { startTimeUtc: data.updatedAt } : {},
2290
+ properties: {
2291
+ mode: data.mode,
2292
+ taskMode: data.taskMode,
2293
+ round: data.round,
2294
+ maxRounds: data.maxRounds,
2295
+ stoppedReason: data.stoppedReason,
2296
+ qualityStatus: data.qualityGate ? data.qualityGate.overallStatus ?? null : null,
2297
+ convergence: data.convergence,
2298
+ findingsBySeverity: data.severityCounts
2299
+ }
2300
+ }
2301
+ ],
2302
+ results
2303
+ }
2304
+ ]
2305
+ };
2306
+ }
2307
+ function runReportFilename(format, opts) {
2308
+ const ext = format === "sarif" ? "sarif" : "md";
2309
+ const stamp = opts?.now && opts.now ? opts.now.replace(/[:.]/g, "-") : (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2310
+ return `iterate-report-${stamp}.${ext}`;
2311
+ }
2312
+
1915
2313
  // src/client/i18n.ts
1916
2314
  var LANG_STORAGE_KEY = "iterate-ui-lang";
1917
2315
  var LANG_NAMES = {
@@ -2075,6 +2473,10 @@ var EN = {
2075
2473
  "\u5C06\u89C2\u6D4B\u53F0\u5168\u90E8\u6570\u636E\u5BFC\u51FA\u4E3A JSON\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09": "Export every observatory panel as JSON (downloads first, falls back to copying)",
2076
2474
  \u5DF2\u5BFC\u51FA: "Exported",
2077
2475
  "\u5BFC\u51FA JSON": "Export JSON",
2476
+ "\u5BFC\u51FA\u62A5\u544A": "Export report",
2477
+ "\u5BFC\u51FA\u53EF\u7C98\u8D34\u8FDB PR \u7684 Markdown \u8FD0\u884C\u62A5\u544A\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09": "Export a PR-pasteable Markdown run report (download first, copy on failure)",
2478
+ "\u5BFC\u51FA SARIF": "Export SARIF",
2479
+ "\u5BFC\u51FA SARIF 2.1.0\uFF08\u4F9B CI / GitHub Code Scanning \u6D88\u8D39\uFF09": "Export SARIF 2.1.0 for CI / GitHub Code Scanning",
2078
2480
  "\u89C2\u6D4B\u53F0\u9875\u7B7E\uFF08\u5FEB\u6377\u952E 1-9 / 0 / -\uFF09": "Observatory tabs (keys 1-9 / 0 / -)",
2079
2481
  "{a0}\uFF08\u6309 {a1} \u5207\u6362\uFF09": "{a0} (press {a1})",
2080
2482
  "{a0}\uFF08\u6309 - \u5207\u6362\uFF09": "{a0} (press -)",
@@ -4083,6 +4485,29 @@ function ObservatoryPanel(props) {
4083
4485
  flash();
4084
4486
  });
4085
4487
  };
4488
+ const exportReport = (format) => {
4489
+ if (!manifest) return;
4490
+ const extra = { qualityGate, experienceBank, defenseEvents };
4491
+ const content = format === "sarif" ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2) : renderRunReportMarkdown(manifest, extra, { lang: getLang() });
4492
+ const filename = runReportFilename(format);
4493
+ const key = `report-${format}`;
4494
+ const flash = () => {
4495
+ setCopiedKey(key);
4496
+ if (copyTimer.current) clearTimeout(copyTimer.current);
4497
+ copyTimer.current = setTimeout(() => setCopiedKey((cur) => cur === key ? null : cur), 1600);
4498
+ };
4499
+ if (downloadTextFile(filename, content)) {
4500
+ flash();
4501
+ return;
4502
+ }
4503
+ copyText(content).then((ok) => {
4504
+ if (!ok) {
4505
+ setCopyFailText(content);
4506
+ return;
4507
+ }
4508
+ flash();
4509
+ });
4510
+ };
4086
4511
  const setAllThreads = (mode) => {
4087
4512
  setThreadMode(mode);
4088
4513
  setExpandedThreads(/* @__PURE__ */ new Set());
@@ -5278,6 +5703,21 @@ function ObservatoryPanel(props) {
5278
5703
  onClick: exportObservatory,
5279
5704
  title: t("\u5C06\u89C2\u6D4B\u53F0\u5168\u90E8\u6570\u636E\u5BFC\u51FA\u4E3A JSON\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09")
5280
5705
  }, copiedKey === "export" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA JSON")),
5706
+ // v3.9: portable report export — Markdown for a PR body, SARIF for
5707
+ // CI / GitHub Code Scanning. Rendered from the same data model as
5708
+ // the `iterate_report` tool.
5709
+ React.createElement("button", {
5710
+ className: "iterate-btn",
5711
+ "data-copied": copiedKey === "report-markdown" ? "" : void 0,
5712
+ onClick: () => exportReport("markdown"),
5713
+ title: t("\u5BFC\u51FA\u53EF\u7C98\u8D34\u8FDB PR \u7684 Markdown \u8FD0\u884C\u62A5\u544A\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09")
5714
+ }, copiedKey === "report-markdown" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA\u62A5\u544A")),
5715
+ React.createElement("button", {
5716
+ className: "iterate-btn",
5717
+ "data-copied": copiedKey === "report-sarif" ? "" : void 0,
5718
+ onClick: () => exportReport("sarif"),
5719
+ title: t("\u5BFC\u51FA SARIF 2.1.0\uFF08\u4F9B CI / GitHub Code Scanning \u6D88\u8D39\uFF09")
5720
+ }, copiedKey === "report-sarif" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA SARIF")),
5281
5721
  // Gap #4: `.iterate/` artifacts are unreachable from the browser;
5282
5722
  // this copies the one instruction that makes the model echo them
5283
5723
  // into the session stream (which is what every panel scans).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,10 @@
48
48
  "0.2.1-alpha.1": "compatible",
49
49
  "0.2.1-alpha.2": "compatible",
50
50
  "0.2.1-alpha.3": "compatible",
51
- "0.2.1-alpha.4": "compatible"
51
+ "0.2.1-alpha.4": "compatible",
52
+ "0.2.1-alpha.5": "compatible",
53
+ "0.2.1-rc.1": "compatible",
54
+ "0.2.1-rc.2": "compatible"
52
55
  }
53
56
  },
54
57
  "client": {
@@ -89,13 +92,17 @@
89
92
  },
90
93
  "peerDependencies": {
91
94
  "@deepseek-ai/cordis": "^4.0.5-alpha.2",
92
- "@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
93
- "@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
95
+ "@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
96
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
97
+ "@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2"
94
98
  },
95
99
  "peerDependenciesMeta": {
96
100
  "@deepseek-ai/cordis": {
97
101
  "optional": true
98
102
  },
103
+ "@deepseek-ai/dsh-session": {
104
+ "optional": true
105
+ },
99
106
  "@deepseek-ai/dsh-tools": {
100
107
  "optional": true
101
108
  },
@@ -104,12 +111,12 @@
104
111
  }
105
112
  },
106
113
  "devDependencies": {
107
- "@deepseek-ai/cordis": "4.0.5-alpha.2",
108
- "@deepseek-ai/dsh-agent": "0.2.1-alpha.4",
109
- "@deepseek-ai/dsh-jobs": "0.2.1-alpha.4",
110
- "@deepseek-ai/dsh-session": "0.2.1-alpha.4",
111
- "@deepseek-ai/dsh-tools": "0.2.1-alpha.4",
112
- "@deepseek-ai/dsh-util-values": "0.2.1-alpha.4",
114
+ "@deepseek-ai/cordis": "4.0.5",
115
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
116
+ "@deepseek-ai/dsh-jobs": "0.2.1-rc.2",
117
+ "@deepseek-ai/dsh-session": "0.2.1-rc.2",
118
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
119
+ "@deepseek-ai/dsh-util-values": "0.2.1-rc.2",
113
120
  "@types/js-yaml": "4.0.9",
114
121
  "@types/node": "22.15.0",
115
122
  "@types/react": "19.2.2",
@@ -253,6 +253,12 @@ export const EN: Record<string, string> = {
253
253
  'Export every observatory panel as JSON (downloads first, falls back to copying)',
254
254
  已导出: 'Exported',
255
255
  '导出 JSON': 'Export JSON',
256
+ '导出报告': 'Export report',
257
+ '导出可粘贴进 PR 的 Markdown 运行报告(优先下载,失败则复制)':
258
+ 'Export a PR-pasteable Markdown run report (download first, copy on failure)',
259
+ '导出 SARIF': 'Export SARIF',
260
+ '导出 SARIF 2.1.0(供 CI / GitHub Code Scanning 消费)':
261
+ 'Export SARIF 2.1.0 for CI / GitHub Code Scanning',
256
262
  '观测台页签(快捷键 1-9 / 0 / -)': 'Observatory tabs (keys 1-9 / 0 / -)',
257
263
  '{a0}(按 {a1} 切换)': '{a0} (press {a1})',
258
264
  '{a0}(按 - 切换)': '{a0} (press -)',
@@ -108,6 +108,13 @@ import {
108
108
  configValueByPath, // #8b: current value of the selected dotted key
109
109
  DISK_SNAPSHOT_SOURCES, // #4/#3: read-side tools listed by the pull instruction
110
110
  } from '../../lib/parse.js'
111
+ // v3.9: report export — shared with the `iterate_report` tool so the button
112
+ // and the tool can never produce different reports.
113
+ import {
114
+ renderRunReportMarkdown, // Markdown run report (PR-pasteable)
115
+ renderRunReportSarif, // SARIF 2.1.0 log (CI / Code Scanning)
116
+ runReportFilename, // deterministic download filename
117
+ } from '../report-render.ts'
111
118
  import {
112
119
  configureLangStorage,
113
120
  detectLang,
@@ -2408,6 +2415,30 @@ function ObservatoryPanel(props: SlotProps) {
2408
2415
  })
2409
2416
  }
2410
2417
 
2418
+ /** v3.9: export the run as a portable report — Markdown (PR-pasteable) or
2419
+ * SARIF 2.1.0 (CI / GitHub Code Scanning). Rendered from the SAME shared
2420
+ * data model as the `iterate_report` tool, so the button and the tool can
2421
+ * never disagree. Download-first with a copy fallback, like `exportObservatory`. */
2422
+ const exportReport = (format: 'markdown' | 'sarif') => {
2423
+ if (!manifest) return
2424
+ const extra = { qualityGate, experienceBank, defenseEvents }
2425
+ const content = format === 'sarif'
2426
+ ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2)
2427
+ : renderRunReportMarkdown(manifest, extra, { lang: getLang() })
2428
+ const filename = runReportFilename(format)
2429
+ const key = `report-${format}`
2430
+ const flash = () => {
2431
+ setCopiedKey(key)
2432
+ if (copyTimer.current) clearTimeout(copyTimer.current)
2433
+ copyTimer.current = setTimeout(() => setCopiedKey((cur) => (cur === key ? null : cur)), 1600)
2434
+ }
2435
+ if (downloadTextFile(filename, content)) { flash(); return }
2436
+ copyText(content).then((ok) => {
2437
+ if (!ok) { setCopyFailText(content); return }
2438
+ flash()
2439
+ })
2440
+ }
2441
+
2411
2442
  /** Force every thread open (or closed) and drop per-thread state. */
2412
2443
  const setAllThreads = (mode: 'all' | 'none') => {
2413
2444
  setThreadMode(mode)
@@ -3449,6 +3480,19 @@ function ObservatoryPanel(props: SlotProps) {
3449
3480
  onClick: exportObservatory,
3450
3481
  title: t('将观测台全部数据导出为 JSON(优先下载,失败则复制)'),
3451
3482
  }, copiedKey === 'export' ? t('已导出') : t('导出 JSON')),
3483
+ // v3.9: portable report export — Markdown for a PR body, SARIF for
3484
+ // CI / GitHub Code Scanning. Rendered from the same data model as
3485
+ // the `iterate_report` tool.
3486
+ React.createElement('button', {
3487
+ className: 'iterate-btn', 'data-copied': copiedKey === 'report-markdown' ? '' : undefined,
3488
+ onClick: () => exportReport('markdown'),
3489
+ title: t('导出可粘贴进 PR 的 Markdown 运行报告(优先下载,失败则复制)'),
3490
+ }, copiedKey === 'report-markdown' ? t('已导出') : t('导出报告')),
3491
+ React.createElement('button', {
3492
+ className: 'iterate-btn', 'data-copied': copiedKey === 'report-sarif' ? '' : undefined,
3493
+ onClick: () => exportReport('sarif'),
3494
+ title: t('导出 SARIF 2.1.0(供 CI / GitHub Code Scanning 消费)'),
3495
+ }, copiedKey === 'report-sarif' ? t('已导出') : t('导出 SARIF')),
3452
3496
  // Gap #4: `.iterate/` artifacts are unreachable from the browser;
3453
3497
  // this copies the one instruction that makes the model echo them
3454
3498
  // into the session stream (which is what every panel scans).