iterate-plugin 2.12.2 → 3.2.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.
Files changed (41) hide show
  1. package/README.md +28 -12
  2. package/README.zh-CN.md +2 -1
  3. package/dist/approval-gate.js +16 -2
  4. package/dist/config-loader.js +5 -0
  5. package/dist/git-scope.js +61 -7
  6. package/dist/index.js +16 -6
  7. package/dist/session-hooks.js +36 -11
  8. package/dist/skill-prompt.js +3 -0
  9. package/dist/tools/decision-log.js +10 -1
  10. package/dist/tools/defense-events.js +260 -0
  11. package/dist/tools/defense-store.js +97 -0
  12. package/dist/tools/experience-bank.js +248 -0
  13. package/dist/tools/experience-store.js +132 -0
  14. package/dist/tools/quality-gate.js +180 -0
  15. package/dist/tools/quality-store.js +174 -0
  16. package/lib/client.js +662 -103
  17. package/lib/parse.js +93 -0
  18. package/package.json +7 -6
  19. package/src/approval-gate.ts +14 -2
  20. package/src/client/index.ts +542 -49
  21. package/src/config-loader.ts +5 -0
  22. package/src/git-scope.ts +48 -7
  23. package/src/index.ts +16 -6
  24. package/src/session-hooks.ts +33 -11
  25. package/src/skill-prompt.ts +3 -0
  26. package/src/tools/checkpoint.ts +1 -1
  27. package/src/tools/config.ts +1 -1
  28. package/src/tools/decision-log.ts +11 -2
  29. package/src/tools/defense-events.ts +295 -0
  30. package/src/tools/defense-store.ts +113 -0
  31. package/src/tools/experience-bank.ts +264 -0
  32. package/src/tools/experience-store.ts +160 -0
  33. package/src/tools/fix.ts +1 -1
  34. package/src/tools/history.ts +1 -1
  35. package/src/tools/prune.ts +1 -1
  36. package/src/tools/quality-gate.ts +193 -0
  37. package/src/tools/quality-store.ts +199 -0
  38. package/src/tools/review.ts +1 -1
  39. package/src/tools/transcript.ts +1 -1
  40. package/src/tools/triage.ts +1 -1
  41. package/src/types.ts +118 -0
package/lib/client.js CHANGED
@@ -994,6 +994,50 @@ function buildRuntimeStatusGuide() {
994
994
  ];
995
995
  return lines.join("\n");
996
996
  }
997
+ function filterLiveEntries(entries, type) {
998
+ const list = Array.isArray(entries) ? entries : [];
999
+ const t = typeof type === "string" ? type.trim() : "";
1000
+ if (!t) return list.slice();
1001
+ return list.filter((e) => e && typeof e === "object" && String(e.type ?? "") === t);
1002
+ }
1003
+ function filterTimelineEntries(entries, opts) {
1004
+ const list = Array.isArray(entries) ? entries : [];
1005
+ const o = opts && typeof opts === "object" ? opts : {};
1006
+ const type = typeof o.type === "string" ? o.type : "";
1007
+ const round = typeof o.round === "string" ? o.round : "";
1008
+ const q = typeof o.search === "string" ? o.search.trim().toLowerCase() : "";
1009
+ const filtered = list.filter((t) => {
1010
+ if (!t || typeof t !== "object") return false;
1011
+ if (type && String(t.type ?? "") !== type) return false;
1012
+ if (round && String(t.round ?? "") !== round) return false;
1013
+ if (q) {
1014
+ const hay = [String(t.type ?? ""), String(t.round ?? ""), JSON.stringify(t.data ?? {})].join(" ").toLowerCase();
1015
+ if (hay.indexOf(q) < 0) return false;
1016
+ }
1017
+ return true;
1018
+ });
1019
+ return filtered.slice().sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
1020
+ }
1021
+ function serializeObservatoryExport(manifest, live) {
1022
+ const payload = {
1023
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1024
+ manifest: manifest && typeof manifest === "object" ? manifest : null,
1025
+ live: Array.isArray(live) ? live : []
1026
+ };
1027
+ try {
1028
+ return JSON.stringify(payload, null, 2);
1029
+ } catch {
1030
+ return JSON.stringify({ exportedAt: payload.exportedAt, manifest: null, live: [] }, null, 2);
1031
+ }
1032
+ }
1033
+ function latestPhase(phases) {
1034
+ const list = Array.isArray(phases) ? phases : [];
1035
+ let latest = "";
1036
+ for (const p of list) {
1037
+ if (typeof p === "string" && p.trim()) latest = p.trim();
1038
+ }
1039
+ return latest;
1040
+ }
997
1041
 
998
1042
  // src/client/index.ts
999
1043
  var React = require("react");
@@ -1103,6 +1147,11 @@ var ITERATE_CSS = `
1103
1147
  .iterate-filter-search { padding: 4px 8px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-size: 11px; min-width: 140px; }
1104
1148
  .iterate-filter-search::placeholder { color: var(--dsw-alias-label-secondary); }
1105
1149
  .iterate-filter-count { margin-left: auto; white-space: nowrap; }
1150
+ .iterate-filter-chip { padding: 3px 9px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
1151
+ .iterate-filter-chip:hover { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
1152
+ .iterate-filter-chip[data-active] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); font-weight: 600; }
1153
+ .iterate-filter-clear { padding: 3px 9px; border-radius: 999px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent); color: var(--dsw-alias-state-warn-primary); background: transparent; font-size: 11px; cursor: pointer; }
1154
+ .iterate-filter-clear:hover { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent); }
1106
1155
  .iterate-batch { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); }
1107
1156
  .iterate-batch-label { margin-right: 2px; }
1108
1157
  .iterate-batch-btn { padding: 3px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
@@ -1155,6 +1204,46 @@ var ITERATE_CSS = `
1155
1204
  /* Interruption / resume + attachment chips (dashboard) */
1156
1205
  .iterate-chip-resume { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; 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); }
1157
1206
  .iterate-chip-images { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
1207
+ /* Live workflow-phase chip (dashboard) */
1208
+ .iterate-chip-phase { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-label-primary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 14%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 26%, transparent); }
1209
+ .iterate-chip-phase[data-live] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
1210
+
1211
+ /* v3.0: task_mode indicator chip */
1212
+ .iterate-chip-taskmode { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
1213
+ .iterate-chip-taskmode[data-mode="code"] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
1214
+ .iterate-chip-taskmode[data-mode="iterate"] { color: #B45309; background: color-mix(in srgb, #B45309 12%, transparent); border: 1px solid color-mix(in srgb, #B45309 28%, transparent); }
1215
+
1216
+ /* v3.0: Quality gate status */
1217
+ .iterate-gate { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
1218
+ .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); }
1219
+ .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); }
1220
+ .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); }
1221
+
1222
+ /* v3.0: Experience bank hit highlight */
1223
+ .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); }
1224
+ .iterate-exp-hit b { font-weight: 600; }
1225
+
1226
+ /* v3.0: Defense event type badges */
1227
+ .iterate-defense-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; white-space: nowrap; }
1228
+ .iterate-defense-badge[data-type="precondition_failed"] { 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); }
1229
+ .iterate-defense-badge[data-type="rollback"] { 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); }
1230
+ .iterate-defense-badge[data-type="invariant_violated"] { 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); }
1231
+ .iterate-defense-badge[data-type="assumption_falsified"] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
1232
+
1233
+ /* v3.0: Command buttons */
1234
+ .iterate-cmd { padding: 4px 10px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 11px; cursor: pointer; font-weight: 500; }
1235
+ .iterate-cmd:hover { background: var(--dsw-alias-bg-layer-2); }
1236
+ .iterate-cmd[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
1237
+ .iterate-cmd[data-danger] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); color: var(--dsw-alias-state-error-primary); }
1238
+ .iterate-cmd:disabled { opacity: 0.5; cursor: not-allowed; }
1239
+
1240
+ /* v3.0: Dimension score bar */
1241
+ .iterate-dim-score { display: flex; align-items: center; gap: 6px; font-size: 11px; }
1242
+ .iterate-dim-score-bar { width: 60px; height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
1243
+ .iterate-dim-score-fill { height: 100%; border-radius: 3px; transition: width 300ms ease; }
1244
+ .iterate-dim-score-fill[data-status="pass"] { background: var(--dsw-alias-state-success-primary); }
1245
+ .iterate-dim-score-fill[data-status="warn"] { background: var(--dsw-alias-state-warn-primary); }
1246
+ .iterate-dim-score-fill[data-status="fail"] { background: var(--dsw-alias-state-error-primary); }
1158
1247
 
1159
1248
  /* Accessibility-switch toggle */
1160
1249
  .iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
@@ -1293,6 +1382,24 @@ function copyText(text) {
1293
1382
  }
1294
1383
  return Promise.resolve(false);
1295
1384
  }
1385
+ function downloadTextFile(filename, text) {
1386
+ if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") return false;
1387
+ try {
1388
+ const blob = new Blob([text], { type: "application/json;charset=utf-8" });
1389
+ const url = URL.createObjectURL(blob);
1390
+ const a = document.createElement("a");
1391
+ a.href = url;
1392
+ a.download = filename;
1393
+ a.rel = "noopener";
1394
+ document.body.appendChild(a);
1395
+ a.click();
1396
+ document.body.removeChild(a);
1397
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1398
+ return true;
1399
+ } catch {
1400
+ return false;
1401
+ }
1402
+ }
1296
1403
  var SEVERITY_KEYS = ["critical", "high", "medium", "low"];
1297
1404
  function coerceSeverity(severity) {
1298
1405
  return SEVERITY_KEYS.includes(severity ?? "") ? severity : "low";
@@ -1415,6 +1522,22 @@ function ConvergenceDashboard(props) {
1415
1522
  key: "images",
1416
1523
  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"
1417
1524
  }, `\u9644\u4EF6\u56FE\u7247 ${String(imageCount)}`) : null;
1525
+ const transcript = latestTranscript(session);
1526
+ const phase = transcript ? latestPhase(transcript.phases) : "";
1527
+ const liveChip = transcript && phase ? React.createElement("span", {
1528
+ className: "iterate-chip-phase",
1529
+ key: "phase",
1530
+ "data-live": transcript.active === true ? "" : void 0,
1531
+ title: transcript.active === true ? `\u5F53\u524D\u5904\u4E8E\u300C${phase}\u300D\u9636\u6BB5\uFF08\u8FD0\u884C\u4E2D\uFF09` : `\u6700\u8FD1\u4E00\u6B21\u6267\u884C\u505C\u7559\u5728\u300C${phase}\u300D\u9636\u6BB5\uFF08\u5DF2\u7ED3\u675F\uFF09`
1532
+ }, `${phase} \xB7 ${transcript.active === true ? "\u8FD0\u884C\u4E2D" : "\u5DF2\u7ED3\u675F"}`) : null;
1533
+ const taskModeTranscript = latestTranscript(session);
1534
+ const taskMode = taskModeTranscript?.taskMode;
1535
+ const taskModeChip = taskMode ? React.createElement("span", {
1536
+ className: "iterate-chip-taskmode",
1537
+ key: "taskmode",
1538
+ "data-mode": taskMode,
1539
+ title: taskMode === "code" ? "\u4EE3\u7801\u6A21\u5F0F\uFF1A\u76F4\u63A5\u4FEE\u6539\u6587\u4EF6" : "\u8FED\u4EE3\u6A21\u5F0F\uFF1A\u5BA1\u67E5-\u4FEE\u590D\u5FAA\u73AF"
1540
+ }, taskMode === "code" ? "\u25CF code" : "\u25CF iterate") : null;
1418
1541
  const dimNames = Object.keys(dims);
1419
1542
  const dimBadges = dimNames.slice(0, 6).map(
1420
1543
  (dim) => React.createElement(
@@ -1478,6 +1601,8 @@ function ConvergenceDashboard(props) {
1478
1601
  fixBadge,
1479
1602
  resumeChip,
1480
1603
  imageChip,
1604
+ liveChip,
1605
+ taskModeChip,
1481
1606
  React.createElement(TrendChart, { points: trend.points }),
1482
1607
  ...dimBadges
1483
1608
  );
@@ -1598,6 +1723,7 @@ function TriagePanel(props) {
1598
1723
  const [filter, setFilter] = React.useState({ severities: [], dimensions: [], search: "" });
1599
1724
  const [selected, setSelected] = React.useState(null);
1600
1725
  const [selectAll, setSelectAll] = React.useState(false);
1726
+ const [cmdCopied, setCmdCopied] = React.useState(null);
1601
1727
  React.useEffect(() => {
1602
1728
  if (storage) storage.set(storageKey, JSON.stringify(verdicts));
1603
1729
  }, [storageKey, verdicts]);
@@ -1661,6 +1787,33 @@ function TriagePanel(props) {
1661
1787
  const ignored = collectIgnoredEntries(verdicts, findings);
1662
1788
  const ignoredCount = ignored.length;
1663
1789
  const counts = countVerdicts(verdicts);
1790
+ const doApproveArchitecturalFix = () => {
1791
+ const cmd = "\u8BF7\u8C03\u7528 `iterate_fix` \u6279\u51C6\u67B6\u6784\u4FEE\u590D\uFF08\u8BBE\u7F6E is_architectural: true\uFF09";
1792
+ copyText(cmd).then((ok) => {
1793
+ if (ok) {
1794
+ setCmdCopied("approve-arch");
1795
+ setTimeout(() => setCmdCopied(null), 1600);
1796
+ }
1797
+ });
1798
+ };
1799
+ const doTriggerNewRound = () => {
1800
+ const cmd = "\u8BF7\u7EE7\u7EED\u6267\u884C\u4E0B\u4E00\u8F6E\u8FED\u4EE3\u5BA1\u67E5";
1801
+ copyText(cmd).then((ok) => {
1802
+ if (ok) {
1803
+ setCmdCopied("new-round");
1804
+ setTimeout(() => setCmdCopied(null), 1600);
1805
+ }
1806
+ });
1807
+ };
1808
+ const doRollbackToCheckpoint = () => {
1809
+ const cmd = "\u8BF7\u8C03\u7528 `iterate_checkpoint` \u56DE\u6EDA\u5230\u4E0A\u4E00\u4E2A\u68C0\u67E5\u70B9";
1810
+ copyText(cmd).then((ok) => {
1811
+ if (ok) {
1812
+ setCmdCopied("rollback");
1813
+ setTimeout(() => setCmdCopied(null), 1600);
1814
+ }
1815
+ });
1816
+ };
1664
1817
  const doCopyYaml = () => {
1665
1818
  const yaml = toKnownIntentionalYaml(ignored);
1666
1819
  if (!yaml) return;
@@ -1816,6 +1969,36 @@ function TriagePanel(props) {
1816
1969
  }, "\u751F\u6210\u5E94\u7528\u6307\u4EE4")
1817
1970
  )
1818
1971
  ),
1972
+ // v3.0: Command buttons for native actions (§8)
1973
+ React.createElement(
1974
+ "div",
1975
+ { className: "iterate-triage-foot", style: { borderTop: "1px solid var(--dsw-alias-border-l1)" } },
1976
+ React.createElement("span", { style: { fontSize: 11, fontWeight: 600, color: "var(--dsw-alias-label-secondary)" } }, "\u6307\u6325\u64CD\u4F5C"),
1977
+ React.createElement(
1978
+ "span",
1979
+ { style: { display: "flex", gap: 6 } },
1980
+ React.createElement("button", {
1981
+ className: "iterate-cmd",
1982
+ "data-primary": "",
1983
+ "data-copied": cmdCopied === "approve-arch" ? "" : void 0,
1984
+ onClick: doApproveArchitecturalFix,
1985
+ title: "\u6279\u51C6\u67B6\u6784\u4FEE\u590D\uFF08\u590D\u5236\u6307\u4EE4\u6587\u672C\uFF09"
1986
+ }, cmdCopied === "approve-arch" ? "\u5DF2\u590D\u5236" : "\u6279\u51C6\u67B6\u6784\u4FEE\u590D"),
1987
+ React.createElement("button", {
1988
+ className: "iterate-cmd",
1989
+ "data-copied": cmdCopied === "new-round" ? "" : void 0,
1990
+ onClick: doTriggerNewRound,
1991
+ title: "\u89E6\u53D1\u65B0\u4E00\u8F6E\u8FED\u4EE3\u5BA1\u67E5"
1992
+ }, cmdCopied === "new-round" ? "\u5DF2\u590D\u5236" : "\u89E6\u53D1\u65B0\u4E00\u8F6E"),
1993
+ React.createElement("button", {
1994
+ className: "iterate-cmd",
1995
+ "data-danger": "",
1996
+ "data-copied": cmdCopied === "rollback" ? "" : void 0,
1997
+ onClick: doRollbackToCheckpoint,
1998
+ title: "\u56DE\u6EDA\u5230\u4E0A\u4E00\u4E2A\u68C0\u67E5\u70B9"
1999
+ }, cmdCopied === "rollback" ? "\u5DF2\u590D\u5236" : "\u56DE\u6EDA\u68C0\u67E5\u70B9")
2000
+ )
2001
+ ),
1819
2002
  payload ? React.createElement("div", { className: "iterate-payload" }, payload) : null
1820
2003
  );
1821
2004
  }
@@ -1899,7 +2082,13 @@ function ProgressCapsule() {
1899
2082
  };
1900
2083
  }, []);
1901
2084
  if (!info) return null;
1902
- return React.createElement("div", { "data-iterate-root": "", className: "iterate-capsule", "data-ok": info.ok ? "" : void 0 }, info.text);
2085
+ return React.createElement("div", {
2086
+ "data-iterate-root": "",
2087
+ className: "iterate-capsule",
2088
+ "data-ok": info.ok ? "" : void 0,
2089
+ role: "status",
2090
+ "aria-live": "polite"
2091
+ }, info.text);
1903
2092
  }
1904
2093
  function SettingsPanel(_props) {
1905
2094
  const [enabled, setEnabled] = React.useState(themeEnabled);
@@ -2092,7 +2281,11 @@ var OBS_TABS = [
2092
2281
  { key: "f4", label: "\u4FEE\u590D\u4E0E\u56DE\u6EDA" },
2093
2282
  { key: "f5", label: "\u65AD\u70B9\u6062\u590D" },
2094
2283
  { key: "f6", label: "\u8FD0\u884C\u63A7\u5236\u53F0" },
2095
- { key: "f7", label: "\u51B3\u7B56\u65F6\u95F4\u7EBF" }
2284
+ { key: "f7", label: "\u51B3\u7B56\u65F6\u95F4\u7EBF" },
2285
+ // v3.0: Quality Command Center tabs
2286
+ { key: "f8", label: "\u8D28\u91CF\u95E8\u7981" },
2287
+ { key: "f9", label: "\u7ECF\u9A8C\u94F6\u884C" },
2288
+ { key: "f10", label: "\u9632\u5FA1\u4E8B\u4EF6" }
2096
2289
  ];
2097
2290
  var OBS_LIVE_META = {
2098
2291
  read: { label: "\u9605\u8BFB", color: "var(--dsw-alias-brand-primary)" },
@@ -2150,11 +2343,20 @@ function ObservatoryPanel(props) {
2150
2343
  const [open, setOpen] = React.useState(false);
2151
2344
  const [tab, setTab] = React.useState("live");
2152
2345
  const [expandedThreads, setExpandedThreads] = React.useState(/* @__PURE__ */ new Set());
2346
+ const [threadMode, setThreadMode] = React.useState("auto");
2153
2347
  const [copiedKey, setCopiedKey] = React.useState(null);
2154
2348
  const [copyFailText, setCopyFailText] = React.useState(null);
2155
2349
  const [nudgeText, setNudgeText] = React.useState("");
2156
2350
  const [timelineType, setTimelineType] = React.useState("");
2351
+ const [timelineRound, setTimelineRound] = React.useState("");
2157
2352
  const [timelineSearch, setTimelineSearch] = React.useState("");
2353
+ const [liveType, setLiveType] = React.useState("");
2354
+ const [f3Filter, setF3Filter] = React.useState(
2355
+ { severities: [], dimensions: [], search: "" }
2356
+ );
2357
+ const [expSearch, setExpSearch] = React.useState("");
2358
+ const [expResults, setExpResults] = React.useState([]);
2359
+ const [defenseFilter, setDefenseFilter] = React.useState("");
2158
2360
  const copyTimer = React.useRef(null);
2159
2361
  React.useEffect(() => () => {
2160
2362
  if (copyTimer.current) clearTimeout(copyTimer.current);
@@ -2172,6 +2374,7 @@ function ObservatoryPanel(props) {
2172
2374
  });
2173
2375
  };
2174
2376
  const toggleThread = (key) => {
2377
+ setThreadMode("auto");
2175
2378
  setExpandedThreads((prev) => {
2176
2379
  const next = new Set(prev);
2177
2380
  if (next.has(key)) next.delete(key);
@@ -2179,6 +2382,36 @@ function ObservatoryPanel(props) {
2179
2382
  return next;
2180
2383
  });
2181
2384
  };
2385
+ const exportObservatory = () => {
2386
+ if (!manifest) return;
2387
+ const json = serializeObservatoryExport(manifest, manifest.live || []);
2388
+ const filename = `iterate-observatory-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2389
+ const flash = () => {
2390
+ setCopiedKey("export");
2391
+ if (copyTimer.current) clearTimeout(copyTimer.current);
2392
+ copyTimer.current = setTimeout(() => setCopiedKey((cur) => cur === "export" ? null : cur), 1600);
2393
+ };
2394
+ if (downloadTextFile(filename, json)) {
2395
+ flash();
2396
+ return;
2397
+ }
2398
+ copyText(json).then((ok) => {
2399
+ if (!ok) {
2400
+ setCopyFailText(json);
2401
+ return;
2402
+ }
2403
+ flash();
2404
+ });
2405
+ };
2406
+ const setAllThreads = (mode) => {
2407
+ setThreadMode(mode);
2408
+ setExpandedThreads(/* @__PURE__ */ new Set());
2409
+ };
2410
+ const isThreadExpanded = (key) => {
2411
+ if (threadMode === "all") return true;
2412
+ if (threadMode === "none") return false;
2413
+ return expandedThreads.has(key);
2414
+ };
2182
2415
  if (!manifest) {
2183
2416
  return React.createElement(
2184
2417
  "div",
@@ -2211,69 +2444,85 @@ function ObservatoryPanel(props) {
2211
2444
  if (rounds.length === 0) {
2212
2445
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u5BA1\u67E5\u7EBF\u7A0B");
2213
2446
  }
2214
- return React.createElement("div", {}, ...rounds.map((r, ri) => {
2215
- const threads = r.threads || [];
2216
- const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0);
2217
- const threadBlocks = threads.length === 0 ? [React.createElement("div", { key: "none", className: "iterate-obs-empty" }, "\u672C\u8F6E\u65E0\u7EBF\u7A0B")] : threads.map((t, ti) => {
2218
- const key = `${ri}-${ti}`;
2219
- const expanded = expandedThreads.has(key);
2220
- const dim = t.dimension || "\u672A\u547D\u540D\u7EF4\u5EA6";
2221
- const tFindings = t.findings || [];
2222
- const files = t.readFiles || [];
2223
- const msgs = t.messages || [];
2224
- return React.createElement(
2225
- "div",
2226
- { key, className: "iterate-obs-block" },
2227
- React.createElement(
2228
- "div",
2229
- { className: "iterate-obs-block-head", "data-click": "", onClick: () => toggleThread(key), title: expanded ? "\u6536\u8D77\u7EBF\u7A0B" : "\u5C55\u5F00\u7EBF\u7A0B" },
2230
- React.createElement("span", {}, expanded ? "\u2212" : "+"),
2231
- React.createElement("b", {}, dim),
2232
- React.createElement("span", { className: "iterate-obs-chip" }, `attempt ${typeof t.attempt === "number" ? t.attempt : "?"}`),
2233
- React.createElement("span", { className: "iterate-obs-chip" }, `${tFindings.length} findings`)
2234
- ),
2235
- expanded ? React.createElement(
2447
+ return React.createElement(
2448
+ "div",
2449
+ {},
2450
+ React.createElement(
2451
+ "div",
2452
+ { className: "iterate-obs-bar", style: { marginBottom: 8, flexWrap: "wrap" } },
2453
+ React.createElement("b", {}, "\u5BA1\u67E5\u7EBF\u7A0B"),
2454
+ React.createElement("button", { className: "iterate-btn", onClick: () => setAllThreads("all"), title: "\u5C55\u5F00\u5168\u90E8\u7EBF\u7A0B" }, "\u5168\u90E8\u5C55\u5F00"),
2455
+ React.createElement("button", { className: "iterate-btn", onClick: () => setAllThreads("none"), title: "\u6536\u8D77\u5168\u90E8\u7EBF\u7A0B" }, "\u5168\u90E8\u6536\u8D77"),
2456
+ React.createElement(
2457
+ "span",
2458
+ { className: "iterate-filter-count" },
2459
+ `${threadMode === "all" ? "\u5168\u90E8\u5C55\u5F00" : threadMode === "none" ? "\u5168\u90E8\u6536\u8D77" : "\u81EA\u7531\u5207\u6362"} \xB7 ${rounds.length} \u8F6E`
2460
+ )
2461
+ ),
2462
+ ...rounds.map((r, ri) => {
2463
+ const threads = r.threads || [];
2464
+ const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0);
2465
+ const threadBlocks = threads.length === 0 ? [React.createElement("div", { key: "none", className: "iterate-obs-empty" }, "\u672C\u8F6E\u65E0\u7EBF\u7A0B")] : threads.map((t, ti) => {
2466
+ const key = `${ri}-${ti}`;
2467
+ const expanded = isThreadExpanded(key);
2468
+ const dim = t.dimension || "\u672A\u547D\u540D\u7EF4\u5EA6";
2469
+ const tFindings = t.findings || [];
2470
+ const files = t.readFiles || [];
2471
+ const msgs = t.messages || [];
2472
+ return React.createElement(
2236
2473
  "div",
2237
- { className: "iterate-obs-block-body" },
2474
+ { key, className: "iterate-obs-block" },
2238
2475
  React.createElement(
2239
2476
  "div",
2240
- { className: "iterate-obs-bar" },
2241
- React.createElement("b", {}, "\u53D9\u8FF0"),
2242
- React.createElement("span", { className: "iterate-obs-head-meta" }, `${msgs.length} \u6761`)
2477
+ { className: "iterate-obs-block-head", "data-click": "", onClick: () => toggleThread(key), title: expanded ? "\u6536\u8D77\u7EBF\u7A0B" : "\u5C55\u5F00\u7EBF\u7A0B" },
2478
+ React.createElement("span", {}, expanded ? "\u2212" : "+"),
2479
+ React.createElement("b", {}, dim),
2480
+ React.createElement("span", { className: "iterate-obs-chip" }, `attempt ${typeof t.attempt === "number" ? t.attempt : "?"}`),
2481
+ React.createElement("span", { className: "iterate-obs-chip" }, `${tFindings.length} findings`)
2243
2482
  ),
2244
- ...msgs.length === 0 ? [React.createElement("div", { key: "msg-none", className: "iterate-obs-empty" }, "\u65E0\u7EBF\u7D22\u53D9\u8FF0")] : msgs.map((m, mi) => React.createElement("div", { key: mi, className: "iterate-obs-msg" }, String(m))),
2245
- ...files.length > 0 ? [
2483
+ expanded ? React.createElement(
2484
+ "div",
2485
+ { className: "iterate-obs-block-body" },
2246
2486
  React.createElement(
2247
2487
  "div",
2248
- { key: "files-h", className: "iterate-obs-bar", style: { marginTop: 6 } },
2249
- React.createElement("b", {}, "\u8BFB\u53D6\u6587\u4EF6")
2488
+ { className: "iterate-obs-bar" },
2489
+ React.createElement("b", {}, "\u53D9\u8FF0"),
2490
+ React.createElement("span", { className: "iterate-obs-head-meta" }, `${msgs.length} \u6761`)
2250
2491
  ),
2251
- ...files.map(
2252
- (f, fi) => React.createElement("div", { key: `f${fi}`, className: "iterate-obs-file" }, String(f))
2253
- )
2254
- ] : [],
2255
- React.createElement(
2256
- "div",
2257
- { className: "iterate-obs-bar", style: { marginTop: 6 } },
2258
- React.createElement("b", {}, "\u53D1\u73B0")
2259
- ),
2260
- ...tFindings.length === 0 ? [React.createElement("div", { key: "finding-none", className: "iterate-obs-empty" }, "\u672C\u7EBF\u7A0B\u65E0\u53D1\u73B0")] : tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))
2261
- ) : null
2262
- );
2263
- });
2264
- return React.createElement(
2265
- "div",
2266
- { key: ri, className: "iterate-obs-block" },
2267
- React.createElement(
2492
+ ...msgs.length === 0 ? [React.createElement("div", { key: "msg-none", className: "iterate-obs-empty" }, "\u65E0\u7EBF\u7D22\u53D9\u8FF0")] : msgs.map((m, mi) => React.createElement("div", { key: mi, className: "iterate-obs-msg" }, String(m))),
2493
+ ...files.length > 0 ? [
2494
+ React.createElement(
2495
+ "div",
2496
+ { key: "files-h", className: "iterate-obs-bar", style: { marginTop: 6 } },
2497
+ React.createElement("b", {}, "\u8BFB\u53D6\u6587\u4EF6")
2498
+ ),
2499
+ ...files.map(
2500
+ (f, fi) => React.createElement("div", { key: `f${fi}`, className: "iterate-obs-file" }, String(f))
2501
+ )
2502
+ ] : [],
2503
+ React.createElement(
2504
+ "div",
2505
+ { className: "iterate-obs-bar", style: { marginTop: 6 } },
2506
+ React.createElement("b", {}, "\u53D1\u73B0")
2507
+ ),
2508
+ ...tFindings.length === 0 ? [React.createElement("div", { key: "finding-none", className: "iterate-obs-empty" }, "\u672C\u7EBF\u7A0B\u65E0\u53D1\u73B0")] : tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))
2509
+ ) : null
2510
+ );
2511
+ });
2512
+ return React.createElement(
2268
2513
  "div",
2269
- { className: "iterate-obs-block-head" },
2270
- React.createElement("b", {}, `Round ${typeof r.round === "number" ? r.round : ri + 1}`),
2271
- React.createElement("span", { className: "iterate-obs-chip" }, `${threads.length} \u7EBF\u7A0B`),
2272
- React.createElement("span", { className: "iterate-obs-head-meta" }, `${fCount} findings`)
2273
- ),
2274
- React.createElement("div", { className: "iterate-obs-block-body" }, ...threadBlocks)
2275
- );
2276
- }));
2514
+ { key: ri, className: "iterate-obs-block" },
2515
+ React.createElement(
2516
+ "div",
2517
+ { className: "iterate-obs-block-head" },
2518
+ React.createElement("b", {}, `Round ${typeof r.round === "number" ? r.round : ri + 1}`),
2519
+ React.createElement("span", { className: "iterate-obs-chip" }, `${threads.length} \u7EBF\u7A0B`),
2520
+ React.createElement("span", { className: "iterate-obs-head-meta" }, `${fCount} findings`)
2521
+ ),
2522
+ React.createElement("div", { className: "iterate-obs-block-body" }, ...threadBlocks)
2523
+ );
2524
+ })
2525
+ );
2277
2526
  };
2278
2527
  const renderTrend = () => {
2279
2528
  const conv = manifest.convergence || [];
@@ -2297,40 +2546,94 @@ function ObservatoryPanel(props) {
2297
2546
  if (findings.length === 0) {
2298
2547
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u53D1\u73B0");
2299
2548
  }
2300
- return React.createElement("div", {}, ...findings.map((f, i) => {
2549
+ const opts = buildFilterOptions(findings);
2550
+ const { filtered } = filterFindingsWithIndices(findings, f3Filter);
2551
+ const filterActive = f3Filter.severities.length > 0 || f3Filter.dimensions.length > 0 || f3Filter.search.trim() !== "";
2552
+ const toggleSeverity = (sev) => {
2553
+ setF3Filter((prev) => ({
2554
+ ...prev,
2555
+ severities: prev.severities.includes(sev) ? prev.severities.filter((s) => s !== sev) : [...prev.severities, sev]
2556
+ }));
2557
+ };
2558
+ const filterBar = React.createElement(
2559
+ "div",
2560
+ { className: "iterate-obs-bar", style: { marginBottom: 8, flexWrap: "wrap" } },
2561
+ React.createElement("b", {}, "\u53D1\u73B0"),
2562
+ ...opts.severities.map(
2563
+ (s) => React.createElement("button", {
2564
+ key: `sev-${s.value}`,
2565
+ className: "iterate-filter-chip",
2566
+ "data-active": f3Filter.severities.includes(s.value) ? "" : void 0,
2567
+ title: `${s.value}\uFF08${s.count} \u9879\uFF09`,
2568
+ onClick: () => toggleSeverity(s.value)
2569
+ }, `${s.value} ${s.count}`)
2570
+ ),
2571
+ React.createElement(
2572
+ "select",
2573
+ {
2574
+ className: "iterate-filter-select",
2575
+ value: f3Filter.dimensions[0] || "",
2576
+ "aria-label": "\u6309\u7EF4\u5EA6\u7B5B\u9009",
2577
+ onChange: (e) => {
2578
+ const v = e.target.value;
2579
+ setF3Filter((prev) => ({ ...prev, dimensions: v ? [v] : [] }));
2580
+ }
2581
+ },
2582
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u7EF4\u5EA6"),
2583
+ ...opts.dimensions.map(
2584
+ (d) => React.createElement("option", { key: d.value, value: d.value }, `${d.value} (${d.count})`)
2585
+ )
2586
+ ),
2587
+ React.createElement("input", {
2588
+ className: "iterate-filter-search",
2589
+ type: "search",
2590
+ placeholder: "\u641C\u7D22\u53D1\u73B0\u2026",
2591
+ "aria-label": "\u641C\u7D22\u53D1\u73B0",
2592
+ value: f3Filter.search,
2593
+ onChange: (e) => setF3Filter((prev) => ({ ...prev, search: e.target.value }))
2594
+ }),
2595
+ filterActive ? React.createElement("button", {
2596
+ className: "iterate-filter-clear",
2597
+ onClick: () => setF3Filter({ severities: [], dimensions: [], search: "" }),
2598
+ title: "\u6E05\u9664\u5168\u90E8\u7B5B\u9009"
2599
+ }, "\u6E05\u9664\u7B5B\u9009") : null,
2600
+ React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${findings.length}`)
2601
+ );
2602
+ const findingBlocks = filtered.map((f, i) => {
2301
2603
  const k = `f3-${i}`;
2302
- const loc = `${String(f.file || "?")}${typeof f.line === "number" && f.line > 0 ? `:${f.line}` : ""}`;
2604
+ const finding = f;
2605
+ const loc = `${String(finding.file || "?")}${typeof finding.line === "number" && finding.line > 0 ? `:${finding.line}` : ""}`;
2303
2606
  const entry = {
2304
- file: String(f.file || ""),
2305
- ...typeof f.line === "number" && f.line > 0 ? { line: f.line } : {},
2306
- dimension: String(f.dimension || ""),
2307
- reason: String(f.summary || "")
2607
+ file: String(finding.file || ""),
2608
+ ...typeof finding.line === "number" && finding.line > 0 ? { line: finding.line } : {},
2609
+ dimension: String(finding.dimension || ""),
2610
+ reason: String(finding.summary || "")
2308
2611
  };
2309
2612
  const knownInstruction = buildApplyInstruction([entry]);
2310
- const fixInstruction = buildObsFixInstruction(f);
2613
+ const fixInstruction = buildObsFixInstruction(finding);
2311
2614
  return React.createElement(
2312
2615
  "div",
2313
2616
  { key: k, className: "iterate-obs-block" },
2314
2617
  React.createElement(
2315
2618
  "div",
2316
2619
  { className: "iterate-obs-block-head" },
2317
- React.createElement("span", { className: "iterate-sev-dot", style: { background: severityColor(f.severity) } }),
2318
- React.createElement("span", {}, severityLabel(f.severity)),
2620
+ React.createElement("span", { className: "iterate-sev-dot", style: { background: severityColor(finding.severity) } }),
2621
+ React.createElement("span", {}, severityLabel(finding.severity)),
2319
2622
  React.createElement("span", { className: "iterate-obs-file" }, loc),
2320
- React.createElement("span", {}, String(f.dimension || "")),
2321
- f.acknowledged === true ? React.createElement("span", { className: "iterate-obs-chip" }, "\u5DF2\u786E\u8BA4") : null
2623
+ React.createElement("span", {}, String(finding.dimension || "")),
2624
+ finding.acknowledged === true ? React.createElement("span", { className: "iterate-obs-chip" }, "\u5DF2\u786E\u8BA4") : null
2322
2625
  ),
2323
2626
  React.createElement(
2324
2627
  "div",
2325
2628
  { className: "iterate-obs-block-body" },
2326
- React.createElement("div", { className: "iterate-obs-msg" }, String(f.summary || "")),
2629
+ React.createElement("div", { className: "iterate-obs-msg" }, String(finding.summary || "")),
2327
2630
  React.createElement(
2328
2631
  "div",
2329
2632
  { className: "iterate-obs-bar", style: { marginTop: 8 } },
2330
2633
  React.createElement("button", {
2331
2634
  className: "iterate-btn",
2332
2635
  "data-copied": copiedKey === `${k}-fix` ? "" : void 0,
2333
- onClick: () => copyInstruction(`${k}-fix`, String(f.suggested_fix || "")),
2636
+ onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || "")),
2334
2637
  title: "\u590D\u5236\u8BE5 finding \u7684\u5EFA\u8BAE\u4FEE\u590D\u6587\u672C"
2335
2638
  }, copiedKey === `${k}-fix` ? "\u5DF2\u590D\u5236" : "\u590D\u5236 suggested_fix"),
2336
2639
  React.createElement("button", {
@@ -2349,7 +2652,16 @@ function ObservatoryPanel(props) {
2349
2652
  )
2350
2653
  )
2351
2654
  );
2352
- }));
2655
+ });
2656
+ if (filtered.length === 0) {
2657
+ return React.createElement(
2658
+ "div",
2659
+ {},
2660
+ filterBar,
2661
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u53D1\u73B0")
2662
+ );
2663
+ }
2664
+ return React.createElement("div", {}, filterBar, ...findingBlocks);
2353
2665
  };
2354
2666
  const renderFixes = () => {
2355
2667
  const fixes = manifest.fixes || [];
@@ -2515,14 +2827,21 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2515
2827
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u65F6\u95F4\u7EBF\u6761\u76EE");
2516
2828
  }
2517
2829
  const types = Array.from(new Set(entries.map((t) => String(t.type || "unknown")).filter(Boolean))).sort();
2518
- const q = timelineSearch.trim().toLowerCase();
2519
- const filtered = entries.filter((t) => {
2520
- if (timelineType && String(t.type || "") !== timelineType) return false;
2521
- if (!q) return true;
2522
- const hay = [String(t.type || ""), String(t.round ?? ""), JSON.stringify(t.data || {})].join(" ").toLowerCase();
2523
- return hay.indexOf(q) >= 0;
2830
+ const rounds = Array.from(
2831
+ new Set(entries.map((t) => typeof t.round === "number" ? String(t.round) : "").filter(Boolean))
2832
+ ).sort((a, b) => Number(a) - Number(b));
2833
+ const filtered = filterTimelineEntries(entries, {
2834
+ type: timelineType,
2835
+ round: timelineRound,
2836
+ search: timelineSearch
2524
2837
  });
2525
- const sorted = filtered.slice().sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")));
2838
+ const filterActive = Boolean(timelineType || timelineRound || timelineSearch.trim());
2839
+ const clearTimelineFilter = () => {
2840
+ setTimelineType("");
2841
+ setTimelineRound("");
2842
+ setTimelineSearch("");
2843
+ };
2844
+ const sorted = filtered.slice();
2526
2845
  const rows = sorted.map((t, i) => {
2527
2846
  const k = `f7-${i}`;
2528
2847
  const dataText = t.data && typeof t.data === "object" ? JSON.stringify(t.data) : "";
@@ -2542,37 +2861,232 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2542
2861
  dataText ? React.createElement("div", { className: "iterate-obs-code" }, dataText) : null
2543
2862
  );
2544
2863
  });
2864
+ const filterBar = React.createElement(
2865
+ "div",
2866
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2867
+ React.createElement("b", {}, "\u65F6\u95F4\u7EBF"),
2868
+ React.createElement(
2869
+ "select",
2870
+ {
2871
+ className: "iterate-filter-select",
2872
+ value: timelineType,
2873
+ "aria-label": "\u6309\u65F6\u95F4\u7EBF\u7C7B\u578B\u7B5B\u9009",
2874
+ onChange: (e) => setTimelineType(e.target.value)
2875
+ },
2876
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u7C7B\u578B"),
2877
+ ...types.map((t) => React.createElement("option", { key: t, value: t }, t))
2878
+ ),
2879
+ React.createElement(
2880
+ "select",
2881
+ {
2882
+ className: "iterate-filter-select",
2883
+ value: timelineRound,
2884
+ "aria-label": "\u6309\u8F6E\u6B21\u7B5B\u9009",
2885
+ onChange: (e) => setTimelineRound(e.target.value)
2886
+ },
2887
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u8F6E\u6B21"),
2888
+ ...rounds.map((r) => React.createElement("option", { key: r, value: r }, `Round ${r}`))
2889
+ ),
2890
+ React.createElement("input", {
2891
+ className: "iterate-filter-search",
2892
+ type: "search",
2893
+ placeholder: "\u641C\u7D22\u65F6\u95F4\u7EBF\u2026",
2894
+ "aria-label": "\u641C\u7D22\u65F6\u95F4\u7EBF",
2895
+ value: timelineSearch,
2896
+ onChange: (e) => setTimelineSearch(e.target.value)
2897
+ }),
2898
+ filterActive ? React.createElement("button", {
2899
+ className: "iterate-filter-clear",
2900
+ onClick: clearTimelineFilter,
2901
+ title: "\u6E05\u9664\u5168\u90E8\u7B5B\u9009"
2902
+ }, "\u6E05\u9664") : null,
2903
+ React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${entries.length}`)
2904
+ );
2545
2905
  if (rows.length === 0) {
2546
- return React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u65F6\u95F4\u7EBF\u6761\u76EE");
2906
+ return React.createElement(
2907
+ "div",
2908
+ {},
2909
+ filterBar,
2910
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u65F6\u95F4\u7EBF\u6761\u76EE")
2911
+ );
2547
2912
  }
2913
+ return React.createElement("div", {}, filterBar, ...rows);
2914
+ };
2915
+ const renderQualityGate = () => {
2916
+ const qualityGateInstruction = "\u8BF7\u8C03\u7528 `iterate_quality_gate` \u67E5\u8BE2\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u72B6\u6001";
2548
2917
  return React.createElement(
2549
2918
  "div",
2550
2919
  {},
2551
2920
  React.createElement(
2552
2921
  "div",
2553
2922
  { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2923
+ React.createElement("b", {}, "\u8D28\u91CF\u95E8\u7981"),
2924
+ React.createElement("button", {
2925
+ className: "iterate-btn",
2926
+ "data-primary": "",
2927
+ "data-copied": copiedKey === "qgate" ? "" : void 0,
2928
+ onClick: () => copyInstruction("qgate", qualityGateInstruction),
2929
+ title: "\u590D\u5236 iterate_quality_gate \u67E5\u8BE2\u6307\u4EE4"
2930
+ }, copiedKey === "qgate" ? "\u5DF2\u590D\u5236" : "\u67E5\u8BE2\u95E8\u7981")
2931
+ ),
2932
+ React.createElement(
2933
+ "div",
2934
+ { className: "iterate-obs-block" },
2554
2935
  React.createElement(
2555
- "select",
2556
- {
2557
- className: "iterate-filter-select",
2558
- value: timelineType,
2559
- "aria-label": "\u6309\u65F6\u95F4\u7EBF\u7C7B\u578B\u7B5B\u9009",
2560
- onChange: (e) => setTimelineType(e.target.value)
2561
- },
2562
- React.createElement("option", { value: "" }, "\u5168\u90E8\u7C7B\u578B"),
2563
- ...types.map((t) => React.createElement("option", { key: t, value: t }, t))
2936
+ "div",
2937
+ { className: "iterate-obs-block-head" },
2938
+ React.createElement("span", {}, "\u8D28\u91CF\u95E8\u7981\u89C6\u56FE"),
2939
+ React.createElement("span", { className: "iterate-obs-badge" }, "v3.0")
2564
2940
  ),
2941
+ React.createElement(
2942
+ "div",
2943
+ { className: "iterate-obs-block-body" },
2944
+ React.createElement("div", { className: "iterate-obs-msg" }, "\u8D28\u91CF\u95E8\u7981\u663E\u793A\u5404\u7EF4\u5EA6\u6536\u655B\u5EA6\u3001\u9A8C\u8BC1\u901A\u8FC7\u7387\u548C\u6574\u4F53 PASS/FAIL \u72B6\u6001\u3002"),
2945
+ React.createElement(
2946
+ "div",
2947
+ { className: "iterate-obs-bar", style: { marginTop: 8 } },
2948
+ React.createElement("button", {
2949
+ className: "iterate-btn",
2950
+ "data-copied": copiedKey === "qgate-instr" ? "" : void 0,
2951
+ onClick: () => copyInstruction("qgate-instr", qualityGateInstruction),
2952
+ title: "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4"
2953
+ }, copiedKey === "qgate-instr" ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4")
2954
+ )
2955
+ )
2956
+ )
2957
+ );
2958
+ };
2959
+ const renderExperienceBank = () => {
2960
+ const expInstruction = expSearch ? `\u8BF7\u8C03\u7528 \`iterate_experience\` \u641C\u7D22\u7ECF\u9A8C\uFF1A
2961
+
2962
+ \`\`\`json
2963
+ ${JSON.stringify({ operation: "search", query: expSearch }, null, 2)}
2964
+ \`\`\`` : "\u8BF7\u8C03\u7528 `iterate_experience` \u5217\u51FA\u6240\u6709\u7ECF\u9A8C";
2965
+ const resultRows = expResults.map((entry, i) => {
2966
+ const e = entry;
2967
+ return React.createElement(
2968
+ "div",
2969
+ { key: `exp-${i}`, className: "iterate-obs-row" },
2970
+ React.createElement("span", { className: "iterate-obs-chip" }, String(e.dimension || "")),
2971
+ React.createElement("span", { className: "iterate-obs-msg" }, String(e.pattern || "")),
2972
+ React.createElement("span", { className: "iterate-exp-hit" }, `\u547D\u4E2D ${e.hitCount ?? 0} \u6B21`)
2973
+ );
2974
+ });
2975
+ return React.createElement(
2976
+ "div",
2977
+ {},
2978
+ React.createElement(
2979
+ "div",
2980
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2981
+ React.createElement("b", {}, "\u7ECF\u9A8C\u94F6\u884C"),
2982
+ React.createElement("button", {
2983
+ className: "iterate-btn",
2984
+ "data-primary": "",
2985
+ "data-copied": copiedKey === "exp-list" ? "" : void 0,
2986
+ onClick: () => copyInstruction("exp-list", "\u8BF7\u8C03\u7528 `iterate_experience` \u5217\u51FA\u6240\u6709\u7ECF\u9A8C"),
2987
+ title: "\u590D\u5236\u5217\u51FA\u7ECF\u9A8C\u6307\u4EE4"
2988
+ }, copiedKey === "exp-list" ? "\u5DF2\u590D\u5236" : "\u5217\u51FA\u7ECF\u9A8C")
2989
+ ),
2990
+ React.createElement(
2991
+ "div",
2992
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2565
2993
  React.createElement("input", {
2566
2994
  className: "iterate-filter-search",
2567
2995
  type: "search",
2568
- placeholder: "\u641C\u7D22\u65F6\u95F4\u7EBF\u2026",
2569
- "aria-label": "\u641C\u7D22\u65F6\u95F4\u7EBF",
2570
- value: timelineSearch,
2571
- onChange: (e) => setTimelineSearch(e.target.value)
2996
+ placeholder: "\u641C\u7D22\u7ECF\u9A8C\u6A21\u5F0F\u2026",
2997
+ "aria-label": "\u641C\u7D22\u7ECF\u9A8C",
2998
+ value: expSearch,
2999
+ onChange: (e) => setExpSearch(e.target.value)
2572
3000
  }),
2573
- React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${entries.length}`)
3001
+ React.createElement("button", {
3002
+ className: "iterate-btn",
3003
+ "data-copied": copiedKey === "exp-search" ? "" : void 0,
3004
+ disabled: !expSearch,
3005
+ onClick: () => copyInstruction("exp-search", expInstruction),
3006
+ title: "\u590D\u5236\u641C\u7D22\u6307\u4EE4"
3007
+ }, copiedKey === "exp-search" ? "\u5DF2\u590D\u5236" : "\u641C\u7D22")
3008
+ ),
3009
+ resultRows.length > 0 ? React.createElement("div", {}, ...resultRows) : React.createElement("div", { className: "iterate-obs-empty" }, "\u8F93\u5165\u5173\u952E\u8BCD\u641C\u7D22\u5386\u53F2\u7ECF\u9A8C\uFF0C\u6216\u70B9\u51FB\u300C\u5217\u51FA\u7ECF\u9A8C\u300D\u67E5\u770B\u5168\u90E8")
3010
+ );
3011
+ };
3012
+ const renderDefenseEvents = () => {
3013
+ const defenseInstruction = defenseFilter ? `\u8BF7\u8C03\u7528 \`iterate_defense_events\` \u67E5\u8BE2\u9632\u5FA1\u4E8B\u4EF6\uFF1A
3014
+
3015
+ \`\`\`json
3016
+ ${JSON.stringify({ operation: "list", type: defenseFilter || void 0 }, null, 2)}
3017
+ \`\`\`` : "\u8BF7\u8C03\u7528 `iterate_defense_events` \u5217\u51FA\u6240\u6709\u9632\u5FA1\u4E8B\u4EF6";
3018
+ const typeOptions = [
3019
+ { value: "", label: "\u5168\u90E8\u7C7B\u578B" },
3020
+ { value: "precondition_failed", label: "\u524D\u7F6E\u6821\u9A8C\u5931\u8D25" },
3021
+ { value: "rollback", label: "\u56DE\u6EDA" },
3022
+ { value: "invariant_violated", label: "\u4E0D\u53D8\u91CF\u8FDD\u53CD" },
3023
+ { value: "assumption_falsified", label: "\u5047\u8BBE\u88AB\u8BC1\u4F2A" }
3024
+ ];
3025
+ return React.createElement(
3026
+ "div",
3027
+ {},
3028
+ React.createElement(
3029
+ "div",
3030
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
3031
+ React.createElement("b", {}, "\u9632\u5FA1\u4E8B\u4EF6"),
3032
+ React.createElement("button", {
3033
+ className: "iterate-btn",
3034
+ "data-primary": "",
3035
+ "data-copied": copiedKey === "defense-list" ? "" : void 0,
3036
+ onClick: () => copyInstruction("defense-list", "\u8BF7\u8C03\u7528 `iterate_defense_events` \u5217\u51FA\u6240\u6709\u9632\u5FA1\u4E8B\u4EF6"),
3037
+ title: "\u590D\u5236\u5217\u51FA\u9632\u5FA1\u4E8B\u4EF6\u6307\u4EE4"
3038
+ }, copiedKey === "defense-list" ? "\u5DF2\u590D\u5236" : "\u5217\u51FA\u4E8B\u4EF6"),
3039
+ React.createElement("button", {
3040
+ className: "iterate-btn",
3041
+ "data-copied": copiedKey === "defense-counts" ? "" : void 0,
3042
+ onClick: () => copyInstruction("defense-counts", "\u8BF7\u8C03\u7528 `iterate_defense_events` \u67E5\u8BE2\u4E8B\u4EF6\u7EDF\u8BA1"),
3043
+ title: "\u590D\u5236\u7EDF\u8BA1\u6307\u4EE4"
3044
+ }, copiedKey === "defense-counts" ? "\u5DF2\u590D\u5236" : "\u7EDF\u8BA1")
2574
3045
  ),
2575
- ...rows
3046
+ React.createElement(
3047
+ "div",
3048
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
3049
+ React.createElement(
3050
+ "select",
3051
+ {
3052
+ className: "iterate-filter-select",
3053
+ value: defenseFilter,
3054
+ "aria-label": "\u6309\u4E8B\u4EF6\u7C7B\u578B\u7B5B\u9009",
3055
+ onChange: (e) => setDefenseFilter(e.target.value)
3056
+ },
3057
+ ...typeOptions.map((opt) => React.createElement("option", { key: opt.value, value: opt.value }, opt.label))
3058
+ ),
3059
+ defenseFilter ? React.createElement("button", {
3060
+ className: "iterate-filter-clear",
3061
+ onClick: () => setDefenseFilter(""),
3062
+ title: "\u6E05\u9664\u7C7B\u578B\u7B5B\u9009"
3063
+ }, "\u6E05\u9664") : null
3064
+ ),
3065
+ React.createElement(
3066
+ "div",
3067
+ { className: "iterate-obs-block" },
3068
+ React.createElement(
3069
+ "div",
3070
+ { className: "iterate-obs-block-head" },
3071
+ React.createElement("span", {}, "\u9632\u5FA1\u4E8B\u4EF6\u6D41"),
3072
+ React.createElement("span", { className: "iterate-obs-badge" }, "v3.0")
3073
+ ),
3074
+ React.createElement(
3075
+ "div",
3076
+ { className: "iterate-obs-block-body" },
3077
+ React.createElement("div", { className: "iterate-obs-msg" }, "\u9632\u5FA1\u4E8B\u4EF6\u5305\u62EC\uFF1A\u524D\u7F6E\u6821\u9A8C\u5931\u8D25\u3001\u56DE\u6EDA\u3001\u4E0D\u53D8\u91CF\u8FDD\u53CD\u3001\u5047\u8BBE\u88AB\u8BC1\u4F2A\u3002"),
3078
+ React.createElement(
3079
+ "div",
3080
+ { className: "iterate-obs-bar", style: { marginTop: 8 } },
3081
+ React.createElement("button", {
3082
+ className: "iterate-btn",
3083
+ "data-copied": copiedKey === "defense-instr" ? "" : void 0,
3084
+ onClick: () => copyInstruction("defense-instr", defenseInstruction),
3085
+ title: "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4"
3086
+ }, copiedKey === "defense-instr" ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4")
3087
+ )
3088
+ )
3089
+ )
2576
3090
  );
2577
3091
  };
2578
3092
  const renderLive = () => {
@@ -2595,12 +3109,14 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2595
3109
  }
2596
3110
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u5B9E\u65F6\u6D3B\u52A8");
2597
3111
  }
2598
- const rows = liveEntries.map((e, i) => {
3112
+ const types = Array.from(new Set(liveEntries.map((e) => String(e.type || "unknown")).filter(Boolean))).sort();
3113
+ const filtered = filterLiveEntries(liveEntries, liveType);
3114
+ const rows = filtered.map((e, i) => {
2599
3115
  const type = String(e.type || "unknown");
2600
3116
  const meta = OBS_LIVE_META[type] || { label: String(e.type || "?"), color: OBS_LIVE_FALLBACK_COLOR };
2601
3117
  const tool = String(e.tool || "");
2602
3118
  const target = String(e.target || "");
2603
- const time = formatObsLiveTime(e.ts);
3119
+ const time = formatObsLiveTime(String(e.ts || ""));
2604
3120
  return React.createElement(
2605
3121
  "div",
2606
3122
  { key: `live-${i}`, className: "iterate-obs-row" },
@@ -2610,17 +3126,37 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2610
3126
  React.createElement("span", { className: "iterate-obs-head-meta" }, time)
2611
3127
  );
2612
3128
  });
2613
- return React.createElement(
3129
+ const filterBar = React.createElement(
2614
3130
  "div",
2615
- {},
3131
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
3132
+ React.createElement("b", {}, "\u5B9E\u65F6\u6D3B\u52A8"),
2616
3133
  React.createElement(
2617
- "div",
2618
- { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2619
- React.createElement("b", {}, "\u5B9E\u65F6\u6D3B\u52A8"),
2620
- React.createElement("span", { className: "iterate-obs-chip" }, `${liveEntries.length} \u9879`)
3134
+ "select",
3135
+ {
3136
+ className: "iterate-filter-select",
3137
+ value: liveType,
3138
+ "aria-label": "\u6309\u6D3B\u52A8\u7C7B\u578B\u7B5B\u9009",
3139
+ onChange: (e) => setLiveType(e.target.value)
3140
+ },
3141
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u6D3B\u52A8"),
3142
+ ...types.map((t) => React.createElement("option", { key: t, value: t }, t))
2621
3143
  ),
2622
- ...rows
3144
+ liveType ? React.createElement("button", {
3145
+ className: "iterate-filter-clear",
3146
+ onClick: () => setLiveType(""),
3147
+ title: "\u6E05\u9664\u7C7B\u578B\u7B5B\u9009"
3148
+ }, "\u6E05\u9664") : null,
3149
+ React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${liveEntries.length}`)
2623
3150
  );
3151
+ if (rows.length === 0) {
3152
+ return React.createElement(
3153
+ "div",
3154
+ {},
3155
+ filterBar,
3156
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u5B9E\u65F6\u6D3B\u52A8")
3157
+ );
3158
+ }
3159
+ return React.createElement("div", {}, filterBar, ...rows);
2624
3160
  };
2625
3161
  const renderBody = () => {
2626
3162
  switch (tab) {
@@ -2640,6 +3176,13 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2640
3176
  return renderConsole();
2641
3177
  case "f7":
2642
3178
  return renderTimeline();
3179
+ // v3.0: Quality Command Center tabs
3180
+ case "f8":
3181
+ return renderQualityGate();
3182
+ case "f9":
3183
+ return renderExperienceBank();
3184
+ case "f10":
3185
+ return renderDefenseEvents();
2643
3186
  default:
2644
3187
  return null;
2645
3188
  }
@@ -2682,6 +3225,22 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2682
3225
  ),
2683
3226
  React.createElement("div", { className: "iterate-obs-code" }, copyFailText)
2684
3227
  ) : null,
3228
+ React.createElement(
3229
+ "div",
3230
+ { className: "iterate-obs-bar", style: { marginBottom: 8, flexWrap: "wrap" } },
3231
+ React.createElement(
3232
+ "span",
3233
+ { className: "iterate-obs-head-meta" },
3234
+ `${manifest.mode || "runtime"} \xB7 ${String(manifest.updatedAt || "")}`
3235
+ ),
3236
+ React.createElement("button", {
3237
+ className: "iterate-btn",
3238
+ "data-primary": "",
3239
+ "data-copied": copiedKey === "export" ? "" : void 0,
3240
+ onClick: exportObservatory,
3241
+ title: "\u5C06\u89C2\u6D4B\u53F0\u5168\u90E8\u6570\u636E\u5BFC\u51FA\u4E3A JSON\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09"
3242
+ }, copiedKey === "export" ? "\u5DF2\u5BFC\u51FA" : "\u5BFC\u51FA JSON")
3243
+ ),
2685
3244
  React.createElement(
2686
3245
  "div",
2687
3246
  { className: "iterate-obs-tabs" },