dsh-config-manager 0.1.72 → 0.1.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +40 -4
  2. package/README.zh-CN.md +40 -4
  3. package/lib/adapters/index.d.ts +1 -0
  4. package/lib/adapters/index.js +5 -0
  5. package/lib/adapters/messages.d.ts +3 -0
  6. package/lib/adapters/messages.js +7 -0
  7. package/lib/adapters/plugin-data.d.ts +114 -0
  8. package/lib/adapters/plugin-data.js +309 -0
  9. package/lib/adapters/self.d.ts +1 -1
  10. package/lib/adapters/self.js +2 -1
  11. package/lib/adapters/test-helpers.d.ts +9 -0
  12. package/lib/adapters/test-helpers.js +14 -0
  13. package/lib/client.d.ts +103 -10
  14. package/lib/client.js +10748 -10119
  15. package/lib/core/analyzer.d.ts +11 -2
  16. package/lib/core/analyzer.js +31 -8
  17. package/lib/core/backup-plan.d.ts +10 -0
  18. package/lib/core/backup-plan.js +21 -3
  19. package/lib/core/backup.d.ts +20 -1
  20. package/lib/core/backup.js +48 -3
  21. package/lib/core/command-rollback.d.ts +6 -2
  22. package/lib/core/command-rollback.js +6 -2
  23. package/lib/core/messages-core.d.ts +2 -0
  24. package/lib/core/messages-core.js +4 -0
  25. package/lib/core/messages.d.ts +2 -1
  26. package/lib/core/messages.js +4 -2
  27. package/lib/core/plugin-data-registry.d.ts +124 -0
  28. package/lib/core/plugin-data-registry.js +198 -0
  29. package/lib/core/rollback-gate.d.ts +33 -0
  30. package/lib/core/rollback-gate.js +57 -0
  31. package/lib/core/rollback-ledger.d.ts +274 -0
  32. package/lib/core/rollback-ledger.js +1400 -0
  33. package/lib/core/rollback-point.d.ts +406 -5
  34. package/lib/core/rollback-point.js +1453 -306
  35. package/lib/core/rollback.js +6 -4
  36. package/lib/core/types.d.ts +19 -0
  37. package/lib/foreign/registry.d.ts +1 -1
  38. package/lib/foreign/registry.js +1 -1
  39. package/lib/index.d.ts +5 -1
  40. package/lib/index.js +83 -13
  41. package/lib/routes/messages.d.ts +1 -0
  42. package/lib/routes/messages.js +2 -0
  43. package/lib/routes/prefs.js +26 -3
  44. package/lib/routes/recovery.d.ts +21 -0
  45. package/lib/routes/recovery.js +127 -7
  46. package/lib/routes/sync.js +11 -1
  47. package/lib/schema/section-registry.d.ts +1 -1
  48. package/lib/schema/section-registry.js +32 -3
  49. package/lib/schema/types.d.ts +1 -1
  50. package/lib/sync/messages.d.ts +1 -0
  51. package/lib/sync/messages.js +3 -0
  52. package/lib/sync/sync-engine.d.ts +21 -7
  53. package/lib/sync/sync-engine.js +108 -18
  54. package/lib/sync/ui-prefs.d.ts +22 -0
  55. package/lib/sync/ui-prefs.js +28 -0
  56. package/lib/ui/diff-view.d.ts +10 -0
  57. package/lib/ui/diff-view.js +17 -0
  58. package/lib/ui/export-flow.js +2 -0
  59. package/lib/ui/i18n.d.ts +5 -3
  60. package/lib/ui/i18n.js +10 -6
  61. package/lib/ui/rollback-view.d.ts +59 -0
  62. package/lib/ui/rollback-view.js +14 -0
  63. package/lib/ui/test-helpers.js +2 -1
  64. package/lib/utils/bundle-scan.d.ts +5 -3
  65. package/lib/utils/bundle-scan.js +5 -3
  66. package/lib/utils/line-diff.d.ts +7 -1
  67. package/lib/utils/line-diff.js +8 -2
  68. package/package.json +1 -1
  69. package/src/adapters/index.ts +5 -0
  70. package/src/adapters/messages.ts +7 -0
  71. package/src/adapters/plugin-data.test.ts +225 -0
  72. package/src/adapters/plugin-data.ts +351 -0
  73. package/src/adapters/self.ts +2 -1
  74. package/src/adapters/test-helpers.ts +15 -0
  75. package/src/client/chat/ChatEntryView.tsx +59 -10
  76. package/src/client/chat/RollbackDiffTab.tsx +49 -2
  77. package/src/client/chat/RollbackFileIcon.tsx +7 -45
  78. package/src/client/chat/RollbackReviewDialog.tsx +20 -4
  79. package/src/client/chat/chat-entry-view.test.ts +164 -1
  80. package/src/client/chat/chat-entry-view.ts +127 -10
  81. package/src/client/chat/chat-node-registration.ts +40 -6
  82. package/src/client/chat/chat-point-window.ts +11 -1
  83. package/src/client/chat/rollback-diff-tab.ts +13 -3
  84. package/src/client/chat/rollback-entry-view.ts +5 -0
  85. package/src/client/chat/sidebar-slots.ts +24 -2
  86. package/src/client/common/DiffTable.tsx +54 -29
  87. package/src/client/common/client-route-parity.test.ts +6 -0
  88. package/src/client/common/host-primitives.ts +78 -0
  89. package/src/client/common/routes.ts +10 -0
  90. package/src/client/common/section-labels.test.ts +1 -0
  91. package/src/client/common/section-labels.ts +2 -0
  92. package/src/client/config-manager.module.css +138 -14
  93. package/src/client/environment/maintenance/DiskUsageCard.tsx +20 -3
  94. package/src/client/index.ts +54 -2
  95. package/src/client/library/RestorePlanView.tsx +0 -1
  96. package/src/client/locales.ts +8 -6
  97. package/src/client/recovery/RollbackCard.tsx +201 -7
  98. package/src/client/recovery/RollbackDiffPane.tsx +26 -8
  99. package/src/client/recovery/recovery-api.ts +28 -0
  100. package/src/client/recovery/recovery-locales.ts +36 -6
  101. package/src/client/recovery/rollback-enabled.ts +41 -0
  102. package/src/client/recovery/rollback-file-changes.test.ts +77 -0
  103. package/src/client/sync/PushPreviewCard.tsx +57 -0
  104. package/src/client/sync/SyncPanel.tsx +2 -37
  105. package/src/client/sync/SyncSettingsView.test.ts +3 -0
  106. package/src/client/sync/sync-push-preview.test.ts +14 -0
  107. package/src/client/sync/sync-view.test.ts +1 -1
  108. package/src/client/sync/sync-view.ts +9 -1
  109. package/src/core/analyzer.ts +31 -9
  110. package/src/core/backup-plan.test.ts +20 -5
  111. package/src/core/backup-plan.ts +22 -3
  112. package/src/core/backup.ts +50 -4
  113. package/src/core/command-rollback.ts +6 -2
  114. package/src/core/messages-core.ts +4 -0
  115. package/src/core/messages.ts +4 -2
  116. package/src/core/plugin-data-registry.test.ts +113 -0
  117. package/src/core/plugin-data-registry.ts +261 -0
  118. package/src/core/rollback-gate.test.ts +66 -0
  119. package/src/core/rollback-gate.ts +61 -0
  120. package/src/core/rollback-ledger.test.ts +655 -0
  121. package/src/core/rollback-ledger.ts +1430 -0
  122. package/src/core/rollback-point.test.ts +866 -41
  123. package/src/core/rollback-point.ts +1582 -291
  124. package/src/core/rollback.ts +5 -3
  125. package/src/core/types.ts +19 -0
  126. package/src/foreign/registry.test.ts +1 -1
  127. package/src/foreign/registry.ts +1 -1
  128. package/src/index.ts +73 -10
  129. package/src/routes/messages.ts +2 -0
  130. package/src/routes/prefs.ts +31 -2
  131. package/src/routes/recovery.ts +122 -7
  132. package/src/routes/sync.ts +15 -1
  133. package/src/schema/manifest-schema.test.ts +4 -4
  134. package/src/schema/registry.test.ts +9 -2
  135. package/src/schema/section-registry.ts +32 -3
  136. package/src/schema/types.ts +2 -1
  137. package/src/sync/messages.ts +3 -0
  138. package/src/sync/sync-credentials.test.ts +119 -0
  139. package/src/sync/sync-engine.ts +106 -18
  140. package/src/sync/sync-preview-push.test.ts +158 -0
  141. package/src/sync/transport.test.ts +1 -0
  142. package/src/sync/ui-prefs.test.ts +44 -1
  143. package/src/sync/ui-prefs.ts +39 -0
  144. package/src/ui/diff-view.test.ts +9 -1
  145. package/src/ui/diff-view.ts +22 -0
  146. package/src/ui/export-flow.ts +2 -0
  147. package/src/ui/i18n.ts +10 -6
  148. package/src/ui/rollback-view.test.ts +17 -0
  149. package/src/ui/rollback-view.ts +66 -0
  150. package/src/ui/test-helpers.ts +2 -1
  151. package/src/utils/bundle-scan.ts +5 -3
  152. package/src/utils/line-diff.test.ts +12 -0
  153. package/src/utils/line-diff.ts +9 -3
@@ -1,9 +1,19 @@
1
1
  import { type DiffHunk } from '../utils/line-diff.ts';
2
+ import { ROLLBACK_DEFAULT_LEDGER_BYTES } from './rollback-ledger.ts';
2
3
  export declare const ROLLBACK_SCHEMA_VERSION = 2;
3
4
  /** 回滚点库目录名(dataDir/rollback-points)——路由族 /recovery/rollback-points/* 同源 */
4
5
  export declare const ROLLBACK_POINTS_DIR_NAME = "rollback-points";
6
+ /**
7
+ * 台账**清单**文件名(v3,issue #89):它只是一份小清单(元数据 + 段摘要),
8
+ * 完整记录在 records/<id>.json、工作区载荷在 payloads/<sha256>.json —— 清单里绝不含大数组。
9
+ */
5
10
  export declare const ROLLBACK_LEDGER_NAME = "index.json";
6
11
  export declare const ROLLBACK_OBJECTS_DIR_NAME = "objects";
12
+ /**
13
+ * 台账字节预算(records/ + payloads/)的缺省值,可由 RollbackEngineOptions.ledgerBytesBudget 覆盖。
14
+ * 为什么需要它:ROLLBACK_MAX_RECORDS(50)只限**条数**;本机实测 50 条 = 473 MB 直接把宿主撑爆。
15
+ */
16
+ export { ROLLBACK_DEFAULT_LEDGER_BYTES };
7
17
  /** 全局自动点上限(S6);protected / guard 点不占预算、只进不出 */
8
18
  export declare const ROLLBACK_MAX_RECORDS = 50;
9
19
  /** 每会话自动点上限(S6) */
@@ -81,6 +91,53 @@ export declare const ROLLBACK_DEFAULT_SESSION_QUIESCENT_MS = 30000;
81
91
  /** 轮询回退的间隔上界(S1:会话日志 mtime ≤5 s 轮询) */
82
92
  export declare const ROLLBACK_DEFAULT_POLL_INTERVAL_MS = 5000;
83
93
  export declare const ROLLBACK_LEFTOVER_LIST_LIMIT = 20;
94
+ /**
95
+ * 工作区捕获的**挂钟时间预算**(缺省 30 s)。
96
+ *
97
+ * 为什么需要(对标 dsh-turn-rewind 的 `deadline.ts`、dsh-checkpoint-rewind 至今未合并的 PR #25):
98
+ * 捕获路径此前只有「文件数 / 总字节 / 单文件字节」三个**规模**上限,没有**时间**上限 ——
99
+ * 一个「文件数很多但总量不大」的工作区能让 walk + stat + sha256 跑很久。
100
+ * 超预算**不是失败**:已收条目照常成点(ok=true),未走查的部分按 `budget-time` 逐条留痕、
101
+ * `truncated` 置真 ⇒ 删除集自动 fail-closed(证明名单不完整就一条都不删)。
102
+ */
103
+ export declare const ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS = 30000;
104
+ /**
105
+ * 自适应预算区间(见 RollbackEngineOptions.captureBudgetMs):上一次留点被截断 ⇒ 下次 ×2,
106
+ * 上一次只用掉不到 1/4 ⇒ 下次 ÷2。区间外不再调整(避免无限拉长 / 缩到不可用)。
107
+ */
108
+ export declare const ROLLBACK_MIN_CAPTURE_BUDGET_MS = 10000;
109
+ export declare const ROLLBACK_MAX_CAPTURE_BUDGET_MS = 120000;
110
+ /**
111
+ * 捕获期「读内容 + 写对象仓」的并发上限。
112
+ *
113
+ * 为什么必须并发:真机实测(34k 文件 / 512 MiB)一次全量捕获 175 s,其中 **148 s 花在
114
+ * 31,702 次对象写**(一次 4.7 ms:temp + fsync + rename),而走查本身只要 0.66 s。
115
+ * 对象写入是纯 I/O,串行做等于把预算全花在等待上 ⇒ 覆盖永远只有一小半。
116
+ * 并发上限与**在飞字节闸门**共同限流(在飞内容上界 = `captureInFlightBytes`)。
117
+ */
118
+ export declare const ROLLBACK_DEFAULT_CAPTURE_CONCURRENCY = 16;
119
+ /**
120
+ * 捕获期**读入内存、尚未落对象仓**的内容字节上限(真正的内存闸门)。
121
+ *
122
+ * 为什么必须独立于 `maxFileBytes`(0.1.74 真机闪退事故,2026-10-16):
123
+ * `maxFileBytes` 的缺省是**总量预算** `maxBytes`(512 MiB)。并发捕获把闸门接到它上面,
124
+ * 于是「同时在飞的内容」可以到 ~512 MiB —— 串行实现的上界是「一份文件」,并发实现的
125
+ * 上界是「闸门限额」,两者差着两三个数量级。真机表现:发出消息后 1–2 s(= `turn/start`
126
+ * 开始留点)DSH Desktop 宿主被系统终止 ⇒ 桌面闪退(无 JS 栈、插件零错误、
127
+ * 退出码是「被外部终止」形状)。这里取一个小常量当闸门;要放宽只能显式传
128
+ * `captureInFlightBytes`,且被 `maxBytes` 夹住。
129
+ */
130
+ export declare const ROLLBACK_DEFAULT_CAPTURE_INFLIGHT_BYTES: number;
131
+ /** 捕获阶段①的 stat 批大小:判定仍严格按顺序,只是 stat 并发取(真机 34k 次 stat 从 5 s → <1 s) */
132
+ export declare const ROLLBACK_CAPTURE_STAT_BATCH = 128;
133
+ /**
134
+ * 捕获期**候选名单**上限(不是入点上限):必须显著大于 maxFiles,否则「缺口优先」排序作用不到
135
+ * 那些排在遍历顺序尾部、上一回合没覆盖到的文件(它们连候选都进不了 ⇒ 覆盖率永远不收敛)。
136
+ * 走查本来就会把每条路径记进 walked(路径索引的来源),这里多留的只是候选数组本身。
137
+ */
138
+ export declare const ROLLBACK_CAPTURE_LIST_LIMIT = 200000;
139
+ /** 走查中断时逐条记下的「没进去的子树」上限;超出即放弃精确范围,退回全局 fail-closed */
140
+ export declare const ROLLBACK_UNWALKED_DIRS_LIMIT = 200;
84
141
  /**
85
142
  * 「删除回退点之后新建的路径」的**整套上限**(用户 2026-10-11 定案 A)。
86
143
  * 超过就整套拒绝(fail-closed),绝不删一半 —— 半套删除既没有可解释的状态,也无法整体回滚。
@@ -112,6 +169,14 @@ export declare const ROLLBACK_PREVIEW_DIFF_TOTAL_WORK = 32000000;
112
169
  export declare const ROLLBACK_FILE_DIFF_LIMITS: {
113
170
  readonly maxFileBytes: number;
114
171
  readonly context: 3;
172
+ /**
173
+ * Myers 编辑距离上限(2026-10-15 用户:不该动辄出现「已按整块替换展示」)。
174
+ * 实测(本仓 rollback-point.ts 的 4983 行 × 2492 行版本对):D 在 3000~4000 就能拿到
175
+ * 最小编辑脚本 —— 耗时 ≤70 ms;代价是回溯的 V 快照 O(D²),RSS 峰值 +~100 MB。
176
+ * 缺省那档 400 会让几千行里的普通改动一律退化成「整块替换」,那正是用户看到的那条提示;
177
+ * 单文件是**按需点开**的一屏,这点内存换「真差异」划算。再大就换算法,不加预算。
178
+ */
179
+ readonly maxD: 4000;
115
180
  };
116
181
  /** 恢复顺序(工作区在前;工作区失败即中止后面的会话段) */
117
182
  export declare const ROLLBACK_REWIND_ORDER: readonly ['workspace', 'session'];
@@ -157,7 +222,14 @@ export type RollbackCode = 'ok' | 'deleted' | 'storage-unavailable' | 'ledger-un
157
222
  * 界面必须说两句不同的话(「这点没记录文件」vs「工作区已与该点一致」),
158
223
  * 而 `detail` 是技术细节、**不能**当用户可见分派依据。只出现在 segments[].reasonCode。
159
224
  */
160
- | 'no-recorded-files' | 'mutation-locked' | 'method-not-allowed' | 'session-missing' | 'session-out-of-root' | 'session-too-large' | 'session-active' | 'session-fork-unavailable' | 'fork-failed' | 'capture-failed' | 'capture-truncated' | 'capture-degraded' | 'no-chunks' | 'chunk-not-absolute' | 'chunk-not-found'
225
+ | 'no-recorded-files' | 'mutation-locked' | 'method-not-allowed' | 'session-missing' | 'session-out-of-root' | 'session-too-large' | 'session-active'
226
+ /**
227
+ * 子代理会话(`header.origin === 'subagent'`):**不自动留点**。
228
+ * 它的工作区与父会话相同、回合号是自己的,留点既与父会话重复又只挤占预算;
229
+ * 用户在侧栏看到的是嵌套在父对话下的子代理,不需要单独回退。
230
+ * 读不到 `origin`(老宿主)时**不加判定** —— 绝不因为读不到就少留点。
231
+ */
232
+ | 'subagent-session' | 'session-fork-unavailable' | 'fork-failed' | 'capture-failed' | 'capture-truncated' | 'capture-degraded' | 'no-chunks' | 'chunk-not-absolute' | 'chunk-not-found'
161
233
  /** 单文件差异:该路径**不在**这个回滚点的记录里(收窄:绝不把本端点做成任意路径读取器)。 */
162
234
  | 'file-not-recorded';
163
235
  /** 结构化指引 / 告警:code 供界面映射字典键,detail 是技术标识(路径 / errno / 计数)。 */
@@ -181,7 +253,8 @@ export interface RollbackSkippedEntry {
181
253
  /**
182
254
  * link(符号链接/junction,不跟随)/ missing / not-a-file / self-store /
183
255
  * dep-dir(运行时依赖目录,整棵忽略)/ ignored(用户忽略规则命中)/ too-large(超单文件上限)/
184
- * budget-bytes(加它会超总量预算)/ budget-files(文件数预算用尽)
256
+ * budget-bytes(加它会超总量预算)/ budget-files(文件数预算用尽)/
257
+ * asar-container(含 `.asar` 段的路径:宿主会把它当容器解析、畸形夹具能带走宿主,因此绝不碰)
185
258
  */
186
259
  reason: string;
187
260
  }
@@ -203,7 +276,28 @@ export interface RollbackPathIndex {
203
276
  export interface RollbackWorkspacePayload {
204
277
  /** 显式路径分块:恢复只在这些分块内覆盖 */
205
278
  chunks: string[];
279
+ /**
280
+ * 记录在案的**文件条目数**。**必须自带**(不能再用 `files.length`):增量视图下 `files` 只是
281
+ * 「相对基准新增/改写的子集」,而摘要(界面上的条数 / 可恢复判定)要的是全量条数。
282
+ */
283
+ fileCount?: number;
206
284
  files: RollbackWorkspaceFile[];
285
+ /**
286
+ * **存盘专用**:这份载荷是「相对某个基准载荷的增量」(见 rollback-ledger 的 resolvePayload)。
287
+ * 只在写盘那一刻存在 —— 任何读路径(readRecord / 预览 / 回退 / 对象回收)拿到的都是**物化后**
288
+ * 的完整 `files`(`delta` 已被抹掉)。**绝不把带 delta 的对象交给消费者**:它看起来像一份
289
+ * 「只有这几条记录」的完整清单,拿去规划删除会删掉其它全部文件。
290
+ */
291
+ delta?: {
292
+ /** 基准载荷的内容寻址哈希(必须在 prune/回收时一并保活) */
293
+ base: string;
294
+ /** 基准自己的依赖链(深→浅,含基准),保活用 */
295
+ baseChain: string[];
296
+ /** 相对基准**不再存在**的路径(逐字,与捕获产出的绝对路径同形) */
297
+ removed: string[];
298
+ /** 相对基准**新增 / 改写**的条目(`files` 在增量视图下为 null) */
299
+ added?: RollbackWorkspaceFile[];
300
+ };
207
301
  skipped: RollbackSkippedEntry[];
208
302
  /**
209
303
  * 整棵跳过的「运行时依赖目录」(忽略规则 = 全仓唯一来源 EXCLUDED_DEP_DIR_NAMES,当前 node_modules)。
@@ -232,6 +326,22 @@ export interface RollbackWorkspacePayload {
232
326
  * 文件级忽略**不**置它:被忽略的文件既进 skipped、也进 walked(路径索引来源)⇒ 名单照样完整。
233
327
  */
234
328
  ignoreIncomplete?: boolean;
329
+ /**
330
+ * true = 走查被**时间预算 / 取消信号**中断 ⇒ 剩余子树**一条都没进任何名单**
331
+ * (不在 files / skipped / walked 里)⇒ 证明名单**不完整** ⇒ 删除集整体拒绝(fail-closed)。
332
+ *
333
+ * 与 `truncated` 的**关键区别**:`truncated` 的三种成因(文件数 / 总字节 / 单文件超限)都是
334
+ * 「继续遍历 + 逐条留痕」,名单依然完整,所以**不**拒删;只有「整棵子树根本没被走查」才必须拒删
335
+ * —— 否则那些文件会被判成「留点后才新建」而被删掉(数据丢失)。
336
+ * 老台账(本字段诞生前写下的点)一律没有它:按「名单完整」处理(与旧行为一致)。
337
+ */
338
+ walkIncomplete?: boolean;
339
+ /**
340
+ * 走查被中断时**栈里还没进去的子树**(精确到目录)。有它 ⇒ 拒删只覆盖这些子树
341
+ * (分子树 fail-closed),而不是把整个工作区的删除全关掉;缺它(老台账 / 栈太深被截断)
342
+ * ⇒ 退回全局拒删。与 walkIncomplete 一起看:后者是「安全判据」,本字段是「精确范围」。
343
+ */
344
+ unwalkedDirs?: string[];
235
345
  /** 命中文件数 / 字节上限(内容未全部进来,已如实留痕) */
236
346
  truncated: boolean;
237
347
  bytes: number;
@@ -347,12 +457,33 @@ export interface RollbackPointSummary {
347
457
  /** 继承视图里「状态真正的归属会话」(= 祖先会话 id;与 `inheritedFrom` 成对出现) */
348
458
  inheritedSessionId?: string | null;
349
459
  }
460
+ /**
461
+ * 台账(records/ + payloads/)的可观测快照。
462
+ * 路由层转发给界面时必须**照抄字段名与形状**(issue #89 的交付契约)。
463
+ */
464
+ export interface RollbackLedgerStatus {
465
+ /** 清单里的记录条数 */
466
+ records: number;
467
+ /** records/ + payloads/ 当前总字节 */
468
+ bytes: number;
469
+ /** 生效的字节预算(缺省 ROLLBACK_DEFAULT_LEDGER_BYTES) */
470
+ budgetBytes: number;
471
+ /** 本进程内被**预算**淘汰的自动点累计数(条数淘汰不计入) */
472
+ evicted: number;
473
+ /**
474
+ * 当前磁盘字节**已超**预算、且已无处可淘汰(典型:单条点本身就大于预算)。
475
+ * 界面据此说「台账已超预算、无法再自动回收」,绝不静默成「一切正常」。
476
+ */
477
+ overBudget: boolean;
478
+ }
350
479
  export interface RollbackStorageStatus {
351
480
  available: boolean;
352
481
  root: string;
353
482
  detail?: string;
354
483
  guidance: RollbackGuidance[];
355
484
  requirements: readonly string[];
485
+ /** 新增,可选(不破坏既有构造点);存储栈不可用时缺省 */
486
+ ledger?: RollbackLedgerStatus;
356
487
  }
357
488
  /** 捕获一个回滚点的输入(宿主/测试构造;无 HTTP capture 端点) */
358
489
  export interface RollbackPointInput {
@@ -372,6 +503,11 @@ export interface RollbackPointInput {
372
503
  chunks?: readonly string[];
373
504
  note?: string | null;
374
505
  protect?: boolean;
506
+ /**
507
+ * 调用方的取消信号(插件卸载 / 会话销毁):已中止时捕获在**下一个文件边界**停下,
508
+ * 已收条目照常成点(部分点 + truncated)。缺省不传 = 只受时间预算约束。
509
+ */
510
+ signal?: AbortSignal;
375
511
  }
376
512
  export interface RollbackCaptureResult {
377
513
  ok: boolean;
@@ -710,6 +846,33 @@ export interface RollbackEngineOptions {
710
846
  maxGuardsPerSession?: number;
711
847
  /** **全局** guard 点上限(缺省 50,见 ROLLBACK_MAX_GUARDS;与自动点的 maxRecords 无关) */
712
848
  maxGuards?: number;
849
+ /**
850
+ * 台账字节预算(records/ + payloads/ 的总字节,缺省 ROLLBACK_DEFAULT_LEDGER_BYTES)。
851
+ * 超预算时**只淘汰最旧的 auto 点**(guard / protected 不淘汰,沿用 selectPrune 的类别口径)。
852
+ */
853
+ ledgerBytesBudget?: number;
854
+ /**
855
+ * 工作区捕获的挂钟时间预算(毫秒,缺省 ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS)。
856
+ * 超时留**部分点**,不拒绝留点;见该常量的说明。
857
+ *
858
+ * 运行期**自适应**:被截断 ⇒ 下次翻倍;用得很少 ⇒ 下次减半(区间见
859
+ * ROLLBACK_MIN/MAX_CAPTURE_BUDGET_MS)。回退期走查的预算见 rewindBudgetMs。
860
+ */
861
+ captureBudgetMs?: number;
862
+ /** 自适应区间下界(缺省 ROLLBACK_MIN_CAPTURE_BUDGET_MS;测试可注入更小值) */
863
+ captureBudgetMinMs?: number;
864
+ /** 自适应区间上界(缺省 ROLLBACK_MAX_CAPTURE_BUDGET_MS;测试可注入更小值) */
865
+ captureBudgetMaxMs?: number;
866
+ /** 回退期走查的时间预算(缺省 = captureBudgetMs 初值;**不参与自适应**) */
867
+ rewindBudgetMs?: number;
868
+ /** 捕获期读 + 写并发上限(缺省 ROLLBACK_DEFAULT_CAPTURE_CONCURRENCY,1..64) */
869
+ captureConcurrency?: number;
870
+ /**
871
+ * 捕获期**在飞内容字节**上限(缺省 ROLLBACK_DEFAULT_CAPTURE_INFLIGHT_BYTES = 32 MiB,
872
+ * 有效范围 1..maxBytes)。与 `maxFileBytes`(单文件「要不要读」的判据)是两件事:
873
+ * **这个才是并发的内存闸门**。
874
+ */
875
+ captureInFlightBytes?: number;
713
876
  guardPolicy?: RollbackGuardPolicy;
714
877
  /** 存储栈探针(缺省 = 真实磁盘探测);注入失败探针可测「缺失时仍可挂载」 */
715
878
  storage?: RollbackStorageProbe;
@@ -722,6 +885,12 @@ export interface RollbackEngineOptions {
722
885
  * 测试用它计数「真的读了几次」,从而钉住路径身份缓存命中时**零重读**。
723
886
  */
724
887
  readFile?: (target: string) => Promise<Uint8Array>;
888
+ /**
889
+ * 台账读盘观测 seam(缺省不观测):每次「读一条记录文件 / 一份载荷文件」前回调一次。
890
+ * 与 readFile seam 同一用途 —— 测试用它**确定性地**计数「一次留点到底读了几份记录」,
891
+ * 而不是靠无法复现的堆采样(issue #89 的内存护栏)。
892
+ */
893
+ ledgerReadFile?: (file: string) => void;
725
894
  /** E1:预览行级统计的规模上限(缺省见 ROLLBACK_PREVIEW_DIFF_*);注入小值即可确定性地覆盖 truncated 分支 */
726
895
  previewDiffMaxFileBytes?: number;
727
896
  previewDiffMaxTotalBytes?: number;
@@ -730,6 +899,18 @@ export interface RollbackEngineOptions {
730
899
  /** best-effort 日志(清理失败等;绝不抛给调用方) */
731
900
  log?: (line: string) => void;
732
901
  }
902
+ /**
903
+ * 路径里是否存在以 `.asar` 结尾的段(**捕获 / 走查期一律不碰这类路径**)。
904
+ *
905
+ * 为什么(真机事故,0.1.76):Electron 宿主对任何含 `.asar` 段的路径**启用了 asar 虚拟化** ——
906
+ * 即使那只是个普通文件,`fs.stat`/`readdir`/`read` 也会被接管去当容器解析。工作区里只要躺着一枚
907
+ * **畸形** `.asar`(测试夹具、被打断的打包残留),宿主进程就会被原生层带走:crashpad
908
+ * `not connected`、退出码 4294930435 ⇒ DSH Desktop 闪退(无 JS 栈、插件零错误)。
909
+ * 实测(本仓真机捕获、同一份输入):`.tmp` 下 19 枚 `.asar` 夹具在场 → Electron 进程死;
910
+ * 全部改名后 → 同一趟捕获 9 s 跑完、退出码 0。
911
+ * `.asar.unpacked` 是**真目录**(段名不以 `.asar` 结尾)⇒ 不拦。
912
+ */
913
+ export declare function hasAsarSegment(target: string): boolean;
733
914
  /** child 是否等于 parent 或位于其下(win32 折叠大小写;段边界判定) */
734
915
  export declare function isInsidePath(child: string, parent: string): boolean;
735
916
  /**
@@ -775,6 +956,11 @@ export declare function resolveTurnBoundary(events: readonly RollbackEventLike[]
775
956
  code: 'no-boundary';
776
957
  detail: string;
777
958
  };
959
+ /**
960
+ * 记录在案的文件条目数:优先用载荷自带的 `fileCount`(增量视图下 `files` 可能只是子集),
961
+ * 老载荷没有该字段才回落到 `files.length`。
962
+ */
963
+ export declare function workspaceFileCount(payload: RollbackWorkspacePayload | null): number;
778
964
  export declare function summarizeRollbackPoint(record: RollbackPointRecord): RollbackPointSummary;
779
965
  /** 浅层形状校验:台账被篡改 / 半写时不把它当记录用(宁可当「读不到」)。 */
780
966
  export declare function isRollbackPointRecord(value: unknown): value is RollbackPointRecord;
@@ -791,6 +977,7 @@ export declare class RollbackPointEngine {
791
977
  private readonly maxFiles;
792
978
  private readonly maxBytes;
793
979
  private readonly maxFileBytes;
980
+ private readonly captureInFlightBytes;
794
981
  private readonly maxSkippedEntries;
795
982
  private readonly sessionMaxBytes;
796
983
  private readonly sessionQuiescentMs;
@@ -820,9 +1007,49 @@ export declare class RollbackPointEngine {
820
1007
  private forkLinksAt;
821
1008
  private forkLinksComplete;
822
1009
  /** 会话日志头缓存(mtime + size 失效;只服务 fork 链扫描) */
1010
+ /**
1011
+ * **本进程**最近一次成功留点的工作区摘要(清单增量化的基准)。
1012
+ *
1013
+ * 为什么是内存而不是读盘:一次留点只准读清单(L-08 / issue #89)—— 读上一份 12 MB 载荷去算差异
1014
+ * 正是那条被消灭的路。内存量 ≈ 每文件一个 `sha:bytes:chunk` 串(3.4 万文件约几 MB),
1015
+ * 与已有的身份缓存(`pathCache`)同量级;进程重启后第一次留点落全量锚点,随后继续链。
1016
+ */
1017
+ private deltaBase;
823
1018
  private readonly forkHeaderCache;
824
- /** 继承判据用的台账快照(TTL 缓存;避免每个 turn/start 都读整份台账) */
1019
+ /** 继承判据用的台账快照(TTL 缓存;只缓存**清单条目**,绝不缓存完整记录) */
825
1020
  private ledgerSnapshotCache;
1021
+ /** 台账存储层(v3:清单 + 单条记录 + 内容寻址载荷;issue #89) */
1022
+ private readonly ledger;
1023
+ private readonly ledgerBytesBudget;
1024
+ /**
1025
+ * 工作区捕获的挂钟时间预算(毫秒;见 ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS)。**可变**:
1026
+ * 每次留点后按「是否被截断 / 用掉多少」自适应(见 ROLLBACK_MIN/MAX_CAPTURE_BUDGET_MS)。
1027
+ */
1028
+ private captureBudgetMs;
1029
+ /** 自适应区间下界 / 上界(构造期定死,运行期只调 captureBudgetMs) */
1030
+ private readonly captureBudgetMinMs;
1031
+ private readonly captureBudgetMaxMs;
1032
+ /** 回退期走查的时间预算(与捕获预算分开:自适应不该顺手拉长回退期的走查) */
1033
+ private readonly rewindBudgetMs;
1034
+ /** 捕获期读 + 写并发上限 */
1035
+ private readonly captureConcurrency;
1036
+ /** 对象仓目录已确认存在(省掉每个对象一次 mkdir) */
1037
+ private objectsDirReady;
1038
+ /**
1039
+ * 本回合对象名单快照(一次 readdir 顶掉「每个候选一次 fs.access」;真机 34k 次 access ≈ 3.7 s)。
1040
+ * null = 读不到名单(首次还没建目录/异常)⇒ 回落逐条 objectExists。
1041
+ * 只保证「写进去的立刻可见」:捕获期间被**并发删除**的对象可能仍被判为存在 —— 那一路由
1042
+ * 读取侧(readObject + 哈希校验)响亮失败,不会静默给出错误的 before 内容。
1043
+ */
1044
+ private captureObjectNames;
1045
+ /**
1046
+ * 启动期一次性初始化(流式迁移 v2 -> v3 + 孤儿 tmp 清理)的惰性单飞。
1047
+ * 为什么惰取而不是构造时同步跑:构造发生在每个请求路径上,同步扫盘/迁移会把请求卡死;
1048
+ * 而「每回合都扫一遍」正是要避免的浪费 —— 所以只跑一次并记住结果。
1049
+ */
1050
+ private initPromise;
1051
+ /** 启动期初始化失败的如实记录(读路径必须报出来,绝不假装台账是空的) */
1052
+ private initError;
826
1053
  constructor(options: RollbackEngineOptions);
827
1054
  /** 回滚点库根目录(只读侦察用;写路径全部经本类的门) */
828
1055
  get storeDir(): string;
@@ -835,8 +1062,37 @@ export declare class RollbackPointEngine {
835
1062
  * node_modules)逐条记录、**不计入 skippedCount**,因此不会把点判成 degraded。
836
1063
  */
837
1064
  private recordSkip;
1065
+ /**
1066
+ * 打开一次回退的前像暂存目录(`<root>/undo/<pid>-<ts36>-<8hex>/`)。
1067
+ * 返回 null = 建不出来(权限 / 磁盘满)⇒ 调用方必须**零写入失败**。
1068
+ */
1069
+ private openUndoStager;
838
1070
  private writeFile;
1071
+ /**
1072
+ * 启动期一次性初始化(惰性单飞):先**流式**迁移 v2 单文件 -> v3,再清理崩溃残留的原子写 tmp。
1073
+ *
1074
+ * 为什么惰取而不是在构造里同步跑:引擎在每个请求路径上都会被新建(路由层不缓存引擎),
1075
+ * 构造里同步扫盘/迁移会把请求卡死;而「每回合扫一遍」又是纯浪费 —— 所以只跑一次并记住结果。
1076
+ * 迁移失败**不抛**:如实记进 initError,由读路径报出来,并**拒写**(绝不覆盖还没迁完的 v2 文件)。
1077
+ */
1078
+ private ensureReady;
1079
+ private runInit;
839
1080
  storageStatus(): Promise<RollbackStorageStatus>;
1081
+ /** 台账可观测快照(清单条数 + 磁盘字节 + 预算 + 本进程淘汰计数) */
1082
+ private ledgerStatus;
1083
+ /** 对象仓目录只 mkdir 一次(每个对象各 mkdir 一次是纯白花的 syscall) */
1084
+ private ensureObjectsDir;
1085
+ /**
1086
+ * 写内容寻址对象(对象文件名 = 内容 SHA-256)。
1087
+ *
1088
+ * **为什么这里不用 atomicWriteFile**:那是给「可写配置文件」用的强原子写(同目录 tmp → fsync →
1089
+ * rename),真机实测一次约 4.7 ms —— 34k 文件的工作区一次全量捕获 148 s 全花在它上面(走查本身
1090
+ * 只要 0.66 s)。对象仓的正确性要求不同:文件名即内容哈希 ⇒
1091
+ * · 用 'wx' **独占创建最终名**本身已是原子的(不覆盖已有对象;并发同写只有一方成功);
1092
+ * · 半截 / 被篡改的内容在读取侧必然过不了哈希校验(readObject + sha256Hex)⇒ 按「读不出」
1093
+ * 处理并 fail-closed,绝不会冒充有效前像;
1094
+ * · 不做 fsync:回滚点是尽力而为的本地快照,不为掉电持久性付出每文件一次 fsync 的代价。
1095
+ */
840
1096
  private writeObjectWithHash;
841
1097
  /** 对象仓里是否真有这个 blob(缓存命中时校验;任何异常一律按「没有」处理) */
842
1098
  private objectExists;
@@ -855,10 +1111,40 @@ export declare class RollbackPointEngine {
855
1111
  /**
856
1112
  * 列出一个显式分块内的候选文件(不跟随符号链接 / junction;跳过与读不到都留痕)。
857
1113
  * 返回 null = 该分块本身不可用(不存在 / 是链接 / 非常规文件),细节已写进 sink。
1114
+ *
1115
+ * `truncated` 与 `walkAborted` 是**两件事**:前者只说「本次列表不完整」(文件数上限 / 提交上限),
1116
+ * 后者说「走查被预算或取消打断,栈里还有没进去的子树」—— 只有后者才意味着那些路径
1117
+ * **一条都没进任何名单**,调用方必须据此让删除集 fail-closed。
858
1118
  */
859
1119
  private listChunkFiles;
860
1120
  private captureWorkspace;
861
1121
  /** 用户忽略规则是否命中该路径(只认「相对分块根」的斜杠路径) */
1122
+ /**
1123
+ * 处理一个分块的候选文件(**两阶段**)。
1124
+ *
1125
+ * ① **顺序判定**(与旧的串行实现逐条一致,保证预算 / 计数 / 字节上限完全确定):
1126
+ * 文件数上限、时间预算、自身数据区、stat、单文件上限、总量上限、身份缓存 + 对象存在性
1127
+ * → 得出「当场入点(缓存命中)」还是「要真读」,该跳过的**当场**按原有 reason 逐条留痕。
1128
+ * stat 按**有序批**并发取(判定仍严格按顺序),并用「一次 readdir 的对象名单快照」代替
1129
+ * 每个文件一次 fs.access —— 这两处是稳态成本的大头(真机实测各占 ~5 s / ~3.7 s)。
1130
+ * ② **并发读 + 哈希 + 落对象仓**:这是捕获耗时的**唯一大头**(真机 148 s / 31,702 个对象,
1131
+ * 96% 的时间),用 captureConcurrency 个 worker 并发跑;并用**在飞字节闸门**保证
1132
+ * 「同时在飞的内容字节 ≤ captureInFlightBytes」。**不是** maxFileBytes —— 它缺省 =
1133
+ * 总量预算(512 MiB),拿它当闸门等于允许把整个点的预算同时驻留(0.1.74 闪退根因)。
1134
+ *
1135
+ * 返回本分块**真正入点**的文件数(调用方据此推进 maxFiles 预算)。
1136
+ * 没判到 / 没读完的条目一律逐条进 skipped(budget-time),绝不静默丢弃。
1137
+ */
1138
+ private captureChunkCandidates;
1139
+ /**
1140
+ * 缺口优先排序:身份缓存里**没有**的路径排前面。
1141
+ *
1142
+ * 为什么:预算固定时按目录顺序走查,同一个「已覆盖且便宜」的前缀每回合都会吃掉全部预算,
1143
+ * 尾部永远进不来(真机:34k 文件每次只覆盖 1/5,且五次都是同一批 outputs/ 前缀)。
1144
+ * 未覆盖的排前面 ⇒ 每个点都在补上一次的缺口,覆盖率随回合**单调收敛**到 100%。
1145
+ * 组内保持走查顺序(稳定),因此同一输入下的结果仍然确定。
1146
+ */
1147
+ private orderByCoverage;
862
1148
  private ignoredByRules;
863
1149
  /** 读文件(可注入 seam;只用于捕获期读内容这一步) */
864
1150
  private readFileBytes;
@@ -870,6 +1156,13 @@ export declare class RollbackPointEngine {
870
1156
  * 用毫秒换算同样不行:会把「同一毫秒内的两次写」当成同一个身份。
871
1157
  * 读不到身份(文件消失 / 权限 / 平台不支持 bigint stat)→ null ⇒ 回落全量真读。
872
1158
  */
1159
+ /**
1160
+ * 一次 bigint stat 拿到阶段①需要的**全部**东西(是否文件 / 大小 / mtime / 身份)。
1161
+ * 旧实现「fs.stat + pathIdentityOf」每文件 stat 两次,真机 34k 文件白花约 5 s。
1162
+ */
1163
+ private fileStatOf;
1164
+ /** 对象名单快照(每个捕获回合一次 readdir);读不到则返回 null ⇒ 调用方回落逐条判断 */
1165
+ private objectNamesSnapshot;
873
1166
  private pathIdentityOf;
874
1167
  private pathCachePath;
875
1168
  /**
@@ -878,6 +1171,20 @@ export declare class RollbackPointEngine {
878
1171
  */
879
1172
  private loadPathCache;
880
1173
  /** 缓存命中:身份键存在、且路径逐字相同(身份被复用也绝不认)。 */
1174
+ /**
1175
+ * 「这条记录的当前内容**仍然是它**吗」的身份快路径 —— **预览与执行共用同一条判据**
1176
+ * (否则「预览说会改的 = 实际会改的」这条不变式会被打破)。
1177
+ *
1178
+ * 判据:路径身份(dev:ino:size:mtimeNs:ctimeNs:mode)命中**身份缓存**,且缓存里的 sha/bytes 与记录一致。
1179
+ * 身份未变 ⇒ 内容未变:缓存里的 sha 就是**这份内容**在留点那一次真读出来的哈希(身份含 size/mtimeNs/
1180
+ * ctimeNs/ino/dev/mode,改内容必动 mtime/ctime,Windows 上 user 代码改不了 ctime)。
1181
+ * 任何一环拿不到(读不到身份 / 缓存没有该身份 / sha 或 bytes 不同)⇒ false,调用方回落「真读 + 哈希」。
1182
+ *
1183
+ * 为什么必须有它:大仓库一次预览/回退要判 3 万+ 条目,逐个真读等于把整个工作区读两遍
1184
+ * (真机 34k 文件 / 609 MiB:预览 14.2 s 里 13 s 全在这,回退 60 s+ 同理)。**记录的指纹被篡改**
1185
+ * 时本判据必然不成立(缓存的 sha 是真算出来的)⇒ 仍走真读 + 哈希的旧路径,护栏不退化。
1186
+ */
1187
+ private unchangedByIdentity;
881
1188
  private lookupCachedFile;
882
1189
  private rememberCachedFile;
883
1190
  /** 缓存落盘(best-effort:写失败只意味着下次全量读,绝不影响留点成败) */
@@ -889,7 +1196,19 @@ export declare class RollbackPointEngine {
889
1196
  private gcPathCache;
890
1197
  /** 拒删成因的如实说明(没有可解释成因时返回空数组,绝不编理由) */
891
1198
  private createdRefusalWarnings;
892
- private readLedger;
1199
+ /**
1200
+ * 读台账**清单**(只读元数据 + 段摘要;代价与工作区文件数**无关**)。
1201
+ *
1202
+ * 与 v2 的差别只在实现:v2 每回合把整份 index.json 读进内存并 parse(473 MB 那条路),
1203
+ * v3 读的是一份小清单。**外部语义一个字没变**:返回 entries + 同一套 error 文案。
1204
+ * 启动期初始化失败时如实回报(不读 v2 文件、不假装台账为空)。
1205
+ */
1206
+ private readManifest;
1207
+ /**
1208
+ * 读**完整**记录(引用解析对调用方透明)。失败一律如实回报,**绝不返回半个记录**:
1209
+ * 载荷缺失 / 哈希不符 / 形状不符 ⇒ error(调用方走 record-incomplete 路径)。
1210
+ */
1211
+ private readFullRecord;
893
1212
  /**
894
1213
  * S6 淘汰(t14 冻结 + 2026-10-12 guard 全局上限):三类点各按各的预算裁,**只删最旧的**。
895
1214
  *
@@ -899,13 +1218,64 @@ export declare class RollbackPointEngine {
899
1218
  * | guard 救援点 | 每会话 `maxGuardsPerSession`(20)→ 全局 `maxGuards`(`ROLLBACK_MAX_GUARDS`,50) | **不占**自动点预算、**不受**自动点的 20/50 影响;用户删不掉 ⇒ 必须有自己的两道预算(单会话挡住不够,跨会话也要收敛) |
900
1219
  * | protected 点 | 无上限(只进不出) | 生产路径**造不出来**(capture 端点已下线、tracker 从不设 protect),只有测试能造 |
901
1220
  */
902
- private selectPrune;
1221
+ /**
1222
+ * 条数淘汰(口径与 v2 逐字相同,只是从「记录」换成「清单条目」):三类点各按各的预算裁,**只删最旧的**。
1223
+ *
1224
+ * | 类别 | 预算 | 说明 |
1225
+ * |---|---|---|
1226
+ * | auto 自动点 | 每会话 maxRecordsPerSession(20)→ 全局 maxRecords(50) | 两次裁剪都只删最旧 |
1227
+ * | guard 救援点 | 每会话 maxGuardsPerSession(20)→ 全局 maxGuards(ROLLBACK_MAX_GUARDS,50) | **不占**自动点预算、**不受**自动点的 20/50 影响;用户删不掉 ⇒ 必须有自己的两道预算(单会话挡住不够,跨会话也要收敛) |
1228
+ * | protected 点 | 无上限(只进不出) | 生产路径**造不出来**(capture 端点已下线、tracker 从不设 protect),只有测试能造 |
1229
+ */
1230
+ private selectPruneEntries;
1231
+ /**
1232
+ * 字节预算淘汰(issue #89 的**根因修复**):条数没超、字节超了照样会 OOM。
1233
+ *
1234
+ * 判据与条数淘汰共用同一类别口径:**只淘汰最旧的 auto 点**(guard / protected 永不淘汰)。
1235
+ * 释放量按「记录文件字节 + 载荷**最后一个引用**消失时的载荷字节」估算(引用计数靠清单,不读载荷)。
1236
+ * 返回淘汰后的清单(调用方负责落盘与删文件)。
1237
+ */
1238
+ private applyByteBudget;
1239
+ /**
1240
+ * 对象仓回收(**重写版**,issue #89):账本字节数不再进内存。
1241
+ *
1242
+ * 旧实现要从「内存里所有记录的全部 files 数组」累计引用 ⇒ 回收一次 = 把整份台账读进内存。
1243
+ * 现在:扫**清单** -> 逐个读引用到的**载荷文件**(一次只驻留一份)-> 累计 referenced 对象键 -> 扫 objects/。
1244
+ * 峰值内存 ∝ 单份载荷 + 对象键集合,**不 ∝ 台账总字节**。
1245
+ * 任何一份被引用的载荷读不出来 ⇒ 整体放弃回收(fail-closed:读不到的载荷里有什么对象谁也不知道)。
1246
+ */
903
1247
  private gcObjects;
904
1248
  /**
905
1249
  * 把记录写进台账(**留点侧唯一写入口**):整段「读全文 → 合并 → S6 淘汰 → 原子写 → 回收对象」
906
1250
  * 都在 {@link withLedgerWriteLock} 的进程内串行队列里 —— 绝不与 tracker / 路由的另一次写交错。
907
1251
  */
1252
+ /**
1253
+ * 写一条记录(**最多三次尝试**)—— 跨进程乐观并发(①B)。
1254
+ *
1255
+ * dataDir 由 `$DSH_HOME` 决定、**不随 profile 分家**(与本插件自带的实例锁同一套多实例假设),
1256
+ * 所以「另一个 DSH 实例正在写同一份清单」是设计内的场景,而 `withLedgerWriteLock` 只串行本进程。
1257
+ * 策略:读清单前后各取一次**代际戳**,被改过就重读重试;三次都变 ⇒ **放弃本次留点并 warn**,
1258
+ * 绝不拿自己的快照覆盖别人的清单(宁可少一条点,也不丢别人的点)。
1259
+ * 诚实的边界:检测与写入之间仍有极小窗口,无跨进程锁时无法彻底消除 —— 这里保证的是
1260
+ * 「不静默覆盖 + 尽量合并」,不是原子性。
1261
+ */
1262
+ /**
1263
+ * 把工作区载荷改写成**增量视图**(相对最近一个同分块的点)——清单增量化,省的是磁盘,不是语义。
1264
+ *
1265
+ * 判据与安全边界(逐条):
1266
+ * · 基准 = 清单里最近一个 `workspace.payload` 非空、**分块完全相同**的点(只试最近几个,代价有界);
1267
+ * · 增量 = 「相对基准新增 / 改写的条目」+「基准里不再存在的路径」,条目**逐字比较**(同形绝对路径);
1268
+ * · 只有增量明显更小(< 全量的 60%)才用;否则原样全量落盘;
1269
+ * · 读路径由 ledger 物化(`resolvePayload`)⇒ 预览 / 回退 / 回收看到的永远是完整清单;
1270
+ * · 基准读不出来 / 分块对不上 / 任何异常 ⇒ 原样全量(绝不因为省字节而让点不可用);
1271
+ * · 基准载荷的保活由 ledger 的 `payloadChain` 负责(淘汰时不会把基准删掉)。
1272
+ */
1273
+ private storagePlanFor;
1274
+ /** 记下这份点作为下一次留点的增量基准(内存摘要,见 {@link deltaBase}) */
1275
+ private rememberDeltaBase;
908
1276
  private appendRecord;
1277
+ /** `appendRecord` 的单次尝试;`conflict: true` = 这期间清单被别的实例改过,调用方应当重试。 */
1278
+ private appendRecordOnce;
909
1279
  /**
910
1280
  * 留一个回滚点(宿主自动留点 / 恢复前救援点 / 测试构造都走这一条)。
911
1281
  * 没有 HTTP capture 端点 —— 手动「拍点」已随规格 S1 下线。
@@ -959,6 +1329,11 @@ export declare class RollbackPointEngine {
959
1329
  error?: string;
960
1330
  storage: RollbackStorageStatus;
961
1331
  }>;
1332
+ /**
1333
+ * 预览的**单条只读判定**(阶段 A,可并发跑):一次 bigint lstat + 身份快路径。
1334
+ * `unchanged = true` 表示「已证明与记录一致」(身份命中身份缓存且 sha/bytes 相同)⇒ 调用方直接跳过。
1335
+ */
1336
+ private previewScan;
962
1337
  private trackWarnings;
963
1338
  /**
964
1339
  * 「证明为留点之后新建」的完整规划 —— 预览(`preview`)与执行(`restoreWorkspace`)**共用这一份**。
@@ -1047,6 +1422,30 @@ export declare class RollbackPointEngine {
1047
1422
  * 会在本方法的读与写之间插入一次 append,把刚删掉的点**复活**(评审 20 轮实测 12 轮复现)。
1048
1423
  */
1049
1424
  remove(id: string): Promise<RollbackRemoveResult>;
1425
+ /**
1426
+ * 磁盘清理入口(引擎侧):删**全部**自动点(includeGuards=true 时连 guard 也删),
1427
+ * protected 点**永不删**(生产路径造不出来,只有测试能造)。随后回收无人引用的载荷与对象。
1428
+ *
1429
+ * 零写入拒绝路径照旧:存储不可用 / 清单读不出来(形状不符、迁移失败、迁移中)⇒ **抛带 code 的错误**,
1430
+ * 一个字节都不动、也绝不静默返回全 0(「什么都没删」与「读不出来所以没删」是两件事)。
1431
+ * 返回的 bytes = 本次真正删掉的字节(记录文件 + 载荷文件 + 对象文件)。
1432
+ */
1433
+ purgeLedger(options?: {
1434
+ includeGuards?: boolean;
1435
+ }): Promise<{
1436
+ removedRecords: number;
1437
+ removedPayloads: number;
1438
+ removedObjects: number;
1439
+ bytes: number;
1440
+ }>;
1441
+ }
1442
+ /**
1443
+ * 台账清理的**显式拒绝**(形状不符 / 存储不可用):带机器可读 code,绝不静默返回全 0。
1444
+ * 路由层按 code 映射 HTTP(与既有 RollbackCode 冻结集同源)。
1445
+ */
1446
+ export declare class RollbackLedgerRefusedError extends Error {
1447
+ readonly code: RollbackCode;
1448
+ constructor(code: RollbackCode, detail: string);
1050
1449
  }
1051
1450
  /** 会话登记(宿主订阅 session/event 时按会话登记一次)。 */
1052
1451
  export interface RollbackSessionBinding {
@@ -1167,6 +1566,8 @@ export declare class RollbackAutoTracker {
1167
1566
  private readonly lastStat;
1168
1567
  private timer;
1169
1568
  private mechanism;
1569
+ /** 在途捕获的取消信号(stop() 时 abort;重新 start() 时换一个新的) */
1570
+ private captureAbort;
1170
1571
  constructor(options: RollbackTrackerOptions);
1171
1572
  /** 实际采用的留点机制(output / 诊断用) */
1172
1573
  get mechanismKind(): 'event' | 'poll';