dsh-log-contract 0.3.7 → 0.3.8
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 +10 -4
- package/README.zh.md +13 -1
- package/lib/checks.js +65 -0
- package/lib/contracts.js +16 -0
- package/lib/index.js +1 -1
- package/lib/prewrite.js +14 -1
- package/lib/validate.js +16 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -274,10 +274,16 @@ pnpm check && pnpm test # syntax check + 79 unit tests (incl. incident regres
|
|
|
274
274
|
node scripts/check-local-fossils.mjs # scans ../ for backup-session-*.jsonl.zstd
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
-
Known truth table
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
Known truth table (updated 2026-08-31 — after 0.3.5 added I1, `spliced-orphan`
|
|
278
|
+
now FAILs; the old PASS row was stale):
|
|
279
|
+
- `2c3f87d4-corrupt` / `b7713ea1-seqgap` / `rewritten-230542` → FAIL (seq gaps)
|
|
280
|
+
- `2c3f87d4-spliced-orphan` → **FAIL (0.3.5+)** (T1/I1: invalid inbox splice + turn-null)
|
|
281
|
+
- `e61d70da-pre-markerfix-20260825` → FAIL (pre-fix sample: turn-null markers remain)
|
|
282
|
+
- `c2d05ce9-pre-cleansession-20260831` → PASS (3256-span replace marker is data-legal;
|
|
283
|
+
official foldSurface replays cleanly — see the plugin ledger)
|
|
284
|
+
The truth table tracks rule evolution (0.3.5's I1 flipped spliced-orphan PASS→FAIL);
|
|
285
|
+
"repaired sessions PASS" must be verified on rebuilt samples — `pre-` backups are
|
|
286
|
+
usually pre-fix bad samples. See the boundary note in
|
|
281
287
|
[docs/CONTRACTS.md](docs/CONTRACTS.md)).
|
|
282
288
|
|
|
283
289
|
---
|
package/README.zh.md
CHANGED
|
@@ -236,7 +236,19 @@ pnpm check && pnpm test # 语法检查 + 79 个单测(含事故回归用例
|
|
|
236
236
|
node scripts/check-local-fossils.mjs # 扫描 ../ 下 backup-session-*.jsonl.zstd
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
已知真值表(2026-08-31 实测更新——0.3.5 加 I1 后,`spliced-orphan` 已可报错,旧表 PASS 过时):
|
|
240
|
+
|
|
241
|
+
| 化石 | 判定 | 违规 |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| `2c3f87d4-corrupt` | FAIL | S8/C1/T1/E2(seq 缺口 → 加载被拒) |
|
|
244
|
+
| `b7713ea1-seqgap` / `recorrupt` | FAIL | S8/C1/T1/E2/S9(seq 缺口/倒退) |
|
|
245
|
+
| `2c3f87d4-rewritten-230542` | FAIL | S8/C1/T1/E2/I1(重写引入缺口) |
|
|
246
|
+
| `2c3f87d4-spliced-orphan` | **FAIL(0.3.5+)** | T1/I1(inbox splice 无效 + turn-null)——旧表 PASS 已过时 |
|
|
247
|
+
| `e61d70da-pre-markerfix-20260825` | FAIL | T1×5(修复前样本:turn-null marker 残留,非「修复后」) |
|
|
248
|
+
| `c2d05ce9-pre-cleansession-20260831` | **PASS** | error 0(3256 跨度 replace marker 数据合规,官方 foldSurface 重放通过——见插件任务台账) |
|
|
249
|
+
|
|
250
|
+
> 说明:真值表随规则演进更新(0.3.5 新增 I1 后 spliced-orphan 从 PASS 变 FAIL);
|
|
251
|
+
> 「修复后会话 PASS」需用重建/修复后的样本验证,pre- 前缀备份多为修复前坏样本。
|
|
240
252
|
|
|
241
253
|
---
|
|
242
254
|
|
package/lib/checks.js
CHANGED
|
@@ -296,6 +296,71 @@ export function finalFold(events) {
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
/**
|
|
300
|
+
* T3 —— step 节点 key 冲突(2026-09-02 · 1e99e1ff 白屏真正根因固化)。
|
|
301
|
+
*
|
|
302
|
+
* 客户端渲染消息列表 = 从事件流构建节点,节点 key = `data.turn:data.step`
|
|
303
|
+
* (React 列表 key)。同 turn 内两个 step/start 的 step 号相同 → key 冲突 →
|
|
304
|
+
* React 渲染死循环 → 白屏/不展示(1e99e1ff:seq 580034 step 95/1 vs 580037
|
|
305
|
+
* 编辑块 step 95/1;修复线 2026-09-02 全量扫描另发现 6924781d / 97786207 /
|
|
306
|
+
* 4b149a4a 同型冲突,均整块重编号修复)。
|
|
307
|
+
*
|
|
308
|
+
* 判定:扫 step/start,`data.turn:data.step` 组合重复 = error。
|
|
309
|
+
* - 无任何 step/start 的日志跳过(与 T1 同款宽松);
|
|
310
|
+
* - turn/step 为 null/undefined 的 step/start 不参与 key 判定(那是 T4 的域,
|
|
311
|
+
* 且 null 自身已是致命)。
|
|
312
|
+
*
|
|
313
|
+
* @param events - 行序事件流(`{event, lineNo}`)。
|
|
314
|
+
* @returns T3 违规列表。
|
|
315
|
+
*/
|
|
316
|
+
export function stepKeyViolations(events) {
|
|
317
|
+
const out = []
|
|
318
|
+
if (!events.some(({ event }) => event.type === 'step/start')) return out
|
|
319
|
+
const seen = new Map() // `${turn}:${step}` → { seq, lineNo }
|
|
320
|
+
for (const { event, lineNo } of events) {
|
|
321
|
+
if (event.type !== 'step/start') continue
|
|
322
|
+
const turn = event.data?.turn
|
|
323
|
+
const step = event.data?.step
|
|
324
|
+
if (turn === null || turn === undefined || step === null || step === undefined) continue // T4 域
|
|
325
|
+
const key = `${String(turn)}:${String(step)}`
|
|
326
|
+
const prev = seen.get(key)
|
|
327
|
+
if (prev !== undefined) {
|
|
328
|
+
out.push(violation('T3', { seq: event.seq, lineNo, eventType: event.type }, `step 节点 key ${key} 冲突:seq ${prev.seq} 与 seq ${event.seq} 的 step/start 同 turn 同 step——客户端 React 渲染死循环白屏(1e99e1ff 事故;修复:同 turn 内 step 递增,不得复用;整块重编号)`))
|
|
329
|
+
} else {
|
|
330
|
+
seen.set(key, { seq: event.seq, lineNo })
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* T4 —— step/消息本体 turn 缺失(null/undefined)(2026-09-01 · D8 1e99e1ff 固化)。
|
|
338
|
+
*
|
|
339
|
+
* 客户端渲染状态机对 turn=null 的 step/消息**无法归属任何 turn** → 渲染死循环 →
|
|
340
|
+
* 白屏「载入历史」(1e99e1ff seq 580037-580039:retrace 0.4.17 写的编辑块
|
|
341
|
+
* step/start+marker+step/end turn 全 null;修复线 check-null-turn.mjs 把 step
|
|
342
|
+
* 包裹/消息本体的 null-turn 判为致命)。
|
|
343
|
+
*
|
|
344
|
+
* 判定:`step/start|step/end|assistant/message` 的 `data.turn === null/undefined` = error。
|
|
345
|
+
* - user/message 天然不带 turn(坐标来自外层 step),不查;
|
|
346
|
+
* - assistant/chunk 坐标可能天然缺失,不查;
|
|
347
|
+
* - retrace/marker(neutralize 产物,ignorable)不查。
|
|
348
|
+
*
|
|
349
|
+
* @param events - 行序事件流(`{event, lineNo}`)。
|
|
350
|
+
* @returns T4 违规列表。
|
|
351
|
+
*/
|
|
352
|
+
export function nullTurnStepViolations(events) {
|
|
353
|
+
const out = []
|
|
354
|
+
for (const { event, lineNo } of events) {
|
|
355
|
+
if (event.type !== 'step/start' && event.type !== 'step/end' && event.type !== 'assistant/message') continue
|
|
356
|
+
const turn = event.data?.turn
|
|
357
|
+
if (turn === null || turn === undefined) {
|
|
358
|
+
out.push(violation('T4', { seq: event.seq, lineNo, eventType: event.type }, `${event.type} 的 data.turn 为 ${String(turn)}(缺失)——客户端渲染状态机无法归属任何 turn → 渲染死循环白屏(D8 1e99e1ff;step/消息本体必须带真实 turn 号)`))
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return out
|
|
362
|
+
}
|
|
363
|
+
|
|
299
364
|
/**
|
|
300
365
|
* P3 —— tool/call ↔ tool/result 配对完整性(考古任务书 B1)。
|
|
301
366
|
* 每个 tool/call 的 `data.callId` 必须能在 tool/result 的
|
package/lib/contracts.js
CHANGED
|
@@ -229,6 +229,22 @@ export const CONTRACT_RULES = [
|
|
|
229
229
|
source: '@deepseek-ai/dsh-token-meter lib/index.js:634-650 (_estimateProviderAssistant,:645 belongs to another step)',
|
|
230
230
|
description: '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。',
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
id: 'T3',
|
|
234
|
+
title: 'step 节点 key 唯一(同 turn 内 step/start 的 step 号不得复用)',
|
|
235
|
+
layer: LAYER.ENGINE,
|
|
236
|
+
severity: SEVERITY.ERROR,
|
|
237
|
+
source: '复盘 2026-09-02 1e99e1ff 白屏(修复线 session-3f9e4f12):客户端渲染节点 key = turn:step,冲突 → React 渲染死循环;工具 tools/check-step-keys.mjs',
|
|
238
|
+
description: '客户端渲染消息列表从事件流构建节点,节点 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)。',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'T4',
|
|
242
|
+
title: 'step/消息本体 turn 缺失(null/undefined)→ 渲染死循环',
|
|
243
|
+
layer: LAYER.ENGINE,
|
|
244
|
+
severity: SEVERITY.ERROR,
|
|
245
|
+
source: '复盘 D8 1e99e1ff(2026-09-01):retrace 0.4.17 编辑块 turn:null;修复线 tools/check-null-turn.mjs 判致命',
|
|
246
|
+
description: '客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 → 白屏「载入历史」(1e99e1ff seq 580037-580039)。user/message 天然无 turn 不查;chunk 坐标可缺失不查。step/消息本体必须带真实 turn 号。',
|
|
247
|
+
},
|
|
232
248
|
{
|
|
233
249
|
id: 'P3',
|
|
234
250
|
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, physicalOrderViolations, inboxReplayViolations } from './checks.js';
|
|
12
|
+
export { tokenMeterViolations, tokenMeterSourceViolations, stepKeyViolations, nullTurnStepViolations, physicalOrderViolations, inboxReplayViolations } from './checks.js';
|
|
13
13
|
export { auditToolCalls, extractText, extractToolOutputs, indexToolCalls, toolCommandOf } from './archaeology.js';
|
package/lib/prewrite.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 所有判定复用 `lib/checks.js`(与离线体检同一套逻辑),
|
|
18
18
|
* 保证"体检看到的问题 = 写入前拦下的问题"。
|
|
19
19
|
*/
|
|
20
|
-
import { envelopeViolations, engineViolations, finalFold, isSafeInt, pluginViolations, replaySurface, tokenMeterViolations, violation } from './checks.js';
|
|
20
|
+
import { envelopeViolations, engineViolations, finalFold, isSafeInt, nullTurnStepViolations, pluginViolations, replaySurface, stepKeyViolations, tokenMeterViolations, violation } from './checks.js';
|
|
21
21
|
|
|
22
22
|
/** retrace 类 marker:data.editor 存在(assistant/message replace,turn/step=null)。 */
|
|
23
23
|
function isKnownMarkerCandidate(event) {
|
|
@@ -98,6 +98,19 @@ export function createPreWriter(input = {}) {
|
|
|
98
98
|
violations.push(t1);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
// T3/T4 —— 渲染层(2026-09-02 1e99e1ff 白屏)。**error 级拒绝**:
|
|
102
|
+
// - T4:拟写事件(step/start|step/end|assistant/message)turn 缺失 → 客户端
|
|
103
|
+
// 渲染死循环白屏(D8),写入前直接拦下(防再犯:任何写 turn:null 的 marker);
|
|
104
|
+
// - T3:拟写事件引入 step 节点 key 冲突(同 turn 同 step 的 step/start 重复)→
|
|
105
|
+
// 同样拒绝。只判拟写事件自身(历史冲突归属离线 check 的 T3/T4 扫描)。
|
|
106
|
+
for (const v of nullTurnStepViolations(candidateEvents.map((event) => ({ event })))) {
|
|
107
|
+
if (v.seq !== lastCandidate?.seq) continue;
|
|
108
|
+
violations.push(v);
|
|
109
|
+
}
|
|
110
|
+
for (const v of stepKeyViolations(candidateEvents.map((event) => ({ event })))) {
|
|
111
|
+
if (v.seq !== lastCandidate?.seq) continue;
|
|
112
|
+
violations.push(v);
|
|
113
|
+
}
|
|
101
114
|
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
102
115
|
for (const v of violations) bySeverity[v.severity] = (bySeverity[v.severity] ?? 0) + 1;
|
|
103
116
|
return {
|
package/lib/validate.js
CHANGED
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
finalFold,
|
|
16
16
|
inboxReplayViolations,
|
|
17
17
|
isSafeInt,
|
|
18
|
+
nullTurnStepViolations,
|
|
18
19
|
physicalOrderViolations,
|
|
19
20
|
pluginViolations,
|
|
20
21
|
replaySurface,
|
|
22
|
+
stepKeyViolations,
|
|
21
23
|
violation,
|
|
22
24
|
tokenMeterViolations,
|
|
23
25
|
tokenMeterSourceViolations,
|
|
@@ -115,6 +117,11 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
115
117
|
violations.push(...tokenMeterViolations(events));
|
|
116
118
|
violations.push(...tokenMeterSourceViolations(events));
|
|
117
119
|
|
|
120
|
+
// ── T3/T4 · 渲染层(复盘 2026-09-02 1e99e1ff 白屏):节点 key 唯一 + turn 缺失 ──
|
|
121
|
+
// 数据层"健康"≠ 客户端能渲染:check/官方加载/分页全绿仍可能白屏(节点 key 冲突)。
|
|
122
|
+
violations.push(...stepKeyViolations(events));
|
|
123
|
+
violations.push(...nullTurnStepViolations(events));
|
|
124
|
+
|
|
118
125
|
// ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
|
|
119
126
|
violations.push(...inboxReplayViolations(events, header));
|
|
120
127
|
|
|
@@ -190,9 +197,9 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
190
197
|
export function resumeVerdict(result) {
|
|
191
198
|
const { ok, violations = [] } = result;
|
|
192
199
|
// 三档各自的「阻断规则集」——按任务书 §L3 档位定义:
|
|
193
|
-
// 可加载:结构层(PERSISTENCE/FRAMING)+ 引擎层非 I1/T1/T2 的 error;
|
|
200
|
+
// 可加载:结构层(PERSISTENCE/FRAMING)+ 引擎层非 I1/T1/T2/T3/T4 的 error;
|
|
194
201
|
// 可继续:+ I1(inbox 重放);
|
|
195
|
-
// 可压缩:+ T1/T2(token-meter
|
|
202
|
+
// 可压缩:+ T1/T2/T3/T4(token-meter 配对 + 渲染层节点 key/turn)。
|
|
196
203
|
const byId = {};
|
|
197
204
|
for (const v of violations) {
|
|
198
205
|
if (v.severity !== 'error') continue;
|
|
@@ -200,11 +207,11 @@ export function resumeVerdict(result) {
|
|
|
200
207
|
}
|
|
201
208
|
const has = (id) => (byId[id]?.length ?? 0) > 0;
|
|
202
209
|
|
|
203
|
-
// 可加载阻断 = 除 I1/T1/T2 外的所有 error 违规(结构/信封/物理序/工具配对等)。
|
|
204
|
-
// 注意:不能用 result.ok(它把 T1/T2/I1 也计为 error)——三档判定按任务书定义,
|
|
205
|
-
// T1/T2 只影响「可压缩」档、I1 只影响「可继续」档。
|
|
210
|
+
// 可加载阻断 = 除 I1/T1/T2/T3/T4 外的所有 error 违规(结构/信封/物理序/工具配对等)。
|
|
211
|
+
// 注意:不能用 result.ok(它把 T1/T2/T3/T4/I1 也计为 error)——三档判定按任务书定义,
|
|
212
|
+
// T1/T2/T3/T4 只影响「可压缩」档、I1 只影响「可继续」档。
|
|
206
213
|
const loadableBlockers = violations.filter(
|
|
207
|
-
(v) => v.severity === 'error' && !['I1', 'T1', 'T2'].includes(v.id),
|
|
214
|
+
(v) => v.severity === 'error' && !['I1', 'T1', 'T2', 'T3', 'T4'].includes(v.id),
|
|
208
215
|
).map((v) => v.id);
|
|
209
216
|
const loadable = loadableBlockers.length === 0;
|
|
210
217
|
|
|
@@ -212,9 +219,9 @@ export function resumeVerdict(result) {
|
|
|
212
219
|
const resumable = loadable && !has('I1');
|
|
213
220
|
|
|
214
221
|
const compactableBlockers = resumable
|
|
215
|
-
? (has('T1') || has('T2') ? ['T1', 'T2'].filter((id) => has(id)) : [])
|
|
222
|
+
? (has('T1') || has('T2') || has('T3') || has('T4') ? ['T1', 'T2', 'T3', 'T4'].filter((id) => has(id)) : [])
|
|
216
223
|
: [];
|
|
217
|
-
const compactable = resumable && !has('T1') && !has('T2');
|
|
224
|
+
const compactable = resumable && !has('T1') && !has('T2') && !has('T3') && !has('T4');
|
|
218
225
|
|
|
219
226
|
// 最差档位
|
|
220
227
|
const verdict = !loadable ? 'broken' : !resumable ? 'loadable' : !compactable ? 'resumable' : 'compactable';
|
|
@@ -232,7 +239,7 @@ export function resumeVerdict(result) {
|
|
|
232
239
|
violationsByTier: {
|
|
233
240
|
structural: [...new Set(loadableBlockers)],
|
|
234
241
|
inbox: has('I1') ? ['I1'] : [],
|
|
235
|
-
tokenMeter: has('T1') || has('T2') ? ['T1', 'T2'].filter((id) => has(id)) : [],
|
|
242
|
+
tokenMeter: has('T1') || has('T2') || has('T3') || has('T4') ? ['T1', 'T2', 'T3', 'T4'].filter((id) => has(id)) : [],
|
|
236
243
|
},
|
|
237
244
|
};
|
|
238
245
|
}
|
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.
|
|
4
|
+
"version": "0.3.8",
|
|
5
5
|
"packageManager": "pnpm@11.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|