iterate-plugin 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/README.zh-CN.md +12 -11
- package/dist/index.js +8 -5
- package/dist/report-render.js +33 -2
- package/dist/tools/preflight.js +239 -0
- package/dist/tools/report.js +20 -8
- package/lib/client.js +359 -9
- package/lib/parse.js +148 -2
- package/package.json +22 -20
- package/src/client/i18n.ts +43 -1
- package/src/client/index.ts +216 -4
- package/src/index.ts +8 -5
- package/src/report-render.ts +69 -3
- package/src/tools/preflight.ts +289 -0
- package/src/tools/report.ts +19 -8
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* iterate-plugin client source-digest:
|
|
1
|
+
/* iterate-plugin client source-digest: 408ff45e22a5041e34cdeb7ccd786225658f6c1fe52a7bb5233fc35450546683 */
|
|
2
2
|
window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
|
|
3
3
|
var module = { exports: {} };
|
|
4
4
|
var exports = module.exports;
|
|
@@ -854,6 +854,62 @@ function scanSessionForDefenseEvents(session) {
|
|
|
854
854
|
const loose = findFirstInObject(session, isDefenseEventsResult);
|
|
855
855
|
return loose ? normalizeDefenseEventsResult(loose) : null;
|
|
856
856
|
}
|
|
857
|
+
function isPreflightReport(obj) {
|
|
858
|
+
if (!obj || typeof obj !== "object") return false;
|
|
859
|
+
const o = (
|
|
860
|
+
/** @type {Record<string, unknown>} */
|
|
861
|
+
obj
|
|
862
|
+
);
|
|
863
|
+
const ready = safeGet(o, "ready");
|
|
864
|
+
const checks = safeGet(o, "checks");
|
|
865
|
+
const blocking = safeGet(o, "blocking");
|
|
866
|
+
const warnings = safeGet(o, "warnings");
|
|
867
|
+
return typeof ready === "boolean" && Array.isArray(checks) && (typeof blocking === "number" || typeof warnings === "number");
|
|
868
|
+
}
|
|
869
|
+
function findPreflightReportInObject(obj) {
|
|
870
|
+
return findFirstInObject(obj, (o) => isPreflightReport(o));
|
|
871
|
+
}
|
|
872
|
+
function normalizePreflightReport(raw) {
|
|
873
|
+
const src = raw && typeof raw === "object" ? (
|
|
874
|
+
/** @type {Record<string, unknown>} */
|
|
875
|
+
raw
|
|
876
|
+
) : {};
|
|
877
|
+
const asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
878
|
+
const asStr = (v) => typeof v === "string" ? v : "";
|
|
879
|
+
const checks = Array.isArray(safeGet(src, "checks")) ? safeGet(src, "checks") : [];
|
|
880
|
+
return {
|
|
881
|
+
ok: safeGet(src, "ok") !== false,
|
|
882
|
+
ready: safeGet(src, "ready") === true,
|
|
883
|
+
blocking: asNum(safeGet(src, "blocking")),
|
|
884
|
+
warnings: asNum(safeGet(src, "warnings")),
|
|
885
|
+
error: asStr(safeGet(src, "error")) || null,
|
|
886
|
+
checks: (
|
|
887
|
+
/** @type {unknown[]} */
|
|
888
|
+
checks.map((c) => {
|
|
889
|
+
const rec = (
|
|
890
|
+
/** @type {Record<string, unknown>} */
|
|
891
|
+
c && typeof c === "object" ? c : {}
|
|
892
|
+
);
|
|
893
|
+
const status = safeGet(rec, "status");
|
|
894
|
+
const detail = safeGet(rec, "detail");
|
|
895
|
+
return {
|
|
896
|
+
id: asStr(safeGet(rec, "id")),
|
|
897
|
+
status: status === "pass" || status === "warn" || status === "fail" ? status : "warn",
|
|
898
|
+
detail: detail && typeof detail === "object" ? detail : {}
|
|
899
|
+
};
|
|
900
|
+
})
|
|
901
|
+
)
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function scanSessionForPreflight(session) {
|
|
905
|
+
const node = latestToolResultNode(session, "iterate_preflight", findPreflightReportInObject);
|
|
906
|
+
if (node !== null) {
|
|
907
|
+
const found = findPreflightReportInObject(node);
|
|
908
|
+
if (found) return normalizePreflightReport(found);
|
|
909
|
+
}
|
|
910
|
+
const loose = findFirstInObject(session, isPreflightReport);
|
|
911
|
+
return loose ? normalizePreflightReport(loose) : null;
|
|
912
|
+
}
|
|
857
913
|
function isRunSummary(obj) {
|
|
858
914
|
if (!obj || typeof obj !== "object") return false;
|
|
859
915
|
const o = (
|
|
@@ -1280,6 +1336,13 @@ function buildQualityGateClearInstruction() {
|
|
|
1280
1336
|
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
1281
1337
|
\`\`\``;
|
|
1282
1338
|
}
|
|
1339
|
+
function buildPreflightInstruction() {
|
|
1340
|
+
return `\u8BF7\u8C03\u7528 \`iterate_preflight\` \u8FD0\u884C\u5F00\u8DD1\u524D\u9884\u68C0\uFF08\u53EA\u8BFB\uFF1A\u6821\u9A8C validation.commands\u3001git\u3001\u810F\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u8D28\u91CF\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\uFF09\uFF1A
|
|
1341
|
+
|
|
1342
|
+
\`\`\`json
|
|
1343
|
+
${JSON.stringify({}, null, 2)}
|
|
1344
|
+
\`\`\``;
|
|
1345
|
+
}
|
|
1283
1346
|
function buildRollbackInstruction(id) {
|
|
1284
1347
|
return `\u8BF7\u8C03\u7528 \`iterate_rollback\` \u56DE\u6EDA\u4EE5\u4E0B\u4FEE\u590D\uFF1A
|
|
1285
1348
|
|
|
@@ -1555,13 +1618,35 @@ function buildCompletionSummary(report, lang = "zh") {
|
|
|
1555
1618
|
const stats = severityStats(report);
|
|
1556
1619
|
const converged = conv.converged === true;
|
|
1557
1620
|
const totalFindings = stats.critical + stats.high + stats.medium + stats.low;
|
|
1621
|
+
const stopped = typeof conv.stoppedReason === "string" ? conv.stoppedReason : typeof report.stoppedReason === "string" ? report.stoppedReason : "";
|
|
1622
|
+
const reason = stopped ? completionReason(stopped, lang, total) : converged ? lang === "en" ? "converged" : "\u5DF2\u6536\u655B" : lang === "en" ? `reached the round limit ${total}` : `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`;
|
|
1558
1623
|
if (lang === "en") {
|
|
1559
|
-
|
|
1560
|
-
return `iterate review complete \xB7 ${rounds}/${total} rounds \xB7 ${totalFindings} findings \xB7 ${reason2}`;
|
|
1624
|
+
return `iterate review complete \xB7 ${rounds}/${total} rounds \xB7 ${totalFindings} findings \xB7 ${reason}`;
|
|
1561
1625
|
}
|
|
1562
|
-
const reason = converged ? "\u5DF2\u6536\u655B" : `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`;
|
|
1563
1626
|
return `iterate \u8BC4\u5BA1\u5B8C\u6210 \xB7 ${rounds}/${total} \u8F6E \xB7 ${totalFindings} \u9879\u53D1\u73B0 \xB7 ${reason}`;
|
|
1564
1627
|
}
|
|
1628
|
+
function completionReason(reason, lang, total) {
|
|
1629
|
+
const zh = {
|
|
1630
|
+
converged: "\u5DF2\u6536\u655B",
|
|
1631
|
+
max_rounds_reached: `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`,
|
|
1632
|
+
aborted_by_validation: "\u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA",
|
|
1633
|
+
aborted_by_config: "\u9A8C\u8BC1\u547D\u4EE4\u672A\u5728\u767D\u540D\u5355",
|
|
1634
|
+
inconclusive: "\u8F6E\u6B21\u7ED3\u8BBA\u4E0D\u660E",
|
|
1635
|
+
schema_invalid: "\u5BA1\u67E5\u8F93\u51FA schema \u6821\u9A8C\u5931\u8D25",
|
|
1636
|
+
no_usable_reviewer_output: "\u65E0\u53EF\u7528\u5BA1\u67E5\u8F93\u51FA"
|
|
1637
|
+
};
|
|
1638
|
+
const en = {
|
|
1639
|
+
converged: "converged",
|
|
1640
|
+
max_rounds_reached: `reached the round limit ${total}`,
|
|
1641
|
+
aborted_by_validation: "rolled back after a validation failure",
|
|
1642
|
+
aborted_by_config: "validation command not allow-listed",
|
|
1643
|
+
inconclusive: "inconclusive rounds",
|
|
1644
|
+
schema_invalid: "review output failed schema validation",
|
|
1645
|
+
no_usable_reviewer_output: "no usable review output"
|
|
1646
|
+
};
|
|
1647
|
+
const table = lang === "en" ? en : zh;
|
|
1648
|
+
return Object.hasOwn(table, reason) ? table[reason] : reason;
|
|
1649
|
+
}
|
|
1565
1650
|
var CONFIG_EDIT_FIELDS = [
|
|
1566
1651
|
{
|
|
1567
1652
|
key: "goal",
|
|
@@ -2311,8 +2396,31 @@ function renderRunReportSarif(manifest, extra, opts) {
|
|
|
2311
2396
|
]
|
|
2312
2397
|
};
|
|
2313
2398
|
}
|
|
2399
|
+
function renderRunReportJson(manifest, extra) {
|
|
2400
|
+
const data = buildRunReportData(manifest, extra);
|
|
2401
|
+
return {
|
|
2402
|
+
$schema: "iterate-run-report/1",
|
|
2403
|
+
project: data.project,
|
|
2404
|
+
goal: data.goal,
|
|
2405
|
+
mode: data.mode,
|
|
2406
|
+
taskMode: data.taskMode,
|
|
2407
|
+
round: data.round,
|
|
2408
|
+
maxRounds: data.maxRounds,
|
|
2409
|
+
active: data.active,
|
|
2410
|
+
stoppedReason: data.stoppedReason,
|
|
2411
|
+
updatedAt: data.updatedAt,
|
|
2412
|
+
convergence: data.convergence,
|
|
2413
|
+
severityCounts: data.severityCounts,
|
|
2414
|
+
findings: data.findings,
|
|
2415
|
+
fixes: data.fixes,
|
|
2416
|
+
validations: data.validations,
|
|
2417
|
+
qualityGate: data.qualityGate,
|
|
2418
|
+
experienceBank: data.experienceBank,
|
|
2419
|
+
defenseEvents: data.defenseEvents
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2314
2422
|
function runReportFilename(format, opts) {
|
|
2315
|
-
const ext = format === "sarif" ? "sarif" : "md";
|
|
2423
|
+
const ext = format === "sarif" ? "sarif" : format === "json" ? "json" : "md";
|
|
2316
2424
|
const stamp = opts?.now && opts.now ? opts.now.replace(/[:.]/g, "-") : (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2317
2425
|
return `iterate-report-${stamp}.${ext}`;
|
|
2318
2426
|
}
|
|
@@ -2484,6 +2592,8 @@ var EN = {
|
|
|
2484
2592
|
"\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
2593
|
"\u5BFC\u51FA SARIF": "Export SARIF",
|
|
2486
2594
|
"\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",
|
|
2595
|
+
"\u5BFC\u51FA JSON \u62A5\u544A": "Export JSON report",
|
|
2596
|
+
"\u5BFC\u51FA\u5B8C\u6574\u8FD0\u884C\u6A21\u578B\u7684 JSON\uFF08\u4F9B CI \u4EEA\u8868\u76D8 / PR \u673A\u5668\u4EBA\u6D88\u8D39\uFF0C\u65E0\u635F\u683C\u5F0F\uFF09": "Export the complete run model as JSON (lossless, for CI dashboards / PR bots)",
|
|
2487
2597
|
"\u89C2\u6D4B\u53F0\u9875\u7B7E\uFF08\u5FEB\u6377\u952E 1-9 / 0 / -\uFF09": "Observatory tabs (keys 1-9 / 0 / -)",
|
|
2488
2598
|
"{a0}\uFF08\u6309 {a1} \u5207\u6362\uFF09": "{a0} (press {a1})",
|
|
2489
2599
|
"{a0}\uFF08\u6309 - \u5207\u6362\uFF09": "{a0} (press -)",
|
|
@@ -2633,6 +2743,38 @@ var EN = {
|
|
|
2633
2743
|
\u6E05\u9664\u7C7B\u578B\u7B5B\u9009: "Clear type filter",
|
|
2634
2744
|
"\u9632\u7EBF\uFF1A{a0}": "Defense: {a0}",
|
|
2635
2745
|
\u6CA1\u6709\u5339\u914D\u8BE5\u7C7B\u578B\u7684\u9632\u5FA1\u4E8B\u4EF6: "No defense events of this type",
|
|
2746
|
+
// ── F11 preflight ────────────────────────────────────────────────────────
|
|
2747
|
+
\u5F00\u8DD1\u9884\u68C0: "Preflight",
|
|
2748
|
+
\u5F85\u9884\u68C0: "Not run yet",
|
|
2749
|
+
\u672A\u5C31\u7EEA: "Not ready",
|
|
2750
|
+
"\u590D\u5236 iterate_preflight \u9884\u68C0\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09": "Copy the iterate_preflight readiness instruction (JSON arguments included)",
|
|
2751
|
+
\u8FD0\u884C\u9884\u68C0: "Run preflight",
|
|
2752
|
+
\u5F00\u8DD1\u9884\u68C0\u6E05\u5355: "Preflight checklist",
|
|
2753
|
+
\u9884\u68C0\u7ED3\u679C: "Preflight result",
|
|
2754
|
+
"\u5F00\u8DD1\u524D\u5148\u9884\u68C0\uFF1A\u786E\u8BA4\u9A8C\u8BC1\u547D\u4EE4\u3001Git\u3001\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\u90FD\u5DF2\u5C31\u7EEA\uFF0C\u907F\u514D\u5F00\u8DD1\u540E\u624D\u53D1\u73B0\u73AF\u5883\u95EE\u9898\u3002": "Run preflight first: confirm validation commands, git, the working tree, a stale gate and an interrupted checkpoint are ready, so you do not discover an environment problem mid-run.",
|
|
2755
|
+
\u590D\u5236\u9884\u68C0\u6307\u4EE4: "Copy preflight instruction",
|
|
2756
|
+
\u9A8C\u8BC1\u547D\u4EE4: "Validation commands",
|
|
2757
|
+
"\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4": "{a0} validation command(s) configured",
|
|
2758
|
+
"\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879": "validation.commands is not configured \u2014 the run cannot prove fixes (blocking)",
|
|
2759
|
+
"Git \u4ED3\u5E93": "Git repository",
|
|
2760
|
+
"\u5DF2\u68C0\u6D4B\u5230 Git \u4ED3\u5E93": "Git repository detected",
|
|
2761
|
+
"\u4E0D\u662F Git \u4ED3\u5E93 \u2014\u2014 changed-only \u8303\u56F4\u4E0E iterate_diff \u9700\u8981\u5B83": "Not a git repository \u2014 changed-only scope and iterate_diff need one",
|
|
2762
|
+
\u5DE5\u4F5C\u533A: "Working tree",
|
|
2763
|
+
"\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u672A\u77E5": "Uncommitted changes: unknown",
|
|
2764
|
+
"{a0} \u4E2A\u672A\u63D0\u4EA4/\u672A\u8DDF\u8E2A\u6587\u4EF6 \u2014\u2014 \u5EFA\u8BAE\u5148 commit \u6216 stash": "{a0} uncommitted/untracked file(s) \u2014 commit or stash first",
|
|
2765
|
+
\u5DE5\u4F5C\u533A\u5E72\u51C0: "Working tree clean",
|
|
2766
|
+
\u9648\u65E7\u95E8\u7981: "Stale gate",
|
|
2767
|
+
"\u78C1\u76D8\u4E0A\u6B8B\u7559 FAIL \u8D28\u91CF\u95E8\u7981 \u2014\u2014 \u5F00\u65B0\u8F6E\u524D\u5148\u6E05\u9664": "A stale FAIL quality gate is persisted \u2014 clear it before a fresh run",
|
|
2768
|
+
"\u8D28\u91CF\u95E8\u7981\uFF1A{a0}": "Quality gate: {a0}",
|
|
2769
|
+
\u65E0\u6301\u4E45\u5316\u8D28\u91CF\u95E8\u7981: "No persisted quality gate",
|
|
2770
|
+
\u4E2D\u65AD\u65AD\u70B9: "Interrupted checkpoint",
|
|
2771
|
+
"\u5B58\u5728\u4E2D\u65AD\u65AD\u70B9\uFF08\u5DF2\u6062\u590D {a0} \u6B21\uFF09\u2014\u2014 \u6062\u590D\u6216\u6E05\u9664\u540E\u518D\u5F00\u8DD1": "An interrupted checkpoint is waiting (resumed {a0}x) \u2014 resume or clear it first",
|
|
2772
|
+
\u65E0\u4E2D\u65AD\u65AD\u70B9: "No interrupted checkpoint",
|
|
2773
|
+
\u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4: "Configure validation",
|
|
2774
|
+
\u6062\u590D\u65AD\u70B9: "Resume checkpoint",
|
|
2775
|
+
"\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}": "Blocking {a0} \xB7 warnings {a1}",
|
|
2776
|
+
"\u9884\u68C0\u5931\u8D25\uFF1A{a0}": "Preflight failed: {a0}",
|
|
2777
|
+
"\u9884\u68C0\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u4FEE\u6539\u914D\u7F6E\u6216\u6587\u4EF6\u3002": "Preflight is read-only; it never modifies config or files.",
|
|
2636
2778
|
// ── copy feedback ────────────────────────────────────────────────────────
|
|
2637
2779
|
\u5DF2\u590D\u5236: "Copied",
|
|
2638
2780
|
\u590D\u5236\u5931\u8D25: "Copy failed"
|
|
@@ -2882,6 +3024,7 @@ var ITERATE_CSS = `
|
|
|
2882
3024
|
.iterate-gate[data-status="pass"] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
2883
3025
|
.iterate-gate[data-status="fail"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
2884
3026
|
.iterate-gate[data-status="pending"] { color: var(--dsw-alias-label-secondary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 28%, transparent); }
|
|
3027
|
+
.iterate-gate[data-status="warn"] { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
2885
3028
|
|
|
2886
3029
|
/* v3.0: Experience bank hit highlight */
|
|
2887
3030
|
.iterate-exp-hit { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); font-size: 11px; color: var(--dsw-alias-state-success-primary); }
|
|
@@ -4318,7 +4461,9 @@ var OBS_TABS = [
|
|
|
4318
4461
|
// v3.0: Quality Command Center tabs
|
|
4319
4462
|
{ key: "f8", label: { zh: "\u8D28\u91CF\u95E8\u7981", en: "Quality gate" }, hotkey: "8" },
|
|
4320
4463
|
{ key: "f9", label: { zh: "\u7ECF\u9A8C\u94F6\u884C", en: "Experience bank" }, hotkey: "9" },
|
|
4321
|
-
{ key: "f10", label: { zh: "\u9632\u5FA1\u4E8B\u4EF6", en: "Defense events" }, hotkey: "0" }
|
|
4464
|
+
{ key: "f10", label: { zh: "\u9632\u5FA1\u4E8B\u4EF6", en: "Defense events" }, hotkey: "0" },
|
|
4465
|
+
// v3.12: pre-run readiness checklist (validation/git/dirty-tree/stale-gate/checkpoint)
|
|
4466
|
+
{ key: "f11", label: { zh: "\u5F00\u8DD1\u9884\u68C0", en: "Preflight" }, hotkey: "" }
|
|
4322
4467
|
];
|
|
4323
4468
|
var OBS_HOTKEY_TABS = {
|
|
4324
4469
|
1: "f1",
|
|
@@ -4389,6 +4534,10 @@ function latestDefenseEvents(session) {
|
|
|
4389
4534
|
if (!session) return null;
|
|
4390
4535
|
return scanSessionForDefenseEvents(session);
|
|
4391
4536
|
}
|
|
4537
|
+
function latestPreflight(session) {
|
|
4538
|
+
if (!session) return null;
|
|
4539
|
+
return scanSessionForPreflight(session);
|
|
4540
|
+
}
|
|
4392
4541
|
function buildObsFixInstruction(f) {
|
|
4393
4542
|
return buildFixInstruction(f);
|
|
4394
4543
|
}
|
|
@@ -4401,6 +4550,7 @@ function ObservatoryPanel(props) {
|
|
|
4401
4550
|
const qualityGate = latestQualityGate(session);
|
|
4402
4551
|
const experienceBank = latestExperienceBank(session);
|
|
4403
4552
|
const defenseEvents = latestDefenseEvents(session);
|
|
4553
|
+
const preflight = latestPreflight(session);
|
|
4404
4554
|
const [open, setOpen] = React.useState(false);
|
|
4405
4555
|
const [tab, setTab] = React.useState("live");
|
|
4406
4556
|
const [expandedThreads, setExpandedThreads] = React.useState(/* @__PURE__ */ new Set());
|
|
@@ -4495,7 +4645,7 @@ function ObservatoryPanel(props) {
|
|
|
4495
4645
|
const exportReport = (format) => {
|
|
4496
4646
|
if (!manifest) return;
|
|
4497
4647
|
const extra = { qualityGate, experienceBank, defenseEvents };
|
|
4498
|
-
const content = format === "sarif" ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2) : renderRunReportMarkdown(manifest, extra, { lang: getLang() });
|
|
4648
|
+
const content = format === "sarif" ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2) : format === "json" ? JSON.stringify(renderRunReportJson(manifest, extra), null, 2) : renderRunReportMarkdown(manifest, extra, { lang: getLang() });
|
|
4499
4649
|
const filename = runReportFilename(format);
|
|
4500
4650
|
const key = `report-${format}`;
|
|
4501
4651
|
const flash = () => {
|
|
@@ -5552,6 +5702,195 @@ function ObservatoryPanel(props) {
|
|
|
5552
5702
|
eventRows.length > 0 ? React.createElement("div", {}, ...eventRows) : React.createElement("div", { className: "iterate-obs-empty" }, t("\u6CA1\u6709\u5339\u914D\u8BE5\u7C7B\u578B\u7684\u9632\u5FA1\u4E8B\u4EF6"))
|
|
5553
5703
|
);
|
|
5554
5704
|
};
|
|
5705
|
+
const renderPreflight = () => {
|
|
5706
|
+
const preflightInstruction = buildPreflightInstruction();
|
|
5707
|
+
const report = preflight;
|
|
5708
|
+
const hasReport = report !== null && Array.isArray(report.checks);
|
|
5709
|
+
const ready = report !== null && report.ready === true;
|
|
5710
|
+
const blocking = Number(report && report.blocking != null ? report.blocking : 0);
|
|
5711
|
+
const warnings = Number(report && report.warnings != null ? report.warnings : 0);
|
|
5712
|
+
const statusLabel = !hasReport ? t("\u5F85\u9884\u68C0") : ready ? t("\u5C31\u7EEA") : t("\u672A\u5C31\u7EEA");
|
|
5713
|
+
const statusTone = !hasReport ? "pending" : ready ? "pass" : "fail";
|
|
5714
|
+
const headerBar = React.createElement(
|
|
5715
|
+
"div",
|
|
5716
|
+
{ className: "iterate-obs-bar", style: { marginBottom: 8 } },
|
|
5717
|
+
React.createElement("b", {}, t("\u5F00\u8DD1\u9884\u68C0")),
|
|
5718
|
+
React.createElement("span", { className: "iterate-gate", "data-status": statusTone }, statusLabel),
|
|
5719
|
+
React.createElement("button", {
|
|
5720
|
+
className: "iterate-btn",
|
|
5721
|
+
"data-primary": "",
|
|
5722
|
+
"data-copied": copiedKey === "preflight" ? "" : void 0,
|
|
5723
|
+
onClick: () => copyInstruction("preflight", preflightInstruction),
|
|
5724
|
+
title: t("\u590D\u5236 iterate_preflight \u9884\u68C0\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09")
|
|
5725
|
+
}, copiedKey === "preflight" ? t("\u5DF2\u590D\u5236") : t("\u8FD0\u884C\u9884\u68C0"))
|
|
5726
|
+
);
|
|
5727
|
+
if (!hasReport) {
|
|
5728
|
+
return React.createElement(
|
|
5729
|
+
"div",
|
|
5730
|
+
{},
|
|
5731
|
+
headerBar,
|
|
5732
|
+
React.createElement(
|
|
5733
|
+
"div",
|
|
5734
|
+
{ className: "iterate-obs-block" },
|
|
5735
|
+
React.createElement(
|
|
5736
|
+
"div",
|
|
5737
|
+
{ className: "iterate-obs-block-head" },
|
|
5738
|
+
React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
|
|
5739
|
+
React.createElement("span", { className: "iterate-obs-badge" }, "v3.12")
|
|
5740
|
+
),
|
|
5741
|
+
React.createElement(
|
|
5742
|
+
"div",
|
|
5743
|
+
{ className: "iterate-obs-block-body" },
|
|
5744
|
+
React.createElement(
|
|
5745
|
+
"div",
|
|
5746
|
+
{ className: "iterate-obs-msg" },
|
|
5747
|
+
// Gap #4: separate "pulled and empty" from "never pulled".
|
|
5748
|
+
diskEmptyStateText(toolCalledInSession(session, "iterate_preflight"), t("\u9884\u68C0\u7ED3\u679C"), getLang())
|
|
5749
|
+
),
|
|
5750
|
+
React.createElement(
|
|
5751
|
+
"div",
|
|
5752
|
+
{ className: "iterate-obs-msg", style: { marginTop: 6 } },
|
|
5753
|
+
t("\u5F00\u8DD1\u524D\u5148\u9884\u68C0\uFF1A\u786E\u8BA4\u9A8C\u8BC1\u547D\u4EE4\u3001Git\u3001\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\u90FD\u5DF2\u5C31\u7EEA\uFF0C\u907F\u514D\u5F00\u8DD1\u540E\u624D\u53D1\u73B0\u73AF\u5883\u95EE\u9898\u3002")
|
|
5754
|
+
),
|
|
5755
|
+
React.createElement(
|
|
5756
|
+
"div",
|
|
5757
|
+
{ className: "iterate-obs-bar", style: { marginTop: 8 } },
|
|
5758
|
+
React.createElement("button", {
|
|
5759
|
+
className: "iterate-btn",
|
|
5760
|
+
"data-copied": copiedKey === "preflight-instr" ? "" : void 0,
|
|
5761
|
+
onClick: () => copyInstruction("preflight-instr", preflightInstruction),
|
|
5762
|
+
title: t("\u590D\u5236\u9884\u68C0\u6307\u4EE4")
|
|
5763
|
+
}, copiedKey === "preflight-instr" ? t("\u5DF2\u590D\u5236") : t("\u590D\u5236\u9884\u68C0\u6307\u4EE4"))
|
|
5764
|
+
)
|
|
5765
|
+
)
|
|
5766
|
+
)
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
if (report && report.ok === false) {
|
|
5770
|
+
return React.createElement(
|
|
5771
|
+
"div",
|
|
5772
|
+
{},
|
|
5773
|
+
headerBar,
|
|
5774
|
+
React.createElement(
|
|
5775
|
+
"div",
|
|
5776
|
+
{ className: "iterate-obs-block" },
|
|
5777
|
+
React.createElement(
|
|
5778
|
+
"div",
|
|
5779
|
+
{ className: "iterate-obs-block-head" },
|
|
5780
|
+
React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
|
|
5781
|
+
React.createElement("span", { className: "iterate-obs-badge" }, "v3.12")
|
|
5782
|
+
),
|
|
5783
|
+
React.createElement(
|
|
5784
|
+
"div",
|
|
5785
|
+
{ className: "iterate-obs-block-body" },
|
|
5786
|
+
React.createElement(
|
|
5787
|
+
"div",
|
|
5788
|
+
{ className: "iterate-obs-msg" },
|
|
5789
|
+
tf("\u9884\u68C0\u5931\u8D25\uFF1A{a0}", { a0: String(report.error || "") })
|
|
5790
|
+
)
|
|
5791
|
+
)
|
|
5792
|
+
)
|
|
5793
|
+
);
|
|
5794
|
+
}
|
|
5795
|
+
const CHECK_META = {
|
|
5796
|
+
validation: {
|
|
5797
|
+
label: t("\u9A8C\u8BC1\u547D\u4EE4"),
|
|
5798
|
+
detail: (d) => {
|
|
5799
|
+
const n = Number(d.commandCount ?? 0);
|
|
5800
|
+
return n > 0 ? tf("\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4", { a0: n }) : t("\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879");
|
|
5801
|
+
}
|
|
5802
|
+
},
|
|
5803
|
+
git: {
|
|
5804
|
+
label: t("Git \u4ED3\u5E93"),
|
|
5805
|
+
detail: (d) => d.isRepo === true ? t("\u5DF2\u68C0\u6D4B\u5230 Git \u4ED3\u5E93") : t("\u4E0D\u662F Git \u4ED3\u5E93 \u2014\u2014 changed-only \u8303\u56F4\u4E0E iterate_diff \u9700\u8981\u5B83")
|
|
5806
|
+
},
|
|
5807
|
+
"dirty-tree": {
|
|
5808
|
+
label: t("\u5DE5\u4F5C\u533A"),
|
|
5809
|
+
detail: (d) => {
|
|
5810
|
+
const n = d.changedFiles;
|
|
5811
|
+
if (n === null || n === void 0) return t("\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u672A\u77E5");
|
|
5812
|
+
const num = Number(n);
|
|
5813
|
+
return num > 0 ? tf("{a0} \u4E2A\u672A\u63D0\u4EA4/\u672A\u8DDF\u8E2A\u6587\u4EF6 \u2014\u2014 \u5EFA\u8BAE\u5148 commit \u6216 stash", { a0: num }) : t("\u5DE5\u4F5C\u533A\u5E72\u51C0");
|
|
5814
|
+
}
|
|
5815
|
+
},
|
|
5816
|
+
"stale-gate": {
|
|
5817
|
+
label: t("\u9648\u65E7\u95E8\u7981"),
|
|
5818
|
+
detail: (d) => {
|
|
5819
|
+
const s = d.overallStatus;
|
|
5820
|
+
if (s === "fail") return t("\u78C1\u76D8\u4E0A\u6B8B\u7559 FAIL \u8D28\u91CF\u95E8\u7981 \u2014\u2014 \u5F00\u65B0\u8F6E\u524D\u5148\u6E05\u9664");
|
|
5821
|
+
return s ? tf("\u8D28\u91CF\u95E8\u7981\uFF1A{a0}", { a0: String(s) }) : t("\u65E0\u6301\u4E45\u5316\u8D28\u91CF\u95E8\u7981");
|
|
5822
|
+
}
|
|
5823
|
+
},
|
|
5824
|
+
"interrupted-checkpoint": {
|
|
5825
|
+
label: t("\u4E2D\u65AD\u65AD\u70B9"),
|
|
5826
|
+
detail: (d) => {
|
|
5827
|
+
if (d.interrupted === true) {
|
|
5828
|
+
const rc = Number(d.resumeCount ?? 0);
|
|
5829
|
+
return tf("\u5B58\u5728\u4E2D\u65AD\u65AD\u70B9\uFF08\u5DF2\u6062\u590D {a0} \u6B21\uFF09\u2014\u2014 \u6062\u590D\u6216\u6E05\u9664\u540E\u518D\u5F00\u8DD1", { a0: rc });
|
|
5830
|
+
}
|
|
5831
|
+
return t("\u65E0\u4E2D\u65AD\u65AD\u70B9");
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
};
|
|
5835
|
+
const fixFor = (id) => {
|
|
5836
|
+
switch (id) {
|
|
5837
|
+
case "validation":
|
|
5838
|
+
return { key: "preflight-fix-validation", label: t("\u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4"), instruction: buildConfigFieldInstruction("validation.commands") };
|
|
5839
|
+
case "stale-gate":
|
|
5840
|
+
return { key: "preflight-fix-gate", label: t("\u6E05\u9664\u95E8\u7981"), instruction: buildQualityGateClearInstruction() };
|
|
5841
|
+
case "interrupted-checkpoint":
|
|
5842
|
+
return { key: "preflight-fix-checkpoint", label: t("\u6062\u590D\u65AD\u70B9"), instruction: buildCheckpointResumeInstruction() };
|
|
5843
|
+
default:
|
|
5844
|
+
return null;
|
|
5845
|
+
}
|
|
5846
|
+
};
|
|
5847
|
+
const rows = (report.checks || []).map((c, i) => {
|
|
5848
|
+
const id = String(c.id || "");
|
|
5849
|
+
const status = String(c.status || "warn");
|
|
5850
|
+
const meta = CHECK_META[id];
|
|
5851
|
+
const label = meta ? meta.label : id || "?";
|
|
5852
|
+
const detail = meta ? meta.detail(c.detail || {}) : "";
|
|
5853
|
+
const fix = status !== "pass" ? fixFor(id) : null;
|
|
5854
|
+
return React.createElement(
|
|
5855
|
+
"div",
|
|
5856
|
+
{ key: `pf-${i}`, className: "iterate-obs-row" },
|
|
5857
|
+
React.createElement("span", { className: "iterate-gate", "data-status": status }, status.toUpperCase()),
|
|
5858
|
+
React.createElement("b", {}, label),
|
|
5859
|
+
React.createElement("span", { className: "iterate-obs-msg" }, detail),
|
|
5860
|
+
fix ? React.createElement("button", {
|
|
5861
|
+
className: "iterate-btn",
|
|
5862
|
+
"data-copied": copiedKey === fix.key ? "" : void 0,
|
|
5863
|
+
onClick: () => copyInstruction(fix.key, fix.instruction),
|
|
5864
|
+
title: fix.instruction
|
|
5865
|
+
}, copiedKey === fix.key ? t("\u5DF2\u590D\u5236") : fix.label) : null
|
|
5866
|
+
);
|
|
5867
|
+
});
|
|
5868
|
+
return React.createElement(
|
|
5869
|
+
"div",
|
|
5870
|
+
{},
|
|
5871
|
+
headerBar,
|
|
5872
|
+
React.createElement(
|
|
5873
|
+
"div",
|
|
5874
|
+
{ className: "iterate-obs-block" },
|
|
5875
|
+
React.createElement(
|
|
5876
|
+
"div",
|
|
5877
|
+
{ className: "iterate-obs-block-head" },
|
|
5878
|
+
React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
|
|
5879
|
+
React.createElement(
|
|
5880
|
+
"span",
|
|
5881
|
+
{ className: "iterate-obs-head-meta" },
|
|
5882
|
+
tf("\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}", { a0: blocking, a1: warnings })
|
|
5883
|
+
)
|
|
5884
|
+
),
|
|
5885
|
+
React.createElement(
|
|
5886
|
+
"div",
|
|
5887
|
+
{ className: "iterate-obs-block-body" },
|
|
5888
|
+
React.createElement("div", { className: "iterate-obs-msg" }, t("\u9884\u68C0\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u4FEE\u6539\u914D\u7F6E\u6216\u6587\u4EF6\u3002")),
|
|
5889
|
+
...rows
|
|
5890
|
+
)
|
|
5891
|
+
)
|
|
5892
|
+
);
|
|
5893
|
+
};
|
|
5555
5894
|
const renderLive = () => {
|
|
5556
5895
|
const liveEntries = manifest.live || [];
|
|
5557
5896
|
if (liveEntries.length === 0) {
|
|
@@ -5648,6 +5987,8 @@ function ObservatoryPanel(props) {
|
|
|
5648
5987
|
return renderExperienceBank();
|
|
5649
5988
|
case "f10":
|
|
5650
5989
|
return renderDefenseEvents();
|
|
5990
|
+
case "f11":
|
|
5991
|
+
return renderPreflight();
|
|
5651
5992
|
default:
|
|
5652
5993
|
return null;
|
|
5653
5994
|
}
|
|
@@ -5711,8 +6052,8 @@ function ObservatoryPanel(props) {
|
|
|
5711
6052
|
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")
|
|
5712
6053
|
}, copiedKey === "export" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA JSON")),
|
|
5713
6054
|
// v3.9: portable report export — Markdown for a PR body, SARIF for
|
|
5714
|
-
// CI / GitHub Code Scanning
|
|
5715
|
-
// the `iterate_report` tool.
|
|
6055
|
+
// CI / GitHub Code Scanning, JSON for CI dashboards / PR bots.
|
|
6056
|
+
// Rendered from the same data model as the `iterate_report` tool.
|
|
5716
6057
|
React.createElement("button", {
|
|
5717
6058
|
className: "iterate-btn",
|
|
5718
6059
|
"data-copied": copiedKey === "report-markdown" ? "" : void 0,
|
|
@@ -5725,6 +6066,15 @@ function ObservatoryPanel(props) {
|
|
|
5725
6066
|
onClick: () => exportReport("sarif"),
|
|
5726
6067
|
title: t("\u5BFC\u51FA SARIF 2.1.0\uFF08\u4F9B CI / GitHub Code Scanning \u6D88\u8D39\uFF09")
|
|
5727
6068
|
}, copiedKey === "report-sarif" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA SARIF")),
|
|
6069
|
+
// v3.11: lossless JSON export — the complete normalized run model
|
|
6070
|
+
// (findings + fixes + validations + quality gate + experience bank
|
|
6071
|
+
// + defense events) for CI dashboards and PR bots.
|
|
6072
|
+
React.createElement("button", {
|
|
6073
|
+
className: "iterate-btn",
|
|
6074
|
+
"data-copied": copiedKey === "report-json" ? "" : void 0,
|
|
6075
|
+
onClick: () => exportReport("json"),
|
|
6076
|
+
title: t("\u5BFC\u51FA\u5B8C\u6574\u8FD0\u884C\u6A21\u578B\u7684 JSON\uFF08\u4F9B CI \u4EEA\u8868\u76D8 / PR \u673A\u5668\u4EBA\u6D88\u8D39\uFF0C\u65E0\u635F\u683C\u5F0F\uFF09")
|
|
6077
|
+
}, copiedKey === "report-json" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA JSON \u62A5\u544A")),
|
|
5728
6078
|
// Gap #4: `.iterate/` artifacts are unreachable from the browser;
|
|
5729
6079
|
// this copies the one instruction that makes the model echo them
|
|
5730
6080
|
// into the session stream (which is what every panel scans).
|
package/lib/parse.js
CHANGED
|
@@ -1124,6 +1124,88 @@ export function scanSessionForDefenseEvents(session) {
|
|
|
1124
1124
|
return loose ? normalizeDefenseEventsResult(loose) : null
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
|
+
// ─── Preflight readiness report detection ────────────────────────────────────
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Check whether `obj` is an `iterate_preflight` report.
|
|
1131
|
+
*
|
|
1132
|
+
* Discriminator: a boolean `ready`, an array `checks`, and a numeric
|
|
1133
|
+
* `blocking`/`warnings` counter. The `ok:false` error shape (empty checks)
|
|
1134
|
+
* still matches so the panel can render the failure instead of claiming
|
|
1135
|
+
* "not pulled yet".
|
|
1136
|
+
*
|
|
1137
|
+
* @param {unknown} obj
|
|
1138
|
+
* @returns {boolean}
|
|
1139
|
+
*/
|
|
1140
|
+
export function isPreflightReport(obj) {
|
|
1141
|
+
if (!obj || typeof obj !== 'object') return false
|
|
1142
|
+
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
1143
|
+
const ready = safeGet(o, 'ready')
|
|
1144
|
+
const checks = safeGet(o, 'checks')
|
|
1145
|
+
const blocking = safeGet(o, 'blocking')
|
|
1146
|
+
const warnings = safeGet(o, 'warnings')
|
|
1147
|
+
return (
|
|
1148
|
+
typeof ready === 'boolean' &&
|
|
1149
|
+
Array.isArray(checks) &&
|
|
1150
|
+
(typeof blocking === 'number' || typeof warnings === 'number')
|
|
1151
|
+
)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Deep-find the first `iterate_preflight` report node inside `obj`. */
|
|
1155
|
+
function findPreflightReportInObject(obj) {
|
|
1156
|
+
return findFirstInObject(obj, (o) => isPreflightReport(o))
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Normalize an `iterate_preflight` report into a JSON-safe object.
|
|
1161
|
+
*
|
|
1162
|
+
* @param {Record<string, unknown> | null | undefined} raw
|
|
1163
|
+
* @returns {Record<string, unknown>}
|
|
1164
|
+
*/
|
|
1165
|
+
export function normalizePreflightReport(raw) {
|
|
1166
|
+
const src = raw && typeof raw === 'object'
|
|
1167
|
+
? /** @type {Record<string, unknown>} */ (raw)
|
|
1168
|
+
: {}
|
|
1169
|
+
const asNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
|
|
1170
|
+
const asStr = (v) => (typeof v === 'string' ? v : '')
|
|
1171
|
+
const checks = Array.isArray(safeGet(src, 'checks')) ? safeGet(src, 'checks') : []
|
|
1172
|
+
return {
|
|
1173
|
+
ok: safeGet(src, 'ok') !== false,
|
|
1174
|
+
ready: safeGet(src, 'ready') === true,
|
|
1175
|
+
blocking: asNum(safeGet(src, 'blocking')),
|
|
1176
|
+
warnings: asNum(safeGet(src, 'warnings')),
|
|
1177
|
+
error: asStr(safeGet(src, 'error')) || null,
|
|
1178
|
+
checks: (/** @type {unknown[]} */ (checks)).map((c) => {
|
|
1179
|
+
const rec = /** @type {Record<string, unknown>} */ (c && typeof c === 'object' ? c : {})
|
|
1180
|
+
const status = safeGet(rec, 'status')
|
|
1181
|
+
const detail = safeGet(rec, 'detail')
|
|
1182
|
+
return {
|
|
1183
|
+
id: asStr(safeGet(rec, 'id')),
|
|
1184
|
+
status: status === 'pass' || status === 'warn' || status === 'fail' ? status : 'warn',
|
|
1185
|
+
detail: detail && typeof detail === 'object' ? detail : {},
|
|
1186
|
+
}
|
|
1187
|
+
}),
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Scan a session snapshot for the latest `iterate_preflight` result and return
|
|
1193
|
+
* its normalized report (or null).
|
|
1194
|
+
*
|
|
1195
|
+
* @param {unknown} session
|
|
1196
|
+
* @returns {Record<string, unknown> | null}
|
|
1197
|
+
*/
|
|
1198
|
+
export function scanSessionForPreflight(session) {
|
|
1199
|
+
const node = latestToolResultNode(session, 'iterate_preflight', findPreflightReportInObject)
|
|
1200
|
+
if (node !== null) {
|
|
1201
|
+
const found = findPreflightReportInObject(node)
|
|
1202
|
+
if (found) return normalizePreflightReport(found)
|
|
1203
|
+
}
|
|
1204
|
+
// Same deep-scan fallback as the quality-gate scanner (see above).
|
|
1205
|
+
const loose = findFirstInObject(session, isPreflightReport)
|
|
1206
|
+
return loose ? normalizePreflightReport(loose) : null
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1127
1209
|
// ─── Run-summary / meta-review verdict detection ─────────────────────────────
|
|
1128
1210
|
|
|
1129
1211
|
/**
|
|
@@ -1857,6 +1939,21 @@ export function buildQualityGateClearInstruction() {
|
|
|
1857
1939
|
)
|
|
1858
1940
|
}
|
|
1859
1941
|
|
|
1942
|
+
/**
|
|
1943
|
+
* `iterate_preflight` readiness-check payload (runs the pre-run checklist).
|
|
1944
|
+
*
|
|
1945
|
+
* @returns {string}
|
|
1946
|
+
*/
|
|
1947
|
+
export function buildPreflightInstruction() {
|
|
1948
|
+
return (
|
|
1949
|
+
'请调用 `iterate_preflight` 运行开跑前预检(只读:校验 validation.commands、git、脏工作区、' +
|
|
1950
|
+
'陈旧质量门禁与中断断点):\n\n' +
|
|
1951
|
+
'```json\n' +
|
|
1952
|
+
`${JSON.stringify({}, null, 2)}\n` +
|
|
1953
|
+
'```'
|
|
1954
|
+
)
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1860
1957
|
/**
|
|
1861
1958
|
* `iterate_rollback` payload for one applied fix id.
|
|
1862
1959
|
*
|
|
@@ -2310,14 +2407,63 @@ export function buildCompletionSummary(report, lang = 'zh') {
|
|
|
2310
2407
|
const stats = severityStats(report)
|
|
2311
2408
|
const converged = conv.converged === true
|
|
2312
2409
|
const totalFindings = stats.critical + stats.high + stats.medium + stats.low
|
|
2410
|
+
// The persisted `stoppedReason` is the authoritative "why" — a validation
|
|
2411
|
+
// abort or a schema failure must NOT be reported as "reached the round
|
|
2412
|
+
// limit". A normalized report nests it under `convergence.stoppedReason`
|
|
2413
|
+
// (normalizeReport); a raw harness report may carry it at the top level.
|
|
2414
|
+
// Fall back to the convergence flag only when no reason was stored (older
|
|
2415
|
+
// transcripts predate the field).
|
|
2416
|
+
const stopped = typeof conv.stoppedReason === 'string'
|
|
2417
|
+
? conv.stoppedReason
|
|
2418
|
+
: typeof report.stoppedReason === 'string'
|
|
2419
|
+
? report.stoppedReason
|
|
2420
|
+
: ''
|
|
2421
|
+
const reason = stopped
|
|
2422
|
+
? completionReason(stopped, lang, total)
|
|
2423
|
+
: converged
|
|
2424
|
+
? (lang === 'en' ? 'converged' : '已收敛')
|
|
2425
|
+
: (lang === 'en' ? `reached the round limit ${total}` : `已达最大轮数 ${total}`)
|
|
2313
2426
|
if (lang === 'en') {
|
|
2314
|
-
const reason = converged ? 'converged' : `reached the round limit ${total}`
|
|
2315
2427
|
return `iterate review complete \u00b7 ${rounds}/${total} rounds \u00b7 ${totalFindings} findings \u00b7 ${reason}`
|
|
2316
2428
|
}
|
|
2317
|
-
const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
|
|
2318
2429
|
return `iterate 评审完成 · ${rounds}/${total} 轮 · ${totalFindings} 项发现 · ${reason}`
|
|
2319
2430
|
}
|
|
2320
2431
|
|
|
2432
|
+
/**
|
|
2433
|
+
* Compact "why the run ended" phrase for the completion summary.
|
|
2434
|
+
* Mirrors `stoppedReasonLabel` but drops the "已结束 · " prefix so it reads
|
|
2435
|
+
* inside the one-line summary. Unknown reasons degrade to the raw value.
|
|
2436
|
+
*
|
|
2437
|
+
* @param {string} reason
|
|
2438
|
+
* @param {string} lang
|
|
2439
|
+
* @param {number} total
|
|
2440
|
+
* @returns {string}
|
|
2441
|
+
*/
|
|
2442
|
+
function completionReason(reason, lang, total) {
|
|
2443
|
+
const zh = {
|
|
2444
|
+
converged: '已收敛',
|
|
2445
|
+
max_rounds_reached: `已达最大轮数 ${total}`,
|
|
2446
|
+
aborted_by_validation: '验证失败后回滚',
|
|
2447
|
+
aborted_by_config: '验证命令未在白名单',
|
|
2448
|
+
inconclusive: '轮次结论不明',
|
|
2449
|
+
schema_invalid: '审查输出 schema 校验失败',
|
|
2450
|
+
no_usable_reviewer_output: '无可用审查输出',
|
|
2451
|
+
}
|
|
2452
|
+
const en = {
|
|
2453
|
+
converged: 'converged',
|
|
2454
|
+
max_rounds_reached: `reached the round limit ${total}`,
|
|
2455
|
+
aborted_by_validation: 'rolled back after a validation failure',
|
|
2456
|
+
aborted_by_config: 'validation command not allow-listed',
|
|
2457
|
+
inconclusive: 'inconclusive rounds',
|
|
2458
|
+
schema_invalid: 'review output failed schema validation',
|
|
2459
|
+
no_usable_reviewer_output: 'no usable review output',
|
|
2460
|
+
}
|
|
2461
|
+
const table = lang === 'en' ? en : zh
|
|
2462
|
+
// Object.hasOwn, NOT `table[reason]`: a hand-edited `__proto__` reason must
|
|
2463
|
+
// not resolve through the prototype chain to a non-string.
|
|
2464
|
+
return Object.hasOwn(table, reason) ? table[reason] : reason
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2321
2467
|
// ─── Config edit guidance ───────────────────────────────────────────────────
|
|
2322
2468
|
|
|
2323
2469
|
/**
|