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
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* iterate-plugin client source-digest:
|
|
1
|
+
/* iterate-plugin client source-digest: 2819f2e6f5f182bb31b9e47c356f60e0b8d97fcdbd232c31b89438619623cf93 */
|
|
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
|
|
505
|
+
const asArray2 = (v) => Array.isArray(v) ? (
|
|
506
506
|
/** @type {Array<Record<string, unknown>>} */
|
|
507
507
|
v
|
|
508
508
|
) : [];
|
|
509
|
-
const rounds =
|
|
509
|
+
const rounds = asArray2(safeGet(src, "rounds")).map((r) => ({
|
|
510
510
|
round: asNum(safeGet(r, "round")),
|
|
511
|
-
threads:
|
|
511
|
+
threads: asArray2(safeGet(r, "threads")).map((t2) => ({
|
|
512
512
|
dimension: asStr(safeGet(t2, "dimension")),
|
|
513
513
|
attempt: asNum(safeGet(t2, "attempt")),
|
|
514
|
-
messages:
|
|
515
|
-
readFiles:
|
|
516
|
-
findings:
|
|
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:
|
|
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:
|
|
550
|
-
findings:
|
|
551
|
-
fixes:
|
|
552
|
-
live:
|
|
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:
|
|
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,411 @@ 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 asInt(value) {
|
|
2027
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
2028
|
+
}
|
|
2029
|
+
function asArray(value) {
|
|
2030
|
+
return Array.isArray(value) ? value : [];
|
|
2031
|
+
}
|
|
2032
|
+
function asObject(value) {
|
|
2033
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2034
|
+
}
|
|
2035
|
+
function mdCell(value) {
|
|
2036
|
+
const s = value === null || value === void 0 ? "" : String(value);
|
|
2037
|
+
return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
|
|
2038
|
+
}
|
|
2039
|
+
function findingSeverity(f) {
|
|
2040
|
+
const s = get(f, "severity");
|
|
2041
|
+
return typeof s === "string" && SEVERITY_ORDER2.includes(s) ? s : "low";
|
|
2042
|
+
}
|
|
2043
|
+
function sarifUri(file) {
|
|
2044
|
+
const s = typeof file === "string" ? file.trim() : "";
|
|
2045
|
+
if (!s) return "";
|
|
2046
|
+
return s.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2047
|
+
}
|
|
2048
|
+
function sarifLevel(severity) {
|
|
2049
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
2050
|
+
if (severity === "medium") return "warning";
|
|
2051
|
+
return "note";
|
|
2052
|
+
}
|
|
2053
|
+
function buildRunReportData(manifest, extra) {
|
|
2054
|
+
const src = asObject(manifest) ?? {};
|
|
2055
|
+
const ex = asObject(extra);
|
|
2056
|
+
const qg = asObject(get(ex, "qualityGate"));
|
|
2057
|
+
const bank = asObject(get(ex, "experienceBank"));
|
|
2058
|
+
const defense = asObject(get(ex, "defenseEvents"));
|
|
2059
|
+
const findings = asArray(get(src, "findings")).map((f) => ({
|
|
2060
|
+
dimension: asString(get(f, "dimension")),
|
|
2061
|
+
file: asString(get(f, "file")),
|
|
2062
|
+
line: asInt(get(f, "line")),
|
|
2063
|
+
severity: findingSeverity(f),
|
|
2064
|
+
summary: asString(get(f, "summary")),
|
|
2065
|
+
failureScenario: asString(get(f, "failure_scenario")),
|
|
2066
|
+
suggestedFix: asString(get(f, "suggested_fix")),
|
|
2067
|
+
isAtomic: get(f, "is_atomic") === true,
|
|
2068
|
+
acknowledged: get(f, "acknowledged") === true
|
|
2069
|
+
}));
|
|
2070
|
+
findings.sort(
|
|
2071
|
+
(a, b) => SEVERITY_ORDER2.indexOf(a.severity) - SEVERITY_ORDER2.indexOf(b.severity)
|
|
2072
|
+
);
|
|
2073
|
+
const fixes = asArray(get(src, "fixes")).map((f) => ({
|
|
2074
|
+
round: asNumber(get(f, "round")),
|
|
2075
|
+
file: asString(get(f, "file")),
|
|
2076
|
+
summary: asString(get(f, "summary")),
|
|
2077
|
+
linesAdded: asNumber(get(f, "linesAdded")),
|
|
2078
|
+
linesRemoved: asNumber(get(f, "linesRemoved")),
|
|
2079
|
+
success: get(f, "success") !== false
|
|
2080
|
+
}));
|
|
2081
|
+
const validations = asArray(get(src, "validations")).map((v) => ({
|
|
2082
|
+
round: asNumber(get(v, "round")),
|
|
2083
|
+
command: asString(get(v, "command")),
|
|
2084
|
+
exitCode: typeof get(v, "exitCode") === "number" ? asInt(get(v, "exitCode")) : null,
|
|
2085
|
+
allowed: get(v, "allowed") === true,
|
|
2086
|
+
rejectReason: asString(get(v, "rejectReason"))
|
|
2087
|
+
}));
|
|
2088
|
+
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
2089
|
+
for (const f of findings) {
|
|
2090
|
+
const key = f.severity;
|
|
2091
|
+
if (key in severityCounts) severityCounts[key] += 1;
|
|
2092
|
+
}
|
|
2093
|
+
const mode = get(src, "mode");
|
|
2094
|
+
const taskMode = get(src, "taskMode");
|
|
2095
|
+
const stoppedReason = get(src, "stoppedReason");
|
|
2096
|
+
return {
|
|
2097
|
+
project: asString(get(src, "project")),
|
|
2098
|
+
goal: asString(get(src, "goal")),
|
|
2099
|
+
mode: mode === "normal" || mode === "dry-run" ? mode : null,
|
|
2100
|
+
taskMode: taskMode === "code" || taskMode === "iterate" ? taskMode : null,
|
|
2101
|
+
round: asNumber(get(src, "round")),
|
|
2102
|
+
maxRounds: asNumber(get(src, "maxRounds")),
|
|
2103
|
+
// An ABSENT `active` means "no run / unknown", not "running": a missing or
|
|
2104
|
+
// empty transcript used to render a live, pulsing run at round 0. The
|
|
2105
|
+
// transcript builder always writes an explicit boolean, so a strict `true`
|
|
2106
|
+
// only ever loses the phantom run, never a real one.
|
|
2107
|
+
active: get(src, "active") === true,
|
|
2108
|
+
stoppedReason: typeof stoppedReason === "string" && stoppedReason ? stoppedReason : null,
|
|
2109
|
+
updatedAt: asString(get(src, "updatedAt")),
|
|
2110
|
+
convergence: asArray(get(src, "convergence")).filter(
|
|
2111
|
+
(n) => typeof n === "number" && Number.isFinite(n) && n >= 0
|
|
2112
|
+
),
|
|
2113
|
+
findings,
|
|
2114
|
+
severityCounts,
|
|
2115
|
+
fixes,
|
|
2116
|
+
validations,
|
|
2117
|
+
qualityGate: qg,
|
|
2118
|
+
experienceBank: bank,
|
|
2119
|
+
defenseEvents: defense
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
function reportStatus(data, labels) {
|
|
2123
|
+
if (data.active === true) return labels.running;
|
|
2124
|
+
if (data.stoppedReason) return `${labels.stopped} \xB7 ${data.stoppedReason}`;
|
|
2125
|
+
return labels.stopped;
|
|
2126
|
+
}
|
|
2127
|
+
function renderRunReportMarkdown(manifest, extra, opts) {
|
|
2128
|
+
const data = buildRunReportData(manifest, extra);
|
|
2129
|
+
const o = opts ?? {};
|
|
2130
|
+
const labels = reportLabels(o.lang === "en" ? "en" : "zh");
|
|
2131
|
+
const when = o.now && o.now ? o.now : (/* @__PURE__ */ new Date()).toISOString();
|
|
2132
|
+
const lines = [];
|
|
2133
|
+
lines.push(`# ${labels.title}`);
|
|
2134
|
+
lines.push("");
|
|
2135
|
+
lines.push(`- **${labels.project}**: \`${mdCell(data.project || labels.notRecorded)}\``);
|
|
2136
|
+
if (data.goal) lines.push(`- **${labels.goal}**: ${mdCell(data.goal)}`);
|
|
2137
|
+
lines.push(
|
|
2138
|
+
`- **${labels.mode}**: ${mdCell(data.mode || labels.notRecorded)}${data.taskMode ? ` \xB7 task_mode: ${data.taskMode}` : ""}`
|
|
2139
|
+
);
|
|
2140
|
+
const roundText = data.maxRounds > 0 ? `${data.round}/${data.maxRounds}` : String(data.round);
|
|
2141
|
+
lines.push(`- **${labels.round}**: ${mdCell(roundText)} \xB7 **${labels.status}**: ${mdCell(reportStatus(data, labels))}`);
|
|
2142
|
+
lines.push(`- **${labels.updated}**: ${mdCell(data.updatedAt || when)}`);
|
|
2143
|
+
lines.push(`- **${labels.generatedAt}**: ${mdCell(when)}`);
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
const qg = data.qualityGate;
|
|
2146
|
+
if (qg) {
|
|
2147
|
+
lines.push(`## ${labels.qualityGate}`);
|
|
2148
|
+
lines.push("");
|
|
2149
|
+
const status = typeof qg.overallStatus === "string" ? qg.overallStatus.toUpperCase() : "PENDING";
|
|
2150
|
+
lines.push(`- **${labels.overall}**: ${mdCell(status)} (${labels.score} ${asNumber(qg.overallScore)})`);
|
|
2151
|
+
lines.push(
|
|
2152
|
+
`- **${labels.verification}**: ${asNumber(qg.passedChecks)}/${asNumber(qg.totalChecks)} (${asNumber(qg.verificationPassRate)}%)`
|
|
2153
|
+
);
|
|
2154
|
+
if (typeof qg.failReason === "string" && qg.failReason) {
|
|
2155
|
+
lines.push(`- ${labels.status}: ${mdCell(qg.failReason)}`);
|
|
2156
|
+
}
|
|
2157
|
+
lines.push("");
|
|
2158
|
+
const dims = asArray(qg.dimensions);
|
|
2159
|
+
if (dims.length > 0) {
|
|
2160
|
+
lines.push(
|
|
2161
|
+
`| ${labels.dimension} | ${labels.qstatus} | ${labels.score} | ${labels.findingsCount} | ${labels.fixed} | ${labels.convergence} |`
|
|
2162
|
+
);
|
|
2163
|
+
lines.push("| --- | --- | --- | --- | --- | --- |");
|
|
2164
|
+
for (const d of dims) {
|
|
2165
|
+
lines.push(
|
|
2166
|
+
`| ${mdCell(get(d, "dimension"))} | ${mdCell(get(d, "status"))} | ${mdCell(get(d, "score"))} | ${mdCell(get(d, "findingsCount"))} | ${mdCell(get(d, "fixedCount"))} | ${mdCell(get(d, "convergenceRate"))}% |`
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
lines.push("");
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
lines.push(`## ${labels.findings} (${data.findings.length})`);
|
|
2173
|
+
lines.push("");
|
|
2174
|
+
if (data.findings.length === 0) {
|
|
2175
|
+
lines.push(labels.none);
|
|
2176
|
+
} else {
|
|
2177
|
+
lines.push(`| ${labels.severity} | ${labels.dimension} | ${labels.location} | ${labels.summary} |`);
|
|
2178
|
+
lines.push("| --- | --- | --- | --- |");
|
|
2179
|
+
for (const f of data.findings) {
|
|
2180
|
+
const loc = f.file ? `${f.file}${f.line > 0 ? `:${f.line}` : ""}` : "";
|
|
2181
|
+
lines.push(`| ${mdCell(f.severity)} | ${mdCell(f.dimension)} | ${mdCell(loc)} | ${mdCell(f.summary)} |`);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
lines.push("");
|
|
2185
|
+
lines.push(`## ${labels.fixes} (${data.fixes.length})`);
|
|
2186
|
+
lines.push("");
|
|
2187
|
+
if (data.fixes.length === 0) {
|
|
2188
|
+
lines.push(labels.none);
|
|
2189
|
+
} else {
|
|
2190
|
+
lines.push(`| # | ${labels.file} | ${labels.delta} | ${labels.result} | ${labels.summary} |`);
|
|
2191
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
2192
|
+
for (const f of data.fixes) {
|
|
2193
|
+
lines.push(
|
|
2194
|
+
`| ${mdCell(f.round)} | ${mdCell(f.file)} | ${mdCell(`+${f.linesAdded}/-${f.linesRemoved}`)} | ${f.success ? labels.applied : labels.failed} | ${mdCell(f.summary)} |`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
lines.push("");
|
|
2199
|
+
if (data.validations.length > 0) {
|
|
2200
|
+
lines.push(`## ${labels.validations}`);
|
|
2201
|
+
lines.push("");
|
|
2202
|
+
lines.push(`| # | ${labels.command} | ${labels.exit} | ${labels.allowed} |`);
|
|
2203
|
+
lines.push("| --- | --- | --- | --- |");
|
|
2204
|
+
for (const v of data.validations) {
|
|
2205
|
+
lines.push(
|
|
2206
|
+
`| ${mdCell(v.round)} | \`${mdCell(v.command)}\` | ${v.exitCode === null ? "\u2014" : mdCell(v.exitCode)} | ${v.allowed ? labels.yes : labels.no} |`
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
lines.push("");
|
|
2210
|
+
}
|
|
2211
|
+
const deEvents = asArray(get(data.defenseEvents, "events"));
|
|
2212
|
+
if (deEvents.length > 0) {
|
|
2213
|
+
lines.push(`## ${labels.defenseEvents} (${deEvents.length})`);
|
|
2214
|
+
lines.push("");
|
|
2215
|
+
lines.push(`| ${labels.severity} | ${labels.defensiveType} | ${labels.round} | ${labels.summary} |`);
|
|
2216
|
+
lines.push("| --- | --- | --- | --- |");
|
|
2217
|
+
for (const e of deEvents) {
|
|
2218
|
+
lines.push(
|
|
2219
|
+
`| ${mdCell(get(e, "severity"))} | ${mdCell(get(e, "type"))} | ${mdCell(get(e, "round"))} | ${mdCell(get(e, "description") || get(e, "outcome"))} |`
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
lines.push("");
|
|
2223
|
+
}
|
|
2224
|
+
const entries = asArray(get(data.experienceBank, "entries"));
|
|
2225
|
+
if (entries.length > 0) {
|
|
2226
|
+
const totalHits = asNumber(get(data.experienceBank, "totalHits"));
|
|
2227
|
+
lines.push(`## ${labels.experienceBank} (${entries.length} ${labels.entries}, ${totalHits} ${labels.hits})`);
|
|
2228
|
+
lines.push("");
|
|
2229
|
+
for (const e of entries) {
|
|
2230
|
+
lines.push(
|
|
2231
|
+
`- **[${mdCell(get(e, "id"))}]** ${mdCell(get(e, "pattern"))} \u2014 ${mdCell(get(e, "description"))} (${labels.hits}: ${mdCell(get(e, "hitCount"))})`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
lines.push("");
|
|
2235
|
+
}
|
|
2236
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
2237
|
+
}
|
|
2238
|
+
function renderRunReportSarif(manifest, extra, opts) {
|
|
2239
|
+
const data = buildRunReportData(manifest, extra);
|
|
2240
|
+
const o = opts ?? {};
|
|
2241
|
+
const rules = [];
|
|
2242
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
2243
|
+
for (const f of data.findings) {
|
|
2244
|
+
const id = f.dimension || "review";
|
|
2245
|
+
if (seenRules.has(id)) continue;
|
|
2246
|
+
seenRules.add(id);
|
|
2247
|
+
rules.push({
|
|
2248
|
+
id,
|
|
2249
|
+
name: id,
|
|
2250
|
+
shortDescription: { text: `${id} review dimension` },
|
|
2251
|
+
defaultConfiguration: { level: "warning" }
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
const results = data.findings.map((f) => {
|
|
2255
|
+
const result = {
|
|
2256
|
+
ruleId: f.dimension || "review",
|
|
2257
|
+
level: sarifLevel(f.severity),
|
|
2258
|
+
message: { text: f.summary || `${f.severity} finding` },
|
|
2259
|
+
properties: {
|
|
2260
|
+
severity: f.severity,
|
|
2261
|
+
atomic: f.isAtomic,
|
|
2262
|
+
acknowledged: f.acknowledged
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
const uri = sarifUri(f.file);
|
|
2266
|
+
if (uri) {
|
|
2267
|
+
result.locations = [
|
|
2268
|
+
{
|
|
2269
|
+
physicalLocation: {
|
|
2270
|
+
artifactLocation: { uri },
|
|
2271
|
+
...f.line > 0 ? { region: { startLine: f.line } } : {}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
];
|
|
2275
|
+
}
|
|
2276
|
+
if (f.suggestedFix) result.fixes = [{ description: { text: f.suggestedFix } }];
|
|
2277
|
+
return result;
|
|
2278
|
+
});
|
|
2279
|
+
const failedValidations = data.validations.filter((v) => v.exitCode !== null && v.exitCode !== 0).length;
|
|
2280
|
+
return {
|
|
2281
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
2282
|
+
version: "2.1.0",
|
|
2283
|
+
runs: [
|
|
2284
|
+
{
|
|
2285
|
+
tool: {
|
|
2286
|
+
driver: {
|
|
2287
|
+
name: "iterate-plugin",
|
|
2288
|
+
informationUri: o.informationUri && o.informationUri ? o.informationUri : "https://github.com/jingzhao-l/iterate-plugin",
|
|
2289
|
+
...o.toolVersion ? { version: o.toolVersion } : {},
|
|
2290
|
+
rules
|
|
2291
|
+
}
|
|
2292
|
+
},
|
|
2293
|
+
invocations: [
|
|
2294
|
+
{
|
|
2295
|
+
executionSuccessful: failedValidations === 0 && data.qualityGate?.overallStatus !== "fail",
|
|
2296
|
+
...data.updatedAt ? { startTimeUtc: data.updatedAt } : {},
|
|
2297
|
+
properties: {
|
|
2298
|
+
mode: data.mode,
|
|
2299
|
+
taskMode: data.taskMode,
|
|
2300
|
+
round: data.round,
|
|
2301
|
+
maxRounds: data.maxRounds,
|
|
2302
|
+
stoppedReason: data.stoppedReason,
|
|
2303
|
+
qualityStatus: data.qualityGate ? data.qualityGate.overallStatus ?? null : null,
|
|
2304
|
+
convergence: data.convergence,
|
|
2305
|
+
findingsBySeverity: data.severityCounts
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
],
|
|
2309
|
+
results
|
|
2310
|
+
}
|
|
2311
|
+
]
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
function runReportFilename(format, opts) {
|
|
2315
|
+
const ext = format === "sarif" ? "sarif" : "md";
|
|
2316
|
+
const stamp = opts?.now && opts.now ? opts.now.replace(/[:.]/g, "-") : (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2317
|
+
return `iterate-report-${stamp}.${ext}`;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
1915
2320
|
// src/client/i18n.ts
|
|
1916
2321
|
var LANG_STORAGE_KEY = "iterate-ui-lang";
|
|
1917
2322
|
var LANG_NAMES = {
|
|
@@ -2075,6 +2480,10 @@ var EN = {
|
|
|
2075
2480
|
"\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
2481
|
\u5DF2\u5BFC\u51FA: "Exported",
|
|
2077
2482
|
"\u5BFC\u51FA JSON": "Export JSON",
|
|
2483
|
+
"\u5BFC\u51FA\u62A5\u544A": "Export report",
|
|
2484
|
+
"\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)",
|
|
2485
|
+
"\u5BFC\u51FA SARIF": "Export SARIF",
|
|
2486
|
+
"\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
2487
|
"\u89C2\u6D4B\u53F0\u9875\u7B7E\uFF08\u5FEB\u6377\u952E 1-9 / 0 / -\uFF09": "Observatory tabs (keys 1-9 / 0 / -)",
|
|
2079
2488
|
"{a0}\uFF08\u6309 {a1} \u5207\u6362\uFF09": "{a0} (press {a1})",
|
|
2080
2489
|
"{a0}\uFF08\u6309 - \u5207\u6362\uFF09": "{a0} (press -)",
|
|
@@ -4083,6 +4492,29 @@ function ObservatoryPanel(props) {
|
|
|
4083
4492
|
flash();
|
|
4084
4493
|
});
|
|
4085
4494
|
};
|
|
4495
|
+
const exportReport = (format) => {
|
|
4496
|
+
if (!manifest) return;
|
|
4497
|
+
const extra = { qualityGate, experienceBank, defenseEvents };
|
|
4498
|
+
const content = format === "sarif" ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2) : renderRunReportMarkdown(manifest, extra, { lang: getLang() });
|
|
4499
|
+
const filename = runReportFilename(format);
|
|
4500
|
+
const key = `report-${format}`;
|
|
4501
|
+
const flash = () => {
|
|
4502
|
+
setCopiedKey(key);
|
|
4503
|
+
if (copyTimer.current) clearTimeout(copyTimer.current);
|
|
4504
|
+
copyTimer.current = setTimeout(() => setCopiedKey((cur) => cur === key ? null : cur), 1600);
|
|
4505
|
+
};
|
|
4506
|
+
if (downloadTextFile(filename, content)) {
|
|
4507
|
+
flash();
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
copyText(content).then((ok) => {
|
|
4511
|
+
if (!ok) {
|
|
4512
|
+
setCopyFailText(content);
|
|
4513
|
+
return;
|
|
4514
|
+
}
|
|
4515
|
+
flash();
|
|
4516
|
+
});
|
|
4517
|
+
};
|
|
4086
4518
|
const setAllThreads = (mode) => {
|
|
4087
4519
|
setThreadMode(mode);
|
|
4088
4520
|
setExpandedThreads(/* @__PURE__ */ new Set());
|
|
@@ -5278,6 +5710,21 @@ function ObservatoryPanel(props) {
|
|
|
5278
5710
|
onClick: exportObservatory,
|
|
5279
5711
|
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
5712
|
}, copiedKey === "export" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA JSON")),
|
|
5713
|
+
// v3.9: portable report export — Markdown for a PR body, SARIF for
|
|
5714
|
+
// CI / GitHub Code Scanning. Rendered from the same data model as
|
|
5715
|
+
// the `iterate_report` tool.
|
|
5716
|
+
React.createElement("button", {
|
|
5717
|
+
className: "iterate-btn",
|
|
5718
|
+
"data-copied": copiedKey === "report-markdown" ? "" : void 0,
|
|
5719
|
+
onClick: () => exportReport("markdown"),
|
|
5720
|
+
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")
|
|
5721
|
+
}, copiedKey === "report-markdown" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA\u62A5\u544A")),
|
|
5722
|
+
React.createElement("button", {
|
|
5723
|
+
className: "iterate-btn",
|
|
5724
|
+
"data-copied": copiedKey === "report-sarif" ? "" : void 0,
|
|
5725
|
+
onClick: () => exportReport("sarif"),
|
|
5726
|
+
title: t("\u5BFC\u51FA SARIF 2.1.0\uFF08\u4F9B CI / GitHub Code Scanning \u6D88\u8D39\uFF09")
|
|
5727
|
+
}, copiedKey === "report-sarif" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA SARIF")),
|
|
5281
5728
|
// Gap #4: `.iterate/` artifacts are unreachable from the browser;
|
|
5282
5729
|
// this copies the one instruction that makes the model echo them
|
|
5283
5730
|
// 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.
|
|
3
|
+
"version": "3.9.1",
|
|
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-
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
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
|
|
108
|
-
"@deepseek-ai/dsh-agent": "0.2.1-
|
|
109
|
-
"@deepseek-ai/dsh-jobs": "0.2.1-
|
|
110
|
-
"@deepseek-ai/dsh-session": "0.2.1-
|
|
111
|
-
"@deepseek-ai/dsh-tools": "0.2.1-
|
|
112
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-
|
|
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",
|
package/src/client/i18n.ts
CHANGED
|
@@ -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 -)',
|
package/src/client/index.ts
CHANGED
|
@@ -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).
|