dsh-config-manager 0.1.78 → 0.1.79

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 (290) hide show
  1. package/lib/adapters/credentials.d.ts +6 -2
  2. package/lib/adapters/credentials.js +19 -3
  3. package/lib/adapters/file-collection.js +2 -1
  4. package/lib/adapters/mcp.d.ts +48 -2
  5. package/lib/adapters/mcp.js +265 -3
  6. package/lib/adapters/messages.d.ts +3 -0
  7. package/lib/adapters/messages.js +7 -0
  8. package/lib/adapters/plugin-files.js +2 -1
  9. package/lib/adapters/plugins.js +105 -20
  10. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  11. package/lib/adapters/pnpm-workspace.js +113 -2
  12. package/lib/adapters/prompts.d.ts +6 -2
  13. package/lib/adapters/prompts.js +17 -2
  14. package/lib/adapters/providers.d.ts +19 -2
  15. package/lib/adapters/providers.js +59 -3
  16. package/lib/adapters/settings.d.ts +6 -2
  17. package/lib/adapters/settings.js +13 -2
  18. package/lib/adapters/skills.d.ts +25 -1
  19. package/lib/adapters/skills.js +60 -0
  20. package/lib/adapters/ui.d.ts +6 -2
  21. package/lib/adapters/ui.js +13 -2
  22. package/lib/adapters/units.d.ts +18 -0
  23. package/lib/adapters/units.js +39 -0
  24. package/lib/adapters/workspaces.js +2 -1
  25. package/lib/client.d.ts +400 -112
  26. package/lib/client.js +2845 -1524
  27. package/lib/core/backup.js +20 -0
  28. package/lib/core/mcp-connector-store.d.ts +159 -0
  29. package/lib/core/mcp-connector-store.js +445 -0
  30. package/lib/core/messages-core.d.ts +2 -0
  31. package/lib/core/messages-core.js +4 -0
  32. package/lib/core/messages.d.ts +20 -0
  33. package/lib/core/messages.js +44 -0
  34. package/lib/core/phase3-host.js +18 -1
  35. package/lib/core/plugin-cli.d.ts +22 -2
  36. package/lib/core/plugin-cli.js +22 -3
  37. package/lib/core/recovery-orchestrator.d.ts +3 -1
  38. package/lib/core/recovery-orchestrator.js +75 -16
  39. package/lib/core/rollback-activity.d.ts +89 -0
  40. package/lib/core/rollback-activity.js +350 -0
  41. package/lib/core/rollback-ledger.d.ts +0 -9
  42. package/lib/core/rollback-ledger.js +37 -0
  43. package/lib/core/rollback-merge.d.ts +22 -2
  44. package/lib/core/rollback-merge.js +63 -13
  45. package/lib/core/rollback-point.d.ts +93 -0
  46. package/lib/core/rollback-point.js +420 -101
  47. package/lib/core/rollback-undo-journal.d.ts +123 -0
  48. package/lib/core/rollback-undo-journal.js +517 -0
  49. package/lib/core/self-update.js +2 -4
  50. package/lib/core/text-fidelity.d.ts +89 -0
  51. package/lib/core/text-fidelity.js +176 -0
  52. package/lib/core/types.d.ts +12 -3
  53. package/lib/core/update-check.js +2 -4
  54. package/lib/core/validator.d.ts +9 -0
  55. package/lib/core/validator.js +12 -0
  56. package/lib/foreign/bundle.js +42 -3
  57. package/lib/foreign/claude-code.js +358 -26
  58. package/lib/foreign/claude-sessions.d.ts +51 -8
  59. package/lib/foreign/claude-sessions.js +111 -13
  60. package/lib/foreign/read-claude-code.d.ts +33 -4
  61. package/lib/foreign/read-claude-code.js +446 -65
  62. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  63. package/lib/foreign/read-claude-plugins.js +403 -0
  64. package/lib/foreign/registry.js +25 -2
  65. package/lib/foreign/truth-table.js +6 -4
  66. package/lib/foreign/types.d.ts +170 -2
  67. package/lib/index.d.ts +14 -5
  68. package/lib/index.js +60 -16
  69. package/lib/market/git-file-writer.js +7 -1
  70. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  71. package/lib/profiles/dsh-profile-launcher.js +58 -1
  72. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  73. package/lib/profiles/dsh-profile-shared.js +8 -2
  74. package/lib/routes/foreign.js +8 -1
  75. package/lib/routes/index.d.ts +1 -1
  76. package/lib/routes/kit.d.ts +2 -2
  77. package/lib/routes/sync.js +173 -9
  78. package/lib/schema/manifest.d.ts +6 -1
  79. package/lib/schema/manifest.js +55 -0
  80. package/lib/schema/types.d.ts +47 -2
  81. package/lib/sync/autosync-config.d.ts +73 -0
  82. package/lib/sync/autosync-config.js +89 -0
  83. package/lib/sync/autosync-scheduler.d.ts +80 -6
  84. package/lib/sync/autosync-scheduler.js +263 -40
  85. package/lib/sync/gist/gist-transport.d.ts +15 -1
  86. package/lib/sync/gist/gist-transport.js +66 -5
  87. package/lib/sync/gist/messages.d.ts +3 -0
  88. package/lib/sync/gist/messages.js +7 -0
  89. package/lib/sync/git/git-transport.d.ts +42 -3
  90. package/lib/sync/git/git-transport.js +146 -3
  91. package/lib/sync/merge.d.ts +47 -14
  92. package/lib/sync/merge.js +242 -222
  93. package/lib/sync/messages.d.ts +8 -0
  94. package/lib/sync/messages.js +19 -0
  95. package/lib/sync/risk.d.ts +19 -2
  96. package/lib/sync/risk.js +10 -3
  97. package/lib/sync/s3/messages.d.ts +6 -0
  98. package/lib/sync/s3/messages.js +13 -0
  99. package/lib/sync/s3/s3-transport.d.ts +9 -1
  100. package/lib/sync/s3/s3-transport.js +78 -6
  101. package/lib/sync/sync-engine.d.ts +193 -4
  102. package/lib/sync/sync-engine.js +450 -37
  103. package/lib/sync/sync-history.d.ts +11 -0
  104. package/lib/sync/sync-history.js +20 -0
  105. package/lib/sync/sync-state.d.ts +43 -0
  106. package/lib/sync/sync-state.js +44 -0
  107. package/lib/sync/sync-watcher.d.ts +76 -0
  108. package/lib/sync/sync-watcher.js +202 -0
  109. package/lib/sync/text-merge.d.ts +85 -0
  110. package/lib/sync/text-merge.js +513 -0
  111. package/lib/sync/transport.d.ts +48 -0
  112. package/lib/sync/transport.js +12 -0
  113. package/lib/sync/watch-scope.d.ts +38 -0
  114. package/lib/sync/watch-scope.js +95 -0
  115. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  116. package/lib/sync/webdav/webdav-transport.js +128 -11
  117. package/lib/ui/foreign-view.js +10 -0
  118. package/lib/ui/i18n.d.ts +8 -0
  119. package/lib/ui/i18n.js +20 -0
  120. package/lib/ui/rollback-view.d.ts +12 -0
  121. package/lib/ui/rollback-view.js +2 -0
  122. package/lib/ui/sync-settings-view.d.ts +47 -0
  123. package/lib/ui/sync-settings-view.js +24 -0
  124. package/lib/ui/types.d.ts +21 -0
  125. package/lib/utils/env-lock.d.ts +1 -0
  126. package/lib/utils/env-lock.js +7 -4
  127. package/package.json +6 -6
  128. package/src/adapters/credentials.ts +22 -4
  129. package/src/adapters/file-collection.ts +2 -1
  130. package/src/adapters/mcp.test.ts +235 -1
  131. package/src/adapters/mcp.ts +276 -5
  132. package/src/adapters/messages.ts +7 -0
  133. package/src/adapters/plugin-files.ts +2 -1
  134. package/src/adapters/plugins.test.ts +176 -2
  135. package/src/adapters/plugins.ts +107 -21
  136. package/src/adapters/pnpm-workspace.test.ts +89 -1
  137. package/src/adapters/pnpm-workspace.ts +127 -2
  138. package/src/adapters/prompts.ts +20 -3
  139. package/src/adapters/providers.ts +62 -4
  140. package/src/adapters/settings.ts +16 -3
  141. package/src/adapters/skills.test.ts +141 -0
  142. package/src/adapters/skills.ts +58 -2
  143. package/src/adapters/ui.ts +16 -3
  144. package/src/adapters/units-entries.test.ts +190 -0
  145. package/src/adapters/units.ts +50 -0
  146. package/src/adapters/workspaces.ts +2 -1
  147. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  148. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  149. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  150. package/src/client/common/Skeleton.tsx +32 -0
  151. package/src/client/common/routes.ts +9 -0
  152. package/src/client/config-manager.module.css +321 -11
  153. package/src/client/home/HomePanel.tsx +14 -6
  154. package/src/client/import/ForeignImportView.tsx +3 -1
  155. package/src/client/import/import-wizard-steps.tsx +2 -1
  156. package/src/client/library/ArtifactRow.tsx +12 -2
  157. package/src/client/library/LibraryActions.tsx +6 -3
  158. package/src/client/library/LibraryPanel.tsx +21 -3
  159. package/src/client/library/RestorePlanView.tsx +4 -2
  160. package/src/client/locales.ts +3 -0
  161. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  162. package/src/client/recovery/RollbackCard.tsx +7 -2
  163. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  164. package/src/client/recovery/recovery-locales.ts +20 -2
  165. package/src/client/recovery/recovery-view.test.ts +39 -1
  166. package/src/client/recovery/recovery-view.ts +23 -1
  167. package/src/client/run-store.test.ts +1 -1
  168. package/src/client/run-store.ts +4 -0
  169. package/src/client/sync/AutosyncCard.tsx +49 -6
  170. package/src/client/sync/MergeModal.tsx +219 -0
  171. package/src/client/sync/MergePreviewView.tsx +306 -0
  172. package/src/client/sync/SyncActivityBar.tsx +31 -0
  173. package/src/client/sync/SyncConfirmView.tsx +2 -1
  174. package/src/client/sync/SyncPanel.tsx +130 -87
  175. package/src/client/sync/SyncProbeRow.tsx +162 -0
  176. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  177. package/src/client/sync/WorkDirCard.tsx +44 -6
  178. package/src/client/sync/history-model.ts +4 -1
  179. package/src/client/sync/merge-view.test.ts +234 -0
  180. package/src/client/sync/merge-view.ts +371 -0
  181. package/src/client/sync/probe-view.test.ts +90 -0
  182. package/src/client/sync/probe-view.ts +63 -0
  183. package/src/client/sync/remote-device-view.test.ts +35 -0
  184. package/src/client/sync/remote-device-view.ts +25 -0
  185. package/src/client/sync/sync-api.ts +212 -2
  186. package/src/client/sync/sync-entry.test.ts +78 -0
  187. package/src/client/sync/sync-entry.ts +53 -0
  188. package/src/client/sync/sync-locales.ts +234 -2
  189. package/src/client/sync/sync-view.test.ts +18 -1
  190. package/src/client/sync/sync-view.ts +20 -0
  191. package/src/core/backup.test.ts +41 -0
  192. package/src/core/backup.ts +20 -0
  193. package/src/core/mcp-connector-store.test.ts +265 -0
  194. package/src/core/mcp-connector-store.ts +516 -0
  195. package/src/core/messages-core.ts +4 -0
  196. package/src/core/messages.ts +46 -0
  197. package/src/core/phase3-host.ts +18 -1
  198. package/src/core/phase3-production-integration.test.ts +4 -0
  199. package/src/core/plugin-cli.test.ts +26 -0
  200. package/src/core/plugin-cli.ts +30 -4
  201. package/src/core/recovery-orchestrator.test.ts +104 -5
  202. package/src/core/recovery-orchestrator.ts +74 -17
  203. package/src/core/rollback-activity.test.ts +311 -0
  204. package/src/core/rollback-activity.ts +367 -0
  205. package/src/core/rollback-cross-session.test.ts +694 -0
  206. package/src/core/rollback-interrupted.test.ts +137 -0
  207. package/src/core/rollback-ledger.ts +32 -0
  208. package/src/core/rollback-merge.test.ts +130 -0
  209. package/src/core/rollback-merge.ts +78 -14
  210. package/src/core/rollback-point.ts +468 -91
  211. package/src/core/rollback-undo-journal.test.ts +508 -0
  212. package/src/core/rollback-undo-journal.ts +571 -0
  213. package/src/core/self-update.ts +2 -5
  214. package/src/core/text-fidelity.test.ts +189 -0
  215. package/src/core/text-fidelity.ts +197 -0
  216. package/src/core/types.ts +11 -3
  217. package/src/core/update-check.ts +2 -5
  218. package/src/core/validator.ts +14 -0
  219. package/src/foreign/bundle-origin.test.ts +265 -0
  220. package/src/foreign/bundle.ts +45 -5
  221. package/src/foreign/claude-code.test.ts +268 -0
  222. package/src/foreign/claude-code.ts +406 -25
  223. package/src/foreign/claude-plugins.test.ts +234 -0
  224. package/src/foreign/claude-sessions.test.ts +141 -0
  225. package/src/foreign/claude-sessions.ts +179 -23
  226. package/src/foreign/file-budget.test.ts +4 -1
  227. package/src/foreign/read-claude-code.test.ts +320 -0
  228. package/src/foreign/read-claude-code.ts +490 -71
  229. package/src/foreign/read-claude-plugins.ts +465 -0
  230. package/src/foreign/registry.ts +25 -2
  231. package/src/foreign/session-ir.test.ts +65 -0
  232. package/src/foreign/truth-table.test.ts +81 -0
  233. package/src/foreign/truth-table.ts +6 -3
  234. package/src/foreign/types.ts +162 -2
  235. package/src/index.facade.test.ts +26 -4
  236. package/src/index.ts +76 -19
  237. package/src/market/git-file-writer.test.ts +4 -0
  238. package/src/market/git-file-writer.ts +7 -1
  239. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  240. package/src/profiles/dsh-profile-launcher.ts +56 -1
  241. package/src/profiles/dsh-profile-shared.ts +8 -2
  242. package/src/routes/foreign.ts +8 -1
  243. package/src/routes/index.ts +1 -1
  244. package/src/routes/kit.ts +2 -2
  245. package/src/routes/sync.ts +176 -9
  246. package/src/schema/manifest-schema.test.ts +87 -2
  247. package/src/schema/manifest.ts +57 -1
  248. package/src/schema/types.ts +45 -2
  249. package/src/sync/autosync-config.test.ts +73 -1
  250. package/src/sync/autosync-config.ts +124 -0
  251. package/src/sync/autosync-risk-tier.test.ts +38 -2
  252. package/src/sync/autosync-scheduler.test.ts +264 -12
  253. package/src/sync/autosync-scheduler.ts +296 -44
  254. package/src/sync/gist/gist-conditional-write.test.ts +223 -0
  255. package/src/sync/gist/gist-transport.ts +73 -7
  256. package/src/sync/gist/messages.ts +7 -0
  257. package/src/sync/git/git-probe.test.ts +80 -0
  258. package/src/sync/git/git-transport.test.ts +185 -0
  259. package/src/sync/git/git-transport.ts +178 -5
  260. package/src/sync/merge.test.ts +188 -55
  261. package/src/sync/merge.ts +282 -224
  262. package/src/sync/messages.ts +19 -0
  263. package/src/sync/risk.ts +29 -5
  264. package/src/sync/s3/messages.ts +13 -0
  265. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  266. package/src/sync/s3/s3-transport.ts +87 -9
  267. package/src/sync/sync-device-identity.test.ts +267 -0
  268. package/src/sync/sync-engine.test.ts +379 -0
  269. package/src/sync/sync-engine.ts +535 -36
  270. package/src/sync/sync-history.test.ts +16 -1
  271. package/src/sync/sync-history.ts +26 -0
  272. package/src/sync/sync-state.test.ts +64 -0
  273. package/src/sync/sync-state.ts +79 -0
  274. package/src/sync/sync-watcher.test.ts +197 -0
  275. package/src/sync/sync-watcher.ts +246 -0
  276. package/src/sync/text-merge.test.ts +358 -0
  277. package/src/sync/text-merge.ts +554 -0
  278. package/src/sync/transport.ts +57 -0
  279. package/src/sync/watch-scope.test.ts +122 -0
  280. package/src/sync/watch-scope.ts +120 -0
  281. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  282. package/src/sync/webdav/webdav-transport.ts +137 -14
  283. package/src/ui/foreign-view.test.ts +34 -0
  284. package/src/ui/foreign-view.ts +10 -0
  285. package/src/ui/i18n.ts +20 -0
  286. package/src/ui/rollback-view.ts +14 -0
  287. package/src/ui/sync-settings-view.test.ts +28 -1
  288. package/src/ui/sync-settings-view.ts +43 -0
  289. package/src/ui/types.ts +22 -0
  290. package/src/utils/env-lock.ts +8 -5
@@ -26,6 +26,19 @@
26
26
  * 本文件**刻意不出现在 file-budget 的 import 白名单里**:它 import 了 `node:fs` 的**类型**用于
27
27
  * 构造只读句柄的回调(`ClaudeReadHandle`,追加能力通道),这是**零运行期 I/O** 的类型依赖。
28
28
  * 守卫按运行时性质放行(见 file-budget.test.ts 的白名单说明)。
29
+ *
30
+ * 会话保真修复(2026-10-18,task-4;四条都在**本文件内**收口,共享合成器 session-ir.ts 一字未改):
31
+ * ① **幂等时间**:墙钟回落只有一个确定性来源(`resolveWallClock`:显式 `options.now` → 记录自身
32
+ * 最早时间戳 → `input.createdAtMs`),只有全都缺失才回落 `Date.now()` 并在
33
+ * `info.counts.timestampsFallback` **如实记账**。旧实现 parse 与 synthesize 各调一次
34
+ * `Date.now()` ⇒ 同一来源两次转码字节必然不同 ⇒ DSH 侧「同内容 → Skip」永不成立。
35
+ * ② **isSidechain**:真实 Claude 子代理记录(`type=user|assistant` + `isSidechain:true`)**一律
36
+ * 不进 IR**,整类计报 `isSidechain`(旧实现零命中 → 子代理正文静默混进父对话)。
37
+ * ③ **sessionId 不一致**:`input.recordSessionId` 存在且 ≠ `input.id` → 非致命发现
38
+ * `session-id-mismatch`(会话 id 仍以文件名 stem 为准,见 `TranscodeResult.findings`)。
39
+ * ④ **推理块**(Claude Code 的 `thinking` / `redacted_thinking`):共享 IR 没有 reasoning
40
+ * 承载位(docs/spec/known-gaps.md F-01)→ 计报 `thinking`,**绝不伪装成正文**;非 Claude 原生的
41
+ * `{type:'reasoning',text:…}` 刻意保持既有口径(理由与切换方式见 `REASONING_BLOCK_TYPES`)。
29
42
  */
30
43
  import type { Stats } from 'node:fs';
31
44
 
@@ -54,8 +67,11 @@ import type {
54
67
  IrMessage,
55
68
  IrParseStats,
56
69
  IrSession,
70
+ IrTimeMs,
57
71
  SynthesisStats,
58
72
  } from './session-ir.ts';
73
+ // types.ts 只被**类型**引用(lead 冻结的输入形态 + 契约码),不做任何值导入
74
+ import type { ClaudeSessionInput, ForeignSkip } from './types.ts';
59
75
 
60
76
  export type { DshSessionHeader, DshSessionRow } from './session-ir.ts';
61
77
 
@@ -78,12 +94,14 @@ const CLAUDE_PROVIDER = 'claude-code';
78
94
  const CLAUDE_REASONING_EFFORT = 'medium';
79
95
  const CLAUDE_MAX_TOKENS = 8192;
80
96
 
81
- export interface ClaudeSessionFile {
82
- /** 文件名(不含 .jsonl)——同时作为 DSH 侧的会话 id(DSH 要求目录名与 header.id 逐字相同) */
83
- id: string;
84
- /** 文件原文 */
85
- text: string;
86
- }
97
+ /**
98
+ * 待转码的 Claude 会话文件。
99
+ *
100
+ * **等价于 `types.ts` 的 `ClaudeSessionInput`**(2026-10-18 起不再重复声明):读盘层额外给的
101
+ * `recordSessionId`(≠ 文件名 stem 时报 session-id-mismatch)与 `createdAtMs`(确定性 createdAt
102
+ * 回落值,见 `resolveWallClock`)因此天然可达本层,而 `{ id, text }` 的老调用方一字不用改。
103
+ */
104
+ export type ClaudeSessionFile = ClaudeSessionInput;
87
105
 
88
106
  /**
89
107
  * **只读**的文件读取面(additive,2026-10):**内部**用它支持「大文件只读头尾」的追加通道。
@@ -109,8 +127,20 @@ export interface ClaudeSessionInfo {
109
127
  assistantMessages: number;
110
128
  toolCalls: number;
111
129
  toolResults: number;
112
- /** 未迁移的记录类型 → 条数(sidechain / attachment / queue-operation / system …) */
130
+ /**
131
+ * 未迁移的记录类型 / 内容块 → 条数(attachment / queue-operation / system / isSidechain /
132
+ * thinking / unparsable …)。**每个键在下游都会被翻成 `unsupported-session-record` 的 detail**
133
+ * (kernel.collectSessionSections),所以这里只放「这条内容本版不迁移」这一件事。
134
+ */
113
135
  ignored: Record<string, number>;
136
+ /**
137
+ * 结算类记账(**刻意不进 `ignored`**:那里的键会被翻成 `unsupported-session-record`,语义不符)。
138
+ *
139
+ * 目前只有 `timestampsFallback`(0/1):记录里与 `input.createdAtMs` **都没有**可用时间戳 ⇒
140
+ * 本次转码只能回落墙钟 `Date.now()`,**同一来源两次转码的字节可能不同**(DSH 侧「同内容 → Skip」
141
+ * 因此不成立、重复导入会判 Conflict)。这是必须如实报出来的不确定性,不是错误。
142
+ */
143
+ counts: Record<string, number>;
114
144
  }
115
145
 
116
146
  export interface TranscodedSession {
@@ -125,7 +155,12 @@ export interface TranscodedSession {
125
155
  export interface TranscodeOptions {
126
156
  /** 目标机 DSH 的 SESSION_FORMAT_VERSION(必填) */
127
157
  formatVersion: number;
128
- /** 时间兜底(记录里没有可解析时间时用) */
158
+ /**
159
+ * 墙钟回落的**最高优先级**注入点(测试 / 调用方显式给定)。
160
+ *
161
+ * 缺省时按 `resolveWallClock` 的回落链取:记录自身最早时间戳 → `input.createdAtMs` →
162
+ * `Date.now()`(最后一级会在返回值里记 `timestampsFallback`)。非法值(非安全整数)视同没给。
163
+ */
129
164
  now?: number;
130
165
  }
131
166
 
@@ -142,13 +177,29 @@ export interface TranscodeSkip {
142
177
  export interface TranscodeResult {
143
178
  session?: TranscodedSession;
144
179
  skip?: TranscodeSkip;
180
+ /**
181
+ * **非致命**发现(会话照样产出):目前只有 `session-id-mismatch`(记录里的 sessionId 与文件名
182
+ * stem 不一致)。会话 id 仍以文件名 stem 为准,只有「不一致」这件事必须可见。
183
+ *
184
+ * 为什么放在结果上而不是 `info.ignored`:`ignored` 的键在下游一律被翻成
185
+ * `unsupported-session-record`(码固定),而本条要用契约里冻结的 `session-id-mismatch`。
186
+ * **调用方负责原样带出**(`skipped.push(...result.findings)`)—— 现有
187
+ * `kernel.collectSessionSections` 尚未读本字段(不在本次改动范围)。空数组 = 没有发现。
188
+ */
189
+ findings: ForeignSkip[];
145
190
  }
146
191
 
147
192
  /* ---------------- ① 解析:Claude 记录 → IR ---------------- */
148
193
 
149
194
  /** Claude 记录里可识别的块(原始形态) */
150
195
  interface ParsedBlock {
151
- readonly kind: 'text' | 'call' | 'result' | 'other';
196
+ /**
197
+ * `thinking` = 推理块(Claude Code 的 `thinking` / `redacted_thinking`):共享 IR 的 `IrBlock`
198
+ * **没有 reasoning 承载位**(docs/spec/known-gaps.md F-01),所以它既不进 IR 也不伪装成正文,
199
+ * 只**逐条计数**(ignored['thinking'])。判定口径与「`reasoning` 为何不在此列」见
200
+ * `REASONING_BLOCK_TYPES` 的注释。
201
+ */
202
+ readonly kind: 'text' | 'call' | 'result' | 'thinking' | 'other';
152
203
  readonly text: string;
153
204
  readonly id: string;
154
205
  readonly name: string;
@@ -161,12 +212,39 @@ function usageField(usage: unknown, key: string): unknown {
161
212
  return typeof usage === 'object' && usage !== null ? (usage as Record<string, unknown>)[key] : undefined;
162
213
  }
163
214
 
215
+ /**
216
+ * 推理块的类型名 = Claude Code **自己**写的两种:`thinking` 与 `redacted_thinking`。
217
+ *
218
+ * 为什么必须按类型名判定:`thinking` 块的正文在 **`thinking` 键**里(没有 `text` 字段),旧实现
219
+ * 沿「对象带 text 才算文本」的兜底把它变成一个**空文本块** —— 内容静默丢失,且那个空块还会多插入
220
+ * 一个换行。本仓其余 20+ 来源一律「推理不伪装成正文、只计数」(read-crush / read-opencode /
221
+ * read-qwen / read-pi / read-openclaw …),这里对齐同一口径;共享 IR 无 reasoning 承载位这件事
222
+ * 记在 docs/spec/known-gaps.md 的 F-01。
223
+ *
224
+ * **为什么不把 `{type:'reasoning', text:'…'}` 一并纳入**(如实记明,别当成漏掉):
225
+ * ① Claude Code 不写这种块(它是别的导出器的归一命名,各来源自己的解析器已各自计报);
226
+ * ② 共享 IR 的文本兜底分支(**对象带 `text` 就当文本块**)是**被逐字节回归钉住的既有行为**
227
+ * (见 `blocksOf` 的注释),而这个块恰好命中它 —— 一并丢掉会让
228
+ * `claude-sessions-bytes.test.ts` 的 `timestamps-numeric-and-unparsable` 一案从 728 字节
229
+ * 变成 722 字节(正文少一段 'r')。那是**用户可见**的正文变化,按本轮的硬约束(既有语料
230
+ * 逐字一致)不由「修推理块静默丢弃」顺手引入。
231
+ * ③ 要一并计报,改这一个数组即可,并同步更新该案基线(现行行为已被
232
+ * `claude-sessions.test.ts` 的「reasoning 形状」用例显式钉住)。
233
+ */
234
+ const REASONING_BLOCK_TYPES: readonly string[] = ['thinking', 'redacted_thinking'];
235
+
236
+ /** 推理块的占位(字段一律空:它不进 IR,只需被识别出来计数) */
237
+ function reasoningBlock(): ParsedBlock {
238
+ return { kind: 'thinking', text: '', id: '', name: '', input: undefined, isError: false };
239
+ }
240
+
164
241
  function blockOf(raw: unknown): ParsedBlock {
165
242
  const rec = typeof raw === 'object' && raw !== null && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined;
166
243
  if (rec === undefined) {
167
244
  return { kind: 'other', text: '', id: '', name: '', input: undefined, isError: false };
168
245
  }
169
246
  const rawType = irStr(rec['type']) ?? '';
247
+ if (REASONING_BLOCK_TYPES.includes(rawType)) return reasoningBlock();
170
248
  const id = irStr(rec['id']) ?? '';
171
249
  const name = irStr(rec['name']);
172
250
  const isText = rawType === 'text' || typeof rec['text'] === 'string';
@@ -287,6 +365,43 @@ interface ParsedClaudeSession {
287
365
  readonly cwd: string | undefined;
288
366
  readonly unparsable: number;
289
367
  readonly title: string;
368
+ /** `isSidechain === true` 的**记录条数**(子代理记录:不进 IR,由转码入口整类计报) */
369
+ readonly sidechain: number;
370
+ /** 内容块里的推理块条数(IR 无承载位:不进 IR,由转码入口整类计报) */
371
+ readonly thinking: number;
372
+ /** 本次解析**唯一**算出的墙钟回落值;转码入口必须复用它,绝不再调一次 Date.now() */
373
+ readonly now: number;
374
+ /** `now` 是否来自墙钟回落(记录与 `input.createdAtMs` 都没有可用时间戳) */
375
+ readonly wallClockFallback: boolean;
376
+ }
377
+
378
+ /**
379
+ * 墙钟回落的**唯一**来源(缺陷 1 的修复)。
380
+ *
381
+ * 为什么必须只有一处:会话产物的字节里带 createdAt / 各行各列的时间;只要有一次 `Date.now()`
382
+ * 参与,同一来源两次转码的字节就必然不同 ⇒ DSH 侧「同内容 → Skip」永不成立、重复导入恒判
383
+ * Conflict。旧实现 parse 与 synthesize **各调一次** `Date.now()`(返回值还可能是两个不同的毫秒)。
384
+ *
385
+ * 回落链(前三级都是确定性的:同一输入 → 同一产物):
386
+ * ① `options.now`(调用方/测试显式给定;**保持最高优先级**,既有字节回归靠它稳定);
387
+ * ② 记录自身的最早可解析时间戳(会话文件里最权威的确定性来源);
388
+ * ③ `input.createdAtMs`(读盘层给的回落值:最早可解析记录时间,没有就给文件 mtime);
389
+ * ④ 都没有 → `Date.now()`,并**如实记账**(`timestampsFallback`)。
390
+ *
391
+ * 非法值(非安全整数 / 非有限数)一律当「没给」:DSH 对时间字段要求安全整数,宁可继续回落也
392
+ * 不写出一条会被拒读的行。
393
+ */
394
+ function resolveWallClock(
395
+ file: ClaudeSessionFile,
396
+ options: TranscodeOptions,
397
+ earliest: IrTimeMs | undefined,
398
+ ): { readonly now: number; readonly fallback: boolean } {
399
+ const explicit = irSafeTime(options.now);
400
+ if (explicit !== undefined) return { now: explicit, fallback: false };
401
+ if (earliest !== undefined) return { now: earliest, fallback: false };
402
+ const given = irSafeTime(file.createdAtMs);
403
+ if (given !== undefined) return { now: given, fallback: false };
404
+ return { now: Date.now(), fallback: true };
290
405
  }
291
406
 
292
407
  /**
@@ -301,35 +416,47 @@ function parseClaudeSessionText(file: ClaudeSessionFile, options: TranscodeOptio
301
416
 
302
417
  let cwd: string | undefined;
303
418
  let createdAt: number | undefined;
419
+ let sidechain = 0;
304
420
  for (const r of records) {
421
+ // 子代理记录(真实 Claude 的子代理对话就是 type=user/assistant + isSidechain:true):
422
+ // 它们**不是父对话的回合**,混进来会静默改写父对话正文(设计文档 2026-10-04-foreign-import-v1.md:89
423
+ // 「不迁移 isSidechain 的子代理会话」,而旧实现连计数都没有)。
424
+ // 它们仍参与 cwd / 最早时间:子代理与父对话同属一份日志文件,cwd 与时间线是**文件级**事实。
425
+ if (r['isSidechain'] === true) sidechain++;
305
426
  if (cwd === undefined) cwd = irStr(r['cwd']);
306
427
  createdAt = irEarlier(createdAt, irSafeTime(r['timestamp']));
307
428
  }
429
+ // 墙钟回落**只在这里解析一次**,转码入口复用 `now`(同一输入一次调用内不可能出现两个 Date.now())
430
+ const wall = resolveWallClock(file, options, createdAt);
308
431
 
309
432
  const messages: IrMessage[] = [];
310
433
  let messageIndex = 0;
434
+ let thinking = 0;
311
435
  let firstUserText = '';
312
436
  for (const rec of records) {
313
437
  const type = typeOf(rec);
314
438
  if (type !== 'user' && type !== 'assistant') continue;
439
+ if (rec['isSidechain'] === true) continue;
315
440
  const blocks = blocksOf(rec['message']);
316
- const message = irMessageOf(rec, blocks, messageIndex);
441
+ // 推理块:先逐条计数,再**整块丢掉**(既不进 IR,也不留空文本块 —— 空块还会污染正文的换行串联)
442
+ thinking += blocks.filter((b) => b.kind === 'thinking').length;
443
+ const carried = blocks.filter((b) => b.kind !== 'thinking');
444
+ const message = irMessageOf(rec, carried, messageIndex);
317
445
  if (message === undefined) continue;
318
446
  messages.push(message);
319
447
  // 消息 id 的兜底序 = 「第几条已知消息」(重构前的序号语义,逐字节回归钉住)
320
448
  messageIndex++;
321
449
  if (type === 'user' && firstUserText === '') {
322
- const text = irTextOfBlocks(blocks.map(irBlockOf));
450
+ const text = irTextOfBlocks(carried.map(irBlockOf));
323
451
  if (text.trim() !== '') firstUserText = text;
324
452
  }
325
453
  }
326
454
 
327
- const now = options.now ?? Date.now();
328
455
  return {
329
456
  session: {
330
457
  id: file.id,
331
458
  cwd: cwd ?? '',
332
- createdAt: createdAt ?? now,
459
+ createdAt: createdAt ?? wall.now,
333
460
  records: records.length,
334
461
  messages,
335
462
  },
@@ -337,6 +464,10 @@ function parseClaudeSessionText(file: ClaudeSessionFile, options: TranscodeOptio
337
464
  cwd,
338
465
  unparsable: stats.unparsable,
339
466
  title: titleOf(records, firstUserText),
467
+ sidechain,
468
+ thinking,
469
+ now: wall.now,
470
+ wallClockFallback: wall.fallback,
340
471
  };
341
472
  }
342
473
 
@@ -359,21 +490,33 @@ export function dshSessionLogName(version: number): string {
359
490
  }
360
491
 
361
492
  export function transcodeClaudeSession(file: ClaudeSessionFile, options: TranscodeOptions): TranscodeResult {
493
+ /**
494
+ * 非致命发现(会话照样产出):记录里的 sessionId 与文件名 stem 不一致时**如实报码**,
495
+ * 但会话 id 仍以文件名 stem 为准(DSH 按「目录名 / 文件名」定位会话)。读盘层没给
496
+ * `recordSessionId`(老调用方)就什么都不报 —— 不猜。
497
+ */
498
+ const findings: ForeignSkip[] = [];
499
+ const recordSessionId = irStr(file.recordSessionId);
500
+ if (recordSessionId !== undefined && recordSessionId !== file.id) {
501
+ findings.push({ code: 'session-id-mismatch', origin: file.id });
502
+ }
503
+
362
504
  if (!SUPPORTED_DSH_SESSION_FORMAT_VERSIONS.includes(options.formatVersion)) {
363
- return { skip: { code: 'session-format-unsupported', detail: String(options.formatVersion) } };
505
+ return { skip: { code: 'session-format-unsupported', detail: String(options.formatVersion) }, findings };
364
506
  }
365
- if (!isSafeIrId(file.id)) return { skip: { code: 'session-unsafe-id', detail: file.id } };
507
+ if (!isSafeIrId(file.id)) return { skip: { code: 'session-unsafe-id', detail: file.id }, findings };
366
508
 
367
509
  const parsed = parseClaudeSessionText(file, options);
368
- if (parsed.records.length === 0) return { skip: { code: 'session-empty' } };
369
- if (parsed.cwd === undefined) return { skip: { code: 'session-missing-cwd', detail: file.id } };
510
+ if (parsed.records.length === 0) return { skip: { code: 'session-empty' }, findings };
511
+ if (parsed.cwd === undefined) return { skip: { code: 'session-missing-cwd', detail: file.id }, findings };
370
512
  // 有内容但一行都解析不出来:如实区分「空文件」与「解析不了」(诊断更准,不新增码 —— 两者
371
513
  // 在下游都是「这条会话没有产出」,区别只在忽略计数里可见)
372
514
  if (parsed.session.messages.length === 0 && parsed.unparsable > 0) {
373
- return { skip: { code: 'session-unparsable', detail: file.id } };
515
+ return { skip: { code: 'session-unparsable', detail: file.id }, findings };
374
516
  }
375
517
 
376
- const now = options.now ?? Date.now();
518
+ // 时间兜底**只有一处**(parseClaudeSessionText 里解析过一次):这里绝不再调 Date.now()
519
+ const now = parsed.now;
377
520
  const stats: SynthesisStats = {
378
521
  ignored: {},
379
522
  userMessages: 0,
@@ -382,10 +525,16 @@ export function transcodeClaudeSession(file: ClaudeSessionFile, options: Transco
382
525
  toolResults: 0,
383
526
  };
384
527
  if (parsed.unparsable > 0) irBump(stats.ignored, 'unparsable', parsed.unparsable);
385
-
386
- // 未迁移的记录类型**逐类计数**(sidechain / attachment / queue-operation / system …)——
387
- // IR 只承载 user/assistant 消息,所以这里按原始记录把其余类型补进忽略计数
528
+ // 推理块:共享 IR 没有承载位 → 计报,绝不伪装成正文(与其余 20+ 来源同口径)
529
+ if (parsed.thinking > 0) irBump(stats.ignored, 'thinking', parsed.thinking);
530
+ // 子代理记录:不进 IR → 整类计报(detail 固定为 isSidechain)
531
+ if (parsed.sidechain > 0) irBump(stats.ignored, 'isSidechain', parsed.sidechain);
532
+
533
+ // 未迁移的记录类型**逐类计数**(attachment / queue-operation / system …)——
534
+ // IR 只承载 user/assistant 消息,所以这里按原始记录把其余类型补进忽略计数。
535
+ // isSidechain 记录已整类记账(上一步),不在这里重复计数。
388
536
  for (const rec of parsed.records) {
537
+ if (rec['isSidechain'] === true) continue;
389
538
  const type = typeOf(rec);
390
539
  if (type === 'user' || type === 'assistant') continue;
391
540
  irBump(stats.ignored, type);
@@ -400,7 +549,7 @@ export function transcodeClaudeSession(file: ClaudeSessionFile, options: Transco
400
549
  maxTokens: CLAUDE_MAX_TOKENS,
401
550
  }, stats, now);
402
551
 
403
- if (rows.length === 0) return { skip: { code: 'session-empty' } };
552
+ if (rows.length === 0) return { skip: { code: 'session-empty' }, findings };
404
553
 
405
554
  const header: DshSessionHeader = {
406
555
  type: 'session',
@@ -412,6 +561,11 @@ export function transcodeClaudeSession(file: ClaudeSessionFile, options: Transco
412
561
  delegationDepth: 0,
413
562
  };
414
563
  const data = encodeDshSessionLog(header, rows);
564
+ // 结算类记账(与 ignored 分开放:ignored 的键会被下游翻成 unsupported-session-record 码)。
565
+ // 0 也写出来:调用方一眼能看出「这次转码是否确定性」,不需要靠键的有无去猜。
566
+ const counts: Record<string, number> = {
567
+ timestampsFallback: parsed.wallClockFallback ? 1 : 0,
568
+ };
415
569
  return {
416
570
  session: {
417
571
  id: file.id,
@@ -425,8 +579,10 @@ export function transcodeClaudeSession(file: ClaudeSessionFile, options: Transco
425
579
  toolCalls: stats.toolCalls,
426
580
  toolResults: stats.toolResults,
427
581
  ignored: stats.ignored,
582
+ counts,
428
583
  },
429
584
  },
585
+ findings,
430
586
  };
431
587
  }
432
588
 
@@ -247,7 +247,10 @@ test('C 来源形状:共享模块清单是封闭集合(新模块必须显式
247
247
  /* t6 新增:本域追加消息目录(零 source 形状;中文只在这里,装配期抛错的文案经 engineMsg 取)。 */
248
248
  'messages.ts',
249
249
  /* 档 B 新增的 5 个共享地基(零 source 形状,但也不是来源模块): */
250
- 'platform-paths.ts', 'registry.ts', 'session-ir.ts', 'session-read.ts', 'session-source.ts',
250
+ 'platform-paths.ts',
251
+ /* 2026-10-18 新增:Claude Code 插件/市场读盘层(名字不含来源 id,故不是来源形状模块;
252
+ 它是 claude-code 专属读盘子模块,由 read-claude-code.ts 动态 import)。清单按字母序比较。 */
253
+ 'read-claude-plugins.ts', 'registry.ts', 'session-ir.ts', 'session-read.ts', 'session-source.ts',
251
254
  'source-modules.ts', 'sqlite.ts', 'truth-table.ts', 'types.ts',
252
255
  ];
253
256
  assert.deepEqual(