dsh-recency-guard 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # dsh-recency-guard
2
2
 
3
- **v0.16.0** · [GitHub](https://github.com/xuanyuanchumo/dsh-recency-guard) · [问题反馈](https://github.com/xuanyuanchumo/dsh-recency-guard/issues) · MIT
3
+ **v0.17.0** · [GitHub](https://github.com/xuanyuanchumo/dsh-recency-guard) · [问题反馈](https://github.com/xuanyuanchumo/dsh-recency-guard/issues) · MIT
4
4
 
5
5
  > DeepSeek Harness 的**插件崩溃恢复守护**:记录第三方 / 实验性插件的安装与更新时序并备份配置;
6
6
  > 崩溃后的**下一次启动之前**,把最近更新的嫌疑插件挡在门外;稳定启动后,再把非首要嫌疑放回来。
7
7
  >
8
- > 版本基线(2026-11-11,北京时间):**0.16.0**。版本四锚点(`package.json` / 本 README /
8
+ > 版本基线(2026-11-11,北京时间):**0.17.0**。版本四锚点(`package.json` / 本 README /
9
9
  > `docs/agents/` / [`dsh-plugin.json`](dsh-plugin.json))一致。
10
- > 测试分四层,当前 **68/68 全绿**(Core 29 / Host 10 / Client 20 / Contract 7)。
10
+ > 测试分四层,当前 **84/84 全绿**(Core 42 / Host 10 / Client 24 / Contract 8)。
11
11
 
12
12
  ---
13
13
 
@@ -51,10 +51,10 @@ dsh plugin --profile <profile名> add dsh-recency-guard
51
51
  锁定版本:
52
52
 
53
53
  ```bash
54
- dsh plugin --profile <profile名> add dsh-recency-guard@0.16.0
54
+ dsh plugin --profile <profile名> add dsh-recency-guard@0.17.0
55
55
  ```
56
56
 
57
- > 版本对应:本 README 描述本仓库 **0.16.0** 源码。npm 上实际可安装的版本以
57
+ > 版本对应:本 README 描述本仓库 **0.17.0** 源码。npm 上实际可安装的版本以
58
58
  > `npm view dsh-recency-guard version` 为准;若 `latest` 落后于本文版本,请按下面的源码方式安装,
59
59
  > 或等待对应版本发布。发布顺序见 [`docs/agents/conventions.md`](docs/agents/conventions.md) §6。
60
60
 
@@ -137,6 +137,8 @@ dsh plugin --profile <profile名> add link:/绝对路径/dsh-recency-guard
137
137
  | 恢复与配置备份 | `restore` / `backups` | [`recovery-actions.ts`](src/host/guard/recovery-actions.ts)、[`recovery-policy.ts`](src/host/guard/recovery-policy.ts) | [`core.test.ts`](tests/unit/core.test.ts)、[`versioned-backup.test.ts`](tests/unit/versioned-backup.test.ts) |
138
138
  | 生命周期状态机 | 生命周期卡 + 迁移史 | [`lifecycle.ts`](src/host/core/lifecycle.ts) | [`lifecycle-steps.test.ts`](tests/unit/lifecycle-steps.test.ts) |
139
139
  | 量化指标与数学模型 | 指标卡 / 控制带 / 转移矩阵 | [`observability.ts`](src/host/observe/observability.ts)、[`markov.ts`](src/host/observe/markov.ts)、[`control-band.ts`](src/host/observe/control-band.ts) | [`observability-math.test.ts`](tests/unit/observability-math.test.ts)、[`hazard-threshold.test.ts`](tests/unit/hazard-threshold.test.ts) |
140
+ | **预防性预警(只预警不行动)** | 面板/`status` 的启动频率控制带与配置变更 CUSUM;不可判定显示原因 | [`preventive.ts`](src/host/observe/preventive.ts)、[`preventive-view.ts`](src/host/surface/preventive-view.ts) | [`observability-math.test.ts`](tests/unit/observability-math.test.ts)、[`preventive-card.test.ts`](tests/client/preventive-card.test.ts) |
141
+ | **自适应刷新** | 稳定时 5s→10s→15s 退避,变化立即回 5s;暂停即真停 | [`poll-adaptive.ts`](src/client/panel/poll-adaptive.ts)、[`GuardPanel.tsx`](src/client/panel/GuardPanel.tsx) | [`poll-adaptive.test.ts`](tests/client/poll-adaptive.test.ts) |
140
142
  | 管理面板 | 中央面板 + 侧栏图标 | [`GuardPanel.tsx`](src/client/panel/GuardPanel.tsx)、[`snapshot-view.ts`](src/client/panel/snapshot-view.ts)、[`PanelShell.tsx`](src/client/panel/PanelShell.tsx) | [`panel-v014.test.ts`](tests/client/panel-v014.test.ts)、[`panel-scroll.test.ts`](tests/client/panel-scroll.test.ts)、[`panel-null-snapshot.test.ts`](tests/client/panel-null-snapshot.test.ts) |
141
143
  | 可交互能力图谱 | 节点 / 边 / 缺口 / 小地图 | [`GuardGraph.tsx`](src/client/graph/GuardGraph.tsx)、[`graph-interaction.ts`](src/client/graph/graph-interaction.ts) | [`graph-model.test.ts`](tests/client/graph-model.test.ts)、[`graph-focus.test.ts`](tests/client/graph-focus.test.ts)、[`graph-keyboard.test.ts`](tests/client/graph-keyboard.test.ts) |
142
144
  | 项目隔离 | 隔离卡 + `?partition=<key>` | [`isolation.ts`](src/host/core/isolation.ts) | [`isolation-journey.test.ts`](tests/host/isolation-journey.test.ts)、[`isolation-parallel.test.ts`](tests/host/isolation-parallel.test.ts) |
@@ -152,9 +154,11 @@ dsh plugin --profile <profile名> add link:/绝对路径/dsh-recency-guard
152
154
 
153
155
  ### 管理面板
154
156
 
155
- - **数据面**:`GET /recency-guard/api/snapshot`,5 秒轮询一次;暂停按钮按下后**不再发请求**
157
+ - **数据面**:`GET /recency-guard/api/snapshot`。**自适应轮询**:内容稳定时 5s→10s→15s
158
+ 退避,内容变化/用户操作立即回 5s,绝对边界 [3s, 30s];暂停按钮按下后**不再发请求**
156
159
  (画面停在暂停时刻,而不是继续跳)。
157
- - **卡片**:稳定性 / 阶段 / 崩溃率 / 崩溃压力 / CUSUM 变点、崩溃决策与因子分解、
160
+ - **卡片**:稳定性 / 阶段 / 崩溃率 / 崩溃压力 / CUSUM 变点、**预防性预警**(启动频率控制带、
161
+ 配置变更 CUSUM;**只预警不行动**,不可判定显示原因)、崩溃决策与因子分解、
158
162
  **声明式冲突**(每条带 `文件:行号` 与声明原文)、**未解决冲突**(上次隔离后仍治不了的部分,
159
163
  与「现在有冲突」分开显示)、量化指标(不可判定显示原因)、能力清单与缺口、项目隔离、归档、事件流。
160
164
  - **可空契约**:宿主在跨分区只读视图会发 `null` / 整段省略的字段,由
@@ -277,6 +281,15 @@ node bin/rescue.mjs status --profile-dir "%USERPROFILE%\.dsh\profiles\desktop"
277
281
  - **降级可查**:启动编排的分区索引提交 / 变更记录 / 官方实验观测失败,都会落一条
278
282
  journal `note` 并带 `degraded: true` —— 不只写 logger,重启后仍可查。
279
283
 
284
+ **研究边界(避免误读)**
285
+
286
+ - **预防性预警只预警、不行动**:启动频率 EWMA 与配置变更 CUSUM 只进观测面
287
+ (面板/`status`),**不被任何关闭决策读取**;样本不足 ⇒ `defined:false` + 原因,不填 0。
288
+ `loadTime` 是「启动频率(次/分钟)」的**代理**,不是真实加载耗时 —— 面板会原样显示代理说明。
289
+ - **共现信号只判定、不加权**:`mayWeightByCooccurrence` 只在满足研究
290
+ [09 §4.3](docs/research/09-整合研究-四方向与既有研究的合流.md) 的条件时给出「可加权」判定,
291
+ 默认决策权重仍为 0;面板显示判定与理由,不把共现当成已生效权重。
292
+
280
293
  **明确不拦 / 不修的**
281
294
 
282
295
  - **entry 级 `apply` 抛错**:宿主只 warning 并自隔离该 entry,进程不崩 —— 不属于本插件的拦截面。
@@ -377,11 +390,11 @@ npm install
377
390
  npm run build # esbuild 双产物:dist/index.js(宿主)+ dist/client.js(客户端)
378
391
  npx tsc --noEmit # 类型
379
392
  npm run lint # 类型 + 未使用符号
380
- npm test # 分层运行器:Core 29 / Host 10 / Client 20 / Contract 7 = 68
393
+ npm test # 分层运行器:Core 42 / Host 10 / Client 24 / Contract 8 = 84
381
394
  npm run extract-runtime # Host 层测试前置(runtime/ 缺失时先解出)
382
395
  ```
383
396
 
384
- - 当前基线:`npm test` **68/68 通过**、`npx tsc --noEmit` 0 错误(2026-11-11 实测)。
397
+ - 当前基线:`npm test` **84/84 通过**、`npx tsc --noEmit` 0 错误(2026-11-11 实测)。
385
398
  - 验证顺序固定:**build → typecheck → lint → test**,全部零错误才算完成。
386
399
  - 文档体系与写码纪律见 [`docs/agents/`](docs/agents/README.md);
387
400
  发布 / 升级 / 热重载见 [`docs/agents/update-guide.md`](docs/agents/update-guide.md)。
@@ -392,6 +405,7 @@ npm run extract-runtime # Host 层测试前置(runtime/ 缺失时先解出)
392
405
 
393
406
  | 版本 | 一句话 |
394
407
  | --- | --- |
408
+ | **0.17.0**(2026-11-11) | 技术栈清理与研究闭环:预防性预警(RQ3)接入生产且**只预警不行动**、面板自适应刷新、状态文件 schema 版本(未知版本保留现场)、变异 72 突变体 / 0 未归因 + 无直接测试 14→2 + rescue/宿主宽度跨实现对拍;技术债状态见 [`docs/agents/tech-debt.md`](docs/agents/tech-debt.md) |
395
409
  | **0.16.0**(2026-11-11) | 确定性冲突证据(4 类 finding + 出处)、贪心最小隔离与依赖联动、可写两阶段 rescue、常态路径锁卫生、PanelSnapshotView 可空契约;新增社区 manifest [`dsh-plugin.json`](dsh-plugin.json),README 面向用户重构,版本四锚点统一 |
396
410
  | 0.15.0 | 架构重构:`host/` + `client/` + `shared/` 分层,client→host 越界与值依赖环写成契约测试 |
397
411
  | 0.14.0 | 误伤反馈闭环、贝叶斯风险并行读数、预防性健康指标(EWMA / CUSUM)、自动化变异测试 |
package/bin/rescue.mjs CHANGED
@@ -45,6 +45,7 @@
45
45
  import { createHash, randomUUID } from 'node:crypto'
46
46
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
47
47
  import { join, resolve } from 'node:path'
48
+ import { pathToFileURL } from 'node:url'
48
49
 
49
50
  const GUARD_DIR = '.recency-guard'
50
51
 
@@ -173,7 +174,36 @@ function fmtTime(iso) {
173
174
  * 的时刻)。这里只实现 status/history 用到的列对齐所需的宽度判定,
174
175
  * 完整的折行/中和原语仍在 src/tool-text.ts(工具输出走那条路)。
175
176
  */
176
- function codePointWidth(cp) {
177
+ /**
178
+ * 零宽码点:它们**不占列**,但也不能从文本里删掉。
179
+ *
180
+ * 为什么 rescue 也要判:它有自己的宽度实现(零依赖纪律,崩溃后 node_modules
181
+ * 可能损坏),但有独立实现就必须有**跨实现对拍**(tests/contract/width-parity)。
182
+ * 此前这一份没跟上 tool-text 的修复 ⇒ 组合字符被算 2 列、零宽空格算 1 列,
183
+ * 状态/历史两块表在中英混排时会整体右移。
184
+ */
185
+ function isZeroWidth(codePoint) {
186
+ return (
187
+ (codePoint >= 0x0300 && codePoint <= 0x036f) ||
188
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
189
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
190
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
191
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f) ||
192
+ (codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
193
+ (codePoint >= 0x1f3fb && codePoint <= 0x1f3ff) ||
194
+ codePoint === 0x200b ||
195
+ codePoint === 0x200c ||
196
+ codePoint === 0x200d ||
197
+ codePoint === 0x200e ||
198
+ codePoint === 0x200f ||
199
+ codePoint === 0x2060 ||
200
+ codePoint === 0xfeff ||
201
+ codePoint === 0x00ad
202
+ )
203
+ }
204
+
205
+ export function codePointWidth(cp) {
206
+ if (isZeroWidth(cp)) return 0
177
207
  const wide =
178
208
  (cp >= 0x1100 && cp <= 0x115f) ||
179
209
  (cp >= 0x2e80 && cp <= 0x303e) ||
@@ -191,15 +221,32 @@ function codePointWidth(cp) {
191
221
  return wide ? 2 : 1
192
222
  }
193
223
 
194
- /** 文本的显示列宽(CJK 全宽算 2 列;`length` 数的是码元,中文会错一半)。 */
195
- function displayWidth(text) {
224
+ /**
225
+ * 文本的显示列宽(CJK 全宽算 2 列;`length` 数的是码元,中文会错一半)。
226
+ *
227
+ * ZWJ 序列(`👨‍👩‍👧`)按**一个字形**算:连接符之后的码点不累加,
228
+ * 否则一个家庭 emoji 会被算成 6 列,padWidth 会把整张表推歪。
229
+ */
230
+ export function displayWidth(text) {
196
231
  let width = 0
197
- for (const ch of String(text)) width += codePointWidth(ch.codePointAt(0) ?? 0)
232
+ let joined = false
233
+ for (const ch of String(text)) {
234
+ const codePoint = ch.codePointAt(0) ?? 0
235
+ if (codePoint === 0x200d) {
236
+ joined = true
237
+ continue
238
+ }
239
+ if (joined) {
240
+ joined = false
241
+ continue
242
+ }
243
+ width += codePointWidth(codePoint)
244
+ }
198
245
  return width
199
246
  }
200
247
 
201
248
  /** 按显示列右填充(`padEnd` 对 CJK 会少补空格 ⇒ 后续列错位,实测踩过)。 */
202
- function padWidth(text, width) {
249
+ export function padWidth(text, width) {
203
250
  const pad = width - displayWidth(text)
204
251
  return pad > 0 ? text + ' '.repeat(pad) : String(text)
205
252
  }
@@ -526,4 +573,10 @@ function main() {
526
573
  process.exit(code)
527
574
  }
528
575
 
529
- main()
576
+ // 只有「被当作 CLI 直接执行」时才跑 main:`tests/contract/width-parity.test.ts`
577
+ // 需要 import 本文件的宽度实现做**跨实现对拍**(零依赖脚本不能 import 宿主
578
+ // 模块,但宿主测试可以 import 它)。不 guard 的话一次 import 就会去解析
579
+ // process.argv 并 exit,测试进程当场死掉。
580
+ const invokedAsCli =
581
+ typeof process.argv[1] === 'string' && import.meta.url === pathToFileURL(process.argv[1]).href
582
+ if (invokedAsCli) main()