dsh-recency-guard 0.10.0 → 0.11.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.
package/README.md CHANGED
@@ -65,6 +65,67 @@
65
65
 
66
66
  当某个插件把 DSH 拖垮时,本插件会在崩溃的那一刻记录下"谁最可疑",并在**下一次启动时、在崩溃插件的代码被加载之前**把它挡在门外。反复崩溃时,关闭的插件会逐个增加(第 1 次关最近更新的 1 个,仍崩溃则关最近更新的 2 个)。稳定之后,除首要嫌疑外,其余被关闭的配置会自动恢复。
67
67
 
68
+ ## v0.11.1 紧急修复(宿主兼容 + 启动崩溃 · 2026-11-08)
69
+
70
+ **事故背景**:桌面端 DSH 自动升级到 `dsh 0.2.2-rc.1` 后连续崩溃(5 次/3 分钟),
71
+ 守护插件被兼容闸门移出 profile、不再保护;重新挂载后又因遗留写锁把宿主拖进
72
+ `fatal load failure`。本版本修复三处,全部是「守护不能拖垮宿主」的底线问题。
73
+
74
+ - **宿主预发布版本兼容(P0)**:`engines.dsh` 与全部 `@deepseek-ai/dsh-*`
75
+ peer 范围改为显式预发布分支
76
+ `>=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0`。
77
+ 原因:node-semver 只放行「与范围中某个比较符 **major.minor.patch 元组相同**
78
+ 且自身带预发布标签」的预发布版本 —— 旧范围 `>=0.2.1-rc.5 <0.3.0-0` 会
79
+ 静默排除 `0.2.2-rc.1`,插件被兼容闸门判为不兼容并从 profile 移除。
80
+ 已在 `dsh 0.2.2-rc.1` 干净沙箱(sb-rg-v0111)实测安装 + 启动 + 守护激活通过。
81
+ - **启动绝不抛出(P0)**:`boot()` 失败改为**降级留痕**(步骤记失败 + 警告
82
+ 日志),不再 rethrow。此前一个可恢复的写锁超时会把宿主变成
83
+ `dsh: fatal load failure`(退出码 1)—— 观测/恢复设施不能拖垮主流程。
84
+ - **失效写锁自愈(P0)**:新增 `cleanStaleLocks`(Store.init 启动时清理 +
85
+ `updateState` 超时后清理重试一次)。判据:锁内容为空/非正整数 PID(不完整
86
+ 记录,atomic-write 永远无法接管)/ PID 对应进程已死,且文件年龄 > 5s
87
+ (避开锁创建与写 PID 之间的零字节窗口,绝不抢活锁)。
88
+ 真机事故:崩溃恰好发生在锁创建与写 PID 之间,遗留 0 字节
89
+ `state.json.lock`,此后每次启动都等满超时并致命。
90
+ - **连续崩溃兜底升级(P0)**:`assessConfidence` 新增安全网 —— 累计崩溃
91
+ `crashSeq ≥ 3` 时即使概率计数未达 `confirmRounds` 也强制行动;同时
92
+ `readCrashHistory` 跳过启动时的「无嫌疑 crash-detected」记录。实测该记录
93
+ 每轮打断连续计数,导致 crashSeq 涨到 3 仍停在「观察中 1/2 轮」,崩溃循环
94
+ 永远无法自愈。
95
+ - 测试:新增 `tests/unit/store-lock.test.ts`(锁自愈 11 断言)与契约层
96
+ 预发布范围守卫;14/14 全绿(Core 7 / Host 2 / Client 4 / Contract 1)。
97
+
98
+ ## v0.11 新增(数据面安全 + 图谱图例一致性 + 马尔可夫去向可见 + 市场准备)
99
+
100
+ - **动作路由方法守卫(P0 安全修复)**:`/action/archive|reenable|restore|recover`
101
+ 此前不检查 HTTP 方法 —— `GET /recency-guard/api/action/archive` 也会执行归档重置
102
+ (浏览器预取/爬虫/误点链接都可能触发)。现在非 POST 一律 405 + `allow: POST`。
103
+ - **`?partition=` 真正生效(P0)**:客户端一直拼分区查询参数,但数据面此前从不读它
104
+ —— 跨分区查看永远静默返回当前分区。现在快照路由在宿主侧实现分区感知:
105
+ 请求其它分区返回**只读磁盘投影**(状态/日志/决策/备份),运行时字段如实标注
106
+ 归属,未知分区返回 `available:false`(绝不假绿)。
107
+ - **面板动作面补齐**:新增 `/action/restore` 路由 + 面板「恢复非嫌疑插件」按钮
108
+ (工具早有此动作,路由此前缺失);暂停轮询时 footer 显示「数据已停留 N 分钟」。
109
+ - **图谱图例一致性(P0)**:图例颜色此前固定语义色调色板,切到「按类型」口径时
110
+ 与节点实际颜色不一致;现在图例色**跟随当前着色口径**(legendColorOf),并直接
111
+ 绘制节点形状(形状=第二编码通道,图例不展示它就只对知道映射的人可读)。
112
+ - **搜索循环跳转**:多匹配时回车依次遍历(第 n/m 指示可见),不再永远停在第一个;
113
+ 「重置」按钮现在同时清掉拖拽偏移(此前节点拖飞后只能刷新页面)。
114
+ - **马尔可夫跨阶段去向(P0)**:矩阵卡此前只渲染自环概率条 —— `monitoring → 崩溃
115
+ 40%` 这类核心去向分布(README 宣称的)在界面上根本看不到。现在每行带 top-2
116
+ 跨阶段去向徽标。
117
+ - **事件流展开**:默认 12 条 + 「展开其余 N 条」显式计数(与嫌疑排名同款纪律,
118
+ 数据层给全量、显示层管截断)。
119
+ - **测试盲区补齐**:新增 `tests/client/graph-model.test.ts`(过滤共源/布局确定性/
120
+ 图例同源/平行边曲率)与 `tests/host/routes.test.ts`(方法守卫/partition/参数
121
+ 校验/请假语义)—— 文件头「已由测试钉死」的注释如今成真。13/13 全绿。
122
+ - **参考插件取长补短**:系统研究 dsh-ops(输出 notice 三要素)、dsh-compat-guardian
123
+ (reconcile 自愈/原子写/乐观锁)、dsh-context(工作预算/资源自适应)、
124
+ dsh-cookie-guard(独立救援 CLI)后按审计清单采纳落地;研究报告见
125
+ `.cycles/cycles/cycle-6/参考插件研究报告.md`。
126
+ - **插件市场准备**:仓库 topics(dsh-plugin 等)补齐、256px 图标、screenshots.json
127
+ 声明、投稿 YAML 按 awesome-dsh-plugin contributing.md 形态定稿。
128
+
68
129
  ## v0.7 新增(崩溃级修复 + 可交互图谱 + 可观测性白盒化)
69
130
 
70
131
  **先说最要紧的一条:v0.6 会把 profile 写崩。**
@@ -267,6 +328,10 @@ npm 发布后市场索引会自动携带 npm 版本;GitHub-only 期间用上
267
328
  includeExperimental: true # 是否把 experimental/beta 命名的包算作嫌疑
268
329
  maxAutoDisable: 2 # 单次恢复最多关闭几个嫌疑插件
269
330
  autoRestore: true # 稳定后自动恢复非首要嫌疑的配置
331
+ confirmRounds: 2 # 概率性证据的行动阈值(唯一嫌疑不受限)
332
+ versionedBackups: true # 插件更新时写带版本的时间线备份
333
+ backupRetain: 5 # 每插件保留几份时间线备份
334
+ backupRoot: '' # 备份自定义目录(留空=profile 内分区目录)
270
335
  ```
271
336
 
272
337
  | 配置 | 默认 | 说明 |
@@ -275,6 +340,10 @@ npm 发布后市场索引会自动携带 npm 版本;GitHub-only 期间用上
275
340
  | `includeExperimental` | `true` | 实验性/beta 命名的包是否算嫌疑 |
276
341
  | `maxAutoDisable` | `2` | 单次恢复最多关闭几个。崩溃次数递增,但封顶于此值 |
277
342
  | `autoRestore` | `true` | 稳定启动后自动重新启用非首要嫌疑 |
343
+ | `confirmRounds` | `2` | 多嫌疑时同一头号嫌疑连续 N 轮崩溃才自动关闭(唯一嫌疑第 1 轮即行动) |
344
+ | `versionedBackups` | `true` | 插件更新时写「带版本的时间线备份」,可逐版对账 |
345
+ | `backupRetain` | `5` | 每个插件保留的备份份数(超出删最旧) |
346
+ | `backupRoot` | `''` | 备份自定义目录;环境变量 `RECENCY_GUARD_BACKUP_ROOT` 优先 |
278
347
 
279
348
  ---
280
349
 
@@ -305,12 +374,18 @@ npm 发布后市场索引会自动携带 npm 版本;GitHub-only 期间用上
305
374
 
306
375
  ```
307
376
  <profile>/.recency-guard/
308
- ├── journal.jsonl # 只追加的事件日志:install / update / crash-detected / disable / restore …
309
- ├── snapshot.json # 上次观测到的 { 插件名: 版本 },用于识别更新
310
- ├── state.json # 崩溃计数、被自动关闭的插件、已恢复的插件
311
- ├── decision.json # 本轮要拦截的插件(守护行的表达式读它)
312
- ├── crashed # 崩溃哨兵:非零退出码时写下
313
- └── backups/ # 关闭前备份的配置
377
+ ├── current.json # 指针文件:当前分区键(守护行的 disabled 表达式经它搭桥)
378
+ ├── partitions.json # 分区索引:项目 → 分区键(多项目隔离的登记处)
379
+ ├── projects/<分区键>/ # 每个项目的分区目录(状态/日志/决策完全独立)
380
+ │ ├── journal.jsonl # 只追加的事件日志:install / update / crash-detected / disable / restore …
381
+ │ ├── snapshot.json # 上次观测到的 { 插件名: 版本 },用于识别更新
382
+ │ ├── state.json # 崩溃计数、被自动关闭的插件、已恢复的插件
383
+ │ ├── decision.json # 本轮要拦截的插件(守护行的表达式读它)
384
+ │ ├── crashed # 崩溃哨兵:非零退出码时写下
385
+ │ ├── observation-*.json # 观测快照(启动事实,量化指标的数据源)
386
+ │ ├── archive/ # 本分区的历史归档(搬走+留档)
387
+ │ └── backups/ # 关闭前备份的配置(versionedBackups 时带版本)
388
+ └── …
314
389
  ```
315
390
 
316
391
  ---
@@ -349,9 +424,9 @@ npm 发布后市场索引会自动携带 npm 版本;GitHub-only 期间用上
349
424
 
350
425
  ```bash
351
426
  npm install
352
- npm run build # esbuild 打包到 lib/
427
+ npm run build # esbuild 双产物:dist/index.js(宿主)+ dist/client.js(客户端)
353
428
  npm run typecheck # tsc --noEmit
354
- npm test # 单元测试 + 工具行为测试
429
+ npm test # 分层测试(Core/Host/Client/Contract)
355
430
  ```
356
431
 
357
432
  工具行为测试需要 DSH 运行时包(harness 由应用提供,不在 profile 的 node_modules 中):
package/dist/client.js CHANGED
@@ -56,7 +56,9 @@ var zh_default = {
56
56
  emptyHint: "\u5B88\u62A4\u63D2\u4EF6\u5C1A\u672A\u8BB0\u5F55\u4EFB\u4F55\u542F\u52A8\u4E8B\u5B9E\u3002",
57
57
  snapshotAt: "\u5FEB\u7167\u65F6\u523B {time}",
58
58
  pollingOn: "\u6BCF {seconds} \u79D2\u8F6E\u8BE2",
59
- pollingPaused: "\u8F6E\u8BE2\u5DF2\u6682\u505C\uFF08\u753B\u9762\u505C\u5728\u6682\u505C\u65F6\u523B\uFF09"
59
+ pollingPaused: "\u8F6E\u8BE2\u5DF2\u6682\u505C\uFF08\u753B\u9762\u505C\u5728\u6682\u505C\u65F6\u523B\uFF09",
60
+ restore: "\u6062\u590D\u975E\u5ACC\u7591\u63D2\u4EF6",
61
+ stale: "\u6570\u636E\u5DF2\u505C\u7559 {minutes} \u5206\u949F\uFF08\u8F6E\u8BE2\u5DF2\u6682\u505C\uFF09"
60
62
  },
61
63
  sections: {
62
64
  backupPolicy: "\u5907\u4EFD\u7B56\u7565",
@@ -267,11 +269,13 @@ var zh_default = {
267
269
  metric: "\u6307\u6807",
268
270
  bundle: "\u63D2\u4EF6",
269
271
  gap: "\u7F3A\u53E3"
270
- }
272
+ },
273
+ matchIndex: "\u7B2C {index}/{total} \u4E2A"
271
274
  },
272
275
  events: {
273
276
  title: "\u4E8B\u4EF6\u6D41",
274
- count: "\u6700\u8FD1 {count} \u6761"
277
+ count: "\u6700\u8FD1 {count} \u6761",
278
+ more: "\u5C55\u5F00\u5176\u4F59 {count} \u6761"
275
279
  },
276
280
  partitions: {
277
281
  title: "\u9879\u76EE\u5206\u533A",
@@ -328,7 +332,9 @@ var en_default = {
328
332
  emptyHint: "The guard has not recorded any startup facts yet.",
329
333
  snapshotAt: "Snapshot at {time}",
330
334
  pollingOn: "polling every {seconds}s",
331
- pollingPaused: "polling paused (the view is frozen at the pause moment)"
335
+ pollingPaused: "polling paused (the view is frozen at the pause moment)",
336
+ restore: "Restore non-suspects",
337
+ stale: "Data is {minutes} min old (polling paused)"
332
338
  },
333
339
  sections: {
334
340
  backupPolicy: "Backup policy",
@@ -539,11 +545,13 @@ var en_default = {
539
545
  metric: "Metric",
540
546
  bundle: "Bundle",
541
547
  gap: "Gap"
542
- }
548
+ },
549
+ matchIndex: "Match {index}/{total}"
543
550
  },
544
551
  events: {
545
552
  title: "Event stream",
546
- count: "last {count}"
553
+ count: "last {count}",
554
+ more: "Show {count} more"
547
555
  },
548
556
  partitions: {
549
557
  title: "Project partitions",
@@ -967,6 +975,33 @@ function kindCounts(nodes) {
967
975
  for (const node of nodes) counts[node.kind] = (counts[node.kind] ?? 0) + 1;
968
976
  return counts;
969
977
  }
978
+ function legendColorOf(kind, mode) {
979
+ if (mode === "kind") return KIND_COLOR[kind];
980
+ return TONE_COLOR[LEGEND_TONE[kind]];
981
+ }
982
+ var LEGEND_TONE = {
983
+ capability: "info",
984
+ stage: "neutral",
985
+ metric: "warn",
986
+ bundle: "neutral",
987
+ gap: "error"
988
+ };
989
+ function legendShapeOf(kind, nodes) {
990
+ const tally = /* @__PURE__ */ new Map();
991
+ for (const node of nodes) {
992
+ if (node.kind !== kind) continue;
993
+ tally.set(node.shape, (tally.get(node.shape) ?? 0) + 1);
994
+ }
995
+ let best = null;
996
+ let bestCount = 0;
997
+ for (const [shape, count] of tally) {
998
+ if (count > bestCount) {
999
+ best = shape;
1000
+ bestCount = count;
1001
+ }
1002
+ }
1003
+ return best ?? "circle";
1004
+ }
970
1005
  var RADIAL_GAP = 28;
971
1006
  var RADIAL_RING_GAP = NODE_HEIGHT + 40;
972
1007
  function radialPositions(nodes) {
@@ -1065,17 +1100,30 @@ function GraphToolbar(props) {
1065
1100
  placeholder: t("graph.search"),
1066
1101
  onChange: (event) => props.onQuery(event.target.value),
1067
1102
  onKeyDown: (event) => {
1068
- if (event.key === "Enter" && props.matches.length > 0) props.onLocate(props.matches[0].id);
1103
+ if (event.key === "Enter" && props.matches.length > 0) {
1104
+ event.preventDefault?.();
1105
+ props.onMatchNext();
1106
+ }
1069
1107
  if (event.key === "Escape") props.onQuery("");
1070
1108
  event.stopPropagation();
1071
1109
  },
1072
- "aria-label": t("graph.search")
1110
+ "aria-label": t("graph.search"),
1111
+ "aria-keyshortcuts": "Enter"
1073
1112
  }
1074
1113
  ),
1075
- props.query.trim() !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "rg-search-results", role: "listbox", "aria-label": t("graph.searchResults"), children: props.matches.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "rg-search-noresult", children: t("graph.noResults") }) : props.matches.map((node) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", onClick: () => props.onLocate(node.id), children: [
1076
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `rg-dot rg-dot-${node.tone}`, "aria-hidden": true }),
1077
- node.label
1078
- ] }) }, node.id)) })
1114
+ props.query.trim() !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "rg-search-results", role: "listbox", "aria-label": t("graph.searchResults"), children: props.matches.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "rg-search-noresult", children: t("graph.noResults") }) : props.matches.map((node, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1115
+ "button",
1116
+ {
1117
+ type: "button",
1118
+ onClick: () => props.onLocate(node.id),
1119
+ "aria-current": index === props.matchIndex,
1120
+ children: [
1121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `rg-dot rg-dot-${node.tone}`, "aria-hidden": true }),
1122
+ node.label
1123
+ ]
1124
+ }
1125
+ ) }, node.id)) }),
1126
+ props.query.trim() !== "" && props.matches.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "rg-search-index", role: "status", children: t("graph.matchIndex", { index: props.matchIndex + 1, total: props.matches.length }) })
1079
1127
  ] }),
1080
1128
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1081
1129
  Segmented,
@@ -1127,7 +1175,7 @@ function GraphToolbar(props) {
1127
1175
  "aria-pressed": !props.hidden.has(kind),
1128
1176
  title: t("graph.legendToggle", { kind: t(kindKey(kind)) }),
1129
1177
  children: [
1130
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `rg-dot rg-dot-${dotToneOf(kind)}`, "aria-hidden": true }),
1178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "rg-legend-shape", viewBox: "-8 -8 16 16", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: shapePath(legendShapeOf(kind, props.allNodes), 7, 0), fill: legendColorOf(kind, props.colorMode) }) }),
1131
1179
  t(kindKey(kind)),
1132
1180
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "rg-legend-count", children: props.counts[kind] ?? 0 })
1133
1181
  ]
@@ -1136,21 +1184,6 @@ function GraphToolbar(props) {
1136
1184
  )) })
1137
1185
  ] });
1138
1186
  }
1139
- function dotToneOf(kind) {
1140
- switch (kind) {
1141
- case "capability":
1142
- return "info";
1143
- case "stage":
1144
- return "neutral";
1145
- case "metric":
1146
- return "warn";
1147
- case "bundle":
1148
- return "neutral";
1149
- case "gap":
1150
- default:
1151
- return "error";
1152
- }
1153
- }
1154
1187
  function Segmented({
1155
1188
  label,
1156
1189
  options,
@@ -1242,6 +1275,7 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1242
1275
  const [hidden, setHidden] = (0, import_react.useState)(/* @__PURE__ */ new Set());
1243
1276
  const [size, setSize] = (0, import_react.useState)({ width: 0, height });
1244
1277
  const [query, setQuery] = (0, import_react.useState)("");
1278
+ const [matchIndex, setMatchIndex] = (0, import_react.useState)(0);
1245
1279
  const [layoutMode, setLayoutMode] = (0, import_react.useState)("layered");
1246
1280
  const [colorMode, setColorMode] = (0, import_react.useState)("tone");
1247
1281
  const [onlyConnected, setOnlyConnected] = (0, import_react.useState)(false);
@@ -1419,9 +1453,13 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1419
1453
  apply2(centerOn(viewport, size, centreOfNode(node)));
1420
1454
  onFocusChange(nodeId);
1421
1455
  setQuery("");
1456
+ setMatchIndex(0);
1422
1457
  },
1423
1458
  [apply2, viewport, size, positionedById, centreOfNode, onFocusChange]
1424
1459
  );
1460
+ (0, import_react.useEffect)(() => {
1461
+ setMatchIndex(0);
1462
+ }, [query]);
1425
1463
  (0, import_react.useEffect)(() => {
1426
1464
  if (locateRequest === null) return;
1427
1465
  if (locateRequest.nonce === lastLocateNonceRef.current) return;
@@ -1461,6 +1499,15 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1461
1499
  return traceFrom(focusId, visibleEdges, "both");
1462
1500
  }, [focusId, visibleEdges]);
1463
1501
  const matches = (0, import_react.useMemo)(() => matchNodes(visibleNodes, query), [visibleNodes, query]);
1502
+ const locateMatchNext = (0, import_react.useCallback)(() => {
1503
+ if (matches.length === 0) return;
1504
+ const next = (matchIndex + 1) % matches.length;
1505
+ setMatchIndex(next);
1506
+ const node = matches[next];
1507
+ if (!node) return;
1508
+ apply2(centerOn(viewport, size, centreOfNode(node)));
1509
+ onFocusChange(node.id);
1510
+ }, [matches, matchIndex, apply2, viewport, size, centreOfNode, onFocusChange]);
1464
1511
  const matchIds = (0, import_react.useMemo)(() => new Set(matches.map((node) => node.id)), [matches]);
1465
1512
  const focusNode = focusId === null ? void 0 : nodeById.get(focusId);
1466
1513
  const focusDegree = (0, import_react.useMemo)(() => focusId === null ? { in: 0, out: 0 } : degreeOf(focusId, visibleEdges), [focusId, visibleEdges]);
@@ -1503,14 +1550,18 @@ function GuardGraph({ graph, height = VIEWPORT_HEIGHT_WIDE, focusId, onFocusChan
1503
1550
  onFocusChange(null);
1504
1551
  setQuery("");
1505
1552
  setHoverId(null);
1553
+ setOffsets({});
1506
1554
  },
1507
- canReset: focusId !== null || query !== "",
1555
+ canReset: focusId !== null || query !== "" || Object.keys(offsets).length > 0,
1508
1556
  query,
1509
1557
  onQuery: setQuery,
1510
1558
  matches,
1511
1559
  onLocate: locateNode,
1560
+ matchIndex,
1561
+ onMatchNext: locateMatchNext,
1512
1562
  counts,
1513
1563
  hidden,
1564
+ allNodes: visibleNodes,
1514
1565
  onToggleKind: (kind) => setHidden((current) => {
1515
1566
  const next = new Set(current);
1516
1567
  if (next.has(kind)) next.delete(kind);
@@ -1833,15 +1884,18 @@ function metricRows(snapshot) {
1833
1884
  );
1834
1885
  return rows;
1835
1886
  }
1887
+ var MARKOV_EXITS_LIMIT = 2;
1836
1888
  function markovView(snapshot) {
1837
1889
  const markov = snapshot.markov;
1838
1890
  const rows = markov.rows.map((row) => {
1839
1891
  const selfLoop = row.probabilities?.[row.stage];
1892
+ const exits = row.probabilities === null ? null : Object.entries(row.probabilities).filter(([stage, p]) => stage !== row.stage && p > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MARKOV_EXITS_LIMIT).map(([stage, p]) => ({ stage, percent: Math.round(p * 100) }));
1840
1893
  return {
1841
1894
  stage: row.stage,
1842
1895
  label: row.label,
1843
1896
  selfLoop: typeof selfLoop === "number" ? selfLoop : null,
1844
- barPercent: typeof selfLoop === "number" ? Math.round(selfLoop * 100) : null
1897
+ barPercent: typeof selfLoop === "number" ? Math.round(selfLoop * 100) : null,
1898
+ exits: exits !== null && exits.length > 0 ? exits : null
1845
1899
  };
1846
1900
  });
1847
1901
  const stationary = markov.stationary === null ? [] : Object.entries(markov.stationary).map(([stage, share]) => ({ stage, share, percent: Math.round(share * 100) })).sort((a, b) => b.share - a.share || a.stage.localeCompare(b.stage));
@@ -2336,18 +2390,26 @@ function PartitionRow({
2336
2390
  !option.current && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "rg-button rg-button-small", onClick: () => onSelect(option.key), children: t("partitions.inspect") })
2337
2391
  ] });
2338
2392
  }
2393
+ var EVENTS_INITIAL = 12;
2339
2394
  function EventsBody({ events }) {
2395
+ const [showAll, setShowAll] = (0, import_react2.useState)(false);
2340
2396
  if (events.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "rg-empty", children: t("panel.emptyHint") });
2341
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ol", { className: "rg-events", children: [...events].slice(-12).reverse().map((event, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "rg-event", children: [
2342
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { className: "rg-event-time", title: event.t, children: formatClock(event.t) }),
2343
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `rg-badge rg-badge-${eventTone(event.kind, event.detail)}`, children: event.kind }),
2344
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "rg-event-detail", title: event.detail ?? event.bundle ?? "", children: [
2345
- event.bundle !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: event.bundle }),
2346
- event.bundle !== void 0 && event.detail !== void 0 && " \xB7 ",
2347
- event.detail ?? ""
2348
- ] }),
2349
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "rg-event-ago", children: agoLabel(event.t) })
2350
- ] }, `${event.t}-${index}`)) });
2397
+ const ordered = [...events].reverse();
2398
+ const visible = showAll ? ordered : ordered.slice(0, EVENTS_INITIAL);
2399
+ const omitted = ordered.length - visible.length;
2400
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2401
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ol", { className: "rg-events", children: visible.map((event, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "rg-event", children: [
2402
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { className: "rg-event-time", title: event.t, children: formatClock(event.t) }),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `rg-badge rg-badge-${eventTone(event.kind, event.detail)}`, children: event.kind }),
2404
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "rg-event-detail", title: event.detail ?? event.bundle ?? "", children: [
2405
+ event.bundle !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: event.bundle }),
2406
+ event.bundle !== void 0 && event.detail !== void 0 && " \xB7 ",
2407
+ event.detail ?? ""
2408
+ ] }),
2409
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "rg-event-ago", children: agoLabel(event.t) })
2410
+ ] }, `${event.t}-${index}`)) }),
2411
+ omitted > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "rg-link rg-risks-expand", onClick: () => setShowAll(true), children: t("events.more", { count: omitted }) })
2412
+ ] });
2351
2413
  }
2352
2414
  function eventTone(kind, detail) {
2353
2415
  if (kind === "crash-detected") return "error";
@@ -2430,7 +2492,14 @@ function MarkovBody({ snapshot }) {
2430
2492
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "rg-markov", "aria-label": t("markov.title"), children: view.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("li", { className: "rg-markov-row", title: row.label, children: [
2431
2493
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-markov-stage", children: stageLabelOf(row.stage, row.stage) }),
2432
2494
  row.barPercent !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-bar", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-bar-fill", style: { width: `${row.barPercent}%` } }) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-markov-nodata", children: t("metrics.undefined") }),
2433
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-markov-value", children: row.selfLoop !== null ? `${Math.round(row.selfLoop * 100)}%` : "\u2014" })
2495
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-markov-value", children: row.selfLoop !== null ? `${Math.round(row.selfLoop * 100)}%` : "\u2014" }),
2496
+ row.exits !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rg-markov-exits", children: row.exits.map((exit) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "rg-chip rg-chip-exit", title: row.label, children: [
2497
+ "\u2192 ",
2498
+ stageLabelOf(exit.stage, exit.stage),
2499
+ " ",
2500
+ exit.percent,
2501
+ "%"
2502
+ ] }, exit.stage)) })
2434
2503
  ] }, row.stage)) }),
2435
2504
  view.stationary.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "rg-markov-stationary", children: [
2436
2505
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { className: "rg-subhead", children: t("markov.stationary") }),
@@ -2592,6 +2661,16 @@ function GuardPanel(props) {
2592
2661
  }, [selectedNodeId, graph]);
2593
2662
  const unavailable = failure !== null && snapshot === null;
2594
2663
  const loading = snapshot === null;
2664
+ const [nowTick, setNowTick] = (0, import_react3.useState)(() => Date.now());
2665
+ (0, import_react3.useEffect)(() => {
2666
+ if (polling) return;
2667
+ const timer = setInterval(() => setNowTick(Date.now()), 6e4);
2668
+ return () => clearInterval(timer);
2669
+ }, [polling]);
2670
+ const staleMinutes = (0, import_react3.useMemo)(() => {
2671
+ if (polling || snapshot === null) return 0;
2672
+ return Math.max(0, Math.round((nowTick - Date.parse(snapshot.at)) / 6e4));
2673
+ }, [polling, snapshot, nowTick]);
2595
2674
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { className: "rg-panel", "data-form": form, "data-recency-guard-panel": "", ref: rootRef, "aria-busy": loading || busy, children: [
2596
2675
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("header", { className: "rg-header", children: [
2597
2676
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "rg-header-main", children: [
@@ -2620,6 +2699,7 @@ function GuardPanel(props) {
2620
2699
  ),
2621
2700
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "rg-button", onClick: () => void load(requestedKey), disabled: busy, children: t("panel.refresh") }),
2622
2701
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "rg-button", onClick: () => void runAction("recover"), disabled: busy, children: t("panel.recover") }),
2702
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "rg-button", onClick: () => void runAction("restore"), disabled: busy, children: t("panel.restore") }),
2623
2703
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2624
2704
  "button",
2625
2705
  {
@@ -2824,7 +2904,7 @@ function GuardPanel(props) {
2824
2904
  failure !== null && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "rg-error", role: "alert", children: failure }),
2825
2905
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("footer", { className: "rg-foot", children: [
2826
2906
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: t("panel.snapshotAt", { time: snapshot.at.replace("T", " ").slice(0, 19) }) }),
2827
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: polling ? t("panel.pollingOn", { seconds: POLL_INTERVAL / 1e3 }) : t("panel.pollingPaused") })
2907
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: polling ? t("panel.pollingOn", { seconds: POLL_INTERVAL / 1e3 }) : t("panel.stale", { minutes: staleMinutes }) })
2828
2908
  ] })
2829
2909
  ] })
2830
2910
  ] });
@@ -3059,7 +3139,7 @@ function openPanel(ctx) {
3059
3139
  }
3060
3140
 
3061
3141
  // src/client/styles.css
3062
- var styles_default = "/**\n * \u7BA1\u7406\u9762\u677F\u7684\u6837\u5F0F\u3002\n *\n * ## \u7EAA\u5F8B\n *\n * 1. **\u53EA\u7528\u4E3B\u9898\u53D8\u91CF**\uFF1A\u989C\u8272\u3001\u5706\u89D2\u3001\u5B57\u4F53\u5168\u90E8\u5F15\u7528 `--dsw-alias-*` / `--dsw-radius-*`\n * / `--dsw-shadow-*` \u5BB6\u65CF\u3002\u5BBF\u4E3B\u91CD\u547D\u540D\u4E00\u4E2A\u53D8\u91CF\u65F6\u9762\u677F\u53EA\u662F\u53D8\u4E11\uFF0C\u4E0D\u4F1A\u5D29 \u2014\u2014\n * \u56E0\u6B64\u6BCF\u4E2A var() \u90FD\u5E26\u4E00\u4E2A\u4FDD\u5B88\u7684\u515C\u5E95\u503C\u3002\n * 2. **\u4E24\u79CD\u5F62\u6001\u6309\u5BB9\u5668\u5BBD\u5EA6**\uFF1A`.rg-panel[data-form=\"narrow\"]` \u7531 JS \u6D4B\u51FA\u7684\n * \u9762\u677F\u5BBD\u5EA6\u9A71\u52A8\uFF08\u9762\u677F\u53EF\u80FD\u6302\u5728\u88AB\u62D6\u7A84\u7684\u4FA7\u680F\u91CC\uFF0C\u5A92\u4F53\u67E5\u8BE2\u91CF\u7684\u662F\u7A97\u53E3\uFF0C\u4F1A\u7B54\u9519\uFF09\u3002\n * 3. **\u5C40\u90E8\u52A8\u6548\u4EE4\u724C**\uFF1A\u8FC7\u6E21\u65F6\u957F\u4E0E\u7F13\u52A8\u5B9A\u4E49\u5728 `.rg-panel` \u4E0A\u7684 `--rg-*`\uFF0C\n * \u4E0D\u4F9D\u8D56\u672A\u7ECF\u9A8C\u8BC1\u7684\u5168\u5C40 `--ds-*`\uFF08\u751F\u6001\u63D2\u4EF6\u540C\u6837\u81EA\u6301\u8FD9\u5957\u4EE4\u724C\uFF09\u3002\n * 4. **\u4E0D\u5199\u6B7B\u5BB9\u5668\u5BBD\u5EA6**\uFF1A\u9762\u677F\u5360\u6EE1\u5BBF\u4E3B\u7ED9\u7684\u4E2D\u592E\u9762\u677F\u533A\u57DF\u3002\n * 5. **\u5BF9\u6BD4\u5EA6**\uFF1A\u6B63\u6587 `--dsw-alias-label-primary`\uFF0C\u6B21\u8981 `--dsw-alias-label-secondary`\uFF0C\n * \u63D0\u793A `--dsw-alias-label-tertiary`\uFF08\u4EAE/\u6697\u4E3B\u9898\u90FD\u7531\u5BBF\u4E3B\u4FDD\u8BC1\u53EF\u8BFB\uFF09\u3002\n */\n\n.rg-panel {\n /* \u5C40\u90E8\u4EE4\u724C\uFF1A\u63A7\u4EF6\u9AD8\u5EA6\u3001\u52A8\u6548\u3001\u95F4\u8DDD\u3002\u6539\u4E00\u5904\uFF0C\u6574\u5757\u9762\u677F\u8DDF\u7740\u4E00\u81F4\u3002 */\n --rg-control-h: 28px;\n --rg-motion-fast: 120ms;\n --rg-motion-medium: 200ms;\n --rg-ease: cubic-bezier(0.2, 0, 0, 1);\n --rg-gap: 16px;\n\n display: flex;\n flex-direction: column;\n gap: var(--rg-gap);\n padding: 20px 24px 32px;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n font-family: var(--dsw-font-family);\n font-size: var(--dsw-font-s-14, 14px);\n line-height: var(--dsw-font-s-14-line-height, 22px);\n\n /**\n * \u9762\u677F\u5FC5\u987B**\u81EA\u5DF1**\u662F\u6EDA\u52A8\u5BB9\u5668\u3002\n *\n * \u4E3A\u4EC0\u4E48\u4E0D\u80FD\u6307\u671B\u5BBF\u4E3B\uFF1A\u5B9E\u6D4B\uFF08\u6C99\u7BB1 sb-rg-c5\uFF09\u5BBF\u4E3B\u69FD\u4F4D\u662F\u8FD9\u6837\u4E00\u4E32\n * .centerCol { overflow-y: hidden } \u2190 \u7956\u5148\u628A\u6EA2\u51FA\u88C1\u6389\n * .mainPanel { height: 714px; overflow: visible }\n * .rg-panel { height: 3965px } \u2190 \u5185\u5BB9\u81EA\u7136\u9AD8\u5EA6\uFF0C\u8D85\u51FA\u540E\u76F4\u63A5\u88AB\u88C1\n * \u6587\u6863\u672C\u8EAB\u4E0D\u6EDA\uFF08docScrollH == clientH == 714\uFF09\uFF0C\u56E0\u6B64**\u4E0B\u65B9\u6570\u636E\u9875\u6C38\u8FDC\u770B\u4E0D\u5230**\u3002\n *\n * \u4E09\u4E2A\u5C5E\u6027\u7F3A\u4E00\u4E0D\u53EF\uFF1A\n * - `height: 100%` + `min-height: 0`\uFF1A\u6491\u6EE1\u5BBF\u4E3B\u7ED9\u7684\u786E\u5B9A\u9AD8\u5EA6\uFF0C\u4E14\u5141\u8BB8\u6536\u7F29\u5230\u53EF\u6EDA\u52A8\uFF1B\n * \u53EA\u5199 overflow \u800C\u4E0D\u9650\u5236\u9AD8\u5EA6\uFF0Cflex \u5B50\u9879\u4ECD\u4F1A\u6309\u5185\u5BB9\u6491\u5F00\uFF0C\u6EDA\u4E0D\u8D77\u6765\u3002\n * - `overflow-y: auto`\uFF1A\u9762\u677F\u81EA\u5DF1\u6EDA\u3002\n * - `overscroll-behavior: contain`\uFF1A\u6EDA\u5230\u5C3D\u5934\u4E0D\u628A\u6EDA\u52A8\u4F20\u7ED9\u5BBF\u4E3B\uFF0C\u5426\u5219\u4F1A\u628A\u6574\u9875/\u4FA7\u680F\n * \u5E26\u7740\u4E00\u8D77\u6EDA\uFF0C\u89C6\u89C9\u4E0A\u50CF\"\u9762\u677F\u5728\u4E71\u8DF3\"\u3002\n */\n height: 100%;\n max-height: 100%;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n overscroll-behavior: contain;\n}\n\n/* \u7A84\u5F62\u6001\uFF08\u4FA7\u680F / \u5206\u5C4F\uFF09\uFF1A\u6536\u7D27\u8FB9\u8DDD\u4E0E\u95F4\u8DDD\uFF0C\u628A\u7EB5\u5411\u7A7A\u95F4\u8BA9\u7ED9\u5185\u5BB9\u3002 */\n.rg-panel[data-form='narrow'] {\n --rg-gap: 12px;\n padding: 14px 14px 24px;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n/* ---------- \u5934\u90E8 ---------- */\n\n.rg-header {\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n}\n\n.rg-header-main {\n min-width: 0;\n flex: 1 1 260px;\n}\n\n.rg-title {\n margin: 0;\n font-size: var(--dsw-font-l-20, 20px);\n font-weight: var(--dsw-font-l-20-font-weight, 600);\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-panel[data-form='narrow'] .rg-title {\n font-size: var(--dsw-font-m-18, 18px);\n}\n\n.rg-subtitle {\n margin: 4px 0 0;\n font-size: var(--dsw-font-xs-13, 13px);\n color: var(--dsw-alias-label-secondary);\n overflow-wrap: anywhere;\n}\n\n.rg-project-name {\n font-weight: var(--dsw-font-xs-strong-13, 600);\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-partition-workspace-full {\n font-size: var(--dsw-font-xxs-12, 12px);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 100%;\n direction: ltr;\n}\n\n.rg-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* ---------- \u63A7\u4EF6 ---------- */\n\n.rg-button {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n min-height: var(--rg-control-h);\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n border-radius: var(--dsw-radius-sm, 8px);\n padding: 4px 12px;\n font: inherit;\n font-size: var(--dsw-font-xs-13, 13px);\n cursor: pointer;\n transition:\n background var(--rg-motion-fast) var(--rg-ease),\n border-color var(--rg-motion-fast) var(--rg-ease);\n}\n\n.rg-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-button:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n\n.rg-button:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n\n.rg-button-small {\n min-height: 24px;\n padding: 2px 9px;\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-button-icon {\n min-width: 28px;\n padding: 2px 6px;\n justify-content: center;\n font-size: var(--dsw-font-s-14, 14px);\n line-height: 1;\n}\n\n.rg-button-danger {\n border-color: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n}\n\n.rg-button-active {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n\n/* \u8F6E\u8BE2\u6682\u505C\u6001\u6309\u94AE\uFF08\u6682\u505C = \u9700\u8981\u88AB\u770B\u89C1\u7684\u72B6\u6001\uFF0C\u4E0D\u80FD\u53EA\u9760 aria-pressed\uFF09\u3002 */\n.rg-button-paused {\n border-color: var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.rg-link {\n appearance: none;\n border: none;\n background: none;\n padding: 0 2px;\n font: inherit;\n font-size: inherit;\n color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n text-decoration: underline;\n}\n\n/* \u5206\u6BB5\u63A7\u4EF6\uFF08\u5E03\u5C40 / \u7740\u8272\u53E3\u5F84\uFF09\u3002 */\n.rg-segmented {\n display: inline-flex;\n align-items: stretch;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n overflow: hidden;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.rg-segment {\n appearance: none;\n border: none;\n border-right: 1px solid var(--dsw-alias-border-l1);\n background: none;\n color: var(--dsw-alias-label-secondary);\n padding: 3px 9px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n cursor: pointer;\n transition: background var(--rg-motion-fast) var(--rg-ease);\n}\n\n.rg-segment:last-child {\n border-right: none;\n}\n\n.rg-segment:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-segment[aria-pressed='true'] {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\n color: var(--dsw-alias-label-primary);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n}\n\n.rg-segment:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.rg-field {\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n}\n\n.rg-field-grow {\n flex: 1 1 160px;\n}\n\n.rg-field-label {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-input,\n.rg-select {\n min-height: var(--rg-control-h);\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n padding: 2px 8px;\n font: inherit;\n font-size: var(--dsw-font-xs-13, 13px);\n max-width: 100%;\n}\n\n.rg-input:focus-visible,\n.rg-select:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* ---------- \u63D0\u793A\u6761 ---------- */\n\n.rg-notice {\n margin: 0;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-error {\n margin: 0;\n padding: 10px 12px;\n border-radius: var(--dsw-radius-sm, 8px);\n border: 1px solid var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-empty {\n margin: 0;\n padding: 8px 0;\n font-size: var(--dsw-font-xs-13, 13px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-muted {\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-strong {\n font-weight: var(--dsw-font-xs-strong-13, 600);\n}\n\n.rg-block {\n display: block;\n}\n\n.rg-ellipsis {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rg-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 16px;\n margin-left: 6px;\n padding: 0 4px;\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xxxs-11, 11px);\n}\n\n/* \u52A0\u8F7D / \u4E0D\u53EF\u7528\u72B6\u6001\u5757\uFF1A\u5360\u4F4D\u9AD8\u5EA6\u7A33\u5B9A\uFF0C\u907F\u514D\u8F6E\u8BE2\u65F6\u9875\u9762\u8DF3\u52A8\u3002 */\n.rg-state-block {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 24px 0;\n}\n\n.rg-state-title {\n margin: 0;\n font-size: var(--dsw-font-m-18, 18px);\n font-weight: var(--dsw-font-m-18-font-weight, 600);\n}\n\n/* ---------- \u5361\u7247\u7F51\u683C ---------- */\n\n.rg-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 12px;\n}\n\n.rg-panel[data-form='narrow'] .rg-grid {\n grid-template-columns: 1fr;\n}\n\n.rg-card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px 16px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n min-height: 0;\n}\n\n/**\n * \u5361\u7247\u5185\u5BB9\u533A\u56FA\u5B9A\u9AD8\u5EA6 + \u5185\u90E8\u6EDA\u52A8\uFF08v0.9.1 \u7528\u6237\u9700\u6C42\uFF09\u3002\n *\n * \u4E3A\u4EC0\u4E48\u9650\u9AD8\u800C\u4E0D\u662F\u8BA9\u5185\u5BB9\u81EA\u7136\u4F38\u5C55\uFF1A\u5C55\u5F00\u7684\u5361\u7247\u591A\u4E86\u4E4B\u540E\uFF0C\u9762\u677F\u88AB\u5355\u4E2A\n * \u5361\u7247\u6491\u5230\u51E0\u5343\u50CF\u7D20\uFF0C\u4E0B\u9762\u7684\u5361\u7247\u8981\u6EDA\u5F88\u4E45\u624D\u770B\u5F97\u5230 \u2014\u2014 \u5747\u5300\u7684\u89C6\u53E3\u5BC6\u5EA6\n * \u6BD4\u5355\u5361\u7684\u5B8C\u6574\u6027\u66F4\u91CD\u8981\uFF08\u8FD9\u6B63\u662F\u7528\u6237\u539F\u59CB\u8BC9\u6C42\uFF09\u3002\n *\n * - \u9ED8\u8BA4 `--rg-card-body-h: 240px`\uFF1A\u8DB3\u591F\u770B\u7EA6 10 \u884C\u5185\u5BB9\uFF0C\u8D85\u51FA\u5185\u90E8\u6EDA\u52A8\uFF1B\n * - `overscroll-behavior: contain`\uFF1A\u6EDA\u5230\u5C3D\u5934\u4E0D\u628A\u5916\u5C42\u9762\u677F\u5E26\u7740\u6EDA\uFF1B\n * - `.rg-card-enlarged` \u89E3\u9664\u9650\u9AD8\uFF08\u2922 \u6309\u94AE\uFF09\uFF0C\u644A\u5F00\u9605\u8BFB\u957F\u6E05\u5355\uFF1B\n * - \u7A84\u5F62\u6001\u628A\u9650\u9AD8\u964D\u5230 180px\uFF08\u4FA7\u680F\u7A7A\u95F4\u5C11\uFF0C\u5BC6\u5EA6\u4F18\u5148\uFF09\u3002\n */\n.rg-card-body {\n display: flex;\n flex-direction: column;\n gap: 10px;\n height: var(--rg-card-body-h, 240px);\n overflow-y: auto;\n overscroll-behavior: contain;\n min-height: 0;\n}\n\n/* \u6269\u5927\u6001\uFF1A\u89E3\u9664\u9650\u9AD8\uFF0C\u5361\u7247\u6309\u5185\u5BB9\u81EA\u7136\u4F38\u5C55\uFF08\u9762\u677F\u81EA\u8EAB\u4ECD\u662F\u6EDA\u52A8\u5BB9\u5668\uFF09\u3002 */\n.rg-card-enlarged .rg-card-body {\n height: auto;\n max-height: none;\n overflow-y: visible;\n}\n\n/* \u6269\u5927\u6309\u94AE\uFF1A\u5934\u90E8\u5185\u7684\u7B2C\u4E8C\u4E2A\u52A8\u4F5C\uFF0C\u5C0F\u5C3A\u5BF8\u3001\u4F4E\u5BF9\u6BD4\uFF0C\u4E0D\u4E0E\u6298\u53E0\u7BAD\u5934\u7ADE\u4E89\u3002 */\n.rg-card-expand {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n margin-left: auto;\n border-radius: var(--dsw-radius-sm, 8px);\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n user-select: none;\n transition: background var(--rg-motion-fast, 120ms) var(--rg-ease, ease);\n}\n.rg-card-expand:hover,\n.rg-card-expand:focus-visible {\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n.rg-card-enlarged .rg-card-expand {\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-card-title {\n margin: 0;\n font-size: var(--dsw-font-s-14, 14px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n}\n\n/* \u7A84\u5F62\u6001\uFF1A\u4FA7\u680F\u7A7A\u95F4\u5C11\uFF0C\u5BC6\u5EA6\u4F18\u5148 \u2014\u2014 \u5185\u5BB9\u533A\u9650\u9AD8\u964D\u4E3A 180px\u3002 */\n.rg-panel[data-form='narrow'] .rg-card-body {\n --rg-card-body-h: 180px;\n}\n\n.rg-card-title {\n margin: 0;\n font-size: var(--dsw-font-s-14, 14px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n}\n\n.rg-card-subtitle {\n margin: 4px 0 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-card-note {\n margin: 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n/* \u53EF\u6298\u53E0\u5361\u7247\uFF1A\u6807\u9898\u884C\u662F\u6309\u94AE\uFF0C\u6574\u4E2A\u5934\u90E8\u53EF\u70B9\u3002 */\n.rg-card-head {\n appearance: none;\n border: none;\n background: none;\n padding: 0;\n margin: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n cursor: pointer;\n font: inherit;\n color: inherit;\n text-align: left;\n}\n\n.rg-card-head:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-card-head-hint {\n margin-left: auto;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rg-card-chevron {\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-card-collapsed {\n padding-bottom: 10px;\n}\n\n.rg-card-body {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* ---------- \u5FBD\u6807 / \u5706\u70B9 / \u8BA1\u6570\u7247 ---------- */\n\n.rg-badge {\n display: inline-flex;\n align-items: center;\n padding: 1px 8px;\n border-radius: var(--dsw-radius-sm, 8px);\n border: 1px solid var(--dsw-alias-border-l2);\n font-size: var(--dsw-font-xxs-12, 12px);\n white-space: nowrap;\n}\n\n.rg-badge-idle,\n.rg-badge-monitoring {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n\n.rg-badge-crashed,\n.rg-badge-error {\n border-color: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n}\n\n.rg-badge-deciding,\n.rg-badge-worsening,\n.rg-badge-warn {\n border-color: var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.rg-badge-stabilizing,\n.rg-badge-recovered,\n.rg-badge-improving,\n.rg-badge-success {\n border-color: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-state-success-primary);\n}\n\n.rg-badge-stable,\n.rg-badge-neutral {\n border-color: var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-chip {\n display: inline-flex;\n align-items: center;\n padding: 0 6px;\n border-radius: var(--dsw-radius-xs, 4px);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xxxs-11, 11px);\n white-space: nowrap;\n}\n\n.rg-chip-warn {\n border: 1px solid var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-state-warn-primary);\n background: none;\n}\n\n.rg-chip-more {\n font-variant-numeric: tabular-nums;\n}\n\n.rg-names {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 3px;\n min-width: 0;\n}\n\n.rg-dot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--dsw-alias-label-secondary);\n flex: none;\n}\n\n.rg-dot-info {\n background: var(--dsw-alias-brand-primary);\n}\n\n.rg-dot-success {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.rg-dot-warn {\n background: var(--dsw-alias-state-warn-primary);\n}\n\n.rg-dot-error {\n background: var(--dsw-alias-state-error-primary);\n}\n\n/* ---------- \u8FDB\u5EA6\u6761 ---------- */\n\n.rg-bar {\n display: inline-block;\n width: 72px;\n height: 4px;\n border-radius: 2px;\n background: var(--dsw-alias-bg-layer-2);\n overflow: hidden;\n flex: none;\n}\n\n.rg-bar-wide {\n width: 100%;\n}\n\n.rg-bar-fill {\n display: block;\n height: 100%;\n background: var(--dsw-alias-brand-primary);\n transition: width var(--rg-motion-medium) var(--rg-ease);\n}\n\n/* ---------- KPI \u6458\u8981\u6761 ---------- */\n\n.rg-kpis {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 10px;\n}\n\n.rg-kpi {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n}\n\n.rg-kpi-label {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-kpi-value {\n font-size: var(--dsw-font-s-14, 14px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n font-variant-numeric: tabular-nums;\n}\n\n.rg-kpi-big {\n font-size: var(--dsw-font-m-18, 18px);\n font-weight: var(--dsw-font-m-18-font-weight, 600);\n}\n\n.rg-kpi-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 100%;\n}\n\n.rg-kpi-meta {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.rg-ring {\n flex: none;\n}\n\n.rg-ring-text {\n fill: var(--dsw-alias-label-primary);\n font-size: 11px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---------- \u63CF\u8FF0\u5217\u8868 / \u9636\u6BB5\u8F68\u9053 ---------- */\n\n.rg-dl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 4px 12px;\n margin: 0;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-dl dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-dl dd {\n margin: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex-wrap: wrap;\n}\n\n.rg-dl-compact {\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-mono {\n font-family: var(--dsw-font-mono, var(--dsw-font-family-mono, ui-monospace, monospace));\n font-size: var(--dsw-font-xxs-12, 12px);\n overflow-wrap: anywhere;\n}\n\n/* \u9636\u6BB5\u8F68\u9053\uFF1A\u72B6\u6001\u673A\u4E0D\u662F\u8FDB\u5EA6\u6761\uFF0Cvisited \u4E0E current \u7528\u4E0D\u540C\u901A\u9053\u8868\u8FBE\u3002 */\n.rg-stage-rail {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-wrap: wrap;\n gap: 4px 10px;\n}\n\n.rg-stage-cell {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-stage-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n border: 1px solid var(--dsw-alias-border-l2);\n background: none;\n flex: none;\n}\n\n.rg-stage-visited {\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-stage-visited .rg-stage-dot {\n background: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-color: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n}\n\n.rg-stage-current {\n color: var(--dsw-alias-label-primary);\n font-weight: var(--dsw-font-xxxs-strong-11, 600);\n}\n\n.rg-stage-current .rg-stage-dot {\n background: var(--dsw-alias-brand-primary);\n border-color: var(--dsw-alias-brand-primary);\n box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-2);\n}\n\n/* ---------- \u6307\u6807 ---------- */\n\n.rg-metrics {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.rg-metric {\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n.rg-metric-label {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-metric-value {\n font-size: var(--dsw-font-xs-13, 13px);\n font-variant-numeric: tabular-nums;\n}\n\n.rg-metric-undefined {\n color: var(--dsw-alias-label-secondary);\n font-style: italic;\n}\n\n.rg-metric-reason {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-metric-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-trend {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* ---------- \u8FC1\u79FB / \u51B3\u7B56 / \u4E8B\u4EF6\u65F6\u95F4\u7EBF ---------- */\n\n.rg-transitions,\n.rg-decisions,\n.rg-events {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n\n.rg-transition,\n.rg-decision,\n.rg-event {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-transition-reason,\n.rg-decision-detail,\n.rg-event-detail {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-decision-bundle {\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-transition-time,\n.rg-decision-time,\n.rg-event-time {\n flex: none;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n font-size: var(--dsw-font-xxxs-11, 11px);\n}\n\n.rg-event-ago {\n flex: none;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n font-size: var(--dsw-font-xxxs-11, 11px);\n}\n\n/* ---------- \u5ACC\u7591\u6392\u540D ---------- */\n\n.rg-risks {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* \u300C\u5C55\u5F00\u5176\u4F59 N \u4E2A\u300D\u6309\u94AE\uFF1A\u5DE6\u5BF9\u9F50\u7684\u94FE\u63A5\u5F62\u6001\uFF0C\u4E0E\u5361\u7247\u5185\u7684\u5176\u5B83\u64CD\u4F5C\u533A\u5206\u3002 */\n.rg-risks-expand {\n align-self: flex-start;\n margin-top: 2px;\n}\n\n.rg-risk {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-bottom: 8px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.rg-risk:last-child {\n border-bottom: none;\n padding-bottom: 0;\n}\n\n.rg-risk-main {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.rg-risk-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-risk-score {\n font-variant-numeric: tabular-nums;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-risk-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-risk-actions {\n display: flex;\n gap: 6px;\n}\n\n/* ---------- \u80FD\u529B\u6E05\u5355\u8868 ---------- */\n\n.rg-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--dsw-font-xxs-12, 12px);\n table-layout: fixed;\n}\n\n.rg-table th,\n.rg-table td {\n text-align: left;\n vertical-align: top;\n padding: 5px 6px;\n border-bottom: 1px solid var(--dsw-alias-hairline, var(--dsw-alias-border-l1));\n overflow-wrap: anywhere;\n}\n\n.rg-table thead th {\n color: var(--dsw-alias-label-secondary);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n white-space: nowrap;\n border-bottom-color: var(--dsw-alias-border-l2);\n}\n\n.rg-table tbody tr {\n cursor: pointer;\n}\n\n.rg-table tbody tr:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n/* \u9009\u4E2D\u884C\uFF1A\u5DE6\u4FA7 2px \u7AD6\u6761 + \u5E95\u8272\uFF08\u53EA\u9760\u5E95\u8272\u5728\u6697\u8272\u4E3B\u9898\u4E0B\u4E0D\u591F\u663E\u773C\uFF09\u3002 */\n.rg-row-selected {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\n box-shadow: inset 2px 0 0 0 var(--dsw-alias-brand-primary);\n}\n\n/* \u7A84\u5F62\u6001\u4E0B\u8868\u683C\u4F1A\u6324\u6210\u4E00\u56E2 \u2014\u2014 \u5217\u5BBD\u6309\u300C\u80FD\u529B\u540D / \u5173\u7CFB\u300D\u4E24\u680F\u91CD\u6392\u3002 */\n.rg-panel[data-form='narrow'] .rg-table {\n table-layout: auto;\n}\n\n.rg-panel[data-form='narrow'] .rg-table th:nth-child(2),\n.rg-panel[data-form='narrow'] .rg-table td:nth-child(2),\n.rg-panel[data-form='narrow'] .rg-table th:nth-child(3),\n.rg-panel[data-form='narrow'] .rg-table td:nth-child(3) {\n display: none;\n}\n\n.rg-gaps {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.rg-gap {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-gap-detail {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---------- \u9879\u76EE\u5206\u533A ---------- */\n\n.rg-partitions {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.rg-partition {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: var(--dsw-font-xxs-12, 12px);\n padding: 3px 4px;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-partition-selected {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\n box-shadow: inset 2px 0 0 0 var(--dsw-alias-brand-primary);\n}\n\n.rg-partition-label {\n font-weight: var(--dsw-font-xs-strong-13, 600);\n max-width: 40%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rg-partition-workspace {\n flex: 1 1 140px;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-partition-time {\n flex: none;\n font-variant-numeric: tabular-nums;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* ---------- \u5F52\u6863 ---------- */\n\n.rg-archive-run {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.rg-archives {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.rg-archive {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-sm, 8px);\n}\n\n.rg-archive-dir {\n font-family: var(--dsw-font-mono, var(--dsw-font-family-mono, ui-monospace, monospace));\n font-size: var(--dsw-font-xxs-12, 12px);\n overflow-wrap: anywhere;\n}\n\n.rg-archive-meta,\n.rg-archive-summary {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* ---------- \u533A\u5757 / \u9875\u811A ---------- */\n\n.rg-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.rg-section-head {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.rg-section-title {\n margin: 0;\n font-size: var(--dsw-font-m-18, 18px);\n font-weight: var(--dsw-font-m-18-font-weight, 600);\n}\n\n.rg-selection-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n padding: 2px 8px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n}\n\n.rg-foot {\n display: flex;\n flex-wrap: wrap;\n gap: 4px 16px;\n border-top: 1px solid var(--dsw-alias-hairline, var(--dsw-alias-border-l1));\n padding-top: 8px;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* ---------- \u56FE\u8C31 ---------- */\n\n.rg-graph {\n display: flex;\n flex-direction: column;\n gap: 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n padding: 10px;\n min-width: 0;\n}\n\n.rg-graph-toolbar {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.rg-graph-zoom {\n display: inline-flex;\n gap: 4px;\n}\n\n.rg-graph-hint {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-graph-viewport {\n position: relative;\n overflow: hidden;\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-2);\n cursor: grab;\n touch-action: none;\n}\n\n.rg-graph-viewport:active {\n cursor: grabbing;\n}\n\n.rg-graph-viewport:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n\n.rg-graph-node:focus-visible {\n outline: none;\n}\n\n/* \u952E\u76D8\u805A\u7126\u7684\u8282\u70B9\u5FC5\u987B\u80FD\u770B\u89C1\u7126\u70B9\uFF08\u5426\u5219 Tab \u8D70\u4E00\u5708\u4E0D\u77E5\u9053\u505C\u5728\u54EA\uFF09\u3002 */\n.rg-graph-node:focus-visible .rg-graph-node-bg {\n stroke: var(--dsw-alias-brand-primary);\n stroke-width: 3px;\n}\n\n.rg-graph-node-label {\n fill: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.rg-graph-node-detail {\n fill: var(--dsw-alias-label-secondary);\n font-size: 11px;\n}\n\n.rg-graph-column-title {\n fill: var(--dsw-alias-label-secondary);\n font-size: 12px;\n font-weight: 600;\n}\n\n/* \u66F2\u7EBF\u8FB9\u7684\u8BED\u4E49\u6807\u7B7E\uFF08\u4EC5\u9AD8\u4EAE\u8FB9\u663E\u793A\uFF0C\u907F\u514D\u566A\u58F0\uFF09\u3002 */\n.rg-graph-edge-label {\n fill: var(--dsw-alias-brand-primary);\n font-size: 10px;\n font-weight: 600;\n}\n\n/* \u56FE\u4F8B\uFF08\u7C7B\u578B\u8FC7\u6EE4 chips\uFF0C\u5E26\u8BA1\u6570\uFF09\u3002 */\n.rg-graph-legend {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-left: 4px;\n}\n\n.rg-legend-chip {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n border-radius: 999px;\n padding: 2px 10px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n cursor: pointer;\n transition: background var(--rg-motion-fast) var(--rg-ease);\n}\n\n.rg-legend-chip:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-legend-chip-off {\n opacity: 0.45;\n text-decoration: line-through;\n}\n\n.rg-legend-chip:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.rg-legend-count {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* \u56FE\u8C31\u4E3B\u4F53\uFF1A\u89C6\u53E3 + \u8BE6\u60C5\u4FA7\u680F\u4E24\u680F\uFF08\u7A84\u5BB9\u5668\u7EB5\u5411\u5806\u53E0\uFF09\u3002 */\n.rg-graph-body {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 10px;\n}\n\n@media (min-width: 960px) {\n .rg-graph-body:has(.rg-graph-aside) {\n grid-template-columns: minmax(0, 1fr) 260px;\n }\n}\n\n.rg-graph-status {\n display: flex;\n align-items: center;\n gap: 6px 14px;\n flex-wrap: wrap;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-graph-zoom-label {\n font-variant-numeric: tabular-nums;\n}\n\n.rg-graph-status-selected {\n color: var(--dsw-alias-label-primary);\n}\n\n/* \u60AC\u505C\u6D6E\u5C42\uFF1A\u6D6E\u52A8\u5361\u7247\uFF08\u552F\u4E00\u5E26\u9634\u5F71\u7684\u8868\u9762\uFF09\u3002 */\n.rg-graph-tooltip {\n position: absolute;\n z-index: 5;\n transform: translate(-50%, -100%);\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-width: 260px;\n padding: 6px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: var(--dsw-shadow-lv3, var(--dsw-shadow-lv1, none));\n pointer-events: none;\n}\n\n.rg-tooltip-title {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: var(--dsw-font-xxs-12, 12px);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-tooltip-meta,\n.rg-tooltip-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n overflow-wrap: anywhere;\n}\n\n/* \u56FE\u8C31\u641C\u7D22\u6846\u4E0E\u7ED3\u679C\u4E0B\u62C9\u3002 */\n.rg-graph-search {\n position: relative;\n display: inline-flex;\n}\n\n.rg-search-input {\n min-height: var(--rg-control-h);\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n border-radius: var(--dsw-radius-sm, 8px);\n padding: 2px 9px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n min-width: 150px;\n}\n\n.rg-search-input:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.rg-search-input::placeholder {\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-search-results {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n z-index: 10;\n margin: 0;\n padding: 4px;\n list-style: none;\n min-width: 200px;\n max-height: 220px;\n overflow: auto;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: var(--dsw-shadow-lv3, var(--dsw-shadow-lv1, none));\n}\n\n.rg-search-results button {\n appearance: none;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 5px 8px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n text-align: left;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-search-results button:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-search-noresult {\n padding: 5px 8px;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n/* \u8BE6\u60C5\u4FA7\u680F\u3002 */\n.rg-graph-aside {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n align-self: start;\n}\n\n.rg-graph-aside-header {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.rg-graph-aside-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--dsw-font-s-14, 14px);\n}\n\n.rg-graph-aside-detail {\n margin: 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-graph-aside-actions {\n display: flex;\n gap: 6px;\n}\n\n/* \u90BB\u5C45\u5BFC\u822A\uFF08\u8BE6\u60C5\u4FA7\u680F\uFF09\uFF1A\u4E0A\u6E38/\u4E0B\u6E38\u5206\u7EC4\u5217\u8868\uFF0C\u70B9\u51FB\u5373\u8DF3\u8F6C\u5B9A\u4F4D\u3002 */\n.rg-graph-neighbors {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.rg-graph-neighbor-title {\n margin: 0;\n display: flex;\n align-items: center;\n font-size: var(--dsw-font-xxs-12, 12px);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-graph-neighbor-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.rg-graph-neighbor-link {\n appearance: none;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 6px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n text-align: left;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-graph-neighbor-link:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-graph-neighbor-link:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* \u52A8\u6548\u4E0E\u504F\u597D\uFF1A\u5C0A\u91CD\u300C\u51CF\u5C11\u52A8\u6001\u6548\u679C\u300D\uFF08\u524D\u5EAD\u5931\u8C03\u7528\u6237\u4F1A\u56E0\u8FC7\u6E21\u4E0D\u9002\uFF09\u3002 */\n@media (prefers-reduced-motion: reduce) {\n .rg-panel *,\n .rg-panel *::before,\n .rg-panel *::after {\n transition-duration: 0.01ms !important;\n animation-duration: 0.01ms !important;\n }\n}\n\n/* ---------- \u6062\u590D\u5224\u5B9A\u6761\uFF08v0.9.1\uFF09 ---------- */\n\n.rg-verdict {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin: 0;\n padding: 8px 12px;\n border-radius: var(--dsw-radius-sm, 8px);\n font-size: var(--dsw-font-xs-13, 13px);\n}\n/* act\uFF08\u5173\u95ED\u4E86\u63D2\u4EF6\uFF09\u4E0E watch\uFF08\u89C2\u5BDF\u8F6E\uFF09\u7528**\u8FB9\u6846+\u6807\u7B7E**\u533A\u5206\u800C\u4E0D\u662F\u53EA\u9760\u989C\u8272\uFF1A\n \u7070\u5EA6\u6253\u5370/\u8272\u5F31\u7528\u6237\u4E5F\u80FD\u5206\u8FA8\u3002 */\n.rg-verdict-act {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n}\n.rg-verdict-watch {\n border: 1px dashed var(--dsw-alias-border-l1);\n background: transparent;\n}\n.rg-verdict-tag {\n padding: 1px 8px;\n border-radius: var(--dsw-radius-sm, 8px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n background: var(--dsw-alias-bg-layer-2);\n}\n.rg-verdict-meta {\n margin-left: auto;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---------- v0.10 \u6D1E\u5BDF\u5361\uFF1A\u9A6C\u5C14\u53EF\u592B / \u6570\u636E\u6D41 / \u7CFB\u7EDF\u8BCA\u65AD ---------- */\n\n/* \u5361\u7247\u9876\u90E8\u7684\u4E00\u884C\u6458\u8981\uFF08\u6837\u672C\u6570\u3001\u6536\u655B\u6027\u7B49\u767D\u76D2\u5143\u4FE1\u606F\uFF09\u3002 */\n.rg-card-note {\n margin: 0 0 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* \u9A6C\u5C14\u53EF\u592B\u884C\uFF1A\u9636\u6BB5\u540D | \u81EA\u73AF\u6982\u7387\u6761 | \u767E\u5206\u6BD4\u3002 */\n.rg-markov {\n list-style: none;\n margin: 0;\n padding: 0;\n display: grid;\n gap: 6px;\n}\n.rg-markov-row {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.rg-markov-stage {\n width: 84px;\n flex: none;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.rg-markov-row .rg-bar {\n flex: 1 1 auto;\n width: auto;\n}\n.rg-markov-value {\n width: 44px;\n flex: none;\n text-align: right;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n}\n.rg-markov-nodata {\n flex: 1 1 auto;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n.rg-markov-stationary {\n margin-top: 10px;\n}\n.rg-subhead {\n margin: 0 0 6px;\n font-size: 12px;\n font-weight: var(--dsw-font-s-strong-14, 600);\n color: var(--dsw-alias-label-secondary);\n}\n.rg-chip-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* \u6570\u636E\u6D41\u94FE\u8DEF\uFF1A\u5E8F\u53F7 | \u540D\u79F0+\u8BF4\u660E | \u8BA1\u6570\u3002 */\n.rg-dataflow {\n list-style: none;\n margin: 0;\n padding: 0;\n display: grid;\n gap: 8px;\n counter-reset: rg-flow;\n}\n.rg-dataflow-step {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n}\n.rg-dataflow-index {\n flex: none;\n width: 20px;\n height: 20px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dsw-radius-full, 999px);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n font-weight: var(--dsw-font-s-strong-14, 600);\n}\n.rg-dataflow-main {\n min-width: 0;\n display: grid;\n gap: 2px;\n}\n.rg-dataflow-label {\n color: var(--dsw-alias-label-primary);\n font-weight: var(--dsw-font-s-strong-14, 600);\n font-size: 13px;\n}\n.rg-dataflow-detail {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.rg-dataflow-count {\n flex: none;\n margin-left: auto;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* \u7CFB\u7EDF\u8BCA\u65AD\u5361\uFF1A\u7D27\u51D1\u952E\u503C\u5BF9 + \u53CD\u9988\u6309\u94AE\u3002 */\n.rg-issues-button {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin-top: 10px;\n text-decoration: none;\n color: var(--dsw-alias-brand-primary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n.rg-issues-button:hover {\n border-color: var(--dsw-alias-brand-primary);\n}\n";
3142
+ var styles_default = "/**\n * \u7BA1\u7406\u9762\u677F\u7684\u6837\u5F0F\u3002\n *\n * ## \u7EAA\u5F8B\n *\n * 1. **\u53EA\u7528\u4E3B\u9898\u53D8\u91CF**\uFF1A\u989C\u8272\u3001\u5706\u89D2\u3001\u5B57\u4F53\u5168\u90E8\u5F15\u7528 `--dsw-alias-*` / `--dsw-radius-*`\n * / `--dsw-shadow-*` \u5BB6\u65CF\u3002\u5BBF\u4E3B\u91CD\u547D\u540D\u4E00\u4E2A\u53D8\u91CF\u65F6\u9762\u677F\u53EA\u662F\u53D8\u4E11\uFF0C\u4E0D\u4F1A\u5D29 \u2014\u2014\n * \u56E0\u6B64\u6BCF\u4E2A var() \u90FD\u5E26\u4E00\u4E2A\u4FDD\u5B88\u7684\u515C\u5E95\u503C\u3002\n * 2. **\u4E24\u79CD\u5F62\u6001\u6309\u5BB9\u5668\u5BBD\u5EA6**\uFF1A`.rg-panel[data-form=\"narrow\"]` \u7531 JS \u6D4B\u51FA\u7684\n * \u9762\u677F\u5BBD\u5EA6\u9A71\u52A8\uFF08\u9762\u677F\u53EF\u80FD\u6302\u5728\u88AB\u62D6\u7A84\u7684\u4FA7\u680F\u91CC\uFF0C\u5A92\u4F53\u67E5\u8BE2\u91CF\u7684\u662F\u7A97\u53E3\uFF0C\u4F1A\u7B54\u9519\uFF09\u3002\n * 3. **\u5C40\u90E8\u52A8\u6548\u4EE4\u724C**\uFF1A\u8FC7\u6E21\u65F6\u957F\u4E0E\u7F13\u52A8\u5B9A\u4E49\u5728 `.rg-panel` \u4E0A\u7684 `--rg-*`\uFF0C\n * \u4E0D\u4F9D\u8D56\u672A\u7ECF\u9A8C\u8BC1\u7684\u5168\u5C40 `--ds-*`\uFF08\u751F\u6001\u63D2\u4EF6\u540C\u6837\u81EA\u6301\u8FD9\u5957\u4EE4\u724C\uFF09\u3002\n * 4. **\u4E0D\u5199\u6B7B\u5BB9\u5668\u5BBD\u5EA6**\uFF1A\u9762\u677F\u5360\u6EE1\u5BBF\u4E3B\u7ED9\u7684\u4E2D\u592E\u9762\u677F\u533A\u57DF\u3002\n * 5. **\u5BF9\u6BD4\u5EA6**\uFF1A\u6B63\u6587 `--dsw-alias-label-primary`\uFF0C\u6B21\u8981 `--dsw-alias-label-secondary`\uFF0C\n * \u63D0\u793A `--dsw-alias-label-tertiary`\uFF08\u4EAE/\u6697\u4E3B\u9898\u90FD\u7531\u5BBF\u4E3B\u4FDD\u8BC1\u53EF\u8BFB\uFF09\u3002\n */\n\n.rg-panel {\n /* \u5C40\u90E8\u4EE4\u724C\uFF1A\u63A7\u4EF6\u9AD8\u5EA6\u3001\u52A8\u6548\u3001\u95F4\u8DDD\u3002\u6539\u4E00\u5904\uFF0C\u6574\u5757\u9762\u677F\u8DDF\u7740\u4E00\u81F4\u3002 */\n --rg-control-h: 28px;\n --rg-motion-fast: 120ms;\n --rg-motion-medium: 200ms;\n --rg-ease: cubic-bezier(0.2, 0, 0, 1);\n --rg-gap: 16px;\n\n display: flex;\n flex-direction: column;\n gap: var(--rg-gap);\n padding: 20px 24px 32px;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n font-family: var(--dsw-font-family);\n font-size: var(--dsw-font-s-14, 14px);\n line-height: var(--dsw-font-s-14-line-height, 22px);\n\n /**\n * \u9762\u677F\u5FC5\u987B**\u81EA\u5DF1**\u662F\u6EDA\u52A8\u5BB9\u5668\u3002\n *\n * \u4E3A\u4EC0\u4E48\u4E0D\u80FD\u6307\u671B\u5BBF\u4E3B\uFF1A\u5B9E\u6D4B\uFF08\u6C99\u7BB1 sb-rg-c5\uFF09\u5BBF\u4E3B\u69FD\u4F4D\u662F\u8FD9\u6837\u4E00\u4E32\n * .centerCol { overflow-y: hidden } \u2190 \u7956\u5148\u628A\u6EA2\u51FA\u88C1\u6389\n * .mainPanel { height: 714px; overflow: visible }\n * .rg-panel { height: 3965px } \u2190 \u5185\u5BB9\u81EA\u7136\u9AD8\u5EA6\uFF0C\u8D85\u51FA\u540E\u76F4\u63A5\u88AB\u88C1\n * \u6587\u6863\u672C\u8EAB\u4E0D\u6EDA\uFF08docScrollH == clientH == 714\uFF09\uFF0C\u56E0\u6B64**\u4E0B\u65B9\u6570\u636E\u9875\u6C38\u8FDC\u770B\u4E0D\u5230**\u3002\n *\n * \u4E09\u4E2A\u5C5E\u6027\u7F3A\u4E00\u4E0D\u53EF\uFF1A\n * - `height: 100%` + `min-height: 0`\uFF1A\u6491\u6EE1\u5BBF\u4E3B\u7ED9\u7684\u786E\u5B9A\u9AD8\u5EA6\uFF0C\u4E14\u5141\u8BB8\u6536\u7F29\u5230\u53EF\u6EDA\u52A8\uFF1B\n * \u53EA\u5199 overflow \u800C\u4E0D\u9650\u5236\u9AD8\u5EA6\uFF0Cflex \u5B50\u9879\u4ECD\u4F1A\u6309\u5185\u5BB9\u6491\u5F00\uFF0C\u6EDA\u4E0D\u8D77\u6765\u3002\n * - `overflow-y: auto`\uFF1A\u9762\u677F\u81EA\u5DF1\u6EDA\u3002\n * - `overscroll-behavior: contain`\uFF1A\u6EDA\u5230\u5C3D\u5934\u4E0D\u628A\u6EDA\u52A8\u4F20\u7ED9\u5BBF\u4E3B\uFF0C\u5426\u5219\u4F1A\u628A\u6574\u9875/\u4FA7\u680F\n * \u5E26\u7740\u4E00\u8D77\u6EDA\uFF0C\u89C6\u89C9\u4E0A\u50CF\"\u9762\u677F\u5728\u4E71\u8DF3\"\u3002\n */\n height: 100%;\n max-height: 100%;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n overscroll-behavior: contain;\n}\n\n/* \u7A84\u5F62\u6001\uFF08\u4FA7\u680F / \u5206\u5C4F\uFF09\uFF1A\u6536\u7D27\u8FB9\u8DDD\u4E0E\u95F4\u8DDD\uFF0C\u628A\u7EB5\u5411\u7A7A\u95F4\u8BA9\u7ED9\u5185\u5BB9\u3002 */\n.rg-panel[data-form='narrow'] {\n --rg-gap: 12px;\n padding: 14px 14px 24px;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n/* ---------- \u5934\u90E8 ---------- */\n\n.rg-header {\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n}\n\n.rg-header-main {\n min-width: 0;\n flex: 1 1 260px;\n}\n\n.rg-title {\n margin: 0;\n font-size: var(--dsw-font-l-20, 20px);\n font-weight: var(--dsw-font-l-20-font-weight, 600);\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-panel[data-form='narrow'] .rg-title {\n font-size: var(--dsw-font-m-18, 18px);\n}\n\n.rg-subtitle {\n margin: 4px 0 0;\n font-size: var(--dsw-font-xs-13, 13px);\n color: var(--dsw-alias-label-secondary);\n overflow-wrap: anywhere;\n}\n\n.rg-project-name {\n font-weight: var(--dsw-font-xs-strong-13, 600);\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-partition-workspace-full {\n font-size: var(--dsw-font-xxs-12, 12px);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 100%;\n direction: ltr;\n}\n\n.rg-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* ---------- \u63A7\u4EF6 ---------- */\n\n.rg-button {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n min-height: var(--rg-control-h);\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n border-radius: var(--dsw-radius-sm, 8px);\n padding: 4px 12px;\n font: inherit;\n font-size: var(--dsw-font-xs-13, 13px);\n cursor: pointer;\n transition:\n background var(--rg-motion-fast) var(--rg-ease),\n border-color var(--rg-motion-fast) var(--rg-ease);\n}\n\n.rg-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-button:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n\n.rg-button:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n\n.rg-button-small {\n min-height: 24px;\n padding: 2px 9px;\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-button-icon {\n min-width: 28px;\n padding: 2px 6px;\n justify-content: center;\n font-size: var(--dsw-font-s-14, 14px);\n line-height: 1;\n}\n\n.rg-button-danger {\n border-color: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n}\n\n.rg-button-active {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n\n/* \u8F6E\u8BE2\u6682\u505C\u6001\u6309\u94AE\uFF08\u6682\u505C = \u9700\u8981\u88AB\u770B\u89C1\u7684\u72B6\u6001\uFF0C\u4E0D\u80FD\u53EA\u9760 aria-pressed\uFF09\u3002 */\n.rg-button-paused {\n border-color: var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.rg-link {\n appearance: none;\n border: none;\n background: none;\n padding: 0 2px;\n font: inherit;\n font-size: inherit;\n color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n text-decoration: underline;\n}\n\n/* \u5206\u6BB5\u63A7\u4EF6\uFF08\u5E03\u5C40 / \u7740\u8272\u53E3\u5F84\uFF09\u3002 */\n.rg-segmented {\n display: inline-flex;\n align-items: stretch;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n overflow: hidden;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.rg-segment {\n appearance: none;\n border: none;\n border-right: 1px solid var(--dsw-alias-border-l1);\n background: none;\n color: var(--dsw-alias-label-secondary);\n padding: 3px 9px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n cursor: pointer;\n transition: background var(--rg-motion-fast) var(--rg-ease);\n}\n\n.rg-segment:last-child {\n border-right: none;\n}\n\n.rg-segment:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-segment[aria-pressed='true'] {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\n color: var(--dsw-alias-label-primary);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n}\n\n.rg-segment:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.rg-field {\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n}\n\n.rg-field-grow {\n flex: 1 1 160px;\n}\n\n.rg-field-label {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-input,\n.rg-select {\n min-height: var(--rg-control-h);\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n padding: 2px 8px;\n font: inherit;\n font-size: var(--dsw-font-xs-13, 13px);\n max-width: 100%;\n}\n\n.rg-input:focus-visible,\n.rg-select:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* ---------- \u63D0\u793A\u6761 ---------- */\n\n.rg-notice {\n margin: 0;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-error {\n margin: 0;\n padding: 10px 12px;\n border-radius: var(--dsw-radius-sm, 8px);\n border: 1px solid var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-empty {\n margin: 0;\n padding: 8px 0;\n font-size: var(--dsw-font-xs-13, 13px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-muted {\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-strong {\n font-weight: var(--dsw-font-xs-strong-13, 600);\n}\n\n.rg-block {\n display: block;\n}\n\n.rg-ellipsis {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rg-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 16px;\n margin-left: 6px;\n padding: 0 4px;\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xxxs-11, 11px);\n}\n\n/* \u52A0\u8F7D / \u4E0D\u53EF\u7528\u72B6\u6001\u5757\uFF1A\u5360\u4F4D\u9AD8\u5EA6\u7A33\u5B9A\uFF0C\u907F\u514D\u8F6E\u8BE2\u65F6\u9875\u9762\u8DF3\u52A8\u3002 */\n.rg-state-block {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 24px 0;\n}\n\n.rg-state-title {\n margin: 0;\n font-size: var(--dsw-font-m-18, 18px);\n font-weight: var(--dsw-font-m-18-font-weight, 600);\n}\n\n/* ---------- \u5361\u7247\u7F51\u683C ---------- */\n\n.rg-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 12px;\n}\n\n.rg-panel[data-form='narrow'] .rg-grid {\n grid-template-columns: 1fr;\n}\n\n.rg-card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px 16px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n min-height: 0;\n}\n\n/**\n * \u5361\u7247\u5185\u5BB9\u533A\u56FA\u5B9A\u9AD8\u5EA6 + \u5185\u90E8\u6EDA\u52A8\uFF08v0.9.1 \u7528\u6237\u9700\u6C42\uFF09\u3002\n *\n * \u4E3A\u4EC0\u4E48\u9650\u9AD8\u800C\u4E0D\u662F\u8BA9\u5185\u5BB9\u81EA\u7136\u4F38\u5C55\uFF1A\u5C55\u5F00\u7684\u5361\u7247\u591A\u4E86\u4E4B\u540E\uFF0C\u9762\u677F\u88AB\u5355\u4E2A\n * \u5361\u7247\u6491\u5230\u51E0\u5343\u50CF\u7D20\uFF0C\u4E0B\u9762\u7684\u5361\u7247\u8981\u6EDA\u5F88\u4E45\u624D\u770B\u5F97\u5230 \u2014\u2014 \u5747\u5300\u7684\u89C6\u53E3\u5BC6\u5EA6\n * \u6BD4\u5355\u5361\u7684\u5B8C\u6574\u6027\u66F4\u91CD\u8981\uFF08\u8FD9\u6B63\u662F\u7528\u6237\u539F\u59CB\u8BC9\u6C42\uFF09\u3002\n *\n * - \u9ED8\u8BA4 `--rg-card-body-h: 240px`\uFF1A\u8DB3\u591F\u770B\u7EA6 10 \u884C\u5185\u5BB9\uFF0C\u8D85\u51FA\u5185\u90E8\u6EDA\u52A8\uFF1B\n * - `overscroll-behavior: contain`\uFF1A\u6EDA\u5230\u5C3D\u5934\u4E0D\u628A\u5916\u5C42\u9762\u677F\u5E26\u7740\u6EDA\uFF1B\n * - `.rg-card-enlarged` \u89E3\u9664\u9650\u9AD8\uFF08\u2922 \u6309\u94AE\uFF09\uFF0C\u644A\u5F00\u9605\u8BFB\u957F\u6E05\u5355\uFF1B\n * - \u7A84\u5F62\u6001\u628A\u9650\u9AD8\u964D\u5230 180px\uFF08\u4FA7\u680F\u7A7A\u95F4\u5C11\uFF0C\u5BC6\u5EA6\u4F18\u5148\uFF09\u3002\n */\n.rg-card-body {\n display: flex;\n flex-direction: column;\n gap: 10px;\n height: var(--rg-card-body-h, 240px);\n overflow-y: auto;\n overscroll-behavior: contain;\n min-height: 0;\n}\n\n/* \u6269\u5927\u6001\uFF1A\u89E3\u9664\u9650\u9AD8\uFF0C\u5361\u7247\u6309\u5185\u5BB9\u81EA\u7136\u4F38\u5C55\uFF08\u9762\u677F\u81EA\u8EAB\u4ECD\u662F\u6EDA\u52A8\u5BB9\u5668\uFF09\u3002 */\n.rg-card-enlarged .rg-card-body {\n height: auto;\n max-height: none;\n overflow-y: visible;\n}\n\n/* \u6269\u5927\u6309\u94AE\uFF1A\u5934\u90E8\u5185\u7684\u7B2C\u4E8C\u4E2A\u52A8\u4F5C\uFF0C\u5C0F\u5C3A\u5BF8\u3001\u4F4E\u5BF9\u6BD4\uFF0C\u4E0D\u4E0E\u6298\u53E0\u7BAD\u5934\u7ADE\u4E89\u3002 */\n.rg-card-expand {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n margin-left: auto;\n border-radius: var(--dsw-radius-sm, 8px);\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n user-select: none;\n transition: background var(--rg-motion-fast, 120ms) var(--rg-ease, ease);\n}\n.rg-card-expand:hover,\n.rg-card-expand:focus-visible {\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n.rg-card-enlarged .rg-card-expand {\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-card-title {\n margin: 0;\n font-size: var(--dsw-font-s-14, 14px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n}\n\n/* \u7A84\u5F62\u6001\uFF1A\u4FA7\u680F\u7A7A\u95F4\u5C11\uFF0C\u5BC6\u5EA6\u4F18\u5148 \u2014\u2014 \u5185\u5BB9\u533A\u9650\u9AD8\u964D\u4E3A 180px\u3002 */\n.rg-panel[data-form='narrow'] .rg-card-body {\n --rg-card-body-h: 180px;\n}\n\n.rg-card-title {\n margin: 0;\n font-size: var(--dsw-font-s-14, 14px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n}\n\n.rg-card-subtitle {\n margin: 4px 0 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-card-note {\n margin: 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n/* \u53EF\u6298\u53E0\u5361\u7247\uFF1A\u6807\u9898\u884C\u662F\u6309\u94AE\uFF0C\u6574\u4E2A\u5934\u90E8\u53EF\u70B9\u3002 */\n.rg-card-head {\n appearance: none;\n border: none;\n background: none;\n padding: 0;\n margin: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n cursor: pointer;\n font: inherit;\n color: inherit;\n text-align: left;\n}\n\n.rg-card-head:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-card-head-hint {\n margin-left: auto;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rg-card-chevron {\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-card-collapsed {\n padding-bottom: 10px;\n}\n\n.rg-card-body {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* ---------- \u5FBD\u6807 / \u5706\u70B9 / \u8BA1\u6570\u7247 ---------- */\n\n.rg-badge {\n display: inline-flex;\n align-items: center;\n padding: 1px 8px;\n border-radius: var(--dsw-radius-sm, 8px);\n border: 1px solid var(--dsw-alias-border-l2);\n font-size: var(--dsw-font-xxs-12, 12px);\n white-space: nowrap;\n}\n\n.rg-badge-idle,\n.rg-badge-monitoring {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n\n.rg-badge-crashed,\n.rg-badge-error {\n border-color: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n}\n\n.rg-badge-deciding,\n.rg-badge-worsening,\n.rg-badge-warn {\n border-color: var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.rg-badge-stabilizing,\n.rg-badge-recovered,\n.rg-badge-improving,\n.rg-badge-success {\n border-color: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-state-success-primary);\n}\n\n.rg-badge-stable,\n.rg-badge-neutral {\n border-color: var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-chip {\n display: inline-flex;\n align-items: center;\n padding: 0 6px;\n border-radius: var(--dsw-radius-xs, 4px);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: var(--dsw-font-xxxs-11, 11px);\n white-space: nowrap;\n}\n\n.rg-chip-warn {\n border: 1px solid var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-state-warn-primary);\n background: none;\n}\n\n.rg-chip-more {\n font-variant-numeric: tabular-nums;\n}\n\n.rg-names {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 3px;\n min-width: 0;\n}\n\n.rg-dot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--dsw-alias-label-secondary);\n flex: none;\n}\n\n.rg-dot-info {\n background: var(--dsw-alias-brand-primary);\n}\n\n.rg-dot-success {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.rg-dot-warn {\n background: var(--dsw-alias-state-warn-primary);\n}\n\n.rg-dot-error {\n background: var(--dsw-alias-state-error-primary);\n}\n\n/* ---------- \u8FDB\u5EA6\u6761 ---------- */\n\n.rg-bar {\n display: inline-block;\n width: 72px;\n height: 4px;\n border-radius: 2px;\n background: var(--dsw-alias-bg-layer-2);\n overflow: hidden;\n flex: none;\n}\n\n.rg-bar-wide {\n width: 100%;\n}\n\n.rg-bar-fill {\n display: block;\n height: 100%;\n background: var(--dsw-alias-brand-primary);\n transition: width var(--rg-motion-medium) var(--rg-ease);\n}\n\n/* ---------- KPI \u6458\u8981\u6761 ---------- */\n\n.rg-kpis {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 10px;\n}\n\n.rg-kpi {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n}\n\n.rg-kpi-label {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-kpi-value {\n font-size: var(--dsw-font-s-14, 14px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n font-variant-numeric: tabular-nums;\n}\n\n.rg-kpi-big {\n font-size: var(--dsw-font-m-18, 18px);\n font-weight: var(--dsw-font-m-18-font-weight, 600);\n}\n\n.rg-kpi-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 100%;\n}\n\n.rg-kpi-meta {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.rg-ring {\n flex: none;\n}\n\n.rg-ring-text {\n fill: var(--dsw-alias-label-primary);\n font-size: 11px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---------- \u63CF\u8FF0\u5217\u8868 / \u9636\u6BB5\u8F68\u9053 ---------- */\n\n.rg-dl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 4px 12px;\n margin: 0;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-dl dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-dl dd {\n margin: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex-wrap: wrap;\n}\n\n.rg-dl-compact {\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-mono {\n font-family: var(--dsw-font-mono, var(--dsw-font-family-mono, ui-monospace, monospace));\n font-size: var(--dsw-font-xxs-12, 12px);\n overflow-wrap: anywhere;\n}\n\n/* \u9636\u6BB5\u8F68\u9053\uFF1A\u72B6\u6001\u673A\u4E0D\u662F\u8FDB\u5EA6\u6761\uFF0Cvisited \u4E0E current \u7528\u4E0D\u540C\u901A\u9053\u8868\u8FBE\u3002 */\n.rg-stage-rail {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-wrap: wrap;\n gap: 4px 10px;\n}\n\n.rg-stage-cell {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-stage-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n border: 1px solid var(--dsw-alias-border-l2);\n background: none;\n flex: none;\n}\n\n.rg-stage-visited {\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-stage-visited .rg-stage-dot {\n background: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-color: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n}\n\n.rg-stage-current {\n color: var(--dsw-alias-label-primary);\n font-weight: var(--dsw-font-xxxs-strong-11, 600);\n}\n\n.rg-stage-current .rg-stage-dot {\n background: var(--dsw-alias-brand-primary);\n border-color: var(--dsw-alias-brand-primary);\n box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-2);\n}\n\n/* ---------- \u6307\u6807 ---------- */\n\n.rg-metrics {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.rg-metric {\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n.rg-metric-label {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-metric-value {\n font-size: var(--dsw-font-xs-13, 13px);\n font-variant-numeric: tabular-nums;\n}\n\n.rg-metric-undefined {\n color: var(--dsw-alias-label-secondary);\n font-style: italic;\n}\n\n.rg-metric-reason {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-metric-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-trend {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* ---------- \u8FC1\u79FB / \u51B3\u7B56 / \u4E8B\u4EF6\u65F6\u95F4\u7EBF ---------- */\n\n.rg-transitions,\n.rg-decisions,\n.rg-events {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n\n.rg-transition,\n.rg-decision,\n.rg-event {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-transition-reason,\n.rg-decision-detail,\n.rg-event-detail {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-decision-bundle {\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-transition-time,\n.rg-decision-time,\n.rg-event-time {\n flex: none;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n font-size: var(--dsw-font-xxxs-11, 11px);\n}\n\n.rg-event-ago {\n flex: none;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n font-size: var(--dsw-font-xxxs-11, 11px);\n}\n\n/* ---------- \u5ACC\u7591\u6392\u540D ---------- */\n\n.rg-risks {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* \u300C\u5C55\u5F00\u5176\u4F59 N \u4E2A\u300D\u6309\u94AE\uFF1A\u5DE6\u5BF9\u9F50\u7684\u94FE\u63A5\u5F62\u6001\uFF0C\u4E0E\u5361\u7247\u5185\u7684\u5176\u5B83\u64CD\u4F5C\u533A\u5206\u3002 */\n.rg-risks-expand {\n align-self: flex-start;\n margin-top: 2px;\n}\n\n.rg-risk {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-bottom: 8px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.rg-risk:last-child {\n border-bottom: none;\n padding-bottom: 0;\n}\n\n.rg-risk-main {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.rg-risk-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-risk-score {\n font-variant-numeric: tabular-nums;\n font-size: var(--dsw-font-xs-13, 13px);\n}\n\n.rg-risk-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-risk-actions {\n display: flex;\n gap: 6px;\n}\n\n/* ---------- \u80FD\u529B\u6E05\u5355\u8868 ---------- */\n\n.rg-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--dsw-font-xxs-12, 12px);\n table-layout: fixed;\n}\n\n.rg-table th,\n.rg-table td {\n text-align: left;\n vertical-align: top;\n padding: 5px 6px;\n border-bottom: 1px solid var(--dsw-alias-hairline, var(--dsw-alias-border-l1));\n overflow-wrap: anywhere;\n}\n\n.rg-table thead th {\n color: var(--dsw-alias-label-secondary);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n white-space: nowrap;\n border-bottom-color: var(--dsw-alias-border-l2);\n}\n\n.rg-table tbody tr {\n cursor: pointer;\n}\n\n.rg-table tbody tr:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n/* \u9009\u4E2D\u884C\uFF1A\u5DE6\u4FA7 2px \u7AD6\u6761 + \u5E95\u8272\uFF08\u53EA\u9760\u5E95\u8272\u5728\u6697\u8272\u4E3B\u9898\u4E0B\u4E0D\u591F\u663E\u773C\uFF09\u3002 */\n.rg-row-selected {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\n box-shadow: inset 2px 0 0 0 var(--dsw-alias-brand-primary);\n}\n\n/* \u7A84\u5F62\u6001\u4E0B\u8868\u683C\u4F1A\u6324\u6210\u4E00\u56E2 \u2014\u2014 \u5217\u5BBD\u6309\u300C\u80FD\u529B\u540D / \u5173\u7CFB\u300D\u4E24\u680F\u91CD\u6392\u3002 */\n.rg-panel[data-form='narrow'] .rg-table {\n table-layout: auto;\n}\n\n.rg-panel[data-form='narrow'] .rg-table th:nth-child(2),\n.rg-panel[data-form='narrow'] .rg-table td:nth-child(2),\n.rg-panel[data-form='narrow'] .rg-table th:nth-child(3),\n.rg-panel[data-form='narrow'] .rg-table td:nth-child(3) {\n display: none;\n}\n\n.rg-gaps {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.rg-gap {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: var(--dsw-font-xxs-12, 12px);\n}\n\n.rg-gap-detail {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---------- \u9879\u76EE\u5206\u533A ---------- */\n\n.rg-partitions {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.rg-partition {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: var(--dsw-font-xxs-12, 12px);\n padding: 3px 4px;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-partition-selected {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\n box-shadow: inset 2px 0 0 0 var(--dsw-alias-brand-primary);\n}\n\n.rg-partition-label {\n font-weight: var(--dsw-font-xs-strong-13, 600);\n max-width: 40%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rg-partition-workspace {\n flex: 1 1 140px;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-partition-time {\n flex: none;\n font-variant-numeric: tabular-nums;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* ---------- \u5F52\u6863 ---------- */\n\n.rg-archive-run {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.rg-archives {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.rg-archive {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-sm, 8px);\n}\n\n.rg-archive-dir {\n font-family: var(--dsw-font-mono, var(--dsw-font-family-mono, ui-monospace, monospace));\n font-size: var(--dsw-font-xxs-12, 12px);\n overflow-wrap: anywhere;\n}\n\n.rg-archive-meta,\n.rg-archive-summary {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* ---------- \u533A\u5757 / \u9875\u811A ---------- */\n\n.rg-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.rg-section-head {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.rg-section-title {\n margin: 0;\n font-size: var(--dsw-font-m-18, 18px);\n font-weight: var(--dsw-font-m-18-font-weight, 600);\n}\n\n.rg-selection-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n padding: 2px 8px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n}\n\n.rg-foot {\n display: flex;\n flex-wrap: wrap;\n gap: 4px 16px;\n border-top: 1px solid var(--dsw-alias-hairline, var(--dsw-alias-border-l1));\n padding-top: 8px;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* ---------- \u56FE\u8C31 ---------- */\n\n.rg-graph {\n display: flex;\n flex-direction: column;\n gap: 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n padding: 10px;\n min-width: 0;\n}\n\n.rg-graph-toolbar {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.rg-graph-zoom {\n display: inline-flex;\n gap: 4px;\n}\n\n.rg-graph-hint {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-graph-viewport {\n position: relative;\n overflow: hidden;\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-2);\n cursor: grab;\n touch-action: none;\n}\n\n.rg-graph-viewport:active {\n cursor: grabbing;\n}\n\n.rg-graph-viewport:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n\n.rg-graph-node:focus-visible {\n outline: none;\n}\n\n/* \u952E\u76D8\u805A\u7126\u7684\u8282\u70B9\u5FC5\u987B\u80FD\u770B\u89C1\u7126\u70B9\uFF08\u5426\u5219 Tab \u8D70\u4E00\u5708\u4E0D\u77E5\u9053\u505C\u5728\u54EA\uFF09\u3002 */\n.rg-graph-node:focus-visible .rg-graph-node-bg {\n stroke: var(--dsw-alias-brand-primary);\n stroke-width: 3px;\n}\n\n.rg-graph-node-label {\n fill: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.rg-graph-node-detail {\n fill: var(--dsw-alias-label-secondary);\n font-size: 11px;\n}\n\n.rg-graph-column-title {\n fill: var(--dsw-alias-label-secondary);\n font-size: 12px;\n font-weight: 600;\n}\n\n/* \u66F2\u7EBF\u8FB9\u7684\u8BED\u4E49\u6807\u7B7E\uFF08\u4EC5\u9AD8\u4EAE\u8FB9\u663E\u793A\uFF0C\u907F\u514D\u566A\u58F0\uFF09\u3002 */\n.rg-graph-edge-label {\n fill: var(--dsw-alias-brand-primary);\n font-size: 10px;\n font-weight: 600;\n}\n\n/* \u56FE\u4F8B\uFF08\u7C7B\u578B\u8FC7\u6EE4 chips\uFF0C\u5E26\u8BA1\u6570\uFF09\u3002 */\n.rg-graph-legend {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-left: 4px;\n}\n\n.rg-legend-chip {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n border-radius: 999px;\n padding: 2px 10px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n cursor: pointer;\n transition: background var(--rg-motion-fast) var(--rg-ease);\n}\n\n.rg-legend-chip:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-legend-chip-off {\n opacity: 0.45;\n text-decoration: line-through;\n}\n\n.rg-legend-chip:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.rg-legend-count {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n/* \u5F62\u72B6\u56FE\u4F8B\uFF08v0.11\uFF09\uFF1A\u56FE\u4F8B\u91CC\u76F4\u63A5\u753B\u8282\u70B9\u5F62\u72B6 \u2014\u2014 \u5F62\u72B6\u662F\u989C\u8272\u4E4B\u5916\u7684\u7B2C\u4E8C\u7F16\u7801\u901A\u9053\uFF0C\n \u56FE\u4F8B\u4E0D\u5C55\u793A\u5B83\uFF0C\u8272\u76F2/\u7070\u5EA6\u6253\u5370\u7528\u6237\u5C31\u6C38\u8FDC\u4E0D\u77E5\u9053\u65B9\u6846=\u5448\u73B0\u3001\u4E09\u89D2=\u7F3A\u53E3\u3002 */\n.rg-legend-shape {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n/* \u641C\u7D22\u904D\u5386\u6307\u793A\uFF08v0.11\uFF09\uFF1A\u7B2C n/m \u4E2A\u5339\u914D\uFF0C\u56DE\u8F66\u5FAA\u73AF\u8DF3\u8F6C\u65F6\u544A\u8BC9\u7528\u6237\u4F4D\u7F6E\u3002 */\n.rg-search-index {\n font-size: var(--dsw-font-xxs-12, 12px);\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n/* \u9A6C\u5C14\u53EF\u592B\u8DE8\u9636\u6BB5\u53BB\u5411\u5FBD\u6807\uFF08v0.11\uFF09\uFF1A`\u2192 \u5D29\u6E83 40%` \u9AD8\u5371\u8FC1\u79FB\u5FC5\u987B\u4E00\u773C\u53EF\u89C1\u3002 */\n.rg-markov-exits {\n display: inline-flex;\n gap: 4px;\n margin-left: 6px;\n flex-wrap: wrap;\n}\n\n.rg-chip-exit {\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n border: 1px dashed var(--dsw-alias-border-l2);\n background: transparent;\n white-space: nowrap;\n}\n\n/* \u56FE\u8C31\u4E3B\u4F53\uFF1A\u89C6\u53E3 + \u8BE6\u60C5\u4FA7\u680F\u4E24\u680F\uFF08\u7A84\u5BB9\u5668\u7EB5\u5411\u5806\u53E0\uFF09\u3002 */\n.rg-graph-body {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 10px;\n}\n\n@media (min-width: 960px) {\n .rg-graph-body:has(.rg-graph-aside) {\n grid-template-columns: minmax(0, 1fr) 260px;\n }\n}\n\n.rg-graph-status {\n display: flex;\n align-items: center;\n gap: 6px 14px;\n flex-wrap: wrap;\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-graph-zoom-label {\n font-variant-numeric: tabular-nums;\n}\n\n.rg-graph-status-selected {\n color: var(--dsw-alias-label-primary);\n}\n\n/* \u60AC\u505C\u6D6E\u5C42\uFF1A\u6D6E\u52A8\u5361\u7247\uFF08\u552F\u4E00\u5E26\u9634\u5F71\u7684\u8868\u9762\uFF09\u3002 */\n.rg-graph-tooltip {\n position: absolute;\n z-index: 5;\n transform: translate(-50%, -100%);\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-width: 260px;\n padding: 6px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: var(--dsw-shadow-lv3, var(--dsw-shadow-lv1, none));\n pointer-events: none;\n}\n\n.rg-tooltip-title {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: var(--dsw-font-xxs-12, 12px);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n color: var(--dsw-alias-label-primary);\n}\n\n.rg-tooltip-meta,\n.rg-tooltip-detail {\n font-size: var(--dsw-font-xxxs-11, 11px);\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n overflow-wrap: anywhere;\n}\n\n/* \u56FE\u8C31\u641C\u7D22\u6846\u4E0E\u7ED3\u679C\u4E0B\u62C9\u3002 */\n.rg-graph-search {\n position: relative;\n display: inline-flex;\n}\n\n.rg-search-input {\n min-height: var(--rg-control-h);\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n border-radius: var(--dsw-radius-sm, 8px);\n padding: 2px 9px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n min-width: 150px;\n}\n\n.rg-search-input:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.rg-search-input::placeholder {\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n}\n\n.rg-search-results {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n z-index: 10;\n margin: 0;\n padding: 4px;\n list-style: none;\n min-width: 200px;\n max-height: 220px;\n overflow: auto;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm, 8px);\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: var(--dsw-shadow-lv3, var(--dsw-shadow-lv1, none));\n}\n\n.rg-search-results button {\n appearance: none;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 5px 8px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n text-align: left;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-search-results button:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-search-noresult {\n padding: 5px 8px;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n/* \u8BE6\u60C5\u4FA7\u680F\u3002 */\n.rg-graph-aside {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-md, 12px);\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n align-self: start;\n}\n\n.rg-graph-aside-header {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.rg-graph-aside-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--dsw-font-s-14, 14px);\n}\n\n.rg-graph-aside-detail {\n margin: 0;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-graph-aside-actions {\n display: flex;\n gap: 6px;\n}\n\n/* \u90BB\u5C45\u5BFC\u822A\uFF08\u8BE6\u60C5\u4FA7\u680F\uFF09\uFF1A\u4E0A\u6E38/\u4E0B\u6E38\u5206\u7EC4\u5217\u8868\uFF0C\u70B9\u51FB\u5373\u8DF3\u8F6C\u5B9A\u4F4D\u3002 */\n.rg-graph-neighbors {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.rg-graph-neighbor-title {\n margin: 0;\n display: flex;\n align-items: center;\n font-size: var(--dsw-font-xxs-12, 12px);\n font-weight: var(--dsw-font-xxs-strong-12, 600);\n color: var(--dsw-alias-label-secondary);\n}\n\n.rg-graph-neighbor-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.rg-graph-neighbor-link {\n appearance: none;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 6px;\n font: inherit;\n font-size: var(--dsw-font-xxs-12, 12px);\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n text-align: left;\n border-radius: var(--dsw-radius-xs, 4px);\n}\n\n.rg-graph-neighbor-link:hover {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\n}\n\n.rg-graph-neighbor-link:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* \u52A8\u6548\u4E0E\u504F\u597D\uFF1A\u5C0A\u91CD\u300C\u51CF\u5C11\u52A8\u6001\u6548\u679C\u300D\uFF08\u524D\u5EAD\u5931\u8C03\u7528\u6237\u4F1A\u56E0\u8FC7\u6E21\u4E0D\u9002\uFF09\u3002 */\n@media (prefers-reduced-motion: reduce) {\n .rg-panel *,\n .rg-panel *::before,\n .rg-panel *::after {\n transition-duration: 0.01ms !important;\n animation-duration: 0.01ms !important;\n }\n}\n\n/* ---------- \u6062\u590D\u5224\u5B9A\u6761\uFF08v0.9.1\uFF09 ---------- */\n\n.rg-verdict {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin: 0;\n padding: 8px 12px;\n border-radius: var(--dsw-radius-sm, 8px);\n font-size: var(--dsw-font-xs-13, 13px);\n}\n/* act\uFF08\u5173\u95ED\u4E86\u63D2\u4EF6\uFF09\u4E0E watch\uFF08\u89C2\u5BDF\u8F6E\uFF09\u7528**\u8FB9\u6846+\u6807\u7B7E**\u533A\u5206\u800C\u4E0D\u662F\u53EA\u9760\u989C\u8272\uFF1A\n \u7070\u5EA6\u6253\u5370/\u8272\u5F31\u7528\u6237\u4E5F\u80FD\u5206\u8FA8\u3002 */\n.rg-verdict-act {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n}\n.rg-verdict-watch {\n border: 1px dashed var(--dsw-alias-border-l1);\n background: transparent;\n}\n.rg-verdict-tag {\n padding: 1px 8px;\n border-radius: var(--dsw-radius-sm, 8px);\n font-weight: var(--dsw-font-s-strong-14, 600);\n background: var(--dsw-alias-bg-layer-2);\n}\n.rg-verdict-meta {\n margin-left: auto;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---------- v0.10 \u6D1E\u5BDF\u5361\uFF1A\u9A6C\u5C14\u53EF\u592B / \u6570\u636E\u6D41 / \u7CFB\u7EDF\u8BCA\u65AD ---------- */\n\n/* \u5361\u7247\u9876\u90E8\u7684\u4E00\u884C\u6458\u8981\uFF08\u6837\u672C\u6570\u3001\u6536\u655B\u6027\u7B49\u767D\u76D2\u5143\u4FE1\u606F\uFF09\u3002 */\n.rg-card-note {\n margin: 0 0 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* \u9A6C\u5C14\u53EF\u592B\u884C\uFF1A\u9636\u6BB5\u540D | \u81EA\u73AF\u6982\u7387\u6761 | \u767E\u5206\u6BD4\u3002 */\n.rg-markov {\n list-style: none;\n margin: 0;\n padding: 0;\n display: grid;\n gap: 6px;\n}\n.rg-markov-row {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.rg-markov-stage {\n width: 84px;\n flex: none;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.rg-markov-row .rg-bar {\n flex: 1 1 auto;\n width: auto;\n}\n.rg-markov-value {\n width: 44px;\n flex: none;\n text-align: right;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n}\n.rg-markov-nodata {\n flex: 1 1 auto;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n.rg-markov-stationary {\n margin-top: 10px;\n}\n.rg-subhead {\n margin: 0 0 6px;\n font-size: 12px;\n font-weight: var(--dsw-font-s-strong-14, 600);\n color: var(--dsw-alias-label-secondary);\n}\n.rg-chip-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* \u6570\u636E\u6D41\u94FE\u8DEF\uFF1A\u5E8F\u53F7 | \u540D\u79F0+\u8BF4\u660E | \u8BA1\u6570\u3002 */\n.rg-dataflow {\n list-style: none;\n margin: 0;\n padding: 0;\n display: grid;\n gap: 8px;\n counter-reset: rg-flow;\n}\n.rg-dataflow-step {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n}\n.rg-dataflow-index {\n flex: none;\n width: 20px;\n height: 20px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dsw-radius-full, 999px);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n font-weight: var(--dsw-font-s-strong-14, 600);\n}\n\n/* \u6D41\u52A8\u8109\u51B2\uFF08v0.11\u300C\u53EF\u89C6\u5316\u6D41\u52A8\u300D\uFF09\uFF1A\u56DB\u73AF\u94FE\u8DEF\u7684\u5E8F\u53F7\u70B9\u6309\u94FE\u8DEF\u987A\u5E8F\u547C\u5438 \u2014\u2014\n \u6570\u636E\u4ECE journal \u6D41\u5411\u753B\u9762\u7684\u65B9\u5411\u611F\u7528\u5EF6\u8FDF\u9519\u76F8\u8868\u8FBE\uFF08\u6BCF\u73AF\u5EF6\u8FDF 0.3s\uFF09\u3002\n \u52A8\u753B\u53EA\u52A8 opacity\uFF08\u5408\u6210\u5C42\u5C5E\u6027\uFF0C\u4E0D\u89E6\u53D1\u91CD\u6392\uFF09\uFF1B\u7CFB\u7EDF\u300C\u51CF\u5C11\u52A8\u6001\u300D\u65F6\u505C\u7528\uFF0C\n \u9759\u6001\u5E8F\u53F7\u4ECD\u7136\u5B8C\u6574\u627F\u8F7D\u4FE1\u606F\uFF08\u52A8\u753B\u662F\u589E\u5F3A\uFF0C\u4E0D\u662F\u4FE1\u606F\u8F7D\u4F53\uFF09\u3002 */\n.rg-dataflow-step .rg-dataflow-index {\n animation: rg-flow-pulse 1.8s var(--rg-ease, ease) infinite;\n}\n.rg-dataflow-step:nth-child(2) .rg-dataflow-index {\n animation-delay: 0.3s;\n}\n.rg-dataflow-step:nth-child(3) .rg-dataflow-index {\n animation-delay: 0.6s;\n}\n.rg-dataflow-step:nth-child(4) .rg-dataflow-index {\n animation-delay: 0.9s;\n}\n@keyframes rg-flow-pulse {\n 0%,\n 100% {\n opacity: 1;\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-brand-primary) 0%, transparent);\n }\n 50% {\n opacity: 0.55;\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent);\n }\n}\n.rg-dataflow-main {\n min-width: 0;\n display: grid;\n gap: 2px;\n}\n.rg-dataflow-label {\n color: var(--dsw-alias-label-primary);\n font-weight: var(--dsw-font-s-strong-14, 600);\n font-size: 13px;\n}\n.rg-dataflow-detail {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.rg-dataflow-count {\n flex: none;\n margin-left: auto;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* \u7CFB\u7EDF\u8BCA\u65AD\u5361\uFF1A\u7D27\u51D1\u952E\u503C\u5BF9 + \u53CD\u9988\u6309\u94AE\u3002 */\n.rg-issues-button {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin-top: 10px;\n text-decoration: none;\n color: var(--dsw-alias-brand-primary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n.rg-issues-button:hover {\n border-color: var(--dsw-alias-brand-primary);\n}\n";
3063
3143
 
3064
3144
  // src/client/styles.ts
3065
3145
  var STYLE_ELEMENT_ID = "dsh-recency-guard-styles";
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ function readHostVersion() {
62
62
 
63
63
  // src/store.ts
64
64
  import { mkdir, readFile, readdir, stat, unlink } from "node:fs/promises";
65
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
65
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
66
66
  import { isAbsolute, join, resolve } from "node:path";
67
67
  import { writeFileAtomic, withFileLock } from "@deepseek-ai/dsh-atomic-write";
68
68
  var STATE_DIRNAME = ".recency-guard";
@@ -70,6 +70,55 @@ var JOURNAL_MAX_LINES = 1e3;
70
70
  function emptyState() {
71
71
  return { bootMarker: null, crashSeq: 0, autoDisabled: [], stabilized: false, restored: [] };
72
72
  }
73
+ var STALE_LOCK_MIN_AGE_MS = 5e3;
74
+ function processAlive(pid) {
75
+ try {
76
+ process.kill(pid, 0);
77
+ return true;
78
+ } catch (error) {
79
+ return error.code === "EPERM";
80
+ }
81
+ }
82
+ function cleanStaleLocks(dir, now = Date.now()) {
83
+ const report = { removed: [], kept: [] };
84
+ let entries;
85
+ try {
86
+ entries = readdirSync(dir);
87
+ } catch {
88
+ return report;
89
+ }
90
+ for (const name2 of entries) {
91
+ if (!name2.endsWith(".lock")) continue;
92
+ const path = join(dir, name2);
93
+ try {
94
+ if (now - statSync(path).mtimeMs < STALE_LOCK_MIN_AGE_MS) {
95
+ report.kept.push(name2);
96
+ continue;
97
+ }
98
+ let text = "";
99
+ try {
100
+ text = readFileSync(path, "utf8").trim();
101
+ } catch {
102
+ report.kept.push(name2);
103
+ continue;
104
+ }
105
+ const pid = Number(text);
106
+ const incomplete = text === "" || !Number.isInteger(pid) || pid <= 0;
107
+ if (!incomplete && processAlive(pid)) {
108
+ report.kept.push(name2);
109
+ continue;
110
+ }
111
+ rmSync(path, { force: true });
112
+ report.removed.push(name2);
113
+ } catch {
114
+ report.kept.push(name2);
115
+ }
116
+ }
117
+ return report;
118
+ }
119
+ function isLockTimeout(error) {
120
+ return error instanceof Error && error.message.includes("timed out waiting for the writer lock");
121
+ }
73
122
  var Store = class {
74
123
  /** profile 根目录下的守护目录(分区索引与归档都在这里)。 */
75
124
  rootDir;
@@ -105,6 +154,10 @@ var Store = class {
105
154
  }
106
155
  async init() {
107
156
  await mkdir(this.backupsDir, { recursive: true });
157
+ try {
158
+ cleanStaleLocks(this.dir);
159
+ } catch {
160
+ }
108
161
  }
109
162
  /** 本分区的绝对路径(供面板与日志引用)。 */
110
163
  get partitionPath() {
@@ -246,12 +299,23 @@ var Store = class {
246
299
  }
247
300
  /** Serialized read-modify-write across processes. */
248
301
  async updateState(mutate) {
249
- return withFileLock(this.statePath, async () => {
250
- const cur = await this.readState();
251
- const mutated = await mutate(cur);
252
- await this.writeState(mutated);
253
- return mutated;
254
- }, { waitMs: 3e3 });
302
+ const run = () => withFileLock(
303
+ this.statePath,
304
+ async () => {
305
+ const cur = await this.readState();
306
+ const mutated = await mutate(cur);
307
+ await this.writeState(mutated);
308
+ return mutated;
309
+ },
310
+ { waitMs: 3e3 }
311
+ );
312
+ try {
313
+ return await run();
314
+ } catch (error) {
315
+ if (!isLockTimeout(error)) throw error;
316
+ cleanStaleLocks(this.dir);
317
+ return run();
318
+ }
255
319
  }
256
320
  /**
257
321
  * Re-arm the crash marker synchronously.
@@ -2391,16 +2455,19 @@ function assessConfidence(history, currentSuspects, options = {}) {
2391
2455
  let streak = 1;
2392
2456
  for (let i = history.length - 1; i >= 0; i -= 1) {
2393
2457
  const entry = history[i];
2458
+ if (entry.suspects.length === 0) continue;
2394
2459
  if ((entry.suspects[0] ?? "") !== head) break;
2395
2460
  streak += 1;
2396
2461
  }
2397
- const act = streak >= confirmRounds;
2462
+ const hardEscalateAfter = Math.max(1, options.hardEscalateAfter ?? 3);
2463
+ const escalated = options.crashSeq !== void 0 && options.crashSeq >= hardEscalateAfter;
2464
+ const act = streak >= confirmRounds || escalated;
2398
2465
  return {
2399
2466
  act,
2400
2467
  evidence: "probabilistic",
2401
2468
  rounds: streak,
2402
2469
  required: confirmRounds,
2403
- label: act ? `\u5934\u53F7\u5ACC\u7591 ${head} \u8FDE\u7EED ${streak} \u8F6E\u590D\u73B0\uFF08\u2265 ${confirmRounds}\uFF09\uFF1A\u8FBE\u5230\u884C\u52A8\u9608\u503C` : `\u5934\u53F7\u5ACC\u7591 ${head} \u590D\u73B0 ${streak}/${confirmRounds} \u8F6E\uFF1A\u89C2\u5BDF\u4E2D\uFF0C\u672C\u8F6E\u53EA\u8BB0\u5F55\u4E0D\u5173\u95ED`
2470
+ label: act ? streak >= confirmRounds ? `\u5934\u53F7\u5ACC\u7591 ${head} \u8FDE\u7EED ${streak} \u8F6E\u590D\u73B0\uFF08\u2265 ${confirmRounds}\uFF09\uFF1A\u8FBE\u5230\u884C\u52A8\u9608\u503C` : `\u8FDE\u7EED ${options.crashSeq} \u6B21\u5D29\u6E83\u4ECD\u672A\u6062\u590D\uFF1A\u515C\u5E95\u5347\u7EA7\u4E3A\u884C\u52A8\uFF08\u5B89\u5168\u7F51\uFF09` : `\u5934\u53F7\u5ACC\u7591 ${head} \u590D\u73B0 ${streak}/${confirmRounds} \u8F6E\uFF1A\u89C2\u5BDF\u4E2D\uFF0C\u672C\u8F6E\u53EA\u8BB0\u5F55\u4E0D\u5173\u95ED`
2404
2471
  };
2405
2472
  }
2406
2473
  function environmentChanged(lastEntry, currentRuntime, currentSuspects) {
@@ -2557,6 +2624,7 @@ var ROUTE_PATHS = {
2557
2624
  archives: "/recency-guard/api/archives",
2558
2625
  archive: "/recency-guard/api/action/archive",
2559
2626
  reenable: "/recency-guard/api/action/reenable",
2627
+ restore: "/recency-guard/api/action/restore",
2560
2628
  recover: "/recency-guard/api/action/recover"
2561
2629
  };
2562
2630
  var ROUTE_SURFACE = [
@@ -2567,6 +2635,7 @@ var ROUTE_SURFACE = [
2567
2635
  { capability: "guard.govern.archive", name: "archives", exposedName: "recency-guard.archives", path: ROUTE_PATHS.archives, method: "GET" },
2568
2636
  { capability: "guard.govern.reset", name: "archive", exposedName: "recency-guard.reset", path: ROUTE_PATHS.archive, method: "POST" },
2569
2637
  { capability: "guard.recover.reenable", name: "reenable", exposedName: "recency-guard.reenable", path: ROUTE_PATHS.reenable, method: "POST" },
2638
+ { capability: "guard.recover.restore", name: "restore", exposedName: "recency-guard.restore", path: ROUTE_PATHS.restore, method: "POST" },
2570
2639
  { capability: "guard.recover.manual", name: "recover", exposedName: "recency-guard.recover", path: ROUTE_PATHS.recover, method: "POST" }
2571
2640
  ];
2572
2641
  function sendJson(res, data, status = 200) {
@@ -2595,21 +2664,38 @@ async function readBody(req) {
2595
2664
  return null;
2596
2665
  }
2597
2666
  }
2667
+ function partitionOf(req) {
2668
+ const url = req.url ?? "";
2669
+ const qIndex = url.indexOf("?");
2670
+ if (qIndex === -1) return void 0;
2671
+ const params = new URLSearchParams(url.slice(qIndex + 1));
2672
+ const value = params.get("partition");
2673
+ return value === null || value === "" ? void 0 : value;
2674
+ }
2675
+ function rejectWrongMethod(req, res) {
2676
+ if ((req.method ?? "GET").toUpperCase() === "POST") return false;
2677
+ res.writeHead(405, { allow: "POST", "content-type": "application/json; charset=utf-8" });
2678
+ res.end(JSON.stringify({ ok: false, reason: `\u52A8\u4F5C\u8DEF\u7531\u53EA\u63A5\u53D7 POST\uFF08\u6536\u5230 ${req.method ?? "GET"}\uFF09` }));
2679
+ return true;
2680
+ }
2598
2681
  function handlerTable(ctx) {
2599
- const read = (load) => async (_req, res) => {
2682
+ const read = (load) => async (req, res) => {
2600
2683
  try {
2601
- sendJson(res, await load());
2684
+ sendJson(res, await load(partitionOf(req)));
2602
2685
  } catch (error) {
2603
2686
  sendJson(res, { available: false, reason: describe(error) }, 500);
2604
2687
  }
2605
2688
  };
2606
2689
  return {
2607
- snapshot: read(() => ctx.snapshot()),
2690
+ snapshot: read(
2691
+ (partition) => partition === void 0 || partition === ctx.projectKey ? ctx.snapshot() : ctx.snapshotPartition(partition)
2692
+ ),
2608
2693
  graph: read(() => ctx.graph()),
2609
2694
  isolation: read(() => ctx.isolation()),
2610
2695
  lifecycle: read(() => ctx.lifecycle()),
2611
2696
  archives: read(() => ctx.archives()),
2612
2697
  archive: async (req, res) => {
2698
+ if (rejectWrongMethod(req, res)) return;
2613
2699
  try {
2614
2700
  const body = await readBody(req);
2615
2701
  const reason = typeof body?.reason === "string" && body.reason.trim() !== "" ? body.reason : "\u624B\u52A8\u5F52\u6863\u91CD\u7F6E";
@@ -2619,6 +2705,7 @@ function handlerTable(ctx) {
2619
2705
  }
2620
2706
  },
2621
2707
  reenable: async (req, res) => {
2708
+ if (rejectWrongMethod(req, res)) return;
2622
2709
  try {
2623
2710
  const body = await readBody(req);
2624
2711
  const bundle = typeof body?.bundle === "string" ? body.bundle : "";
@@ -2631,7 +2718,16 @@ function handlerTable(ctx) {
2631
2718
  sendJson(res, { ok: false, reason: describe(error) }, 500);
2632
2719
  }
2633
2720
  },
2634
- recover: async (_req, res) => {
2721
+ restore: async (req, res) => {
2722
+ if (rejectWrongMethod(req, res)) return;
2723
+ try {
2724
+ sendJson(res, await ctx.restore());
2725
+ } catch (error) {
2726
+ sendJson(res, { ok: false, reason: describe(error) }, 500);
2727
+ }
2728
+ },
2729
+ recover: async (req, res) => {
2730
+ if (rejectWrongMethod(req, res)) return;
2635
2731
  try {
2636
2732
  sendJson(res, await ctx.recover());
2637
2733
  } catch (error) {
@@ -2833,6 +2929,9 @@ var CAPABILITY_SPECS = [
2833
2929
  stage: "recovered",
2834
2930
  exposed: true,
2835
2931
  exposedAs: "recency_guard",
2932
+ // v0.11:restore 同时作为**动作路由**暴露(/action/restore),面板的
2933
+ // 「恢复非嫌疑插件」按钮走这条路由 —— 名称必须声明,契约测试双向核对。
2934
+ exposedAlsoAs: ["recency-guard.restore"],
2836
2935
  exposedActions: ["restore"],
2837
2936
  hostServices: ["tools", "pluginManager"]
2838
2937
  },
@@ -3505,8 +3604,69 @@ function partitionHealth(index) {
3505
3604
  }));
3506
3605
  }
3507
3606
 
3607
+ // src/partition-snapshot.ts
3608
+ async function buildPartitionSnapshot(input, key) {
3609
+ const index = readPartitionIndex(input.profileDir);
3610
+ const target = index.partitions.find((p) => p.key === key);
3611
+ if (!target) {
3612
+ return {
3613
+ available: false,
3614
+ reason: `\u672A\u77E5\u5206\u533A ${key}\uFF08\u672C profile \u53EA\u6709 ${index.partitions.length} \u4E2A\u5206\u533A\uFF09`
3615
+ };
3616
+ }
3617
+ const viewer = new Store(input.profileDir, key);
3618
+ const [state, journal] = await Promise.all([viewer.readState(), viewer.readJournal(40)]);
3619
+ const events = journal.slice(-40).map((e) => ({ t: e.t, kind: e.kind, bundle: e.bundle, detail: e.detail }));
3620
+ return {
3621
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3622
+ profile: input.profileName,
3623
+ projectKey: key,
3624
+ // 运行时字段如实归属:阶段与评分是**当前进程**的状态机事实,
3625
+ // 别的分区没有自己的守护进程在跑,这里不能替它编一个阶段。
3626
+ stage: "idle",
3627
+ stageLabel: "\u8DE8\u5206\u533A\u53EA\u8BFB\u89C6\u56FE\uFF08\u9636\u6BB5\u4E3A\u5F53\u524D\u5206\u533A\u8BED\u4E49\uFF0C\u975E\u5BF9\u65B9\u5B9E\u65F6\u72B6\u6001\uFF09",
3628
+ transitions: [],
3629
+ stabilityScore: null,
3630
+ metrics: null,
3631
+ risks: [],
3632
+ capability: null,
3633
+ graphConnectivity: null,
3634
+ restored: state.restored,
3635
+ autoDisabled: state.autoDisabled.map((d) => ({
3636
+ name: d.name,
3637
+ reason: d.reason,
3638
+ crashSeq: d.crashSeq,
3639
+ disabledAt: d.disabledAt
3640
+ })),
3641
+ lastRecoveryVerdict: state.lastRecoveryVerdict ?? null,
3642
+ partitions: index.partitions.map((p) => ({ ...p })),
3643
+ eventFlow: [],
3644
+ recentEvents: events,
3645
+ markov: null,
3646
+ backupPolicy: {
3647
+ versionedBackups: input.config.versionedBackups,
3648
+ retain: input.config.backupRetain,
3649
+ customRoot: false,
3650
+ root: viewer.backupsRoot,
3651
+ configEditorAvailable: input.config.configEditorAvailable
3652
+ },
3653
+ diagnostics: {
3654
+ pluginVersion: input.selfPackage.version,
3655
+ hostVersion: readHostVersion(),
3656
+ platform: process.platform,
3657
+ nodeVersion: process.version,
3658
+ dataPlane: true,
3659
+ snapshotMs: null,
3660
+ repositoryUrl: input.selfPackage.repositoryUrl,
3661
+ issuesUrl: input.selfPackage.issuesUrl
3662
+ },
3663
+ readonly: true,
3664
+ workspace: target.workspace
3665
+ };
3666
+ }
3667
+
3508
3668
  // src/archive.ts
3509
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync7, renameSync as renameSync4, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
3669
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync7, renameSync as renameSync4, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
3510
3670
  import { join as join7 } from "node:path";
3511
3671
  function classify(name2) {
3512
3672
  if (name2 === "journal.jsonl") return "journal";
@@ -3539,7 +3699,7 @@ function archiveAndReset(partitionDir2, workspace, reason, now = /* @__PURE__ */
3539
3699
  }
3540
3700
  if (existsSync7(stateDir)) {
3541
3701
  mkdirSync6(archiveDir, { recursive: true });
3542
- for (const name2 of readdirSync(stateDir)) {
3702
+ for (const name2 of readdirSync2(stateDir)) {
3543
3703
  if (name2 === "archive") continue;
3544
3704
  const source = join7(stateDir, name2);
3545
3705
  const target = join7(archiveDir, name2);
@@ -3584,7 +3744,7 @@ function directoryBytes(dir) {
3584
3744
  let total = 0;
3585
3745
  let names;
3586
3746
  try {
3587
- names = readdirSync(dir);
3747
+ names = readdirSync2(dir);
3588
3748
  } catch {
3589
3749
  return 0;
3590
3750
  }
@@ -3603,7 +3763,7 @@ function listArchives(partitionDir2) {
3603
3763
  const archiveRoot = join7(partitionDir2, "archive");
3604
3764
  if (!existsSync7(archiveRoot)) return [];
3605
3765
  const out = [];
3606
- for (const name2 of readdirSync(archiveRoot)) {
3766
+ for (const name2 of readdirSync2(archiveRoot)) {
3607
3767
  const dir = join7(archiveRoot, name2);
3608
3768
  try {
3609
3769
  const manifest = JSON.parse(readFileSync7(join7(dir, "manifest.json"), "utf8"));
@@ -4184,11 +4344,14 @@ function apply(ctx, config) {
4184
4344
  profileDir: profile.dir,
4185
4345
  workspace: partition.workspace,
4186
4346
  profileName: profile.name,
4347
+ projectKey: partition.key,
4187
4348
  snapshot: () => guard.panelSnapshot(),
4349
+ snapshotPartition: (key) => guard.partitionSnapshot(key),
4188
4350
  graph: () => guard.graphData(),
4189
4351
  archive: (reason) => guard.archiveReset(reason),
4190
4352
  archives: () => guard.archives(),
4191
4353
  reenable: (bundle) => guard.reenable(bundle),
4354
+ restore: () => guard.restore(),
4192
4355
  recover: () => guard.recoverNow(),
4193
4356
  isolation: async () => guard.isolationView(),
4194
4357
  lifecycle: async () => guard.lifecycleView(),
@@ -4481,6 +4644,7 @@ var Guard = class {
4481
4644
  for (const e of events) {
4482
4645
  if (e.kind !== "crash-detected") continue;
4483
4646
  const suspects = Array.isArray(e.suspects) ? e.suspects.filter((s) => typeof s === "string") : [];
4647
+ if (suspects.length === 0) continue;
4484
4648
  out.push({
4485
4649
  at: e.t,
4486
4650
  exitCode: typeof e.exitCode === "number" ? e.exitCode : -1,
@@ -4656,7 +4820,10 @@ var Guard = class {
4656
4820
  const targets = pickSuspects(ranked, n);
4657
4821
  const history = this.readCrashHistory();
4658
4822
  const envChanged = environmentChanged(history[history.length - 1], safeVersion(), ranked);
4659
- const verdict = assessConfidence(envChanged ? [] : history, ranked, { confirmRounds: this.config.confirmRounds });
4823
+ const verdict = assessConfidence(envChanged ? [] : history, ranked, {
4824
+ confirmRounds: this.config.confirmRounds,
4825
+ crashSeq: seq
4826
+ });
4660
4827
  this.store.appendSync({
4661
4828
  kind: "note",
4662
4829
  detail: `\u6062\u590D\u5224\u5B9A\uFF1A${verdict.label}`
@@ -4750,7 +4917,10 @@ ${error?.stack ?? ""}
4750
4917
  this.endStep("boot", true, "\u542F\u52A8\u6D41\u7A0B\u5B8C\u6210");
4751
4918
  } catch (error) {
4752
4919
  this.endStep("boot", false, describeError(error));
4753
- throw error;
4920
+ try {
4921
+ this.ctx.logger?.warn?.(`[recency-guard] \u5B88\u62A4\u542F\u52A8\u964D\u7EA7\uFF08\u5BBF\u4E3B\u7EE7\u7EED\u542F\u52A8\uFF09\uFF1A${describeError(error)}`);
4922
+ } catch {
4923
+ }
4754
4924
  }
4755
4925
  }
4756
4926
  /** Boot 的实现体(外层负责步骤计时与失败原因留痕)。 */
@@ -5080,6 +5250,31 @@ ${error?.stack ?? ""}
5080
5250
  async graphData() {
5081
5251
  return buildGraphDataFor(buildCapabilityGraph());
5082
5252
  }
5253
+ /**
5254
+ * 跨分区只读快照(v0.11 P0:数据面真正支持 `?partition=` 查询)。
5255
+ *
5256
+ * 投影实现在 `partition-snapshot.ts`(独立模块:磁盘投影有自己的一条
5257
+ * 隔离边界要讲清楚,且 index.ts 复杂度预算 100 不该被它撑破)。
5258
+ */
5259
+ async partitionSnapshot(key) {
5260
+ return buildPartitionSnapshot(
5261
+ {
5262
+ profileDir: this.profileDir,
5263
+ profileName: this.profileName,
5264
+ config: {
5265
+ versionedBackups: this.config.versionedBackups,
5266
+ backupRetain: this.config.backupRetain,
5267
+ configEditorAvailable: this.configEditorAvailable
5268
+ },
5269
+ selfPackage: {
5270
+ version: this.selfPackage.version,
5271
+ repositoryUrl: this.selfPackage.repositoryUrl,
5272
+ issuesUrl: this.selfPackage.issuesUrl
5273
+ }
5274
+ },
5275
+ key
5276
+ );
5277
+ }
5083
5278
  /**
5084
5279
  * 实际注册出去的对外名称。
5085
5280
  *
package/locale/en.json CHANGED
@@ -17,7 +17,9 @@
17
17
  "emptyHint": "The guard has not recorded any startup facts yet.",
18
18
  "snapshotAt": "Snapshot at {time}",
19
19
  "pollingOn": "polling every {seconds}s",
20
- "pollingPaused": "polling paused (the view is frozen at the pause moment)"
20
+ "pollingPaused": "polling paused (the view is frozen at the pause moment)",
21
+ "restore": "Restore non-suspects",
22
+ "stale": "Data is {minutes} min old (polling paused)"
21
23
  },
22
24
  "sections": {
23
25
  "backupPolicy": "Backup policy",
@@ -228,11 +230,13 @@
228
230
  "metric": "Metric",
229
231
  "bundle": "Bundle",
230
232
  "gap": "Gap"
231
- }
233
+ },
234
+ "matchIndex": "Match {index}/{total}"
232
235
  },
233
236
  "events": {
234
237
  "title": "Event stream",
235
- "count": "last {count}"
238
+ "count": "last {count}",
239
+ "more": "Show {count} more"
236
240
  },
237
241
  "partitions": {
238
242
  "title": "Project partitions",
package/locale/zh.json CHANGED
@@ -17,7 +17,9 @@
17
17
  "emptyHint": "守护插件尚未记录任何启动事实。",
18
18
  "snapshotAt": "快照时刻 {time}",
19
19
  "pollingOn": "每 {seconds} 秒轮询",
20
- "pollingPaused": "轮询已暂停(画面停在暂停时刻)"
20
+ "pollingPaused": "轮询已暂停(画面停在暂停时刻)",
21
+ "restore": "恢复非嫌疑插件",
22
+ "stale": "数据已停留 {minutes} 分钟(轮询已暂停)"
21
23
  },
22
24
  "sections": {
23
25
  "backupPolicy": "备份策略",
@@ -228,11 +230,13 @@
228
230
  "metric": "指标",
229
231
  "bundle": "插件",
230
232
  "gap": "缺口"
231
- }
233
+ },
234
+ "matchIndex": "第 {index}/{total} 个"
232
235
  },
233
236
  "events": {
234
237
  "title": "事件流",
235
- "count": "最近 {count} 条"
238
+ "count": "最近 {count} 条",
239
+ "more": "展开其余 {count} 条"
236
240
  },
237
241
  "partitions": {
238
242
  "title": "项目分区",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recency-guard",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Crash-recovery guard for DeepSeek Harness: records third-party/experimental plugin install & update recency, backs up their config, and after a crash auto-disables the most-recently-updated plugin(s) so the profile boots again, then restores stable config.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,10 +43,10 @@
43
43
  "extract-runtime": "node scripts/extract-runtime.mjs",
44
44
  "repair-patch": "tsx scripts/repair-patch.mjs",
45
45
  "test": "tsx tests/run.ts",
46
- "test:unit": "tsx tests/unit.test.ts",
47
- "test:tool": "tsx tests/tool.test.ts",
46
+ "test:unit": "tsx tests/unit/core.test.ts",
47
+ "test:routes": "tsx tests/host/routes.test.ts",
48
48
  "test:coverage": "tsx tests/run.ts --coverage",
49
- "test:e2e": "tsx tests/tool.test.ts"
49
+ "test:e2e": "tsx tests/host/tool.test.ts"
50
50
  },
51
51
  "dsh": {
52
52
  "manifestVersion": 1,
@@ -67,10 +67,10 @@
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@deepseek-ai/cordis": "~4.0.5",
70
- "@deepseek-ai/dsh-app-boot": ">=0.2.1-rc.5 <0.3.0-0",
71
- "@deepseek-ai/dsh-config-editor": ">=0.2.1-rc.5 <0.3.0-0",
72
- "@deepseek-ai/dsh-plugin-manager": ">=0.2.1-rc.5 <0.3.0-0",
73
- "@deepseek-ai/dsh-tools": ">=0.2.1-rc.5 <0.3.0-0",
70
+ "@deepseek-ai/dsh-app-boot": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
71
+ "@deepseek-ai/dsh-config-editor": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
72
+ "@deepseek-ai/dsh-plugin-manager": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
73
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
74
74
  "@deepseek-ai/schemastery": ">=3.18.0 <4.0.0-0"
75
75
  },
76
76
  "peerDependenciesMeta": {
@@ -122,6 +122,6 @@
122
122
  "typescript": "^5.9.0"
123
123
  },
124
124
  "engines": {
125
- "dsh": ">=0.2.1-rc.5 <0.3.0-0"
125
+ "dsh": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0"
126
126
  }
127
127
  }