dsh-log-contract 0.3.16 → 0.3.17

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
@@ -344,11 +344,11 @@ MIT © OfferKuai Team
344
344
 
345
345
  ---
346
346
 
347
- ## ⚠️ Upgrading to 0.3.16 — behaviour changes you must know (0.3.12 → 0.3.16)
347
+ ## ⚠️ Upgrading to 0.3.17 — behaviour changes you must know (0.3.12 → 0.3.17)
348
348
 
349
- > **0.3.16 changes no behaviour.** It rewrites comments, test titles, a few diagnostic strings and the
349
+ > **0.3.17 changes no behaviour.** It rewrites comments, test titles, a few diagnostic strings and the
350
350
  > README so that they describe the product and its contract only. Every verdict, exit code and `--json`
351
- > field is unchanged from 0.3.15, so everything below applies unchanged when going from 0.3.12 to 0.3.16.
351
+ > field is unchanged from 0.3.15, so everything below applies unchanged when going from 0.3.12 to 0.3.17.
352
352
  >
353
353
  > 0.3.15 was a documentation-only release as well.
354
354
 
package/README.zh.md CHANGED
@@ -295,11 +295,11 @@ MIT © OfferKuai Team
295
295
 
296
296
  ---
297
297
 
298
- ## ⚠️ 升级到 0.3.16 —— 必须知道的行为变更(0.3.12 → 0.3.16
298
+ ## ⚠️ 升级到 0.3.17 —— 必须知道的行为变更(0.3.12 → 0.3.17
299
299
 
300
- > **0.3.16 不含行为变更。** 它只重写了注释、测试标题、少量诊断文案与 README,让它们
300
+ > **0.3.17 不含行为变更。** 它只重写了注释、测试标题、少量诊断文案与 README,让它们
301
301
  > 只描述产品与契约本身。所有判定结果、退出码与 `--json` 字段与 0.3.15 完全一致,
302
- > 因此从 0.3.12 升到 0.3.16 时,下面各条与原来完全一致。
302
+ > 因此从 0.3.12 升到 0.3.17 时,下面各条与原来完全一致。
303
303
  >
304
304
  > 0.3.15 同样只是文档层面的发布。
305
305
 
package/docs/CONTRACTS.md CHANGED
@@ -22,7 +22,7 @@
22
22
  >
23
23
  > 严重度:**error** = 违反即会话不可加载/写入被拒(fail-loud);**warning** = 合法但可疑。
24
24
 
25
- ## 规则索引(共 44 条)
25
+ ## 规则索引(共 46 条)
26
26
 
27
27
  | id | 严重度 | 层级 | 规则 |
28
28
  |---|---|---|---|
@@ -65,6 +65,8 @@
65
65
  | C1 | warning | concurrency | seq 缺口/倒退提示多写入者 |
66
66
  | Z1 | warning | framing | zstd 尾帧撕裂 |
67
67
  | Z2 | error | framing | zstd 帧解码失败 = 单帧全损 |
68
+ | Z4 | error | framing | 首帧必须恰好一行 header |
69
+ | Z5 | error | framing | 多行日志不得压成单帧 |
68
70
  | W1 | error | engine | wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后 |
69
71
  | W2 | error | engine | wire 流:user 文本不得插在 tool_calls 与其 tool 结果之间 |
70
72
  | G1 | warning | migration | 迁移预检:v0 源文件的 subagent/descriptor.data.version 必须为 3 |
@@ -307,6 +309,18 @@
307
309
  - **出处**: 多帧单帧全损 → 整会话不可读
308
310
  - **契约**: 任一帧解码失败(磁盘 bitrot / 传输截断 / 并发写撕裂)即整会话不可读;帧越多,单帧损坏下丢失概率线性上升。
309
311
 
312
+ ### Z4 — 首帧必须恰好一行 header
313
+
314
+ - **层级**: framing | **严重度**: error
315
+ - **出处**: 宿主 assertZstdHeaderFrame(0.1.5-rc.2 lib/index.js:2184):首帧非"恰好一行"即抛
316
+ - **契约**: 官方写盘形状是 header **独占第一帧**。首帧装多行时宿主抛错,且抛点在 listArtifacts **遍历全库**、无 per-session 容错 ⇒ **整个客户端起不来**(2026-09-16 真机事故:有人把整份日志重压成单帧写回)。
317
+
318
+ ### Z5 — 多行日志不得压成单帧
319
+
320
+ - **层级**: framing | **严重度**: error
321
+ - **出处**: 官方形状:header 第一帧 + 事件批次逐帧追加
322
+ - **契约**: 把整份日志重压成 1 帧会同时触发 Z4;任何"重写会话"的路径都必须**保形**(逐帧解、逐帧回写,或走宿主写入器),不得整体重压缩。
323
+
310
324
  ### W1 — wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后
311
325
 
312
326
  - **层级**: engine | **严重度**: error
package/lib/contracts.js CHANGED
@@ -408,6 +408,22 @@ export const CONTRACT_RULES = [
408
408
  source: '多帧单帧全损 → 整会话不可读',
409
409
  description: '任一帧解码失败(磁盘 bitrot / 传输截断 / 并发写撕裂)即整会话不可读;帧越多,单帧损坏下丢失概率线性上升。',
410
410
  },
411
+ {
412
+ id: 'Z4',
413
+ title: '首帧必须恰好一行 header',
414
+ layer: LAYER.FRAMING,
415
+ severity: SEVERITY.ERROR,
416
+ source: '宿主 assertZstdHeaderFrame(0.1.5-rc.2 lib/index.js:2184):首帧非"恰好一行"即抛',
417
+ description: '官方写盘形状是 header **独占第一帧**。首帧装多行时宿主抛错,且抛点在 listArtifacts **遍历全库**、无 per-session 容错 ⇒ **整个客户端起不来**(2026-09-16 真机事故:有人把整份日志重压成单帧写回)。',
418
+ },
419
+ {
420
+ id: 'Z5',
421
+ title: '多行日志不得压成单帧',
422
+ layer: LAYER.FRAMING,
423
+ severity: SEVERITY.ERROR,
424
+ source: '官方形状:header 第一帧 + 事件批次逐帧追加',
425
+ description: '把整份日志重压成 1 帧会同时触发 Z4;任何"重写会话"的路径都必须**保形**(逐帧解、逐帧回写,或走宿主写入器),不得整体重压缩。',
426
+ },
411
427
 
412
428
  // ── W · wire 消息流(模型请求序列)───────────────────────────────────────
413
429
  {
package/lib/log-reader.js CHANGED
@@ -173,7 +173,19 @@ export function loadSessionLog(path) {
173
173
  plain = Buffer.alloc(0);
174
174
  }
175
175
  const { frames, torn } = scanZstdFrames(buf);
176
- frameInfo = { frames: frames.length, torn, compressedBytes: buf.length, plaintextBytes: plain.length };
176
+ // 帧形状事实(Z4/Z5,2026-09-16 真机事故):官方写盘形状 = **header 独占第一帧** +
177
+ // 事件逐帧追加;读取端有硬断言 `assertZstdHeaderFrame`(首帧必须**恰好一行**)。
178
+ // 这里把该事实带出来给 validate 判:整文件被重压成单帧时,首帧装的是全部行 ⇒ 宿主必抛,
179
+ // 而抛点在 `listArtifacts` **遍历全库**、无 per-session 容错 ⇒ 整个客户端起不来。
180
+ let firstFrameOneLine = true, firstFrameLines = 1;
181
+ try {
182
+ const [s0, e0] = frames[0];
183
+ const first = zstdDecompressSync(buf.subarray(s0, e0)).toString('utf8');
184
+ const nl = first.indexOf('\n');
185
+ firstFrameOneLine = nl === first.length - 1 && first.length > 1;
186
+ firstFrameLines = first.split('\n').filter((x) => x !== '').length;
187
+ } catch { /* 首帧解码失败另有 Z2 报 */ }
188
+ frameInfo = { frames: frames.length, torn, compressedBytes: buf.length, plaintextBytes: plain.length, firstFrameOneLine, firstFrameLines };
177
189
  } else {
178
190
  plain = buf;
179
191
  frameInfo = { frames: 0, torn: false, compressedBytes: 0, plaintextBytes: plain.length };
package/lib/validate.js CHANGED
@@ -81,6 +81,16 @@ export function validateSessionLog(log, opts = {}) {
81
81
  if (frameInfo?.error) {
82
82
  violations.push(violation('Z2', { lineNo: null }, frameInfo.error));
83
83
  }
84
+ // Z4 · 首帧必须恰好一行 header(2026-09-16 真机事故:整份日志被重压成单帧 ⇒ 客户端起不来)
85
+ if (frameInfo?.firstFrameOneLine === false) {
86
+ violations.push(violation('Z4', { lineNo: null },
87
+ `首帧不是"恰好一行 header"(实测 ${frameInfo.firstFrameLines ?? '?'} 行)——宿主 assertZstdHeaderFrame 必抛;抛点在 listArtifacts 遍历全库 ⇒ 整个客户端不可用`));
88
+ }
89
+ // Z5 · 多行日志不得压成单帧
90
+ if (Number.isSafeInteger(frameInfo?.frames) && frameInfo.frames === 1 && Array.isArray(rows) && rows.length > 1) {
91
+ violations.push(violation('Z5', { lineNo: null },
92
+ `整份日志被压成 1 帧(${rows.length} 行)——官方形状是 header 独占第一帧 + 事件逐帧追加;不要这样写回`));
93
+ }
84
94
 
85
95
  // ── H · header ─────────────────────────────────────────────────────────
86
96
  if (header === null) {
package/package.json CHANGED
@@ -1,7 +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.16",
4
+ "version": "0.3.17",
5
5
  "packageManager": "pnpm@11.7.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",