dsh-log-contract 0.3.8 → 0.3.9
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/lib/checks.js +26 -0
- package/lib/contracts.js +8 -0
- package/lib/index.js +1 -1
- package/lib/prewrite.js +6 -1
- package/lib/validate.js +8 -6
- package/package.json +1 -1
package/lib/checks.js
CHANGED
|
@@ -361,6 +361,32 @@ export function nullTurnStepViolations(events) {
|
|
|
361
361
|
return out
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* T5 —— turn/end 缺 reason.kind(2026-09-02 · 1f4d986e malformed turn/end 固化)。
|
|
366
|
+
*
|
|
367
|
+
* 官方 agent-loop 写 turn/end 恒带 `reason: { kind }`(dsh-agent-loop/lib/index.js:620;
|
|
368
|
+
* kind ∈ completed|max-tokens|blocked|aborted|error|interrupted,中断恢复补
|
|
369
|
+
* interrupted,见 dsh-session interruptedTurnClosers)。官方 validation 强制
|
|
370
|
+
* reason.kind 存在——缺失 = malformed → 会话加载失败
|
|
371
|
+
* (SessionPersistenceCorruptionError "malformed pre-react-loop turn/end",1f4d986e)。
|
|
372
|
+
*
|
|
373
|
+
* 判定:turn/end 的 `data.reason?.kind` 缺失/非字符串 = error。
|
|
374
|
+
*
|
|
375
|
+
* @param events - 行序事件流(`{event, lineNo}`)。
|
|
376
|
+
* @returns T5 违规列表。
|
|
377
|
+
*/
|
|
378
|
+
export function turnEndReasonViolations(events) {
|
|
379
|
+
const out = []
|
|
380
|
+
for (const { event, lineNo } of events) {
|
|
381
|
+
if (event.type !== 'turn/end') continue
|
|
382
|
+
const reason = event.data?.reason
|
|
383
|
+
if (!reason || typeof reason.kind !== 'string') {
|
|
384
|
+
out.push(violation('T5', { seq: event.seq, lineNo, eventType: event.type }, `turn/end 缺 data.reason.kind(reason=${JSON.stringify(reason)})——官方 validation 拒绝 → 会话加载失败(1f4d986e malformed turn/end;turn/end 必须带 reason.kind,镜像 dsh-agent-loop:620)`))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return out
|
|
388
|
+
}
|
|
389
|
+
|
|
364
390
|
/**
|
|
365
391
|
* P3 —— tool/call ↔ tool/result 配对完整性(考古任务书 B1)。
|
|
366
392
|
* 每个 tool/call 的 `data.callId` 必须能在 tool/result 的
|
package/lib/contracts.js
CHANGED
|
@@ -245,6 +245,14 @@ export const CONTRACT_RULES = [
|
|
|
245
245
|
source: '复盘 D8 1e99e1ff(2026-09-01):retrace 0.4.17 编辑块 turn:null;修复线 tools/check-null-turn.mjs 判致命',
|
|
246
246
|
description: '客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 → 白屏「载入历史」(1e99e1ff seq 580037-580039)。user/message 天然无 turn 不查;chunk 坐标可缺失不查。step/消息本体必须带真实 turn 号。',
|
|
247
247
|
},
|
|
248
|
+
{
|
|
249
|
+
id: 'T5',
|
|
250
|
+
title: 'turn/end 必须带 data.reason.kind',
|
|
251
|
+
layer: LAYER.ENGINE,
|
|
252
|
+
severity: SEVERITY.ERROR,
|
|
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
|
+
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
|
+
},
|
|
248
256
|
{
|
|
249
257
|
id: 'P3',
|
|
250
258
|
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, physicalOrderViolations, inboxReplayViolations } from './checks.js';
|
|
12
|
+
export { tokenMeterViolations, tokenMeterSourceViolations, stepKeyViolations, nullTurnStepViolations, turnEndReasonViolations, 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, nullTurnStepViolations, pluginViolations, replaySurface, stepKeyViolations, tokenMeterViolations, violation } from './checks.js';
|
|
20
|
+
import { envelopeViolations, engineViolations, finalFold, isSafeInt, nullTurnStepViolations, pluginViolations, replaySurface, stepKeyViolations, tokenMeterViolations, turnEndReasonViolations, violation } from './checks.js';
|
|
21
21
|
|
|
22
22
|
/** retrace 类 marker:data.editor 存在(assistant/message replace,turn/step=null)。 */
|
|
23
23
|
function isKnownMarkerCandidate(event) {
|
|
@@ -111,6 +111,11 @@ export function createPreWriter(input = {}) {
|
|
|
111
111
|
if (v.seq !== lastCandidate?.seq) continue;
|
|
112
112
|
violations.push(v);
|
|
113
113
|
}
|
|
114
|
+
// T5 —— 拟写 turn/end 缺 reason.kind → error 拒绝(1f4d986e 防再犯)
|
|
115
|
+
for (const v of turnEndReasonViolations(candidateEvents.map((event) => ({ event })))) {
|
|
116
|
+
if (v.seq !== lastCandidate?.seq) continue;
|
|
117
|
+
violations.push(v);
|
|
118
|
+
}
|
|
114
119
|
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
115
120
|
for (const v of violations) bySeverity[v.severity] = (bySeverity[v.severity] ?? 0) + 1;
|
|
116
121
|
return {
|
package/lib/validate.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
pluginViolations,
|
|
21
21
|
replaySurface,
|
|
22
22
|
stepKeyViolations,
|
|
23
|
+
turnEndReasonViolations,
|
|
23
24
|
violation,
|
|
24
25
|
tokenMeterViolations,
|
|
25
26
|
tokenMeterSourceViolations,
|
|
@@ -117,10 +118,11 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
117
118
|
violations.push(...tokenMeterViolations(events));
|
|
118
119
|
violations.push(...tokenMeterSourceViolations(events));
|
|
119
120
|
|
|
120
|
-
// ── T3/T4 ·
|
|
121
|
-
// 数据层"健康"≠
|
|
121
|
+
// ── T3/T4/T5 · 渲染/契约层(复盘 2026-09-02):节点 key 唯一 + turn 缺失 + turn/end reason ──
|
|
122
|
+
// 数据层"健康"≠ 客户端能渲染/能加载:check/官方加载/分页全绿仍可能白屏或加载失败。
|
|
122
123
|
violations.push(...stepKeyViolations(events));
|
|
123
124
|
violations.push(...nullTurnStepViolations(events));
|
|
125
|
+
violations.push(...turnEndReasonViolations(events));
|
|
124
126
|
|
|
125
127
|
// ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
|
|
126
128
|
violations.push(...inboxReplayViolations(events, header));
|
|
@@ -211,7 +213,7 @@ export function resumeVerdict(result) {
|
|
|
211
213
|
// 注意:不能用 result.ok(它把 T1/T2/T3/T4/I1 也计为 error)——三档判定按任务书定义,
|
|
212
214
|
// T1/T2/T3/T4 只影响「可压缩」档、I1 只影响「可继续」档。
|
|
213
215
|
const loadableBlockers = violations.filter(
|
|
214
|
-
(v) => v.severity === 'error' && !['I1', 'T1', 'T2', 'T3', 'T4'].includes(v.id),
|
|
216
|
+
(v) => v.severity === 'error' && !['I1', 'T1', 'T2', 'T3', 'T4', 'T5'].includes(v.id),
|
|
215
217
|
).map((v) => v.id);
|
|
216
218
|
const loadable = loadableBlockers.length === 0;
|
|
217
219
|
|
|
@@ -219,9 +221,9 @@ export function resumeVerdict(result) {
|
|
|
219
221
|
const resumable = loadable && !has('I1');
|
|
220
222
|
|
|
221
223
|
const compactableBlockers = resumable
|
|
222
|
-
? (has('T1') || has('T2') || has('T3') || has('T4') ? ['T1', 'T2', 'T3', 'T4'].filter((id) => has(id)) : [])
|
|
224
|
+
? (has('T1') || has('T2') || has('T3') || has('T4') || has('T5') ? ['T1', 'T2', 'T3', 'T4', 'T5'].filter((id) => has(id)) : [])
|
|
223
225
|
: [];
|
|
224
|
-
const compactable = resumable && !has('T1') && !has('T2') && !has('T3') && !has('T4');
|
|
226
|
+
const compactable = resumable && !has('T1') && !has('T2') && !has('T3') && !has('T4') && !has('T5');
|
|
225
227
|
|
|
226
228
|
// 最差档位
|
|
227
229
|
const verdict = !loadable ? 'broken' : !resumable ? 'loadable' : !compactable ? 'resumable' : 'compactable';
|
|
@@ -239,7 +241,7 @@ export function resumeVerdict(result) {
|
|
|
239
241
|
violationsByTier: {
|
|
240
242
|
structural: [...new Set(loadableBlockers)],
|
|
241
243
|
inbox: has('I1') ? ['I1'] : [],
|
|
242
|
-
tokenMeter: has('T1') || has('T2') || has('T3') || has('T4') ? ['T1', 'T2', 'T3', 'T4'].filter((id) => has(id)) : [],
|
|
244
|
+
tokenMeter: has('T1') || has('T2') || has('T3') || has('T4') || has('T5') ? ['T1', 'T2', 'T3', 'T4', 'T5'].filter((id) => has(id)) : [],
|
|
243
245
|
},
|
|
244
246
|
};
|
|
245
247
|
}
|
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.9",
|
|
5
5
|
"packageManager": "pnpm@11.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|