iterate-plugin 2.9.4 → 2.11.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
@@ -40,7 +40,10 @@ var SEVERITY_LABEL = {
40
40
  var SEVERITY_COLOR = {
41
41
  critical: "#ef4444",
42
42
  high: "#f97316",
43
- medium: "#eab308",
43
+ // medium is used both for dots/fills and as TEXT (stat numbers, table
44
+ // headers); #eab308 is illegible as text on light backgrounds (~1.6:1).
45
+ // amber-600 (#d97706) ~3.2:1 — still short of AA; go darker for legibility.
46
+ medium: "#b45309",
44
47
  low: "#6b7280"
45
48
  };
46
49
  function safeGet(o, key) {
@@ -194,8 +197,6 @@ function findReportInObject(obj, seen, maxDepth = 20) {
194
197
  }
195
198
  function scanSessionForReport(session) {
196
199
  if (!session || typeof session !== "object") return null;
197
- const direct = findReportInObject(session);
198
- if (direct) return direct;
199
200
  const s = (
200
201
  /** @type {Record<string, unknown>} */
201
202
  session
@@ -304,8 +305,6 @@ function findRunSummaryInObject(obj, seen, maxDepth = 20) {
304
305
  }
305
306
  function scanSessionForRunSummary(session) {
306
307
  if (!session || typeof session !== "object") return null;
307
- const direct = findRunSummaryInObject(session);
308
- if (direct) return direct;
309
308
  const s = (
310
309
  /** @type {Record<string, unknown>} */
311
310
  session
@@ -503,22 +502,24 @@ function buildTriageState(report) {
503
502
  return state;
504
503
  }
505
504
  function hashReport(report) {
506
- const convergence = (
507
- /** @type {Record<string, unknown>} */
508
- report.convergence ?? {}
509
- );
510
- const totalRounds = String(convergence.totalRounds ?? "");
511
- const findingsCount = String(
512
- /** @type {Array<unknown>} */
513
- (report.findings ?? []).length
514
- );
515
- const firstFinding = (
505
+ const findings = (
516
506
  /** @type {Array<Record<string, unknown>>} */
517
- (report.findings ?? [])[0]
507
+ report.findings ?? []
518
508
  );
519
- const firstSummary = firstFinding ? String(firstFinding.summary ?? "") : "";
520
- const mode = String(report.mode ?? "");
521
- return `iterate-triage-${mode}-${totalRounds}-${findingsCount}-${firstSummary.slice(0, 20)}`;
509
+ let h = 2166136261;
510
+ const mix = (s) => {
511
+ for (let i = 0; i < s.length; i++) {
512
+ h ^= s.charCodeAt(i);
513
+ h = Math.imul(h, 16777619) >>> 0;
514
+ }
515
+ };
516
+ mix(`${String(report.mode ?? "")}|`);
517
+ for (const f of findings) {
518
+ if (!f || typeof f !== "object") continue;
519
+ mix(`${String(f.file ?? "")}|${typeof f.line === "number" ? f.line : 0}|${String(f.dimension ?? "")}|${String(f.summary ?? "")}
520
+ `);
521
+ }
522
+ return `iterate-triage-${h.toString(36)}`;
522
523
  }
523
524
  function toKnownIntentionalYaml(entries) {
524
525
  if (!entries || entries.length === 0) return "";
@@ -955,6 +956,27 @@ var ITERATE_CSS = `
955
956
  .iterate-switch[data-on] { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); }
956
957
  .iterate-switch[data-on] .iterate-switch-knob { transform: translateX(18px); background: #FFFFFF; }
957
958
 
959
+ /* Shared keyboard focus ring for every iterate interactive control */
960
+ .iterate-btn:focus-visible, .iterate-vbtn:focus-visible, .iterate-batch-btn:focus-visible,
961
+ .iterate-filter-select:focus-visible, .iterate-filter-search:focus-visible,
962
+ .iterate-finding:focus-visible, .iterate-switch:focus-visible {
963
+ outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px;
964
+ }
965
+
966
+ /* Dashboard empty/onboarding state */
967
+ .iterate-dashboard-empty { opacity: 0.75; }
968
+ .iterate-empty-hint { font-size: 12px; color: var(--dsw-alias-label-secondary); }
969
+
970
+ /* Convergence-completed progress fill */
971
+ .iterate-progress-fill-done { background: var(--dsw-alias-state-success-primary); }
972
+
973
+ /* Batch scope segmented control */
974
+ .iterate-batch-scope { opacity: 0.6; }
975
+ .iterate-batch-scope-on { opacity: 1; border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-label-primary); }
976
+
977
+ /* Overflow dimension chip */
978
+ .iterate-dim-more { opacity: 0.7; font-style: italic; }
979
+
958
980
  /* Button variants */
959
981
  .iterate-btn[data-ghost] { background: transparent; }
960
982
  .iterate-btn[data-danger] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); color: var(--dsw-alias-state-error-primary); background: transparent; }
@@ -1025,13 +1047,12 @@ function removeStorageByPrefix(prefix) {
1025
1047
  }
1026
1048
  function copyText(text) {
1027
1049
  if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
1028
- navigator.clipboard.writeText(text).then(
1050
+ return navigator.clipboard.writeText(text).then(
1029
1051
  () => true,
1030
1052
  () => false
1031
1053
  );
1032
- return true;
1033
1054
  }
1034
- return false;
1055
+ return Promise.resolve(false);
1035
1056
  }
1036
1057
  var SEVERITY_KEYS = ["critical", "high", "medium", "low"];
1037
1058
  function coerceSeverity(severity) {
@@ -1111,7 +1132,12 @@ function TrendChart({ points }) {
1111
1132
  style: { height: `${Math.max(4, Math.round(p.count / max * 24))}px` }
1112
1133
  })
1113
1134
  );
1114
- return React.createElement("div", { className: "iterate-trend", title: "\u5404\u8F6E\u53D1\u73B0\u6570\u91CF\u8D8B\u52BF" }, ...bars);
1135
+ const summary = points.map((p) => `Round ${p.round}: ${p.count}`).join(", ");
1136
+ return React.createElement("div", {
1137
+ className: "iterate-trend",
1138
+ role: "img",
1139
+ "aria-label": `\u5404\u8F6E\u53D1\u73B0\u6570\u91CF\u8D8B\u52BF\uFF1A${summary}`
1140
+ }, ...bars);
1115
1141
  }
1116
1142
  function ConvergenceDashboard(props) {
1117
1143
  const [pulseKey, setPulseKey] = React.useState(0);
@@ -1124,7 +1150,14 @@ function ConvergenceDashboard(props) {
1124
1150
  emitRoundPulse(cur, conv?.converged === true);
1125
1151
  setPulseKey((k) => k + 1);
1126
1152
  }, [report && hashReport(report) + ":" + getCurrentRound(report)]);
1127
- if (!report) return null;
1153
+ if (!report) {
1154
+ return React.createElement(
1155
+ "div",
1156
+ { "data-iterate-root": "", "data-iterate": "dashboard", className: "iterate-dashboard iterate-dashboard-empty" },
1157
+ React.createElement("span", { className: "iterate-round-badge" }, "iterate"),
1158
+ React.createElement("span", { className: "iterate-empty-hint" }, "\u8FD0\u884C\u4E00\u6B21\u8BC4\u5BA1\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u6536\u655B\u8FDB\u5EA6\u4E0E\u53D1\u73B0\u7EDF\u8BA1\u3002\u8BD5\u8BD5\u300Creview this project\u300D\u6216\u300C/iterate review-only\u300D")
1159
+ );
1160
+ }
1128
1161
  const round = getCurrentRound(report);
1129
1162
  const total = getTotalRounds(report);
1130
1163
  const progress = computeConvergenceProgress(report);
@@ -1143,13 +1176,24 @@ function ConvergenceDashboard(props) {
1143
1176
  key: "images",
1144
1177
  title: "\u4F1A\u8BDD\u4E2D\u68C0\u6D4B\u5230\u7528\u6237\u9644\u5E26\u7684\u56FE\u7247\uFF0C\u8BC4\u5BA1\u5C06\u4F5C\u4E3A\u89C6\u89C9\u8BC1\u636E\u53C2\u8003"
1145
1178
  }, `\u9644\u4EF6\u56FE\u7247 ${String(imageCount)}`) : null;
1146
- const dimBadges = Object.keys(dims).slice(0, 6).map(
1179
+ const dimNames = Object.keys(dims);
1180
+ const dimBadges = dimNames.slice(0, 6).map(
1147
1181
  (dim) => React.createElement(
1148
1182
  "span",
1149
1183
  { key: dim, className: "iterate-dim-badge" },
1150
1184
  `${dim} \xB7 ${dims[dim]?.length ?? 0}`
1151
1185
  )
1152
1186
  );
1187
+ const overflow = dimNames.length - 6;
1188
+ if (overflow > 0) {
1189
+ dimBadges.push(
1190
+ React.createElement(
1191
+ "span",
1192
+ { key: "+more", className: "iterate-dim-badge iterate-dim-more", title: dimNames.slice(6).join(", ") },
1193
+ `+${overflow} \u66F4\u591A`
1194
+ )
1195
+ );
1196
+ }
1153
1197
  const mode = report.mode;
1154
1198
  const summary = report.summary;
1155
1199
  const isNormal = mode === "normal";
@@ -1159,6 +1203,18 @@ function ConvergenceDashboard(props) {
1159
1203
  key: "fixes",
1160
1204
  title: "\u672C\u8F6E\u5DF2\u5E94\u7528\u7684\u539F\u5B50\u4FEE\u590D\u6570\uFF08\u6B63\u5E38\u6A21\u5F0F\uFF09"
1161
1205
  }, `${String(fixCount)} fixes`) : null;
1206
+ const converged = report.convergence && report.convergence.converged === true;
1207
+ const convChip = converged ? React.createElement("span", {
1208
+ className: "iterate-chip-resume",
1209
+ key: "converged",
1210
+ title: "\u5BA1\u67E5\u5DF2\u6536\u655B\uFF1A\u6700\u540E\u4E00\u8F6E\u672A\u53D1\u73B0\u65B0\u95EE\u9898"
1211
+ }, "\u2713 \u5DF2\u6536\u655B") : null;
1212
+ const sevMetric = (key, label) => React.createElement(
1213
+ "span",
1214
+ { className: "iterate-metric", key, title: label },
1215
+ React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR[key] } }),
1216
+ `${label} ${String(stats[key])}`
1217
+ );
1162
1218
  return React.createElement(
1163
1219
  "div",
1164
1220
  { "data-iterate-root": "", "data-iterate": "dashboard", className: "iterate-dashboard" },
@@ -1170,26 +1226,16 @@ function ConvergenceDashboard(props) {
1170
1226
  React.createElement(
1171
1227
  "div",
1172
1228
  { className: "iterate-progress" },
1173
- React.createElement("div", { className: "iterate-progress-fill", style: { width: `${progress}%` } })
1174
- ),
1175
- React.createElement(
1176
- "span",
1177
- { className: "iterate-metric" },
1178
- React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR.critical } }),
1179
- stats.critical
1180
- ),
1181
- React.createElement(
1182
- "span",
1183
- { className: "iterate-metric" },
1184
- React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR.high } }),
1185
- stats.high
1186
- ),
1187
- React.createElement(
1188
- "span",
1189
- { className: "iterate-metric" },
1190
- React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR.medium } }),
1191
- stats.medium
1229
+ React.createElement("div", {
1230
+ className: converged ? "iterate-progress-fill iterate-progress-fill-done" : "iterate-progress-fill",
1231
+ style: { width: `${progress}%` }
1232
+ })
1192
1233
  ),
1234
+ convChip,
1235
+ sevMetric("critical", "CRIT"),
1236
+ sevMetric("high", "HIGH"),
1237
+ sevMetric("medium", "MED"),
1238
+ sevMetric("low", "LOW"),
1193
1239
  fixBadge,
1194
1240
  resumeChip,
1195
1241
  imageChip,
@@ -1313,12 +1359,11 @@ function TriagePanel(props) {
1313
1359
  const [filter, setFilter] = React.useState({ severities: [], dimensions: [], search: "" });
1314
1360
  const [selected, setSelected] = React.useState(null);
1315
1361
  const [selectAll, setSelectAll] = React.useState(false);
1316
- const persistVerdicts = (next) => {
1317
- if (storage) storage.set(storageKey, JSON.stringify(next));
1318
- return next;
1319
- };
1362
+ React.useEffect(() => {
1363
+ if (storage) storage.set(storageKey, JSON.stringify(verdicts));
1364
+ }, [storageKey, verdicts]);
1320
1365
  const setVerdict = (index, verdict) => {
1321
- setVerdicts((prev) => persistVerdicts({ ...prev, [String(index)]: verdict }));
1366
+ setVerdicts((prev) => ({ ...prev, [String(index)]: verdict }));
1322
1367
  };
1323
1368
  const { filtered, indices } = filterFindingsWithIndices(findings, filter);
1324
1369
  const indicesKey = indices.join(",");
@@ -1331,21 +1376,20 @@ function TriagePanel(props) {
1331
1376
  const allIndices = allVerdictKeys(verdicts);
1332
1377
  const batchTarget = selectAll ? allIndices : indices;
1333
1378
  const applyBatch = (verdict) => {
1334
- setVerdicts((prev) => persistVerdicts(batchSetVerdict(prev, batchTarget, verdict)));
1335
- };
1336
- const applyBatchAll = (verdict) => {
1337
- setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, verdict)));
1379
+ setVerdicts((prev) => batchSetVerdict(prev, batchTarget, verdict));
1338
1380
  };
1339
1381
  const doResetVerdicts = () => {
1340
- setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, "keep")));
1382
+ setVerdicts((prev) => setAllVerdicts(prev, "keep"));
1341
1383
  setSelectAll(false);
1342
1384
  };
1343
1385
  React.useEffect(() => {
1344
1386
  const doc = typeof document !== "undefined" ? document : null;
1345
1387
  if (!doc) return;
1346
1388
  const onKeyDown = (ev) => {
1389
+ if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
1347
1390
  const t = ev.target;
1348
1391
  if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT")) return;
1392
+ if (t && typeof t.isContentEditable === "boolean" && t.isContentEditable) return;
1349
1393
  const verdict = keyToVerdict(ev.key);
1350
1394
  if (verdict && selected !== null && indices.includes(selected)) {
1351
1395
  ev.preventDefault();
@@ -1376,10 +1420,14 @@ function TriagePanel(props) {
1376
1420
  const doCopyYaml = () => {
1377
1421
  const yaml = toKnownIntentionalYaml(ignored);
1378
1422
  if (!yaml) return;
1379
- const ok = copyText(yaml);
1380
- setCopied(true);
1381
- setTimeout(() => setCopied(false), 1600);
1382
- if (!ok) setPayload(yaml);
1423
+ copyText(yaml).then((ok) => {
1424
+ if (ok) {
1425
+ setCopied(true);
1426
+ setTimeout(() => setCopied(false), 1600);
1427
+ } else {
1428
+ setPayload(yaml);
1429
+ }
1430
+ });
1383
1431
  };
1384
1432
  const doBuildInstruction = () => {
1385
1433
  const text = buildApplyInstruction(ignored);
@@ -1408,7 +1456,17 @@ function TriagePanel(props) {
1408
1456
  key: String(index),
1409
1457
  className: "iterate-finding",
1410
1458
  "data-selected": isSelected ? "" : void 0,
1411
- onClick: () => setSelected(index)
1459
+ role: "option",
1460
+ "aria-selected": isSelected,
1461
+ tabIndex: 0,
1462
+ onClick: () => setSelected(index),
1463
+ onFocus: () => setSelected(index),
1464
+ onKeyDown: (e) => {
1465
+ if (e.key === "Enter" || e.key === " ") {
1466
+ e.preventDefault();
1467
+ setSelected(index);
1468
+ }
1469
+ }
1412
1470
  },
1413
1471
  React.createElement(
1414
1472
  "div",
@@ -1435,7 +1493,7 @@ function TriagePanel(props) {
1435
1493
  React.createElement(
1436
1494
  "div",
1437
1495
  { className: "iterate-triage-head" },
1438
- React.createElement("span", {}, `Iterate \xB7 Findings \u5206\u8BCA (${filtered.length}/${findings.length})`),
1496
+ React.createElement("span", { role: "heading", "aria-level": 3 }, `Iterate \xB7 Findings \u5206\u8BCA (${filtered.length}/${findings.length})`),
1439
1497
  React.createElement("span", { className: "iterate-triage-hint" }, "y=\u4FEE\u590D \xB7 n=\u8DF3\u8FC7 \xB7 a=\u5DF2\u77E5\u6709\u610F \xB7 \u2191/\u2193 \u9009\u62E9")
1440
1498
  ),
1441
1499
  React.createElement(
@@ -1443,7 +1501,7 @@ function TriagePanel(props) {
1443
1501
  { className: "iterate-filter" },
1444
1502
  React.createElement(
1445
1503
  "select",
1446
- { className: "iterate-filter-select", value: filter.severities[0] || "", onChange: (e) => setSeverityFilter(e.target.value), title: "\u6309\u4E25\u91CD\u5EA6\u7B5B\u9009" },
1504
+ { className: "iterate-filter-select", value: filter.severities[0] || "", onChange: (e) => setSeverityFilter(e.target.value), "aria-label": "\u6309\u4E25\u91CD\u5EA6\u7B5B\u9009" },
1447
1505
  React.createElement("option", { value: "" }, "\u5168\u90E8\u4E25\u91CD\u5EA6"),
1448
1506
  ...options.severities.map(
1449
1507
  (s) => React.createElement("option", { key: s.value, value: s.value }, `${severityLabel(s.value)} (${s.count})`)
@@ -1451,7 +1509,7 @@ function TriagePanel(props) {
1451
1509
  ),
1452
1510
  React.createElement(
1453
1511
  "select",
1454
- { className: "iterate-filter-select", value: filter.dimensions[0] || "", onChange: (e) => setDimensionFilter(e.target.value), title: "\u6309\u7EF4\u5EA6\u7B5B\u9009" },
1512
+ { className: "iterate-filter-select", value: filter.dimensions[0] || "", onChange: (e) => setDimensionFilter(e.target.value), "aria-label": "\u6309\u7EF4\u5EA6\u7B5B\u9009" },
1455
1513
  React.createElement("option", { value: "" }, "\u5168\u90E8\u7EF4\u5EA6"),
1456
1514
  ...options.dimensions.map(
1457
1515
  (d) => React.createElement("option", { key: d.value, value: d.value }, `${d.value} (${d.count})`)
@@ -1461,6 +1519,7 @@ function TriagePanel(props) {
1461
1519
  className: "iterate-filter-search",
1462
1520
  type: "search",
1463
1521
  placeholder: "\u641C\u7D22\u6587\u4EF6 / \u6458\u8981\u2026",
1522
+ "aria-label": "\u641C\u7D22\u6587\u4EF6\u6216\u6458\u8981",
1464
1523
  value: filter.search,
1465
1524
  onChange: (e) => setSearchFilter(e.target.value)
1466
1525
  }),
@@ -1473,20 +1532,20 @@ function TriagePanel(props) {
1473
1532
  React.createElement(
1474
1533
  "div",
1475
1534
  { className: "iterate-batch" },
1476
- React.createElement("span", { className: "iterate-batch-label" }, "\u6279\u91CF\uFF1A"),
1477
- React.createElement(
1478
- "label",
1479
- { className: "iterate-batch-check", title: "\u52FE\u9009\u540E\u6279\u91CF\u6309\u94AE\u4F5C\u7528\u4E8E\u5168\u90E8 findings\uFF0C\u5426\u5219\u4EC5\u5F53\u524D\u53EF\u89C1" },
1480
- React.createElement("input", { type: "checkbox", checked: selectAll, onChange: (e) => setSelectAll(e.target.checked) }),
1481
- selectAll ? `\u5168\u90E8 ${allIndices.length}` : "\u5168\u9009"
1482
- ),
1483
- React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("keep") }, "\u5168\u90E8 y"),
1484
- React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("skip") }, "\u5168\u90E8 n"),
1485
- React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("ignore") }, "\u5168\u90E8 a"),
1486
- React.createElement("span", { className: "iterate-batch-label", style: { marginLeft: 8 } }, "\u5168\u90E8\uFF1A"),
1487
- React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatchAll("keep") }, "y"),
1488
- React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatchAll("skip") }, "n"),
1489
- React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatchAll("ignore") }, "a"),
1535
+ React.createElement("span", { className: "iterate-batch-label" }, "\u6279\u91CF\u4F5C\u7528\u4E8E\uFF1A"),
1536
+ React.createElement("button", {
1537
+ className: selectAll ? "iterate-batch-btn iterate-batch-scope" : "iterate-batch-btn iterate-batch-scope iterate-batch-scope-on",
1538
+ onClick: () => setSelectAll(false),
1539
+ title: "\u6279\u91CF\u6309\u94AE\u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u7B5B\u9009\u53EF\u89C1\u7684 findings"
1540
+ }, `\u53EF\u89C1 ${indices.length}`),
1541
+ React.createElement("button", {
1542
+ className: selectAll ? "iterate-batch-btn iterate-batch-scope iterate-batch-scope-on" : "iterate-batch-btn iterate-batch-scope",
1543
+ onClick: () => setSelectAll(true),
1544
+ title: "\u6279\u91CF\u6309\u94AE\u4F5C\u7528\u4E8E\u5168\u90E8 findings"
1545
+ }, `\u5168\u90E8 ${allIndices.length}`),
1546
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("keep") }, "y"),
1547
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("skip") }, "n"),
1548
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("ignore") }, "a"),
1490
1549
  React.createElement("button", { className: "iterate-batch-btn", onClick: doResetVerdicts, title: "\u628A\u6240\u6709\u5224\u5B9A\u6062\u590D\u4E3A\u9ED8\u8BA4 y\uFF08\u4FEE\u590D\uFF09" }, "\u91CD\u7F6E")
1491
1550
  ),
1492
1551
  ...rows,
@@ -1497,8 +1556,20 @@ function TriagePanel(props) {
1497
1556
  React.createElement(
1498
1557
  "span",
1499
1558
  { style: { display: "flex", gap: 6 } },
1500
- React.createElement("button", { className: "iterate-btn", "data-primary": "", "data-copied": copied ? "" : void 0, onClick: doCopyYaml }, copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236 known_intentional"),
1501
- React.createElement("button", { className: "iterate-btn", onClick: doBuildInstruction }, "\u751F\u6210\u5E94\u7528\u6307\u4EE4")
1559
+ React.createElement("button", {
1560
+ className: "iterate-btn",
1561
+ "data-primary": "",
1562
+ "data-copied": copied ? "" : void 0,
1563
+ onClick: doCopyYaml,
1564
+ disabled: ignoredCount === 0,
1565
+ title: ignoredCount === 0 ? "\u5F53\u524D\u6CA1\u6709\u6807\u8BB0\u4E3A\u300C\u5DF2\u77E5\u6709\u610F\u300D\u7684 finding" : "\u590D\u5236 known_intentional YAML"
1566
+ }, copied ? "\u5DF2\u590D\u5236" : `\u590D\u5236 known_intentional${ignoredCount > 0 ? `\uFF08${ignoredCount}\uFF09` : ""}`),
1567
+ React.createElement("button", {
1568
+ className: "iterate-btn",
1569
+ onClick: doBuildInstruction,
1570
+ disabled: ignoredCount === 0,
1571
+ title: ignoredCount === 0 ? "\u5F53\u524D\u6CA1\u6709\u6807\u8BB0\u4E3A\u300C\u5DF2\u77E5\u6709\u610F\u300D\u7684 finding" : "\u751F\u6210 iterate_triage \u5E94\u7528\u6307\u4EE4"
1572
+ }, "\u751F\u6210\u5E94\u7528\u6307\u4EE4")
1502
1573
  )
1503
1574
  ),
1504
1575
  payload ? React.createElement("div", { className: "iterate-payload" }, payload) : null
@@ -1607,8 +1678,9 @@ function SettingsPanel(_props) {
1607
1678
  setTimeout(() => setter(false), 1600);
1608
1679
  };
1609
1680
  const doCopy = (text, slot) => {
1610
- copyText(text);
1611
- flashCopied(slot);
1681
+ copyText(text).then((ok) => {
1682
+ if (ok) flashCopied(slot);
1683
+ });
1612
1684
  };
1613
1685
  const requestClear = () => {
1614
1686
  if (confirming) {
package/lib/parse.js CHANGED
@@ -24,7 +24,10 @@ export const SEVERITY_LABEL = {
24
24
  export const SEVERITY_COLOR = {
25
25
  critical: '#ef4444',
26
26
  high: '#f97316',
27
- medium: '#eab308',
27
+ // medium is used both for dots/fills and as TEXT (stat numbers, table
28
+ // headers); #eab308 is illegible as text on light backgrounds (~1.6:1).
29
+ // amber-600 (#d97706) ~3.2:1 — still short of AA; go darker for legibility.
30
+ medium: '#b45309',
28
31
  low: '#6b7280',
29
32
  }
30
33
 
@@ -250,13 +253,11 @@ export function findReportInObject(obj, seen, maxDepth = 20) {
250
253
  export function scanSessionForReport(session) {
251
254
  if (!session || typeof session !== 'object') return null
252
255
 
253
- // Try direct find first
254
- const direct = findReportInObject(session)
255
- if (direct) return direct
256
-
257
- // Try common session structures
258
256
  const s = /** @type {Record<string, unknown>} */ (session)
259
257
 
258
+ // LATEST-first scan: walk the chronological structures in reverse before the
259
+ // generic deep find, so a conversation with several reviews surfaces the
260
+ // most recent report — the generic find would return the FIRST match.
260
261
  // Common pattern: session.toolCalls[].result.report
261
262
  const toolCalls = safeGet(s, 'toolCalls')
262
263
  if (Array.isArray(toolCalls)) {
@@ -382,11 +383,10 @@ export function findRunSummaryInObject(obj, seen, maxDepth = 20) {
382
383
  export function scanSessionForRunSummary(session) {
383
384
  if (!session || typeof session !== 'object') return null
384
385
 
385
- const direct = findRunSummaryInObject(session)
386
- if (direct) return direct
387
-
388
386
  const s = /** @type {Record<string, unknown>} */ (session)
389
387
 
388
+ // LATEST-first: walk chronological structures in reverse before the generic
389
+ // deep find (which would return the FIRST match, i.e. the oldest run).
390
390
  // Common pattern: session.toolCalls[].result contains a run summary.
391
391
  const toolCalls = safeGet(s, 'toolCalls')
392
392
  if (Array.isArray(toolCalls)) {
@@ -654,14 +654,23 @@ export function buildTriageState(report) {
654
654
  * @returns {string}
655
655
  */
656
656
  export function hashReport(report) {
657
- const convergence = /** @type {Record<string, unknown>} */ (report.convergence ?? {})
658
- const totalRounds = String(convergence.totalRounds ?? '')
659
- const findingsCount = String((/** @type {Array<unknown>} */ (report.findings ?? [])).length)
660
- const firstFinding = /** @type {Array<Record<string, unknown>>} */ (report.findings ?? [])[0]
661
- const firstSummary = firstFinding ? String(firstFinding.summary ?? '') : ''
662
- const mode = String(report.mode ?? '')
663
- // Use mode + totalRounds + findingsCount + first 20 chars of first finding summary
664
- return `iterate-triage-${mode}-${totalRounds}-${findingsCount}-${firstSummary.slice(0, 20)}`
657
+ const findings = /** @type {Array<Record<string, unknown>>} */ (report.findings ?? [])
658
+ // FNV-1a over EVERY finding (file|line|dimension|summary) so two different
659
+ // reports never share a verdict store, while re-running the identical review
660
+ // restores the same verdicts.
661
+ let h = 0x811c9dc5
662
+ const mix = (s) => {
663
+ for (let i = 0; i < s.length; i++) {
664
+ h ^= s.charCodeAt(i)
665
+ h = Math.imul(h, 0x01000193) >>> 0
666
+ }
667
+ }
668
+ mix(`${String(report.mode ?? '')}|`)
669
+ for (const f of findings) {
670
+ if (!f || typeof f !== 'object') continue
671
+ mix(`${String(f.file ?? '')}|${typeof f.line === 'number' ? f.line : 0}|${String(f.dimension ?? '')}|${String(f.summary ?? '')}\n`)
672
+ }
673
+ return `iterate-triage-${h.toString(36)}`
665
674
  }
666
675
 
667
676
  // ─── Known-intentional YAML builder ──────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.9.4",
3
+ "version": "2.11.0",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus a dry-run pure-review mode with multi-round convergence and a meta-review that audits the report and emits a final review report.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,11 +61,11 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@deepseek-ai/cordis": "4.0.1",
64
- "@deepseek-ai/dsh-tools": "0.1.0-rc.8",
64
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.1",
65
65
  "js-yaml": "4.3.1"
66
66
  },
67
67
  "devDependencies": {
68
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
68
+ "@deepseek-ai/dsh-session": "0.1.1-rc.1",
69
69
  "@types/js-yaml": "4.0.9",
70
70
  "@types/node": "22.15.0",
71
71
  "@types/react": "19.2.2",