iterate-plugin 2.12.3 → 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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * src/tools/quality-store.ts — quality gate storage layer.
3
+ *
4
+ * Provides read/write access to quality gate data stored in
5
+ * .iterate/quality-gate.json. Quality gate snapshots are generated
6
+ * from review results and validation outcomes.
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ const QUALITY_GATE_FILE = 'quality-gate.json';
11
+ /** Default empty quality gate snapshot. */
12
+ function emptySnapshot() {
13
+ return {
14
+ timestamp: new Date().toISOString(),
15
+ overallStatus: 'pending',
16
+ overallScore: 0,
17
+ dimensions: [],
18
+ verificationPassRate: 0,
19
+ totalChecks: 0,
20
+ passedChecks: 0,
21
+ failedChecks: 0,
22
+ totalFindings: 0,
23
+ criticalCount: 0,
24
+ highCount: 0,
25
+ mediumCount: 0,
26
+ lowCount: 0,
27
+ };
28
+ }
29
+ /** Read the quality gate snapshot from disk. */
30
+ export function readQualityGate(projectRoot) {
31
+ const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE);
32
+ try {
33
+ const content = fs.readFileSync(filePath, 'utf-8');
34
+ const parsed = JSON.parse(content);
35
+ if (parsed && typeof parsed === 'object') {
36
+ return parsed;
37
+ }
38
+ }
39
+ catch {
40
+ // File not found or invalid JSON
41
+ }
42
+ return emptySnapshot();
43
+ }
44
+ /** Write the quality gate snapshot to disk. */
45
+ export function writeQualityGate(projectRoot, snapshot) {
46
+ const dirPath = path.join(projectRoot, '.iterate');
47
+ const filePath = path.join(dirPath, QUALITY_GATE_FILE);
48
+ try {
49
+ if (!fs.existsSync(dirPath)) {
50
+ fs.mkdirSync(dirPath, { recursive: true });
51
+ }
52
+ fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf-8');
53
+ }
54
+ catch {
55
+ // Silently fail - quality gate is not critical
56
+ }
57
+ }
58
+ /**
59
+ * Compute the convergence rate for a dimension.
60
+ *
61
+ * Convergence measures how much NEW-finding volume shrank across rounds:
62
+ * `(first - last) / first` from the dimension's per-round findings series,
63
+ * expressed as a 0-100 percentage, clamped. A series with no fresh findings
64
+ * (or a dimension never reporting a first-round reading) counts as fully
65
+ * converged (100). Returns 0 — no measurable improvement — when a reading
66
+ * exists but the series is empty or malformed.
67
+ */
68
+ export function convergenceRateFor(series, currentCount) {
69
+ if (Array.isArray(series) && series.length > 0) {
70
+ const first = series.find((n) => typeof n === 'number' && Number.isFinite(n));
71
+ const last = [...series].reverse().find((n) => typeof n === 'number' && Number.isFinite(n));
72
+ if (first === undefined || last === undefined)
73
+ return currentCount === 0 ? 100 : 0;
74
+ if (first <= 0)
75
+ return currentCount === 0 ? 100 : 0;
76
+ const raw = ((first - Math.max(0, last)) / first) * 100;
77
+ return Math.max(0, Math.min(100, Math.round(raw)));
78
+ }
79
+ return currentCount === 0 ? 100 : 0;
80
+ }
81
+ /** Compute a quality gate snapshot from review data. */
82
+ export function computeQualityGate(opts) {
83
+ const { dimensions, findings, validationResults, findingsByRound, fixedByDimension } = opts;
84
+ // Count findings by severity
85
+ const criticalCount = findings.filter((f) => f.severity === 'critical').length;
86
+ const highCount = findings.filter((f) => f.severity === 'high').length;
87
+ const mediumCount = findings.filter((f) => f.severity === 'medium').length;
88
+ const lowCount = findings.filter((f) => f.severity === 'low').length;
89
+ const totalFindings = findings.length;
90
+ // Compute dimension scores
91
+ const dimensionStats = {};
92
+ for (const dim of dimensions) {
93
+ dimensionStats[dim] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
94
+ }
95
+ for (const finding of findings) {
96
+ const stats = dimensionStats[finding.dimension];
97
+ if (stats) {
98
+ stats.count++;
99
+ if (finding.severity === 'critical')
100
+ stats.critical++;
101
+ else if (finding.severity === 'high')
102
+ stats.high++;
103
+ else if (finding.severity === 'medium')
104
+ stats.medium++;
105
+ else
106
+ stats.low++;
107
+ }
108
+ }
109
+ // Compute dimension-level quality gates
110
+ const dimensionGates = dimensions.map((dim) => {
111
+ const stats = dimensionStats[dim] || { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
112
+ // Score: 100 - (critical*30 + high*15 + medium*5 + low*1), capped at 0
113
+ const penalty = stats.critical * 30 + stats.high * 15 + stats.medium * 5 + stats.low * 1;
114
+ const score = Math.max(0, 100 - penalty);
115
+ const status = score >= 80 ? 'pass' : score >= 50 ? 'warn' : 'fail';
116
+ const series = findingsByRound?.[dim];
117
+ const convergenceRate = convergenceRateFor(Array.isArray(series) ? series : undefined, stats.count);
118
+ return {
119
+ dimension: dim,
120
+ convergenceRate,
121
+ findingsCount: stats.count,
122
+ fixedCount: fixedByDimension?.[dim] ?? 0,
123
+ score,
124
+ status,
125
+ };
126
+ });
127
+ // Compute verification pass rate
128
+ const totalChecks = validationResults?.length ?? 0;
129
+ const passedChecks = validationResults?.filter((r) => r.exitCode === 0).length ?? 0;
130
+ const failedChecks = totalChecks - passedChecks;
131
+ const verificationPassRate = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0;
132
+ // Compute overall score (weighted average of dimension scores)
133
+ const overallScore = dimensionGates.length > 0
134
+ ? Math.round(dimensionGates.reduce((sum, d) => sum + d.score, 0) / dimensionGates.length)
135
+ : 0;
136
+ // Determine overall status
137
+ const hasCritical = criticalCount > 0;
138
+ const hasHighFail = dimensionGates.some((d) => d.status === 'fail');
139
+ const verificationFails = totalChecks > 0 && failedChecks > 0;
140
+ let overallStatus = 'pass';
141
+ let failReason;
142
+ if (hasCritical) {
143
+ overallStatus = 'fail';
144
+ failReason = `${criticalCount} critical findings present`;
145
+ }
146
+ else if (hasHighFail) {
147
+ overallStatus = 'fail';
148
+ failReason = 'One or more dimensions failed quality gate';
149
+ }
150
+ else if (verificationFails) {
151
+ overallStatus = 'fail';
152
+ failReason = `${failedChecks} validation checks failed`;
153
+ }
154
+ else if (overallScore < 70) {
155
+ overallStatus = 'fail';
156
+ failReason = `Overall score ${overallScore} below threshold (70)`;
157
+ }
158
+ return {
159
+ timestamp: new Date().toISOString(),
160
+ overallStatus,
161
+ overallScore,
162
+ dimensions: dimensionGates,
163
+ verificationPassRate,
164
+ totalChecks,
165
+ passedChecks,
166
+ failedChecks,
167
+ failReason,
168
+ totalFindings,
169
+ criticalCount,
170
+ highCount,
171
+ mediumCount,
172
+ lowCount,
173
+ };
174
+ }
package/lib/client.js CHANGED
@@ -1208,6 +1208,43 @@ var ITERATE_CSS = `
1208
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
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
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); }
1247
+
1211
1248
  /* Accessibility-switch toggle */
1212
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; }
1213
1250
  .iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
@@ -1493,6 +1530,14 @@ function ConvergenceDashboard(props) {
1493
1530
  "data-live": transcript.active === true ? "" : void 0,
1494
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`
1495
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;
1496
1541
  const dimNames = Object.keys(dims);
1497
1542
  const dimBadges = dimNames.slice(0, 6).map(
1498
1543
  (dim) => React.createElement(
@@ -1557,6 +1602,7 @@ function ConvergenceDashboard(props) {
1557
1602
  resumeChip,
1558
1603
  imageChip,
1559
1604
  liveChip,
1605
+ taskModeChip,
1560
1606
  React.createElement(TrendChart, { points: trend.points }),
1561
1607
  ...dimBadges
1562
1608
  );
@@ -1677,6 +1723,7 @@ function TriagePanel(props) {
1677
1723
  const [filter, setFilter] = React.useState({ severities: [], dimensions: [], search: "" });
1678
1724
  const [selected, setSelected] = React.useState(null);
1679
1725
  const [selectAll, setSelectAll] = React.useState(false);
1726
+ const [cmdCopied, setCmdCopied] = React.useState(null);
1680
1727
  React.useEffect(() => {
1681
1728
  if (storage) storage.set(storageKey, JSON.stringify(verdicts));
1682
1729
  }, [storageKey, verdicts]);
@@ -1740,6 +1787,33 @@ function TriagePanel(props) {
1740
1787
  const ignored = collectIgnoredEntries(verdicts, findings);
1741
1788
  const ignoredCount = ignored.length;
1742
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
+ };
1743
1817
  const doCopyYaml = () => {
1744
1818
  const yaml = toKnownIntentionalYaml(ignored);
1745
1819
  if (!yaml) return;
@@ -1895,6 +1969,36 @@ function TriagePanel(props) {
1895
1969
  }, "\u751F\u6210\u5E94\u7528\u6307\u4EE4")
1896
1970
  )
1897
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
+ ),
1898
2002
  payload ? React.createElement("div", { className: "iterate-payload" }, payload) : null
1899
2003
  );
1900
2004
  }
@@ -1978,7 +2082,13 @@ function ProgressCapsule() {
1978
2082
  };
1979
2083
  }, []);
1980
2084
  if (!info) return null;
1981
- 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);
1982
2092
  }
1983
2093
  function SettingsPanel(_props) {
1984
2094
  const [enabled, setEnabled] = React.useState(themeEnabled);
@@ -2171,7 +2281,11 @@ var OBS_TABS = [
2171
2281
  { key: "f4", label: "\u4FEE\u590D\u4E0E\u56DE\u6EDA" },
2172
2282
  { key: "f5", label: "\u65AD\u70B9\u6062\u590D" },
2173
2283
  { key: "f6", label: "\u8FD0\u884C\u63A7\u5236\u53F0" },
2174
- { 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" }
2175
2289
  ];
2176
2290
  var OBS_LIVE_META = {
2177
2291
  read: { label: "\u9605\u8BFB", color: "var(--dsw-alias-brand-primary)" },
@@ -2240,6 +2354,9 @@ function ObservatoryPanel(props) {
2240
2354
  const [f3Filter, setF3Filter] = React.useState(
2241
2355
  { severities: [], dimensions: [], search: "" }
2242
2356
  );
2357
+ const [expSearch, setExpSearch] = React.useState("");
2358
+ const [expResults, setExpResults] = React.useState([]);
2359
+ const [defenseFilter, setDefenseFilter] = React.useState("");
2243
2360
  const copyTimer = React.useRef(null);
2244
2361
  React.useEffect(() => () => {
2245
2362
  if (copyTimer.current) clearTimeout(copyTimer.current);
@@ -2266,6 +2383,7 @@ function ObservatoryPanel(props) {
2266
2383
  });
2267
2384
  };
2268
2385
  const exportObservatory = () => {
2386
+ if (!manifest) return;
2269
2387
  const json = serializeObservatoryExport(manifest, manifest.live || []);
2270
2388
  const filename = `iterate-observatory-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2271
2389
  const flash = () => {
@@ -2483,38 +2601,39 @@ function ObservatoryPanel(props) {
2483
2601
  );
2484
2602
  const findingBlocks = filtered.map((f, i) => {
2485
2603
  const k = `f3-${i}`;
2486
- 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}` : ""}`;
2487
2606
  const entry = {
2488
- file: String(f.file || ""),
2489
- ...typeof f.line === "number" && f.line > 0 ? { line: f.line } : {},
2490
- dimension: String(f.dimension || ""),
2491
- 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 || "")
2492
2611
  };
2493
2612
  const knownInstruction = buildApplyInstruction([entry]);
2494
- const fixInstruction = buildObsFixInstruction(f);
2613
+ const fixInstruction = buildObsFixInstruction(finding);
2495
2614
  return React.createElement(
2496
2615
  "div",
2497
2616
  { key: k, className: "iterate-obs-block" },
2498
2617
  React.createElement(
2499
2618
  "div",
2500
2619
  { className: "iterate-obs-block-head" },
2501
- React.createElement("span", { className: "iterate-sev-dot", style: { background: severityColor(f.severity) } }),
2502
- 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)),
2503
2622
  React.createElement("span", { className: "iterate-obs-file" }, loc),
2504
- React.createElement("span", {}, String(f.dimension || "")),
2505
- 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
2506
2625
  ),
2507
2626
  React.createElement(
2508
2627
  "div",
2509
2628
  { className: "iterate-obs-block-body" },
2510
- React.createElement("div", { className: "iterate-obs-msg" }, String(f.summary || "")),
2629
+ React.createElement("div", { className: "iterate-obs-msg" }, String(finding.summary || "")),
2511
2630
  React.createElement(
2512
2631
  "div",
2513
2632
  { className: "iterate-obs-bar", style: { marginTop: 8 } },
2514
2633
  React.createElement("button", {
2515
2634
  className: "iterate-btn",
2516
2635
  "data-copied": copiedKey === `${k}-fix` ? "" : void 0,
2517
- onClick: () => copyInstruction(`${k}-fix`, String(f.suggested_fix || "")),
2636
+ onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || "")),
2518
2637
  title: "\u590D\u5236\u8BE5 finding \u7684\u5EFA\u8BAE\u4FEE\u590D\u6587\u672C"
2519
2638
  }, copiedKey === `${k}-fix` ? "\u5DF2\u590D\u5236" : "\u590D\u5236 suggested_fix"),
2520
2639
  React.createElement("button", {
@@ -2793,6 +2912,183 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2793
2912
  }
2794
2913
  return React.createElement("div", {}, filterBar, ...rows);
2795
2914
  };
2915
+ const renderQualityGate = () => {
2916
+ const qualityGateInstruction = "\u8BF7\u8C03\u7528 `iterate_quality_gate` \u67E5\u8BE2\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u72B6\u6001";
2917
+ return React.createElement(
2918
+ "div",
2919
+ {},
2920
+ React.createElement(
2921
+ "div",
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" },
2935
+ React.createElement(
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")
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 } },
2993
+ React.createElement("input", {
2994
+ className: "iterate-filter-search",
2995
+ type: "search",
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)
3000
+ }),
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")
3045
+ ),
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
+ )
3090
+ );
3091
+ };
2796
3092
  const renderLive = () => {
2797
3093
  const liveEntries = manifest.live || [];
2798
3094
  if (liveEntries.length === 0) {
@@ -2820,7 +3116,7 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2820
3116
  const meta = OBS_LIVE_META[type] || { label: String(e.type || "?"), color: OBS_LIVE_FALLBACK_COLOR };
2821
3117
  const tool = String(e.tool || "");
2822
3118
  const target = String(e.target || "");
2823
- const time = formatObsLiveTime(e.ts);
3119
+ const time = formatObsLiveTime(String(e.ts || ""));
2824
3120
  return React.createElement(
2825
3121
  "div",
2826
3122
  { key: `live-${i}`, className: "iterate-obs-row" },
@@ -2880,6 +3176,13 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2880
3176
  return renderConsole();
2881
3177
  case "f7":
2882
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();
2883
3186
  default:
2884
3187
  return null;
2885
3188
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.12.3",
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.",
3
+ "version": "3.2.1",
4
+ "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.2). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -61,13 +61,13 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@deepseek-ai/cordis": "4.0.2",
64
- "@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
65
- "@deepseek-ai/dsh-util-values": "0.1.2-alpha.3",
64
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
65
+ "@deepseek-ai/dsh-util-values": "0.1.2-rc.1",
66
66
  "js-yaml": "4.3.1"
67
67
  },
68
68
  "devDependencies": {
69
- "@deepseek-ai/dsh-jobs": "^0.1.2-alpha.3",
70
- "@deepseek-ai/dsh-session": "0.1.2-alpha.3",
69
+ "@deepseek-ai/dsh-jobs": "^0.1.2-rc.1",
70
+ "@deepseek-ai/dsh-session": "0.1.2-rc.1",
71
71
  "@types/js-yaml": "4.0.9",
72
72
  "@types/node": "22.15.0",
73
73
  "@types/react": "19.2.2",