iterate-plugin 3.7.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: 7da1ef5af84458a763f4b0ad8cd75ba2de3fb16b9831e00c0a9767bc16fece65 */
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")),
@@ -1539,8 +1539,11 @@ function stoppedReasonLabel(reason, lang = "zh") {
1539
1539
  schema_invalid: "Done \xB7 repeated schema validation failures in review output",
1540
1540
  no_usable_reviewer_output: "Done \xB7 no usable review output"
1541
1541
  };
1542
- if (lang === "en") return en[reason] ?? (reason ? `Done \xB7 ${reason}` : "Done");
1543
- return zh[reason] ?? (reason ? `\u5DF2\u7ED3\u675F \xB7 ${reason}` : "\u5DF2\u7ED3\u675F");
1542
+ const table = lang === "en" ? en : zh;
1543
+ if (typeof reason === "string" && Object.hasOwn(table, reason)) return table[reason];
1544
+ const text = typeof reason === "string" && reason !== "" ? reason : "";
1545
+ if (lang === "en") return text ? `Done \xB7 ${text}` : "Done";
1546
+ return text ? `\u5DF2\u7ED3\u675F \xB7 ${text}` : "\u5DF2\u7ED3\u675F";
1544
1547
  }
1545
1548
  function buildCompletionSummary(report, lang = "zh") {
1546
1549
  const conv = (
@@ -1626,8 +1629,9 @@ function buildConfigEditGuide() {
1626
1629
  "---------------------",
1627
1630
  "\u914D\u7F6E\u6587\u4EF6\uFF1A\u9879\u76EE\u6839\u76EE\u5F55 iterate.config.yaml\u3002",
1628
1631
  "",
1629
- "\u26A0 \u672A\u914D\u7F6E validation.commands \u65F6\u8FED\u4EE3\u4E0D\u88AB\u4FDD\u62A4\uFF1A\u6BCF\u8F6E validate \u62FF\u5230\u7A7A\u7ED3\u679C\u4F1A\u88AB\u5F53\u4F5C",
1630
- " \u201C\u9A8C\u8BC1\u901A\u8FC7\u201D\uFF0C\u7B49\u4E8E\u5728\u6CA1\u6709\u4EFB\u4F55\u6D4B\u8BD5\u4FDD\u62A4\u7684\u60C5\u51B5\u4E0B\u7EE7\u7EED\u6536\u655B\u3002\u7B2C\u4E00\u6B21\u542F\u52A8\u8FED\u4EE3\u524D\uFF0C",
1632
+ "\u26A0 \u672A\u914D\u7F6E validation.commands \u65F6\u8FED\u4EE3\u4E0D\u88AB\u4FDD\u62A4\uFF1A\u8BE5\u8F6E\u4F1A\u76F4\u63A5\u4E2D\u6B62\uFF08fail-closed\uFF09\uFF0C",
1633
+ " \u7A7A\u9A8C\u8BC1\u7ED3\u679C/\u9A8C\u8BC1\u672A\u6267\u884C\u7EDD\u4E0D\u5F53\u4F5C\u201C\u9A8C\u8BC1\u901A\u8FC7\u201D\uFF0C\u8FD0\u884C\u4EE5 converged_unverified /",
1634
+ " aborted_by_config \u6536\u5C3E\u800C\u4E0D\u662F plain converged\u3002\u7B2C\u4E00\u6B21\u542F\u52A8\u8FED\u4EE3\u524D\uFF0C",
1631
1635
  " \u8BF7\u5148\u7ED9 validation.commands \u914D\u4E0A\u81F3\u5C11\u4E00\u6761\u771F\u5B9E\u547D\u4EE4\uFF08\u5982 npm test\uFF09\u3002",
1632
1636
  "",
1633
1637
  "\u53EF\u7F16\u8F91\u5B57\u6BB5\uFF1A",
@@ -1844,7 +1848,7 @@ function pickExportExtras(extra) {
1844
1848
  }
1845
1849
  return out;
1846
1850
  }
1847
- function buildRoundComparison(report, validations) {
1851
+ function buildRoundComparison(report, validations, fixes) {
1848
1852
  const rounds = /* @__PURE__ */ new Map();
1849
1853
  const rowsFor = (round) => {
1850
1854
  const n = Number(round);
@@ -1861,7 +1865,18 @@ function buildRoundComparison(report, validations) {
1861
1865
  row.findings += 1;
1862
1866
  const sev = typeof f.severity === "string" && f.severity ? f.severity : "unknown";
1863
1867
  row.severities[sev] = (row.severities[sev] || 0) + 1;
1864
- if (f.status === "fixed") row.fixed += 1;
1868
+ }
1869
+ const fixList = Array.isArray(fixes) ? fixes : [];
1870
+ for (const fx of fixList) {
1871
+ if (!fx || typeof fx !== "object") continue;
1872
+ if (
1873
+ /** @type {Record<string, unknown>} */
1874
+ fx.success !== true
1875
+ ) continue;
1876
+ rowsFor(
1877
+ /** @type {Record<string, unknown>} */
1878
+ fx.round
1879
+ ).fixed += 1;
1865
1880
  }
1866
1881
  const vals = Array.isArray(validations) ? validations : [];
1867
1882
  for (const v of vals) {
@@ -1897,6 +1912,404 @@ function latestPhase(phases) {
1897
1912
  return latest;
1898
1913
  }
1899
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
+
1900
2313
  // src/client/i18n.ts
1901
2314
  var LANG_STORAGE_KEY = "iterate-ui-lang";
1902
2315
  var LANG_NAMES = {
@@ -2060,6 +2473,10 @@ var EN = {
2060
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)",
2061
2474
  \u5DF2\u5BFC\u51FA: "Exported",
2062
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",
2063
2480
  "\u89C2\u6D4B\u53F0\u9875\u7B7E\uFF08\u5FEB\u6377\u952E 1-9 / 0 / -\uFF09": "Observatory tabs (keys 1-9 / 0 / -)",
2064
2481
  "{a0}\uFF08\u6309 {a1} \u5207\u6362\uFF09": "{a0} (press {a1})",
2065
2482
  "{a0}\uFF08\u6309 - \u5207\u6362\uFF09": "{a0} (press -)",
@@ -2841,9 +3258,17 @@ function ConvergenceDashboard(props) {
2841
3258
  noteSession(session);
2842
3259
  }, [session]);
2843
3260
  const report = latestReport(session);
3261
+ const pulseSeenRef = React.useRef(null);
2844
3262
  React.useEffect(() => {
2845
3263
  if (!report) return;
2846
3264
  const cur = getCurrentRound(report);
3265
+ const key = hashReport(report) + ":" + String(cur);
3266
+ if (pulseSeenRef.current === null) {
3267
+ pulseSeenRef.current = key;
3268
+ return;
3269
+ }
3270
+ if (pulseSeenRef.current === key) return;
3271
+ pulseSeenRef.current = key;
2847
3272
  const conv = report.convergence;
2848
3273
  emitRoundPulse(cur, conv?.converged === true);
2849
3274
  setPulseKey((k) => k + 1);
@@ -3146,6 +3571,7 @@ function TriagePanel(props) {
3146
3571
  setVerdicts((prev) => setAllVerdicts(prev, "keep"));
3147
3572
  setSelectAll(false);
3148
3573
  };
3574
+ const rootRef = React.useRef(null);
3149
3575
  React.useEffect(() => {
3150
3576
  const doc = typeof document !== "undefined" ? document : null;
3151
3577
  if (!doc) return;
@@ -3154,7 +3580,7 @@ function TriagePanel(props) {
3154
3580
  const t2 = ev.target;
3155
3581
  if (t2 && (t2.tagName === "INPUT" || t2.tagName === "TEXTAREA" || t2.tagName === "SELECT")) return;
3156
3582
  if (t2 && typeof t2.isContentEditable === "boolean" && t2.isContentEditable) return;
3157
- const rootEl = doc.querySelector('[data-iterate="triage"]');
3583
+ const rootEl = rootRef.current;
3158
3584
  if (!rootEl) return;
3159
3585
  const activeEl = doc.activeElement;
3160
3586
  if (!activeEl) return;
@@ -3292,7 +3718,7 @@ function TriagePanel(props) {
3292
3718
  });
3293
3719
  return React.createElement(
3294
3720
  "div",
3295
- { "data-iterate-root": "", "data-iterate": "triage", className: "iterate-triage" },
3721
+ { ref: rootRef, "data-iterate-root": "", "data-iterate": "triage", className: "iterate-triage" },
3296
3722
  React.createElement(
3297
3723
  "div",
3298
3724
  { className: "iterate-triage-head" },
@@ -4059,6 +4485,29 @@ function ObservatoryPanel(props) {
4059
4485
  flash();
4060
4486
  });
4061
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
+ };
4062
4511
  const setAllThreads = (mode) => {
4063
4512
  setThreadMode(mode);
4064
4513
  setExpandedThreads(/* @__PURE__ */ new Set());
@@ -4196,7 +4645,7 @@ function ObservatoryPanel(props) {
4196
4645
  }
4197
4646
  const points = conv.map((n, i) => ({ round: i + 1, count: n }));
4198
4647
  const report = latestReport(session);
4199
- const comparison = buildRoundComparison(report, manifest.validations || []);
4648
+ const comparison = buildRoundComparison(report, manifest.validations || [], manifest.fixes || []);
4200
4649
  const comparisonTable = comparison.length === 0 ? null : React.createElement(
4201
4650
  "table",
4202
4651
  { className: "iterate-obs-table" },
@@ -5254,6 +5703,21 @@ function ObservatoryPanel(props) {
5254
5703
  onClick: exportObservatory,
5255
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")
5256
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")),
5257
5721
  // Gap #4: `.iterate/` artifacts are unreachable from the browser;
5258
5722
  // this copies the one instruction that makes the model echo them
5259
5723
  // into the session stream (which is what every panel scans).