iterate-plugin 2.12.3 → 3.3.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.
@@ -73,6 +73,9 @@ import {
73
73
  countSessionImages,
74
74
  scanSessionForTranscript,
75
75
  normalizeTranscript,
76
+ scanSessionForQualityGate,
77
+ scanSessionForExperienceBank,
78
+ scanSessionForDefenseEvents,
76
79
  filterLiveEntries,
77
80
  filterTimelineEntries,
78
81
  serializeObservatoryExport,
@@ -228,6 +231,7 @@ interface ObsManifest {
228
231
  updatedAt?: string
229
232
  active?: boolean
230
233
  mode?: string
234
+ taskMode?: string | null
231
235
  goal?: string
232
236
  phases?: string[]
233
237
  round?: number
@@ -243,6 +247,80 @@ interface ObsManifest {
243
247
  approval?: ObsApproval
244
248
  }
245
249
 
250
+ // ─── Quality command center data types (v3.1+; mirrors src/types.ts) ────────
251
+ // These mirror the JSON the three command-center tools return so the F8/F9/F10
252
+ // tabs can render session-scanned data with full optional chaining.
253
+
254
+ interface ObsQualityGateDimension {
255
+ dimension?: string
256
+ convergenceRate?: number
257
+ findingsCount?: number
258
+ fixedCount?: number
259
+ score?: number
260
+ status?: string
261
+ }
262
+
263
+ interface ObsQualityGate {
264
+ timestamp?: string
265
+ overallStatus?: string
266
+ overallScore?: number
267
+ verificationPassRate?: number
268
+ totalChecks?: number
269
+ passedChecks?: number
270
+ failedChecks?: number
271
+ failReason?: string | null
272
+ totalFindings?: number
273
+ criticalCount?: number
274
+ highCount?: number
275
+ mediumCount?: number
276
+ lowCount?: number
277
+ dimensions?: ObsQualityGateDimension[]
278
+ }
279
+
280
+ interface ObsExperienceEntry {
281
+ id?: string
282
+ timestamp?: string
283
+ dimension?: string
284
+ pattern?: string
285
+ description?: string
286
+ verifiedFix?: string
287
+ findingSummary?: string
288
+ severity?: string
289
+ hitCount?: number
290
+ lastHitAt?: string | null
291
+ files?: string[]
292
+ tags?: string[]
293
+ }
294
+
295
+ interface ObsExperienceResult {
296
+ operation?: string
297
+ count?: number
298
+ totalHits?: number
299
+ added?: boolean
300
+ entries?: ObsExperienceEntry[]
301
+ }
302
+
303
+ interface ObsDefenseEvent {
304
+ id?: string
305
+ timestamp?: string
306
+ round?: number
307
+ type?: string
308
+ description?: string
309
+ defense?: string
310
+ outcome?: string
311
+ file?: string | null
312
+ line?: number | null
313
+ severity?: string
314
+ }
315
+
316
+ interface ObsDefenseResult {
317
+ operation?: string
318
+ count?: number
319
+ language?: string
320
+ counts?: Record<string, number>
321
+ events?: ObsDefenseEvent[]
322
+ }
323
+
246
324
  /** Opaque slot props: heterogeneous per-slot payloads. */
247
325
  type SlotProps = Record<string, unknown>
248
326
 
@@ -435,6 +513,43 @@ const ITERATE_CSS = `
435
513
  .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); }
436
514
  .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); }
437
515
 
516
+ /* v3.0: task_mode indicator chip */
517
+ .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; }
518
+ .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); }
519
+ .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); }
520
+
521
+ /* v3.0: Quality gate status */
522
+ .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; }
523
+ .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); }
524
+ .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); }
525
+ .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); }
526
+
527
+ /* v3.0: Experience bank hit highlight */
528
+ .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); }
529
+ .iterate-exp-hit b { font-weight: 600; }
530
+
531
+ /* v3.0: Defense event type badges */
532
+ .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; }
533
+ .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); }
534
+ .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); }
535
+ .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); }
536
+ .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); }
537
+
538
+ /* v3.0: Command buttons */
539
+ .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; }
540
+ .iterate-cmd:hover { background: var(--dsw-alias-bg-layer-2); }
541
+ .iterate-cmd[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
542
+ .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); }
543
+ .iterate-cmd:disabled { opacity: 0.5; cursor: not-allowed; }
544
+
545
+ /* v3.0: Dimension score bar */
546
+ .iterate-dim-score { display: flex; align-items: center; gap: 6px; font-size: 11px; }
547
+ .iterate-dim-score-bar { width: 60px; height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
548
+ .iterate-dim-score-fill { height: 100%; border-radius: 3px; transition: width 300ms ease; }
549
+ .iterate-dim-score-fill[data-status="pass"] { background: var(--dsw-alias-state-success-primary); }
550
+ .iterate-dim-score-fill[data-status="warn"] { background: var(--dsw-alias-state-warn-primary); }
551
+ .iterate-dim-score-fill[data-status="fail"] { background: var(--dsw-alias-state-error-primary); }
552
+
438
553
  /* Accessibility-switch toggle */
439
554
  .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; }
440
555
  .iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
@@ -795,6 +910,19 @@ function ConvergenceDashboard(props: SlotProps) {
795
910
  }, `${phase} · ${transcript.active === true ? '运行中' : '已结束'}`)
796
911
  : null
797
912
 
913
+ // v3.0: task_mode indicator (code/iterate)
914
+ // task_mode is read from the session transcript if available
915
+ const taskModeTranscript = latestTranscript(session)
916
+ const taskMode = (taskModeTranscript as unknown as { taskMode?: string })?.taskMode
917
+ const taskModeChip = taskMode
918
+ ? React.createElement('span', {
919
+ className: 'iterate-chip-taskmode',
920
+ key: 'taskmode',
921
+ 'data-mode': taskMode,
922
+ title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
923
+ }, taskMode === 'code' ? '● code' : '● iterate')
924
+ : null
925
+
798
926
  const dimNames = Object.keys(dims)
799
927
  const dimBadges = dimNames.slice(0, 6).map((dim) =>
800
928
  React.createElement(
@@ -869,6 +997,7 @@ function ConvergenceDashboard(props: SlotProps) {
869
997
  resumeChip,
870
998
  imageChip,
871
999
  liveChip,
1000
+ taskModeChip,
872
1001
  React.createElement(TrendChart, { points: trend.points }),
873
1002
  ...dimBadges,
874
1003
  )
@@ -974,6 +1103,8 @@ function TriagePanel(props: SlotProps) {
974
1103
  const [filter, setFilter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>({ severities: [], dimensions: [], search: '' })
975
1104
  const [selected, setSelected] = React.useState<number | null>(null)
976
1105
  const [selectAll, setSelectAll] = React.useState(false)
1106
+ // v3.0: Command mode for native action buttons
1107
+ const [cmdCopied, setCmdCopied] = React.useState<string | null>(null)
977
1108
 
978
1109
  /** Persist the verdicts to localStorage whenever they change. Kept OUT of
979
1110
  * the state updater (updaters must stay pure — React may double-invoke them
@@ -1059,6 +1190,64 @@ function TriagePanel(props: SlotProps) {
1059
1190
  const ignoredCount = ignored.length
1060
1191
  const counts = countVerdicts(verdicts)
1061
1192
 
1193
+ // v3.0: Command handlers for native action buttons
1194
+ const doApproveArchitecturalFix = () => {
1195
+ const cmd = '请调用 `iterate_fix` 批准架构修复(设置 is_architectural: true)'
1196
+ copyText(cmd).then((ok) => {
1197
+ if (ok) {
1198
+ setCmdCopied('approve-arch')
1199
+ setTimeout(() => setCmdCopied(null), 1600)
1200
+ }
1201
+ })
1202
+ }
1203
+
1204
+ const doTriggerNewRound = () => {
1205
+ const cmd = '请继续执行下一轮迭代审查'
1206
+ copyText(cmd).then((ok) => {
1207
+ if (ok) {
1208
+ setCmdCopied('new-round')
1209
+ setTimeout(() => setCmdCopied(null), 1600)
1210
+ }
1211
+ })
1212
+ }
1213
+
1214
+ const doRollbackToCheckpoint = () => {
1215
+ const cmd = '请调用 `iterate_checkpoint` 回滚到上一个检查点'
1216
+ copyText(cmd).then((ok) => {
1217
+ if (ok) {
1218
+ setCmdCopied('rollback')
1219
+ setTimeout(() => setCmdCopied(null), 1600)
1220
+ }
1221
+ })
1222
+ }
1223
+
1224
+ // v3.1+ §8: assign the currently scoped findings to the model for fixing.
1225
+ // Scoping mirrors the batch buttons (visible when selectAll is off, all
1226
+ // otherwise); produces one paste-able iterate_fix instruction carrying each
1227
+ // finding's file/line/dimension/severity/summary.
1228
+ const doAssignFindings = () => {
1229
+ const targetAll = selectAll
1230
+ const scopeIndices = targetAll ? allIndices : indices
1231
+ const scopeFindings = (findings as IterateFinding[])
1232
+ .filter((f, i) => scopeIndices.includes(i))
1233
+ .map((f) => ({
1234
+ file: String(f.file || ''),
1235
+ ...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
1236
+ dimension: String(f.dimension || ''),
1237
+ severity: String(f.severity || ''),
1238
+ summary: String(f.summary || ''),
1239
+ ...(f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}),
1240
+ }))
1241
+ if (scopeFindings.length === 0) return
1242
+ const cmd = `请调用 \`iterate_fix\` 指派并修复以下 findings:\n\n\`\`\`json\n${JSON.stringify(scopeFindings, null, 2)}\n\`\`\``
1243
+ copyText(cmd).then((ok) => {
1244
+ if (ok) {
1245
+ setCmdCopied('assign')
1246
+ setTimeout(() => setCmdCopied(null), 1600)
1247
+ }
1248
+ })
1249
+ }
1250
+
1062
1251
  const doCopyYaml = () => {
1063
1252
  const yaml = toKnownIntentionalYaml(ignored)
1064
1253
  if (!yaml) return
@@ -1191,6 +1380,33 @@ function TriagePanel(props: SlotProps) {
1191
1380
  }, '生成应用指令'),
1192
1381
  ),
1193
1382
  ),
1383
+ // v3.0: Command buttons for native actions (§8)
1384
+ React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
1385
+ React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
1386
+ React.createElement('span', { style: { display: 'flex', gap: 6 } },
1387
+ React.createElement('button', {
1388
+ className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
1389
+ onClick: doApproveArchitecturalFix,
1390
+ title: '批准架构修复(复制指令文本)',
1391
+ }, cmdCopied === 'approve-arch' ? '已复制' : '批准架构修复'),
1392
+ React.createElement('button', {
1393
+ className: 'iterate-cmd', 'data-copied': cmdCopied === 'new-round' ? '' : undefined,
1394
+ onClick: doTriggerNewRound,
1395
+ title: '触发新一轮迭代审查',
1396
+ }, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
1397
+ React.createElement('button', {
1398
+ className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'assign' ? '' : undefined,
1399
+ onClick: doAssignFindings,
1400
+ disabled: (selectAll ? allIndices : indices).length === 0,
1401
+ title: selectAll ? `指派全部 ${allIndices.length} 个 findings 修复` : `指派当前可见 ${indices.length} 个 findings 修复`,
1402
+ }, cmdCopied === 'assign' ? '已复制' : '指派修复'),
1403
+ React.createElement('button', {
1404
+ className: 'iterate-cmd', 'data-danger': '', 'data-copied': cmdCopied === 'rollback' ? '' : undefined,
1405
+ onClick: doRollbackToCheckpoint,
1406
+ title: '回滚到上一个检查点',
1407
+ }, cmdCopied === 'rollback' ? '已复制' : '回滚检查点'),
1408
+ ),
1409
+ ),
1194
1410
  payload
1195
1411
  ? React.createElement('div', { className: 'iterate-payload' }, payload)
1196
1412
  : null,
@@ -1281,7 +1497,13 @@ function ProgressCapsule() {
1281
1497
  }
1282
1498
  }, [])
1283
1499
  if (!info) return null
1284
- return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-capsule', 'data-ok': info.ok ? '' : undefined }, info.text)
1500
+ return React.createElement('div', {
1501
+ 'data-iterate-root': '',
1502
+ className: 'iterate-capsule',
1503
+ 'data-ok': info.ok ? '' : undefined,
1504
+ role: 'status',
1505
+ 'aria-live': 'polite',
1506
+ }, info.text)
1285
1507
  }
1286
1508
 
1287
1509
  /** Settings page section (root scope; reads localStorage + latest known data). */
@@ -1452,6 +1674,10 @@ const OBS_TABS = [
1452
1674
  { key: 'f5', label: '断点恢复' },
1453
1675
  { key: 'f6', label: '运行控制台' },
1454
1676
  { key: 'f7', label: '决策时间线' },
1677
+ // v3.0: Quality Command Center tabs
1678
+ { key: 'f8', label: '质量门禁' },
1679
+ { key: 'f9', label: '经验银行' },
1680
+ { key: 'f10', label: '防御事件' },
1455
1681
  ] as const
1456
1682
 
1457
1683
  /** Live subagent activity badges: type → Chinese label + dsw token color accent. */
@@ -1499,6 +1725,24 @@ function latestTranscript(session: unknown): ObsManifest | null {
1499
1725
  return raw ? normalizeTranscript(raw) as ObsManifest : null
1500
1726
  }
1501
1727
 
1728
+ /** Find the latest quality-gate snapshot inside a session snapshot. */
1729
+ function latestQualityGate(session: unknown): ObsQualityGate | null {
1730
+ if (!session) return null
1731
+ return scanSessionForQualityGate(session) as ObsQualityGate | null
1732
+ }
1733
+
1734
+ /** Find the latest experience-bank result inside a session snapshot. */
1735
+ function latestExperienceBank(session: unknown): ObsExperienceResult | null {
1736
+ if (!session) return null
1737
+ return scanSessionForExperienceBank(session) as ObsExperienceResult | null
1738
+ }
1739
+
1740
+ /** Find the latest defense-events result inside a session snapshot. */
1741
+ function latestDefenseEvents(session: unknown): ObsDefenseResult | null {
1742
+ if (!session) return null
1743
+ return scanSessionForDefenseEvents(session) as ObsDefenseResult | null
1744
+ }
1745
+
1502
1746
  /** Build a human-pasteable `iterate_fix` instruction for one finding. */
1503
1747
  function buildObsFixInstruction(f: ObsFinding): string {
1504
1748
  const payload = JSON.stringify({
@@ -1523,6 +1767,10 @@ function buildObsFixInstruction(f: ObsFinding): string {
1523
1767
  function ObservatoryPanel(props: SlotProps) {
1524
1768
  const session = props && props.session ? props.session : null
1525
1769
  const manifest = latestTranscript(session)
1770
+ // v3.1+: command-center data scanned straight out of the session stream.
1771
+ const qualityGate = latestQualityGate(session)
1772
+ const experienceBank = latestExperienceBank(session)
1773
+ const defenseEvents = latestDefenseEvents(session)
1526
1774
 
1527
1775
  const [open, setOpen] = React.useState(false)
1528
1776
  const [tab, setTab] = React.useState('live')
@@ -1543,6 +1791,13 @@ function ObservatoryPanel(props: SlotProps) {
1543
1791
  const [f3Filter, setF3Filter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>(
1544
1792
  { severities: [], dimensions: [], search: '' },
1545
1793
  )
1794
+ // v3.0: Experience bank search state
1795
+ const [expSearch, setExpSearch] = React.useState('')
1796
+ // v3.1+: session-scanned experience entries seed the list; expSearch filters
1797
+ // them client-side (matching pattern/description/dimension/tags). The "搜索"
1798
+ // button still copies a back-end iterate_experience search instruction.
1799
+ // v3.0: Defense events filter
1800
+ const [defenseFilter, setDefenseFilter] = React.useState('')
1546
1801
 
1547
1802
  // Single shared "copied" flash timer (reused across all copy buttons).
1548
1803
  const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -1571,6 +1826,7 @@ function ObservatoryPanel(props: SlotProps) {
1571
1826
 
1572
1827
  /** Export the full observatory snapshot as JSON (download, then copy fallback). */
1573
1828
  const exportObservatory = () => {
1829
+ if (!manifest) return
1574
1830
  const json = serializeObservatoryExport(manifest, manifest.live || [])
1575
1831
  const filename = `iterate-observatory-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
1576
1832
  const flash = () => {
@@ -1722,8 +1978,8 @@ function ObservatoryPanel(props: SlotProps) {
1722
1978
  if (findings.length === 0) {
1723
1979
  return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
1724
1980
  }
1725
- const opts = buildFilterOptions(findings)
1726
- const { filtered } = filterFindingsWithIndices(findings, f3Filter)
1981
+ const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
1982
+ const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
1727
1983
  const filterActive = f3Filter.severities.length > 0 || f3Filter.dimensions.length > 0 || f3Filter.search.trim() !== ''
1728
1984
  const toggleSeverity = (sev: string) => {
1729
1985
  setF3Filter((prev) => ({
@@ -1778,31 +2034,32 @@ function ObservatoryPanel(props: SlotProps) {
1778
2034
  )
1779
2035
  const findingBlocks = filtered.map((f, i) => {
1780
2036
  const k = `f3-${i}`
1781
- const loc = `${String(f.file || '?')}${typeof f.line === 'number' && f.line > 0 ? `:${f.line}` : ''}`
2037
+ const finding = f as unknown as ObsFinding
2038
+ const loc = `${String(finding.file || '?')}${typeof finding.line === 'number' && finding.line > 0 ? `:${finding.line}` : ''}`
1782
2039
  const entry = {
1783
- file: String(f.file || ''),
1784
- ...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
1785
- dimension: String(f.dimension || ''),
1786
- reason: String(f.summary || ''),
2040
+ file: String(finding.file || ''),
2041
+ ...(typeof finding.line === 'number' && finding.line > 0 ? { line: finding.line } : {}),
2042
+ dimension: String(finding.dimension || ''),
2043
+ reason: String(finding.summary || ''),
1787
2044
  }
1788
2045
  const knownInstruction = buildApplyInstruction([entry])
1789
- const fixInstruction = buildObsFixInstruction(f)
2046
+ const fixInstruction = buildObsFixInstruction(finding)
1790
2047
  return React.createElement('div', { key: k, className: 'iterate-obs-block' },
1791
2048
  React.createElement('div', { className: 'iterate-obs-block-head' },
1792
- React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(f.severity) } }),
1793
- React.createElement('span', {}, severityLabel(f.severity)),
2049
+ React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(finding.severity) } }),
2050
+ React.createElement('span', {}, severityLabel(finding.severity)),
1794
2051
  React.createElement('span', { className: 'iterate-obs-file' }, loc),
1795
- React.createElement('span', {}, String(f.dimension || '')),
1796
- f.acknowledged === true
2052
+ React.createElement('span', {}, String(finding.dimension || '')),
2053
+ finding.acknowledged === true
1797
2054
  ? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
1798
2055
  : null,
1799
2056
  ),
1800
2057
  React.createElement('div', { className: 'iterate-obs-block-body' },
1801
- React.createElement('div', { className: 'iterate-obs-msg' }, String(f.summary || '')),
2058
+ React.createElement('div', { className: 'iterate-obs-msg' }, String(finding.summary || '')),
1802
2059
  React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
1803
2060
  React.createElement('button', {
1804
2061
  className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
1805
- onClick: () => copyInstruction(`${k}-fix`, String(f.suggested_fix || '')),
2062
+ onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
1806
2063
  title: '复制该 finding 的建议修复文本',
1807
2064
  }, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
1808
2065
  React.createElement('button', {
@@ -2033,6 +2290,293 @@ function ObservatoryPanel(props: SlotProps) {
2033
2290
  return React.createElement('div', {}, filterBar, ...rows)
2034
2291
  }
2035
2292
 
2293
+ // ── F8: Quality Gate ──────────────────────────────────────────────────────
2294
+ // v3.1+ renders the latest iterate_quality_gate snapshot found in the
2295
+ // session (dimension convergence, verification pass rate, overall PASS/FAIL);
2296
+ // falls back to copy-able query instructions when no snapshot exists yet.
2297
+ const renderQualityGate = () => {
2298
+ const gateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
2299
+ const gate = qualityGate
2300
+ const dims = gate && gate.dimensions ? gate.dimensions : []
2301
+ const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
2302
+ const status = gate && gate.overallStatus ? String(gate.overallStatus) : 'pending'
2303
+ const statusLabel = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'PENDING'
2304
+ const num = (v: number | undefined) => Number(v != null ? v : 0)
2305
+
2306
+ const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2307
+ React.createElement('b', {}, '质量门禁'),
2308
+ React.createElement('button', {
2309
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
2310
+ onClick: () => copyInstruction('qgate', gateInstruction),
2311
+ title: '复制 iterate_quality_gate 查询指令',
2312
+ }, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
2313
+ )
2314
+
2315
+ if (!hasGate) {
2316
+ return React.createElement('div', {},
2317
+ headerBar,
2318
+ React.createElement('div', { className: 'iterate-obs-block' },
2319
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2320
+ React.createElement('span', {}, '质量门禁视图'),
2321
+ React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
2322
+ ),
2323
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2324
+ React.createElement('div', { className: 'iterate-obs-msg' }, '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
2325
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
2326
+ React.createElement('button', {
2327
+ className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
2328
+ onClick: () => copyInstruction('qgate-instr', gateInstruction),
2329
+ title: '复制查询指令',
2330
+ }, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
2331
+ ),
2332
+ ),
2333
+ ),
2334
+ )
2335
+ }
2336
+
2337
+ return React.createElement('div', {},
2338
+ headerBar,
2339
+ React.createElement('div', { className: 'iterate-obs-block' },
2340
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2341
+ React.createElement('span', { className: 'iterate-gate', 'data-status': status }, statusLabel),
2342
+ React.createElement('span', { className: 'iterate-obs-head-meta' },
2343
+ `整体评分 ${num(gate && gate.overallScore)} · 验证 ${num(gate && gate.passedChecks)}/${num(gate && gate.totalChecks)} · 通过率 ${num(gate && gate.verificationPassRate)}%`,
2344
+ ),
2345
+ ),
2346
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2347
+ dims.length > 0
2348
+ ? React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6 } },
2349
+ ...dims.map((d, i) =>
2350
+ React.createElement('span', { key: `gdim-${i}`, className: 'iterate-obs-chip' },
2351
+ `${String(d.dimension || '?')} · 收敛 ${num(d.convergenceRate)}% · 评分 ${num(d.score)}`,
2352
+ ),
2353
+ ),
2354
+ )
2355
+ : null,
2356
+ React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap' } },
2357
+ ...dims.map((d, i) => {
2358
+ const fill = Math.min(100, Math.max(0, num(d.score)))
2359
+ return React.createElement('div', { key: `gbar-${i}`, className: 'iterate-dim-score' },
2360
+ React.createElement('span', {}, String(d.dimension || '?')),
2361
+ React.createElement('div', { className: 'iterate-dim-score-bar' },
2362
+ React.createElement('div', {
2363
+ className: 'iterate-dim-score-fill',
2364
+ 'data-status': String(d.status || 'warn'),
2365
+ style: { width: `${fill}%` },
2366
+ }),
2367
+ ),
2368
+ React.createElement('span', {}, `评分 ${num(d.score)} · 发现 ${num(d.findingsCount)} · 已修复 ${num(d.fixedCount)}`),
2369
+ )
2370
+ }),
2371
+ ),
2372
+ gate && gate.failReason
2373
+ ? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } }, `失败原因:${String(gate.failReason)}`)
2374
+ : null,
2375
+ ),
2376
+ ),
2377
+ )
2378
+ }
2379
+
2380
+ // ── F9: Experience Bank ────────────────────────────────────────────────────
2381
+ // v3.1+ renders experience entries scanned from the session plus a
2382
+ // client-side keyword filter; the copy buttons still reach the back end
2383
+ // (list all / search / adopt one entry).
2384
+ const renderExperienceBank = () => {
2385
+ const expInstruction = expSearch
2386
+ ? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n${JSON.stringify({ operation: 'search', query: expSearch }, null, 2)}\n\`\`\``
2387
+ : '请调用 `iterate_experience` 列出所有经验'
2388
+
2389
+ const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
2390
+ const query = expSearch.trim().toLowerCase()
2391
+ const visible = query
2392
+ ? expEntries.filter((e) =>
2393
+ [e.pattern, e.description, e.id, e.dimension, e.severity, ...(e.tags || [])]
2394
+ .filter(Boolean)
2395
+ .some((t) => String(t).toLowerCase().includes(query)),
2396
+ )
2397
+ : expEntries
2398
+
2399
+ const resultRows = visible.map((e, i) => {
2400
+ const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n${JSON.stringify({ operation: 'get', id: e.id }, null, 2)}\n\`\`\``
2401
+ return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-block' },
2402
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2403
+ React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
2404
+ React.createElement('span', {}, String(e.pattern || '?')),
2405
+ React.createElement('span', { className: 'iterate-exp-hit' }, `命中 ${e.hitCount ?? 0} 次`),
2406
+ ),
2407
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2408
+ React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
2409
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
2410
+ e.verifiedFix
2411
+ ? React.createElement('button', {
2412
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `exp-adopt-${i}` ? '' : undefined,
2413
+ onClick: () => copyInstruction(`exp-adopt-${i}`, adoptInstruction),
2414
+ title: '复制采纳指令(查询该经验并应用到当前上下文)',
2415
+ }, copiedKey === `exp-adopt-${i}` ? '已复制' : '采纳')
2416
+ : null,
2417
+ ),
2418
+ ),
2419
+ )
2420
+ })
2421
+
2422
+ return React.createElement('div', {},
2423
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2424
+ React.createElement('b', {}, '经验银行'),
2425
+ React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${expEntries.length} 条`),
2426
+ React.createElement('button', {
2427
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
2428
+ onClick: () => copyInstruction('exp-list', '请调用 `iterate_experience` 列出所有经验'),
2429
+ title: '复制列出经验指令',
2430
+ }, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
2431
+ ),
2432
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2433
+ React.createElement('input', {
2434
+ className: 'iterate-filter-search',
2435
+ type: 'search',
2436
+ placeholder: '搜索经验模式…',
2437
+ 'aria-label': '搜索经验',
2438
+ value: expSearch,
2439
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
2440
+ }),
2441
+ React.createElement('button', {
2442
+ className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
2443
+ disabled: !expSearch,
2444
+ onClick: () => copyInstruction('exp-search', expInstruction),
2445
+ title: '复制搜索指令',
2446
+ }, copiedKey === 'exp-search' ? '已复制' : '搜索'),
2447
+ ),
2448
+ resultRows.length > 0
2449
+ ? React.createElement('div', {}, ...resultRows)
2450
+ : expEntries.length > 0
2451
+ ? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
2452
+ : React.createElement('div', { className: 'iterate-obs-empty' }, '本次会话暂无经验记录,可点击「列出经验」查询历史经验'),
2453
+ )
2454
+ }
2455
+
2456
+ // ── F10: Defense Events ────────────────────────────────────────────────────
2457
+ // v3.1+ renders defense events + type counts scanned from the session
2458
+ // (precondition failures, rollbacks, invariant violations, assumption
2459
+ // falsifications); copy buttons reach the back end for the full history.
2460
+ const renderDefenseEvents = () => {
2461
+ const defenseInstruction = defenseFilter
2462
+ ? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n${JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2)}\n\`\`\``
2463
+ : '请调用 `iterate_defense_events` 列出所有防御事件'
2464
+
2465
+ const typeOptions = [
2466
+ { value: '', label: '全部类型' },
2467
+ { value: 'precondition_failed', label: '前置校验失败' },
2468
+ { value: 'rollback', label: '回滚' },
2469
+ { value: 'invariant_violated', label: '不变量违反' },
2470
+ { value: 'assumption_falsified', label: '假设被证伪' },
2471
+ ]
2472
+ const TYPE_LABEL: Record<string, string> = {
2473
+ precondition_failed: '前置校验失败',
2474
+ rollback: '回滚',
2475
+ invariant_violated: '不变量违反',
2476
+ assumption_falsified: '假设被证伪',
2477
+ }
2478
+
2479
+ const counts = (defenseEvents && defenseEvents.counts) || {}
2480
+ const allEvents = defenseEvents && defenseEvents.events ? defenseEvents.events : []
2481
+ const totalCount = typeOptions.slice(1).reduce((acc, o) => acc + (counts[o.value] || 0), 0)
2482
+ const visibleEvents = defenseFilter
2483
+ ? allEvents.filter((e) => e.type === defenseFilter)
2484
+ : allEvents
2485
+
2486
+ const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2487
+ React.createElement('b', {}, '防御事件'),
2488
+ React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${allEvents.length} 条 / 累计 ${totalCount}`),
2489
+ React.createElement('button', {
2490
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
2491
+ onClick: () => copyInstruction('defense-list', '请调用 `iterate_defense_events` 列出所有防御事件'),
2492
+ title: '复制列出防御事件指令',
2493
+ }, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
2494
+ React.createElement('button', {
2495
+ className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
2496
+ onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
2497
+ title: '复制统计指令',
2498
+ }, copiedKey === 'defense-counts' ? '已复制' : '统计'),
2499
+ )
2500
+
2501
+ if (allEvents.length === 0 && totalCount === 0) {
2502
+ return React.createElement('div', {},
2503
+ headerBar,
2504
+ React.createElement('div', { className: 'iterate-obs-block' },
2505
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2506
+ React.createElement('span', {}, '防御事件流'),
2507
+ React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
2508
+ ),
2509
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2510
+ React.createElement('div', { className: 'iterate-obs-msg' }, '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
2511
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
2512
+ React.createElement('button', {
2513
+ className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
2514
+ onClick: () => copyInstruction('defense-instr', defenseInstruction),
2515
+ title: '复制查询指令',
2516
+ }, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
2517
+ ),
2518
+ ),
2519
+ ),
2520
+ )
2521
+ }
2522
+
2523
+ const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2524
+ React.createElement('select', {
2525
+ className: 'iterate-filter-select',
2526
+ value: defenseFilter,
2527
+ 'aria-label': '按事件类型筛选',
2528
+ onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
2529
+ },
2530
+ ...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
2531
+ ),
2532
+ defenseFilter
2533
+ ? React.createElement('button', {
2534
+ className: 'iterate-filter-clear',
2535
+ onClick: () => setDefenseFilter(''),
2536
+ title: '清除类型筛选',
2537
+ }, '清除')
2538
+ : null,
2539
+ )
2540
+
2541
+ let eventRows: React.ReactNode[] = []
2542
+ if (visibleEvents.length > 0) {
2543
+ eventRows = visibleEvents.map((e, i) => {
2544
+ const type = String(e.type || 'unknown')
2545
+ const loc = `${String(e.file || '?')}${typeof e.line === 'number' && e.line > 0 ? `:${e.line}` : ''}`
2546
+ return React.createElement('div', { key: `def-${i}`, className: 'iterate-obs-block' },
2547
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2548
+ React.createElement('span', { className: 'iterate-defense-badge', 'data-type': type }, TYPE_LABEL[type] || type),
2549
+ typeof e.round === 'number'
2550
+ ? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${e.round}`)
2551
+ : null,
2552
+ React.createElement('span', { className: 'iterate-obs-head-meta' }, loc),
2553
+ ),
2554
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2555
+ React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
2556
+ e.defense
2557
+ ? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } }, `防线:${String(e.defense)}`)
2558
+ : null,
2559
+ ),
2560
+ )
2561
+ })
2562
+ }
2563
+
2564
+ return React.createElement('div', {},
2565
+ headerBar,
2566
+ filterBar,
2567
+ React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6, marginBottom: 8 } },
2568
+ ...typeOptions.slice(1).map((opt) =>
2569
+ React.createElement('span', { key: `cnt-${opt.value}`, className: 'iterate-defense-badge', 'data-type': opt.value },
2570
+ `${opt.label} ${counts[opt.value] || 0}`,
2571
+ ),
2572
+ ),
2573
+ ),
2574
+ eventRows.length > 0
2575
+ ? React.createElement('div', {}, ...eventRows)
2576
+ : React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配该类型的防御事件'),
2577
+ )
2578
+ }
2579
+
2036
2580
  // ── Live: real-time secondary-subagent activity stream (newest first) ─────
2037
2581
  const renderLive = () => {
2038
2582
  const liveEntries = manifest.live || []
@@ -2060,7 +2604,7 @@ function ObservatoryPanel(props: SlotProps) {
2060
2604
  const meta = OBS_LIVE_META[type] || { label: String(e.type || '?'), color: OBS_LIVE_FALLBACK_COLOR }
2061
2605
  const tool = String(e.tool || '')
2062
2606
  const target = String(e.target || '')
2063
- const time = formatObsLiveTime(e.ts)
2607
+ const time = formatObsLiveTime(String(e.ts || ''))
2064
2608
  return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
2065
2609
  React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: meta.color } }, meta.label),
2066
2610
  tool
@@ -2109,6 +2653,10 @@ function ObservatoryPanel(props: SlotProps) {
2109
2653
  case 'f5': return renderCheckpoint()
2110
2654
  case 'f6': return renderConsole()
2111
2655
  case 'f7': return renderTimeline()
2656
+ // v3.0: Quality Command Center tabs
2657
+ case 'f8': return renderQualityGate()
2658
+ case 'f9': return renderExperienceBank()
2659
+ case 'f10': return renderDefenseEvents()
2112
2660
  default: return null
2113
2661
  }
2114
2662
  }