dsh-log-contract 0.3.10 → 0.3.11

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.
File without changes
package/docs/CONTRACTS.md CHANGED
@@ -1,15 +1,20 @@
1
1
  # 契约规则目录(CONTRACTS)
2
2
 
3
+ > **自动生成**(2026-09-06 起):本文件由 `node scripts/gen-contracts-doc.mjs`
4
+ > 从 `lib/contracts.js` 的 `CONTRACT_RULES` 注册表生成——**勿手改**,规则只增不减,
5
+ > 新增规则后跑一次生成即同步(此前手工维护滞后 15+ 条,外部审计指出)。
6
+ >
3
7
  > DSH 会话日志契约的**可执行 spec**。每条规则在 `lib/checks.js`(逐事件判定)
4
8
  > 与 `lib/prewrite.js`(写前校验)中有对应实现;离线体检(`lib/validate.js`)
5
9
  > 逐条执行并在最后用官方 `foldSurface` 终验(S8)。
6
10
  >
7
11
  > 规则来源:`dsh-scale-audit-疑点记录.md`(59 条审计发现)+ `复盘-会话修复事故-20260825.md`
8
- > (三层契约)+ `@deepseek-ai/dsh-session@0.1.0-rc.7` 官方源码逐行核对。
12
+ > (三层契约)+ `@deepseek-ai/dsh-session@0.1.0-rc.7` 官方源码逐行核对
13
+ > (后续规则随官方版本演进追加:T3/T4 渲染层 = 1e99e1ff 复盘,T5 = 1f4d986e malformed)。
9
14
  >
10
15
  > 严重度:**error** = 违反即会话不可加载/写入被拒(fail-loud);**warning** = 合法但可疑。
11
16
 
12
- ## 规则索引
17
+ ## 规则索引(共 38 条)
13
18
 
14
19
  | id | 严重度 | 层级 | 规则 |
15
20
  |---|---|---|---|
@@ -20,6 +25,8 @@
20
25
  | R3 | error | persistence | chunk 行展开后成员 seq/time 安全 |
21
26
  | E1 | error | persistence | 每个事件携带非负安全整数 seq |
22
27
  | E2 | error | persistence | seq 严格连续(单写入者假设) |
28
+ | S9 | error | persistence | 文件物理序 seq 单调(多写入者交织现场特征) |
29
+ | I1 | error | engine | inbox seed 相对重放(fork 边界孤儿 spliced) |
23
30
  | E3 | error | persistence | type 必须在已知词汇表内(或带 ignorable 标记) |
24
31
  | E4 | error | persistence | data 与 surface 元数据必须 JSON 无损 |
25
32
  | E5 | error | persistence | 禁用遗留词汇 |
@@ -28,16 +35,27 @@
28
35
  | S2 | error | persistence | 非 surface 类型不得携带 surface 元数据 |
29
36
  | S3 | error | persistence | append 的 sourceEventSeqs 契约 |
30
37
  | S4 | error | persistence | replace 操作数与范围合法性 |
31
- | S5 | error | persistence | replace 的 sourceEventSeqs 必须完整覆盖被替换节点 |
38
+ | S5 | error | persistence | replace 的 sourceEventSeqs 必须完整覆盖被替换节点 |
32
39
  | S6 | error | persistence | sourceEventSeqs 自身约束 |
33
40
  | S7 | error | persistence | tool/result 替换仅允许单节点内容改写 |
34
- | S8 | error | persistence | 整日志 foldSurface 可重放(终验) |
41
+ | S8 | error | persistence | 整日志 foldSurface 可重放 |
42
+ | T1 | error | engine | token-meter 配对:assistant/message 与 step/end 必须匹配当前打开的 step/start |
43
+ | T2 | error | engine | token-meter 源引用:assistant/message 的 sourceEventSeqs 引用的 chunk 必须同 turn/step |
44
+ | T3 | error | engine | step 节点 key 唯一(同 turn 内 step/start 的 step 号不得复用) |
45
+ | T4 | error | engine | step/消息本体 turn 缺失(null/undefined)→ 渲染死循环 |
46
+ | T5 | error | engine | turn/end 必须带 data.reason.kind |
47
+ | E7 | warning | persistence | ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者) |
48
+ | Z3 | warning | framing | 空会话文件(有 header 无事件)显式报出 |
49
+ | P3 | warning | plugin | tool/call ↔ tool/result 配对完整性(考古 B1) |
50
+ | P4 | warning | plugin | tool/result 输出结构可解析(考古 B2) |
35
51
  | M1 | error | engine | turn/step 为 null 的 assistant/message 只能 replace,不能 append |
36
52
  | P1 | warning | plugin | marker id 前缀必须被识别 |
37
53
  | P2 | error | plugin | marker 自身 seq 不得出现在自身 shadowed 集 |
38
54
  | C1 | warning | concurrency | seq 缺口/倒退提示多写入者 |
39
55
  | Z1 | warning | framing | zstd 尾帧撕裂 |
40
56
  | Z2 | error | framing | zstd 帧解码失败 = 单帧全损 |
57
+ | W1 | error | engine | wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后 |
58
+ | W2 | error | engine | wire 流:user 文本不得插在 tool_calls 与其 tool 结果之间 |
41
59
 
42
60
  ## 详细规则
43
61
 
@@ -83,6 +101,18 @@
83
101
  - **出处**: @deepseek-ai/dsh-session lib/index.js:398 (planSurfaceEvent "not contiguous");审计 S2/N6
84
102
  - **契约**: seq 必须从 0(或窗口 baseSeq)严格连续递增。缺口/倒退 = 违反单写入者假设(多实例共享存储并发写的痕迹),加载时直接 throw。
85
103
 
104
+ ### S9 — 文件物理序 seq 单调(多写入者交织现场特征)
105
+
106
+ - **层级**: persistence | **严重度**: error
107
+ - **出处**: 2026-08-28 实锤:526f1835 文件物理序 734056→733539→735470;单进程 appendCore 断言 seq==cursor+i 且按 id 串行化不可能写出
108
+ - **契约**: 按文件物理行序要求展开后事件 seq 严格单调递增。E2 在排序后检查(loadSessionLog 会 sort),物理序倒退被掩盖;S9 在排序前按行序检查,非单调 = 多写入者/旧光标回放交织的直接现场证据,加载会被拒。
109
+
110
+ ### I1 — inbox seed 相对重放(fork 边界孤儿 spliced)
111
+
112
+ - **层级**: engine | **严重度**: error
113
+ - **出处**: @deepseek-ai/dsh-agent lib/types/inbox.js:155-178 (apply/validate);2026-08-28 实锤:62c5b531/73ed35d8 fork 边界 removedCount=1 孤儿
114
+ - **契约**: 从 header.seedLength 起重放 agent/inbox/spliced,next-turn/next-step 双队列;start+removedCount 不得超过队列长、不得产生重复 pending id。fork 时"移除父待处理提示词"的 splice 假设父会话 inbox,子会话 seed 相对空 inbox 上非法 → resume 被拒(invalid persisted inbox splice)。
115
+
86
116
  ### E3 — type 必须在已知词汇表内(或带 ignorable 标记)
87
117
 
88
118
  - **层级**: persistence | **严重度**: error
@@ -155,6 +185,60 @@
155
185
  - **出处**: @deepseek-ai/dsh-session lib/index.js:444-455 (foldSurface);复盘"官方 foldSurface 不抛 = 通过"
156
186
  - **契约**: 终验:把全部事件按序喂给官方 foldSurface,不抛 = 持久化层通过。S1–S7 任何一条违反都会在此暴露。
157
187
 
188
+ ### T1 — token-meter 配对:assistant/message 与 step/end 必须匹配当前打开的 step/start
189
+
190
+ - **层级**: engine | **严重度**: error
191
+ - **出处**: @deepseek-ai/dsh-token-meter lib/index.js:566-625 (_foldEvent)
192
+ - **契约**: token meter 折叠要求 assistant/message 与 step/end 与打开的 step/start(turn/step 完全一致)匹配;违反即 /compact 与压力测量永久失败。retrace 的 turn-null 编辑/撤回 marker(空 assistant/message replace)命中此条——foldSurface 认可其合法性但 token meter 崩溃(M1 只约束 append 形态的盲区),压缩前需清理。
193
+
194
+ ### T2 — token-meter 源引用:assistant/message 的 sourceEventSeqs 引用的 chunk 必须同 turn/step
195
+
196
+ - **层级**: engine | **严重度**: error
197
+ - **出处**: @deepseek-ai/dsh-token-meter lib/index.js:634-650 (_estimateProviderAssistant,:645 belongs to another step)
198
+ - **契约**: token meter 重建 provider 输出时,逐条检查 assistant/message 的 sourceEventSeqs:指向 assistant/chunk 的引用必须与消息同 turn/step,且 seq 更早、不重复;跨 step 引用 → 官方抛 belongs to another step → 每次事件追加都重抛(consumedEvents 不前进)→ 刷屏压垮 host(2026-08-30 实测 526f1835 seq 936047 跨 step 7/8/9)。T1 只查 step 配对不查源引用,此条补盲区;修复用 fix --clip-crossstep。
199
+
200
+ ### T3 — step 节点 key 唯一(同 turn 内 step/start 的 step 号不得复用)
201
+
202
+ - **层级**: engine | **严重度**: error
203
+ - **出处**: 复盘 2026-09-02 1e99e1ff 白屏(修复线 session-3f9e4f12):客户端渲染节点 key = turn:step,冲突 → React 渲染死循环;工具 tools/check-step-keys.mjs
204
+ - **契约**: 客户端渲染消息列表从事件流构建节点,节点 key = data.turn:data.step。同 turn 内两个 step/start 的 step 号相同 → key 冲突 → React 渲染死循环 → 白屏/不展示(1e99e1ff:580034 step 95/1 vs 580037 编辑块 step 95/1;6924781d/97786207/4b149a4a 同型)。修复:同 turn 内 step 递增、整块重编号(含块内 chunk/tool/assistant)。
205
+
206
+ ### T4 — step/消息本体 turn 缺失(null/undefined)→ 渲染死循环
207
+
208
+ - **层级**: engine | **严重度**: error
209
+ - **出处**: 复盘 D8 1e99e1ff(2026-09-01):retrace 0.4.17 编辑块 turn:null;修复线 tools/check-null-turn.mjs 判致命
210
+ - **契约**: 客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 → 白屏「载入历史」(1e99e1ff seq 580037-580039)。user/message 天然无 turn 不查;chunk 坐标可缺失不查。step/消息本体必须带真实 turn 号。
211
+
212
+ ### T5 — turn/end 必须带 data.reason.kind
213
+
214
+ - **层级**: engine | **严重度**: error
215
+ - **出处**: 官方 dsh-agent-loop lib/index.js:620(turn/end = {turn, reason:{kind}});1f4d986e malformed turn/end 事故(2026-09-02,修复线 check-turn-end-reason.mjs)
216
+ - **契约**: 官方 validation 强制 turn/end 的 data.reason.kind 存在(kind ∈ completed|max-tokens|blocked|aborted|error|interrupted)。缺失 = malformed → 官方 SessionPersistenceCorruptionError → 会话加载失败。1f4d986e:retrace 情形③信封 turn/end 漏 reason → 每次编辑后加载失败(已修 0.4.18)。
217
+
218
+ ### E7 — ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者)
219
+
220
+ - **层级**: persistence | **严重度**: warning
221
+ - **出处**: 反向挑刺 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)
222
+ - **契约**: 未知 type + ignorable:true 被读路径接纳但无人消费 = 静默垃圾。排除已知消费者白名单(retrace/marker、retrace/goal-marker、message-editor/ 前缀等 retrace 客户端消费的插件 marker)后,其余 ignorable 未知事件报 warning。
223
+
224
+ ### Z3 — 空会话文件(有 header 无事件)显式报出
225
+
226
+ - **层级**: framing | **严重度**: warning
227
+ - **出处**: 反向挑刺 2026-09-09 T3(36 条规则全来自有内容事故,空态无覆盖)
228
+ - **契约**: 有 header 但零事件 = 异常空会话(新建即空或写入未落盘)。空态不在任何有内容规则的覆盖下,显式 warning 供人判断。
229
+
230
+ ### P3 — tool/call ↔ tool/result 配对完整性(考古 B1)
231
+
232
+ - **层级**: plugin | **严重度**: warning
233
+ - **出处**: dsh-会话日志考古-插件任务与方法.md §2/§4.2(callId 配对,不可用"上一个 call"推断)
234
+ - **契约**: 每个 tool/call 的 data.callId 必须能在 tool/result 的 data.message.source.callId 中找到配对;孤儿 call(无 result)告警——中断/失败轮次可能产生孤儿(合法但要审计),考古提取将缺该输出。
235
+
236
+ ### P4 — tool/result 输出结构可解析(考古 B2)
237
+
238
+ - **层级**: plugin | **严重度**: warning
239
+ - **出处**: dsh-会话日志考古-插件任务与方法.md §2/§4.2(content 递归 text 结构)
240
+ - **契约**: tool/result 的 data.message.content 必须可递归解析(list[dict{type:text,text}] 或等价);不可解析片段 = 考古提取将漏数据。空 content(失败/无输出)合法。
241
+
158
242
  ### M1 — turn/step 为 null 的 assistant/message 只能 replace,不能 append
159
243
 
160
244
  - **层级**: engine | **严重度**: error
@@ -191,19 +275,15 @@
191
275
  - **出处**: 审计 N5:多帧单帧全损 → 整会话不可读
192
276
  - **契约**: 任一帧解码失败(磁盘 bitrot / 传输截断 / 并发写撕裂)即整会话不可读;帧越多,单帧损坏下丢失概率线性上升。
193
277
 
194
- ---
195
-
196
- ## 边界说明(诚实声明)
197
-
198
- 1. **本工具守护"持久化契约层"**。`#3632` 的"one log, two consumers, two verdicts"中,
199
- `agent/inbox/spliced` 孤儿在持久化层**是合法的**(官方 `foldSurface` 可通过)——
200
- 违规发生在**消费路径**(`sessionQuery`/UI 判不可读)。本工具的 `check` 会如实报 PASS,
201
- 不冒充能判消费路径契约;该层契约属另一类问题(可配合 dsh-retrace / 上游修复)。
202
- 2. **M1 是引擎层启发式规则**:`data.turn/step` null 的 `assistant/message` 以 append 进入
203
- surface 会触发客户端引擎崩溃(rt.js:6816)——依据是 2026-08-25 事故第 2 轮实证。
204
- 离线场景无法渲染客户端,故以 error 级保守拦截,避免事故重演。
205
- 3. **写前校验以"官方 foldSurface 不抛"为最终权威**:逐事件归因(S1–S7)负责定位,
206
- 官方重放(S8)负责背书;两套都绿才算通过。若官方实现更新导致判定漂移,
207
- 以官方为准并更新本 spec(本工具自己就是契约漂移的哨兵)。
208
- 4. **seq 严格连续是单写入者假设**(N6):离线 `check` 只能看到缺口/倒退的结果,
209
- 无法观测竞态本身;`C1` 给出解释性告警而非臆断。
278
+ ### W1 — wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后
279
+
280
+ - **层级**: engine | **严重度**: error
281
+ - **出处**: 2026-08-27 实锤:MiMo 等严格端点对悬空 tool 直接 INVALID_REQUEST(Messages with role "tool" must be a response to a preceding message with "tool_calls");marker 遮蔽 assistant(tool_calls) 而未盖住 tool/result、或中断回合重放重复 tool/result 写在 marker 之后都会产生
282
+ - **契约**: surface 折叠顺序展开 wire 消息流:每个 role=tool 消息必须消费一个仍未满足的 assistant tool-call;不足 = 悬空(provider 拒绝)。常见来源:marker 范围漏盖 tool/result、重放重复事件。
283
+
284
+ ### W2 wire 流:user 文本不得插在 tool_calls 与其 tool 结果之间
285
+
286
+ - **层级**: engine **严重度**: error
287
+ - **出处**: OpenAI 兼容端点对 tool 消息顺序的严格校验;DSH 序列化器将混合 user 消息展开为 text 在前、tool-result 在后
288
+ - **契约**: 当仍有未满足的 assistant tool-call 时出现 user 文本消息,会产生 [assistant(tool_calls), user(text), tool] 序列,严格端点同样拒绝。
289
+
package/lib/checks.js CHANGED
@@ -387,6 +387,35 @@ export function turnEndReasonViolations(events) {
387
387
  return out
388
388
  }
389
389
 
390
+ /**
391
+ * E7 —— ignorable 未知 type 合法性(2026-09-09 反向挑刺 T2 增量)。
392
+ *
393
+ * 盲区:E3 对"未知 type + ignorable:true"直接放行(容忍更新版本 harness 写入),
394
+ * 但 ignorable 标记无合法性校验 = 后门——被读路径接纳却无人消费的未知事件 =
395
+ * 静默垃圾(违 R3 精神)。补:未知 type + ignorable + **不在已知消费者白名单** →
396
+ * warning"有被忽略的未知事件"。
397
+ *
398
+ * 已知消费者(不算垃圾,排除):retrace 家族 marker(retrace/marker、retrace/goal-marker,
399
+ * neutralize/插件产物,retrace 客户端识别消费)与旧前缀 message-editor。
400
+ * 判定:type ∉ KNOWN && ignorable && 非已知插件前缀 → warning。
401
+ */
402
+ const KNOWN_IGNORABLE_CONSUMERS = new Set(['retrace/marker', 'retrace/goal-marker'])
403
+ function isKnownIgnorableType(type) {
404
+ if (typeof type !== 'string') return false
405
+ if (KNOWN_IGNORABLE_CONSUMERS.has(type)) return true
406
+ return type.startsWith('message-editor/') || type.startsWith('retrace/')
407
+ }
408
+ export function ignorableTypeViolations(events) {
409
+ const out = []
410
+ for (const { event, lineNo } of events) {
411
+ if (typeof event?.type !== 'string' || event.ignorable !== true) continue
412
+ if (KNOWN_SESSION_EVENT_TYPES.has(event.type)) continue
413
+ if (isKnownIgnorableType(event.type)) continue
414
+ out.push(violation('E7', { seq: event.seq, lineNo, eventType: event.type }, `type "${event.type}" 不在已知词汇表且带 ignorable 标记、无已知消费者——被读路径接纳但无人消费 = 静默垃圾(ignorable 后门;若你的插件消费它,登记到 KNOWN_IGNORABLE_CONSUMERS)`))
415
+ }
416
+ return out
417
+ }
418
+
390
419
  /**
391
420
  * P3 —— tool/call ↔ tool/result 配对完整性(考古任务书 B1)。
392
421
  * 每个 tool/call 的 `data.callId` 必须能在 tool/result 的
@@ -396,7 +425,7 @@ export function turnEndReasonViolations(events) {
396
425
  export function toolPairingViolations(events) {
397
426
  const out = [];
398
427
  const calls = new Map(); // callId → { command, loc }
399
- const results = new Set();
428
+ const results = new Map(); // callId → loc(双向:孤儿 result 也要指认,2026-09-09 反向挑刺 T1)
400
429
  for (const { event, lineNo } of events) {
401
430
  if (event.type === 'tool/call') {
402
431
  const callId = event.data?.callId;
@@ -410,7 +439,7 @@ export function toolPairingViolations(events) {
410
439
  }
411
440
  } else if (event.type === 'tool/result') {
412
441
  const callId = event.data?.message?.source?.callId;
413
- if (typeof callId === 'string' && callId !== '') results.add(callId);
442
+ if (typeof callId === 'string' && callId !== '') results.set(callId, { seq: event.seq, lineNo, eventType: event.type });
414
443
  }
415
444
  }
416
445
  for (const [callId, { command, loc }] of calls) {
@@ -418,6 +447,14 @@ export function toolPairingViolations(events) {
418
447
  out.push(violation('P3', loc, `tool/call ${callId}(命令 ${command || '(未知)'})没有配对的 tool/result——孤儿调用(中断/失败未落结果),考古提取将缺该输出`));
419
448
  }
420
449
  }
450
+ // 双向(反向挑刺 T1 增量):孤儿 result = result 无对应 tool/call。
451
+ // 折叠后 wire 流中无主 tool 消息 = provider 拒绝风险(W1/W2 同族、不同层);
452
+ // 与孤儿 call 同为 warning 级——合法场景(中断/修复产物)不破坏日志。
453
+ for (const [callId, loc] of results) {
454
+ if (!calls.has(callId)) {
455
+ out.push(violation('P3', loc, `tool/result ${callId} 没有配对的 tool/call——孤儿结果(无主 tool 消息,wire 流 provider 拒绝风险;中断/修复产物合法但要审计),考古配对将无法归属`));
456
+ }
457
+ }
421
458
  return out;
422
459
  }
423
460
 
package/lib/contracts.js CHANGED
@@ -253,6 +253,22 @@ export const CONTRACT_RULES = [
253
253
  source: '官方 dsh-agent-loop lib/index.js:620(turn/end = {turn, reason:{kind}});1f4d986e malformed turn/end 事故(2026-09-02,修复线 check-turn-end-reason.mjs)',
254
254
  description: '官方 validation 强制 turn/end 的 data.reason.kind 存在(kind ∈ completed|max-tokens|blocked|aborted|error|interrupted)。缺失 = malformed → 官方 SessionPersistenceCorruptionError → 会话加载失败。1f4d986e:retrace 情形③信封 turn/end 漏 reason → 每次编辑后加载失败(已修 0.4.18)。',
255
255
  },
256
+ {
257
+ id: 'E7',
258
+ title: 'ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者)',
259
+ layer: LAYER.PERSISTENCE,
260
+ severity: SEVERITY.WARNING,
261
+ source: '反向挑刺 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)',
262
+ description: '未知 type + ignorable:true 被读路径接纳但无人消费 = 静默垃圾。排除已知消费者白名单(retrace/marker、retrace/goal-marker、message-editor/ 前缀等 retrace 客户端消费的插件 marker)后,其余 ignorable 未知事件报 warning。',
263
+ },
264
+ {
265
+ id: 'Z3',
266
+ title: '空会话文件(有 header 无事件)显式报出',
267
+ layer: LAYER.FRAMING,
268
+ severity: SEVERITY.WARNING,
269
+ source: '反向挑刺 2026-09-09 T3(36 条规则全来自有内容事故,空态无覆盖)',
270
+ description: '有 header 但零事件 = 异常空会话(新建即空或写入未落盘)。空态不在任何有内容规则的覆盖下,显式 warning 供人判断。',
271
+ },
256
272
  {
257
273
  id: 'P3',
258
274
  title: 'tool/call ↔ tool/result 配对完整性(考古 B1)',
package/lib/index.js CHANGED
@@ -9,5 +9,5 @@ export { validateSessionLog, resumeVerdict } from './validate.js';
9
9
  export { createPreWriter, preWriterFromLog } from './prewrite.js';
10
10
  export { repairSession, strictScanText, removeMarkersText, neutralizeMarkersText, clipCrossStepSourcesText, dropFailedTurnsText, trimLastMessagesText, trimLastMessagesByBudget, estimateTokensText, compactLastMessagesText, rebuildZstdText, tailRenumberText, neutralizeOrphanText, extractTurnText, keepRangesText } from './repair.js';
11
11
  export { CONTRACT_RULES, LAYER, SEVERITY, ruleById } from './contracts.js';
12
- export { tokenMeterViolations, tokenMeterSourceViolations, stepKeyViolations, nullTurnStepViolations, turnEndReasonViolations, physicalOrderViolations, inboxReplayViolations } from './checks.js';
12
+ export { tokenMeterViolations, tokenMeterSourceViolations, stepKeyViolations, nullTurnStepViolations, turnEndReasonViolations, ignorableTypeViolations, physicalOrderViolations, inboxReplayViolations } from './checks.js';
13
13
  export { auditToolCalls, extractText, extractToolOutputs, indexToolCalls, toolCommandOf } from './archaeology.js';
package/lib/validate.js CHANGED
@@ -11,6 +11,7 @@ import { ruleById } from './contracts.js';
11
11
  import {
12
12
  CHUNK_ROW_TYPES,
13
13
  envelopeViolations,
14
+ ignorableTypeViolations,
14
15
  engineViolations,
15
16
  finalFold,
16
17
  inboxReplayViolations,
@@ -78,6 +79,13 @@ export function validateSessionLog(log, opts = {}) {
78
79
  }
79
80
  }
80
81
 
82
+ // ── Z3 · 空文件体检(2026-09-09 反向挑刺 T3 增量)──────────────────────────
83
+ // 空态无覆盖:36 条规则全来自"有内容"事故,空会话文件应显式报(不管 DSH 编排层怎么处理)。
84
+ // 有 header 但零事件 = 异常空会话(warning,不破坏 ok);连 header 都没有 → H1 已报。
85
+ if (header !== null && events.length === 0) {
86
+ violations.push(violation('Z3', { lineNo: headerLine }, `会话文件无任何事件(仅 header)——异常空会话(新建即空或写入未落盘);空态不在任何有内容规则的覆盖下,显式报出供人判断`));
87
+ }
88
+
81
89
  // ── R · 存储行 ─────────────────────────────────────────────────────────
82
90
  for (const row of rows) {
83
91
  if (row.error && row.value === null) {
@@ -124,6 +132,9 @@ export function validateSessionLog(log, opts = {}) {
124
132
  violations.push(...nullTurnStepViolations(events));
125
133
  violations.push(...turnEndReasonViolations(events));
126
134
 
135
+ // ── E7 · ignorable 未知 type 合法性(反向挑刺 T2)────────────────────
136
+ violations.push(...ignorableTypeViolations(events));
137
+
127
138
  // ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
128
139
  violations.push(...inboxReplayViolations(events, header));
129
140
 
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "dsh-log-contract",
3
3
  "description": "日志契约守护 — DSH session log contract guard: offline health check (CLI) + pre-write validation for DeepSeek Harness session logs",
4
- "version": "0.3.10",
5
- "packageManager": "pnpm@11.7.0",
4
+ "version": "0.3.11",
6
5
  "type": "module",
7
6
  "main": "lib/index.js",
8
7
  "bin": {
@@ -28,11 +27,6 @@
28
27
  "LICENSE",
29
28
  "README.zh.md"
30
29
  ],
31
- "scripts": {
32
- "check": "node scripts/check-syntax.mjs",
33
- "test": "vitest run",
34
- "prepublishOnly": "pnpm check && pnpm test"
35
- },
36
30
  "keywords": [
37
31
  "dsh",
38
32
  "deepseek-harness",
@@ -65,5 +59,9 @@
65
59
  "@deepseek-ai/dsh-session": "0.1.0-rc.7",
66
60
  "esbuild": "0.28.2",
67
61
  "vitest": "4.1.11"
62
+ },
63
+ "scripts": {
64
+ "check": "node scripts/check-syntax.mjs",
65
+ "test": "vitest run"
68
66
  }
69
- }
67
+ }