dsh-log-contract 0.3.11 → 0.3.13
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 +32 -0
- package/README.zh.md +28 -0
- package/bin/dsh-log-contract.mjs +194 -64
- package/lib/checks.js +151 -23
- package/lib/compat.js +282 -0
- package/lib/contracts.js +36 -4
- package/lib/index.js +2 -1
- package/lib/legacy-fold.js +177 -0
- package/lib/log-reader.js +8 -4
- package/lib/prewrite.js +33 -15
- package/lib/repair.js +70 -31
- package/lib/validate.js +243 -13
- package/lib/vocab.js +193 -0
- package/package.json +10 -8
package/lib/validate.js
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 离线体检引擎:对 `loadSessionLog` 的结果逐条跑契约规则,产出违规报告。
|
|
5
5
|
*
|
|
6
|
-
* 判定哲学(复盘事故 §四-1
|
|
6
|
+
* 判定哲学(复盘事故 §四-1):**持久化层以 fold 不抛为通过**,
|
|
7
7
|
* 但为定位问题,先用与官方同语义的增量重放做逐事件归因(S1–S7),
|
|
8
|
-
*
|
|
8
|
+
* 再跑 fold 终验(S8)——两套都绿才算过。
|
|
9
|
+
* 终验按**被检文件 header.version** 选:v3 → 运行时官方 `foldSurface`;
|
|
10
|
+
* v0/v1/v2 → 本地等价实现 `legacyFoldSurface`(0.1.5 的 foldSurface 是 v3 语义,对旧格式会误报)。
|
|
9
11
|
*/
|
|
10
12
|
import { ruleById } from './contracts.js';
|
|
13
|
+
import { normalizeEventSeqRanges } from './compat.js';
|
|
14
|
+
import { HOST_MAX_FILE_VERSION, MIGRATION_TARGET_VERSION, hostCapability, isAssessableFileVersion } from './vocab.js';
|
|
11
15
|
import {
|
|
12
16
|
CHUNK_ROW_TYPES,
|
|
13
17
|
envelopeViolations,
|
|
@@ -16,6 +20,7 @@ import {
|
|
|
16
20
|
finalFold,
|
|
17
21
|
inboxReplayViolations,
|
|
18
22
|
isSafeInt,
|
|
23
|
+
migrationPrecheckViolations,
|
|
19
24
|
nullTurnStepViolations,
|
|
20
25
|
physicalOrderViolations,
|
|
21
26
|
pluginViolations,
|
|
@@ -30,6 +35,9 @@ import {
|
|
|
30
35
|
wireViolations,
|
|
31
36
|
} from './checks.js';
|
|
32
37
|
|
|
38
|
+
/** 宿主能力闸触发时**跳过**的规则(v3/被检版本语义相关)——报告里显式列出,避免"静默通过"。 */
|
|
39
|
+
const VERSION_DEPENDENT_RULES = ['E3', 'E7', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8', 'W1', 'W2', 'T1', 'T2', 'T3', 'T4', 'T5', 'G1', 'G2'];
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* 对会话日志执行全量离线体检。
|
|
35
43
|
*
|
|
@@ -45,7 +53,23 @@ import {
|
|
|
45
53
|
export function validateSessionLog(log, opts = {}) {
|
|
46
54
|
const { baseSeq = 0 } = opts;
|
|
47
55
|
const violations = [];
|
|
48
|
-
const { header, headerLine, rows,
|
|
56
|
+
const { header, headerLine, rows, frameInfo } = log;
|
|
57
|
+
// C1 防御(多入口一致):即使调用方手搓 log 对象、未经 `loadSessionLog`,也在体检入口
|
|
58
|
+
// 归一一次 v3 区间编码。正常路径(loadSessionLog 已展开)下这是幂等的恒等映射。
|
|
59
|
+
const events = (log.events ?? []).map((e) => ({ ...e, event: normalizeEventSeqRanges(e.event) }));
|
|
60
|
+
// 1.3 第二半:词表/折叠路径按**被检文件自身版本**选择(不按运行时)。
|
|
61
|
+
// C2:版本是**本次调用的局部量**,显式传给每条按版本择路的判定——不再写模块级全局
|
|
62
|
+
// (旧实现 `setFileFormatVersion()` 会被同进程后调用的 prewrite 读到,造成结论翻转)。
|
|
63
|
+
const formatVersion = Number.isSafeInteger(header?.version) && header.version >= 0 ? header.version : 0;
|
|
64
|
+
|
|
65
|
+
// ── 宿主能力闸(S2·安全)──────────────────────────────────────────────────
|
|
66
|
+
// 被检文件版本 > 本宿主支持的最大文件版本 ⇒ 本宿主没有该版本的词表/折叠语义。继续按
|
|
67
|
+
// 本宿主语义判定会产出**假阳性**(独立复核实测:rc.7 上同一真实 v3 文件 →
|
|
68
|
+
// `S8×1 + E3×40 / verdict=broken / loadable:false`),危险是用户以为日志坏了去跑
|
|
69
|
+
// `fix --apply`。改为:只跑与版本无关的结构检查 + 显式"不可在本宿主评估"档。
|
|
70
|
+
if (!isAssessableFileVersion(formatVersion)) {
|
|
71
|
+
return unassessableResult({ header, headerLine, rows, events, frameInfo, baseSeq, formatVersion });
|
|
72
|
+
}
|
|
49
73
|
|
|
50
74
|
// ── Z · 帧结构 ─────────────────────────────────────────────────────────
|
|
51
75
|
if (frameInfo?.torn) {
|
|
@@ -62,8 +86,8 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
62
86
|
if (header.type !== 'session') {
|
|
63
87
|
violations.push(violation('H1', { lineNo: headerLine }, `首行 type 必须为 "session"(实际 ${String(header.type)})`));
|
|
64
88
|
}
|
|
65
|
-
if (header.version
|
|
66
|
-
violations.push(violation('H2', { lineNo: headerLine }, `header.version
|
|
89
|
+
if (![0, 1, 2, 3].includes(header.version)) {
|
|
90
|
+
violations.push(violation('H2', { lineNo: headerLine }, `header.version 未知(实际 ${String(header.version)})——已知 0/1/2/3;未知版本按 0 处理会误判,请先确认格式`));
|
|
67
91
|
}
|
|
68
92
|
if (typeof header.id !== 'string' || header.id === '') {
|
|
69
93
|
violations.push(violation('H2', { lineNo: headerLine }, 'header.id 必须为非空字符串'));
|
|
@@ -100,7 +124,7 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
100
124
|
let seqBroken = false;
|
|
101
125
|
for (const { event, lineNo } of events) {
|
|
102
126
|
const loc = { seq: event.seq, lineNo, eventType: event.type };
|
|
103
|
-
violations.push(...envelopeViolations(event, loc));
|
|
127
|
+
violations.push(...envelopeViolations(event, loc, formatVersion));
|
|
104
128
|
if (typeof event.seq === 'number' && Number.isSafeInteger(event.seq) && event.seq >= 0) {
|
|
105
129
|
if (event.seq !== expectedSeq) {
|
|
106
130
|
const kind = event.seq < expectedSeq ? '倒退(backward)' : '缺口(gap)';
|
|
@@ -117,10 +141,10 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
117
141
|
violations.push(...physicalOrderViolations(rows));
|
|
118
142
|
|
|
119
143
|
// ── S · surface 增量重放(归因)+ 官方 foldSurface 终验 ────────────────
|
|
120
|
-
const replay = replaySurface(events);
|
|
144
|
+
const replay = replaySurface(events, formatVersion);
|
|
121
145
|
violations.push(...replay.violations);
|
|
122
146
|
|
|
123
|
-
const folded = finalFold(events.map((e) => e.event));
|
|
147
|
+
const folded = finalFold(events.map((e) => e.event), formatVersion);
|
|
124
148
|
|
|
125
149
|
// ── T · token meter 配对(事故根因 3 + 2026-08-30 两类刷屏)──
|
|
126
150
|
violations.push(...tokenMeterViolations(events));
|
|
@@ -133,7 +157,7 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
133
157
|
violations.push(...turnEndReasonViolations(events));
|
|
134
158
|
|
|
135
159
|
// ── E7 · ignorable 未知 type 合法性(反向挑刺 T2)────────────────────
|
|
136
|
-
violations.push(...ignorableTypeViolations(events));
|
|
160
|
+
violations.push(...ignorableTypeViolations(events, formatVersion));
|
|
137
161
|
|
|
138
162
|
// ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
|
|
139
163
|
violations.push(...inboxReplayViolations(events, header));
|
|
@@ -142,11 +166,11 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
142
166
|
violations.push(...toolPairingViolations(events));
|
|
143
167
|
violations.push(...toolResultStructureViolations(events));
|
|
144
168
|
if (folded.error) {
|
|
145
|
-
violations.push(violation('S8', { lineNo: null },
|
|
169
|
+
violations.push(violation('S8', { lineNo: null }, `foldSurface 重放失败(按文件版本选:v3 官方 / v0–v2 本地等价):${folded.error.message} —— 会话加载会被拒(SessionPersistenceCorruptionError)`));
|
|
146
170
|
}
|
|
147
171
|
|
|
148
172
|
// ── W · wire 消息流(严格端点拒绝的悬空 tool / 顺序破坏)──────────────
|
|
149
|
-
violations.push(...wireViolations(events));
|
|
173
|
+
violations.push(...wireViolations(events, formatVersion));
|
|
150
174
|
|
|
151
175
|
// ── M / P ──────────────────────────────────────────────────────────────
|
|
152
176
|
for (const { event, lineNo } of events) {
|
|
@@ -160,6 +184,11 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
160
184
|
violations.push(violation('C1', { lineNo: null }, 'seq 缺口/倒退是多写入者(≥2 个 Host 进程共享同一 session 目录)并发写的典型后果;离线体检无法观测竞态本身,但此痕迹需人工核查(N6)'));
|
|
161
185
|
}
|
|
162
186
|
|
|
187
|
+
// ── G · 迁移预检(**独立维度**:官方升级路径会不会拒;不进 ok/verdict)──────
|
|
188
|
+
// 官方把 v0/v1/v2 升到当前格式时的规则集比本工具的"读取/折叠"规则集更严:本工具判
|
|
189
|
+
// ok/compactable **不代表**官方迁移会接受。这里只加 migration 层 warning(见 contracts G1/G2)。
|
|
190
|
+
violations.push(...migrationPrecheckViolations(events, formatVersion));
|
|
191
|
+
|
|
163
192
|
// ── 汇总 ───────────────────────────────────────────────────────────────
|
|
164
193
|
violations.sort((a, b) => (a.seq ?? -1) - (b.seq ?? -1) || (a.lineNo ?? -1) - (b.lineNo ?? -1));
|
|
165
194
|
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
@@ -178,14 +207,196 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
178
207
|
frames: frameInfo?.frames ?? 0,
|
|
179
208
|
compressedBytes: frameInfo?.compressedBytes ?? 0,
|
|
180
209
|
plaintextBytes: frameInfo?.plaintextBytes ?? 0,
|
|
210
|
+
fileVersion: formatVersion,
|
|
211
|
+
hostMaxFileVersion: HOST_MAX_FILE_VERSION,
|
|
212
|
+
assessable: true,
|
|
181
213
|
};
|
|
182
214
|
|
|
183
|
-
|
|
215
|
+
const result = {
|
|
184
216
|
ok: bySeverity.error === 0,
|
|
217
|
+
assessable: true,
|
|
185
218
|
violations,
|
|
186
219
|
summary,
|
|
187
220
|
surface: folded.surface ?? { nodes: replay.nodes, replacements: [] },
|
|
188
221
|
};
|
|
222
|
+
result.migration = migrationVerdict(result);
|
|
223
|
+
result.assessmentScope = result.migration.assessmentScope;
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 宿主能力闸的结果(S2):被检文件版本 > 本宿主支持的最大版本。
|
|
229
|
+
*
|
|
230
|
+
* 只跑**与版本无关**的结构检查(Z 帧 / H header / R 行 / E1·E2·E4·E5·E6 信封不含 E3 词表 /
|
|
231
|
+
* S9 物理序 / M·P / P3·P4 / I1),其余(S1–S8/W/T/E3/E7/G)显式跳过并在 `notAssessable.skippedRules`
|
|
232
|
+
* 里列出。**ok 固定 false**(不认证),但 `structuralOk` 单独给出结构层结论——
|
|
233
|
+
* 语义是"本宿主评估不了",不是"日志坏了"。
|
|
234
|
+
*/
|
|
235
|
+
function unassessableResult({ header, headerLine, rows, events, frameInfo, baseSeq, formatVersion }) {
|
|
236
|
+
const violations = [];
|
|
237
|
+
const cap = hostCapability();
|
|
238
|
+
const notAssessable = {
|
|
239
|
+
fileVersion: formatVersion,
|
|
240
|
+
hostMaxFileVersion: HOST_MAX_FILE_VERSION,
|
|
241
|
+
hostPackage: cap.hostPackage,
|
|
242
|
+
skippedRules: VERSION_DEPENDENT_RULES,
|
|
243
|
+
reason: `被检文件 version=${formatVersion} 高于本宿主 @deepseek-ai/dsh-session@${cap.hostPackage} 支持的最大文件版本 ${HOST_MAX_FILE_VERSION}`
|
|
244
|
+
+ `(本宿主只有 ≤${HOST_MAX_FILE_VERSION} 的词表/折叠语义)⇒ 按本宿主语义判定会产出假阳性(如 S8/E3)。`
|
|
245
|
+
+ '这不表示日志损坏;请用 0.1.5+ 宿主评估。',
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ── Z · 帧结构(版本无关)──
|
|
249
|
+
if (frameInfo?.torn) violations.push(violation('Z1', { lineNo: null }, `zstd 尾帧撕裂:可能是写入中的 in-flight 帧或文件被截断(帧数 ${frameInfo.frames})`));
|
|
250
|
+
if (frameInfo?.error) violations.push(violation('Z2', { lineNo: null }, frameInfo.error));
|
|
251
|
+
|
|
252
|
+
// ── H · header(版本无关)──
|
|
253
|
+
if (header === null) {
|
|
254
|
+
violations.push(violation('H1', { lineNo: headerLine }, '首行不是合法 JSON —— 整个会话不可读'));
|
|
255
|
+
} else {
|
|
256
|
+
if (header.type !== 'session') violations.push(violation('H1', { lineNo: headerLine }, `首行 type 必须为 "session"(实际 ${String(header.type)})`));
|
|
257
|
+
if (![0, 1, 2, 3].includes(header.version)) violations.push(violation('H2', { lineNo: headerLine }, `header.version 未知(实际 ${String(header.version)})`));
|
|
258
|
+
if (typeof header.id !== 'string' || header.id === '') violations.push(violation('H2', { lineNo: headerLine }, 'header.id 必须为非空字符串'));
|
|
259
|
+
if (!Number.isSafeInteger(header.createdAt) || header.createdAt < 0) violations.push(violation('H2', { lineNo: headerLine }, 'header.createdAt 必须为非负安全整数'));
|
|
260
|
+
if (header.cwd !== undefined && (typeof header.cwd !== 'string' || !header.cwd.startsWith('/'))) violations.push(violation('H2', { lineNo: headerLine }, 'header.cwd 若存在必须为绝对路径'));
|
|
261
|
+
if (header.origin !== undefined && header.origin !== 'subagent') violations.push(violation('H2', { lineNo: headerLine }, `header.origin 只能为 "subagent"(实际 ${String(header.origin)})`));
|
|
262
|
+
}
|
|
263
|
+
if (header !== null && events.length === 0) violations.push(violation('Z3', { lineNo: headerLine }, '会话文件无任何事件(仅 header)——异常空会话'));
|
|
264
|
+
|
|
265
|
+
// ── R · 存储行(版本无关)──
|
|
266
|
+
for (const row of rows) {
|
|
267
|
+
if (row.error && row.value === null) violations.push(violation('R1', { lineNo: row.lineNo }, '该行不是合法 JSON(损坏行)'));
|
|
268
|
+
else if (row.error && CHUNK_ROW_TYPES.has(row.value?.type)) violations.push(violation('R2', { lineNo: row.lineNo }, `chunk 行 "${row.value.type}" 损坏:${row.error.message}`));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── E · 信封 + seq(E3 词表判定**跳过**:词表随被检版本而变,本宿主没有该版本词表)──
|
|
272
|
+
let expectedSeq = baseSeq;
|
|
273
|
+
let seqBroken = false;
|
|
274
|
+
for (const { event, lineNo } of events) {
|
|
275
|
+
const loc = { seq: event.seq, lineNo, eventType: event.type };
|
|
276
|
+
violations.push(...envelopeViolations(event, loc, formatVersion).filter((v) => v.id !== 'E3'));
|
|
277
|
+
if (typeof event.seq === 'number' && Number.isSafeInteger(event.seq) && event.seq >= 0) {
|
|
278
|
+
if (event.seq !== expectedSeq) {
|
|
279
|
+
violations.push(violation('E2', loc, `seq ${event.seq} 不连续:${event.seq < expectedSeq ? '倒退(backward)' : '缺口(gap)'},期望 ${expectedSeq}`));
|
|
280
|
+
seqBroken = true;
|
|
281
|
+
expectedSeq = event.seq + 1;
|
|
282
|
+
} else expectedSeq = event.seq + 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
violations.push(...physicalOrderViolations(rows));
|
|
286
|
+
violations.push(...inboxReplayViolations(events, header));
|
|
287
|
+
violations.push(...toolPairingViolations(events));
|
|
288
|
+
violations.push(...toolResultStructureViolations(events));
|
|
289
|
+
for (const { event, lineNo } of events) {
|
|
290
|
+
const loc = { seq: event.seq, lineNo, eventType: event.type };
|
|
291
|
+
violations.push(...engineViolations(event, loc));
|
|
292
|
+
violations.push(...pluginViolations(event, loc));
|
|
293
|
+
}
|
|
294
|
+
if (seqBroken) violations.push(violation('C1', { lineNo: null }, 'seq 缺口/倒退是多写入者并发写的典型后果(N6)'));
|
|
295
|
+
|
|
296
|
+
violations.sort((a, b) => (a.seq ?? -1) - (b.seq ?? -1) || (a.lineNo ?? -1) - (b.lineNo ?? -1));
|
|
297
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
298
|
+
const byLayer = {};
|
|
299
|
+
for (const v of violations) {
|
|
300
|
+
bySeverity[v.severity] = (bySeverity[v.severity] ?? 0) + 1;
|
|
301
|
+
byLayer[v.layer] = (byLayer[v.layer] ?? 0) + 1;
|
|
302
|
+
}
|
|
303
|
+
const result = {
|
|
304
|
+
ok: false, // 不认证(**不是**"broken"——语义见 notAssessable.reason)
|
|
305
|
+
assessable: false,
|
|
306
|
+
structuralOk: bySeverity.error === 0,
|
|
307
|
+
notAssessable,
|
|
308
|
+
violations,
|
|
309
|
+
summary: {
|
|
310
|
+
total: violations.length,
|
|
311
|
+
bySeverity,
|
|
312
|
+
byLayer,
|
|
313
|
+
events: events.length,
|
|
314
|
+
surfaceNodes: null,
|
|
315
|
+
replaceGeneration: null,
|
|
316
|
+
frames: frameInfo?.frames ?? 0,
|
|
317
|
+
compressedBytes: frameInfo?.compressedBytes ?? 0,
|
|
318
|
+
plaintextBytes: frameInfo?.plaintextBytes ?? 0,
|
|
319
|
+
fileVersion: formatVersion,
|
|
320
|
+
hostMaxFileVersion: HOST_MAX_FILE_VERSION,
|
|
321
|
+
assessable: false,
|
|
322
|
+
},
|
|
323
|
+
surface: null,
|
|
324
|
+
};
|
|
325
|
+
result.migration = migrationVerdict(result);
|
|
326
|
+
result.assessmentScope = result.migration.assessmentScope;
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 迁移预检结论(S1)——**独立维度**:官方把 v0/v1/v2 升到当前格式会不会拒。
|
|
332
|
+
*
|
|
333
|
+
* 与 `ok` / `loadable` / `resumable` / `compactable` **并列且互不蕴含**:
|
|
334
|
+
* 本工具判 ok **不代表**官方迁移会接受(反之亦然)。只统计 `layer === 'migration'` 的违规
|
|
335
|
+
* (G1/G2),并**显式声明覆盖边界**——未覆盖的官方迁移规则不等于通过。
|
|
336
|
+
*
|
|
337
|
+
* @param {object} result `validateSessionLog()` 的返回值
|
|
338
|
+
*/
|
|
339
|
+
export function migrationVerdict(result) {
|
|
340
|
+
const violations = result?.violations ?? [];
|
|
341
|
+
const fileVersion = result?.summary?.fileVersion ?? result?.notAssessable?.fileVersion ?? null;
|
|
342
|
+
const coverage = {
|
|
343
|
+
implemented: [
|
|
344
|
+
'G1 v0 源:subagent/descriptor.data.version !== 3',
|
|
345
|
+
'G2 v0 源:事件类型不在官方 v0 dispositions 内(含 ignorable:true)',
|
|
346
|
+
'G3 v0 源:session/title 系列 messageSeqs 必须引用更早的人类 user/message(含"空 ⟺ 用户标题")',
|
|
347
|
+
],
|
|
348
|
+
uncovered: [
|
|
349
|
+
// 2026-09-15 第五轮:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
|
|
350
|
+
// 官方状态机(assertReleasedArtifactRelationships)由 v1→v2 在**变换后的 v1/v2 artifact**
|
|
351
|
+
// 上调用(v1-to-v2/lib/index.js:104),带 cut/继承切点处理;在原始 v0 上照抄会在已 seed
|
|
352
|
+
// 的会话上狂报(样本 session-62c5b531:0.1.5 链并不以该规则拒它,原始 v0 上会报 19 条)。
|
|
353
|
+
// ⇒ 忠实复现需先做 v0→v1→v2 变换;本轮不做,保持未覆盖。
|
|
354
|
+
'turn/start 闭合/预期轮(官方 v1→v2 在**变换后** artifact 上判:does not close the prior turn / does not open expected turn)',
|
|
355
|
+
'assistant/attempt 配对/闭合(migration refuses the transformed artifact)',
|
|
356
|
+
'Session inheritedEventCut / 继承切点(官方在变换后 artifact 上按 cut 判;header 只有 seedLength)',
|
|
357
|
+
'stored log corrupt(SessionFormatError)',
|
|
358
|
+
'v0→v1 对其余事件的形状拒绝',
|
|
359
|
+
'turn/end 无匹配 open turn / open step 穿越 / 未解析 tool / open compaction 穿越(官方同一状态机的其余分支)',
|
|
360
|
+
],
|
|
361
|
+
note: '未覆盖 ≠ 通过:本维度只对 implemented 三条负责。官方判据版本:@deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 + v1-to-v2@0.1.5-rc.2',
|
|
362
|
+
};
|
|
363
|
+
if (result?.assessable === false) {
|
|
364
|
+
return { assessable: false, applies: null, ready: null, fileVersion, blocked: [], coverage, assessmentScope: 'none', reason: result?.notAssessable?.reason ?? null };
|
|
365
|
+
}
|
|
366
|
+
const blocked = violations.filter((v) => v.layer === 'migration');
|
|
367
|
+
if (fileVersion !== null && fileVersion === MIGRATION_TARGET_VERSION) {
|
|
368
|
+
return { assessable: true, applies: false, ready: null, fileVersion, targetFileVersion: MIGRATION_TARGET_VERSION, blocked, coverage, assessmentScope: 'full', reason: `被检文件已是当前官方格式(version=${fileVersion}),无需迁移` };
|
|
369
|
+
}
|
|
370
|
+
const applies = fileVersion !== null && fileVersion < MIGRATION_TARGET_VERSION;
|
|
371
|
+
return {
|
|
372
|
+
assessable: true,
|
|
373
|
+
applies,
|
|
374
|
+
ready: applies ? blocked.length === 0 : null,
|
|
375
|
+
fileVersion,
|
|
376
|
+
targetFileVersion: MIGRATION_TARGET_VERSION,
|
|
377
|
+
hostMaxFileVersion: HOST_MAX_FILE_VERSION,
|
|
378
|
+
blocked,
|
|
379
|
+
coverage,
|
|
380
|
+
// 2026-09-14 第四轮(最终复核):待迁移文件(applies=true)**只**被 2 条 G 规则覆盖,
|
|
381
|
+
// 另有 6 类官方迁移规则未覆盖 ⇒ 评估范围只能是 "partial"。工具无法逐文件知道
|
|
382
|
+
// 本文件是否命中未覆盖类(那要先把 6 类实现出来或跑官方迁移),但它**知道**这件事。
|
|
383
|
+
assessmentScope: applies ? 'partial' : 'full',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 评估范围(S1 收窄后的机器可判字段)。
|
|
389
|
+
* - `'partial'`:被检文件需要迁移(`migration.applies === true`)⇒ 迁移维度只覆盖 2 条规则,
|
|
390
|
+
* 另有 6 类未覆盖;`ok`/`loadable`/`compactable` **不构成**"官方升级会接受"。
|
|
391
|
+
* - `'full'`:被检文件已是当前官方格式(无需迁移)⇒ 迁移维度不适用,结构维度为全集。
|
|
392
|
+
* - `'none'`:本宿主能力不足以评估该文件版本(S2 闸)⇒ 不给任何档位结论。
|
|
393
|
+
* @param {object} result `validateSessionLog()` 的返回值
|
|
394
|
+
* @returns {'full'|'partial'|'none'}
|
|
395
|
+
*/
|
|
396
|
+
export function assessmentScope(result) {
|
|
397
|
+
return result?.migration?.assessmentScope
|
|
398
|
+
?? result?.assessmentScope
|
|
399
|
+
?? migrationVerdict(result).assessmentScope;
|
|
189
400
|
}
|
|
190
401
|
|
|
191
402
|
/**
|
|
@@ -202,13 +413,30 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
202
413
|
*
|
|
203
414
|
* @param {{ok: boolean, violations: Array, summary: object}} result validateSessionLog 的返回值
|
|
204
415
|
* @returns {{
|
|
205
|
-
* verdict: 'loadable' | 'resumable' | 'compactable' | 'broken',
|
|
416
|
+
* verdict: 'loadable' | 'resumable' | 'compactable' | 'broken' | 'not-assessable',
|
|
417
|
+
* assessable: boolean,
|
|
206
418
|
* loadable: boolean, resumable: boolean, compactable: boolean,
|
|
419
|
+
* migration: object,
|
|
207
420
|
* blocking: { loadable: Array, resumable: Array, compactable: Array },
|
|
208
421
|
* }}
|
|
209
422
|
*/
|
|
210
423
|
export function resumeVerdict(result) {
|
|
211
424
|
const { ok, violations = [] } = result;
|
|
425
|
+
// 宿主能力闸(S2):本宿主评估不了 ⇒ 新档位 `not-assessable`(**不是** broken)。
|
|
426
|
+
// 三档 loadable/resumable/compactable 一律 false(不认证),原因在 notAssessable.reason。
|
|
427
|
+
if (result?.assessable === false) {
|
|
428
|
+
return {
|
|
429
|
+
verdict: 'not-assessable',
|
|
430
|
+
assessable: false,
|
|
431
|
+
loadable: false,
|
|
432
|
+
resumable: false,
|
|
433
|
+
compactable: false,
|
|
434
|
+
notAssessable: result.notAssessable ?? null,
|
|
435
|
+
migration: result.migration ?? migrationVerdict(result),
|
|
436
|
+
blocking: { loadable: [], resumable: [], compactable: [] },
|
|
437
|
+
violationsByTier: { structural: [], inbox: [], tokenMeter: [] },
|
|
438
|
+
};
|
|
439
|
+
}
|
|
212
440
|
// 三档各自的「阻断规则集」——按任务书 §L3 档位定义:
|
|
213
441
|
// 可加载:结构层(PERSISTENCE/FRAMING)+ 引擎层非 I1/T1/T2/T3/T4 的 error;
|
|
214
442
|
// 可继续:+ I1(inbox 重放);
|
|
@@ -241,9 +469,11 @@ export function resumeVerdict(result) {
|
|
|
241
469
|
|
|
242
470
|
return {
|
|
243
471
|
verdict,
|
|
472
|
+
assessable: true,
|
|
244
473
|
loadable,
|
|
245
474
|
resumable,
|
|
246
475
|
compactable,
|
|
476
|
+
migration: result.migration ?? migrationVerdict(result),
|
|
247
477
|
blocking: {
|
|
248
478
|
loadable: [...new Set(loadableBlockers)],
|
|
249
479
|
resumable: resumableBlockers,
|
package/lib/vocab.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**(1.3 第二半,engineer comm-369 裁定)
|
|
3
|
+
*
|
|
4
|
+
* 背景(一手实测):同一健康会话,在**生产 0.1.1 解析**下 0 违规,在 **0.1.5 解析**下 3606 违规——
|
|
5
|
+
* 根因是本包原先用**运行时导出**的 `KNOWN_SESSION_EVENT_TYPES` / `foldSurface` 去判**旧格式(v0)文件**:
|
|
6
|
+
* 官方 0.1.5 的词汇表已不含 `assistant/chunk`(v0 词表 51 条里**有**),于是每个 chunk 事件被 E3 误报。
|
|
7
|
+
* 裁定原文(comm-369 §二):**"判据必须来自文件自身的版本,不是判官(运行时)的版本"**。
|
|
8
|
+
*
|
|
9
|
+
* 2026-09-14 复核补强(同一裁定的完整落地)——一手证据:
|
|
10
|
+
* 1. **0.1.5 的 `foldSurface` 不是"放宽",而是换成了 v3 语义**(对照 `dsh-session@0.1.0-rc.7`
|
|
11
|
+
* 与 `0.1.5-rc.1` 的 `isReplaceOp` / `assertProvenance`):
|
|
12
|
+
* - replace 字段改名:rc.7 `{op,start,end}`(`lib/index.js:300-303`)→ 0.1.5 `{op,startSeq,endSeq}`
|
|
13
|
+
* (`lib/index.js:279`);旧 marker 在 0.1.5 直接 “carries an invalid replace surfaceOp”;
|
|
14
|
+
* - 0.1.5 `lib/index.js:285` 起 `assistant/message` **一律**不得携带 `sourceEventSeqs`
|
|
15
|
+
* (“embeds its source stream”);rc.7 允许(空数组仅限 assistant/message)。
|
|
16
|
+
* ⇒ 0.1.5 的 `foldSurface` **只能**终验 v3 文件;v0/v1/v2 用它会误报,必须走本地等价实现
|
|
17
|
+
* (`./legacy-fold.js`,rc.7 `foldSurface` 逐条移植、同样 fail-loud)。
|
|
18
|
+
* 2. **版本边界是 v3,不是 v2**:App 2.0.9 内置 `dsh-session-format-v2-to-v3`,其
|
|
19
|
+
* `lib/index.js:361-371` 明示 v2 仍是 `{start,end}`、由迁移改名为 `startSeq/endSeq`
|
|
20
|
+
* ⇒ v2 属于"旧格式"。App 内置 `SESSION_FORMAT_VERSION = 3` 并带 v0→v1→v2→v3 三个迁移包。
|
|
21
|
+
* 3. **顶层 chunk 行只在旧格式里存在**:App 2.0.9 的真实持久化层
|
|
22
|
+
* `dsh-session-persistence-jsonl@0.1.5-rc.1/lib/worker.cjs:4237-4280` 把 chunk run 内嵌进
|
|
23
|
+
* `assistant/attempt.data.stream`(紧凑记录 `{type,time0,index,dt,texts}`,**无 seq0/turn/step**),
|
|
24
|
+
* 不再写顶层 `text-chunks` 行;v0/v1/v2 才用顶层行(`dsh-session/lib/types/chunk-rows.js`)。
|
|
25
|
+
* ⇒ v3 词表**不**需要 `assistant/chunk`(官方 `KNOWN_SESSION_EVENT_TYPES` 原样即正确);
|
|
26
|
+
* v2 需要 `assistant/attempt`(官方 v2 dispositions = v0 − assistant/chunk + assistant/attempt)。
|
|
27
|
+
*
|
|
28
|
+
* 形态:
|
|
29
|
+
* - 文件版本 0/1 → **vendored v0 词表**(来源:App 内置 `dsh-session-format-v0-to-v1` 的
|
|
30
|
+
* `RELEASED_V0_EVENT_DISPOSITIONS`,51 条,逐条比对零差异);
|
|
31
|
+
* - 文件版本 2 → (v0 词表 − `assistant/chunk`) ∪ {`assistant/attempt`}(官方 v2 dispositions
|
|
32
|
+
* 构造式实测:chunk 被显式剔除;见 `V2_EVENT_TYPES` 注释);
|
|
33
|
+
* - 文件版本 3 → **运行时导出**的 `KNOWN_SESSION_EVENT_TYPES`;
|
|
34
|
+
* - 折叠终验:v0/v1/v2 → `legacyFoldSurface`;v3 → 运行时 `foldSurface`。
|
|
35
|
+
*/
|
|
36
|
+
import { foldSurface, KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session';
|
|
37
|
+
import { createRequire } from 'node:module';
|
|
38
|
+
import { legacyFoldSurface } from './legacy-fold.js';
|
|
39
|
+
|
|
40
|
+
/** v0/v1 词表(vendored;来源见文件头注释,与官方 v0 dispositions 逐条对应)。 */
|
|
41
|
+
export const V0_EVENT_TYPES = new Set([
|
|
42
|
+
'agent-preset/selected', 'agent/inbox/spliced', 'approval/asked', 'approval/decided', 'approval/policy',
|
|
43
|
+
'assistant/chunk', 'assistant/message', 'command/done', 'command/run', 'compaction/end', 'compaction/prune',
|
|
44
|
+
'compaction/start', 'compaction/summary', 'feedback/record', 'goal/change', 'hook/invoked', 'hook/result',
|
|
45
|
+
'llm/retry', 'llm/retry-started', 'model/selection', 'permission/preset', 'plan/mode', 'request/context',
|
|
46
|
+
'request/header', 'sandbox/mode', 'schedule/change', 'session-log-deepseek/delivery-accepted',
|
|
47
|
+
'session/end-seed', 'session/title', 'session/title-llm-request', 'step/end', 'step/start',
|
|
48
|
+
'subagent/descriptor', 'subagent/model-selection-policy', 'team/member', 'team/message/delivered',
|
|
49
|
+
'team/message/queued', 'team/task', 'todo/write', 'tool-workflow/agent-end', 'tool-workflow/agent-start',
|
|
50
|
+
'tool-workflow/run-end', 'tool-workflow/run-start', 'tool/call', 'tool/code-dispatch',
|
|
51
|
+
'tool/code-dispatch-start', 'tool/result', 'turn/end', 'turn/start', 'user/message',
|
|
52
|
+
'web/deepseek-search-llm-request',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* v2 词表(官方 `RELEASED_V2_EVENT_DISPOSITIONS` 推导;App 2.0.9 内置
|
|
57
|
+
* `dsh-session-format-v2-to-v3` 的构造式逐字实测):
|
|
58
|
+
* retained = v0 − {assistant/chunk, assistant/message, session-log-deepseek/delivery-accepted, session/end-seed}
|
|
59
|
+
* v2 = retained ∪ {assistant/attempt, assistant/message, delivery-accepted, session/end-seed}
|
|
60
|
+
* = v0 − assistant/chunk + assistant/attempt
|
|
61
|
+
* ⇒ **v2 不含 `assistant/chunk`**(旧实现误用 `V0 ∪ {attempt}`,把 chunk 留在 v2 词表里 =
|
|
62
|
+
* 宽松口径,会漏报 v2 里的顶层 chunk 行 ⇒ F9 修正)。
|
|
63
|
+
*/
|
|
64
|
+
export const V2_EVENT_TYPES = new Set([
|
|
65
|
+
...[...V0_EVENT_TYPES].filter((t) => t !== 'assistant/chunk'),
|
|
66
|
+
'assistant/attempt',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/** 旧的顶层 `{start,end}` replace 语义适用的最高文件版本(v3 起改为 `{startSeq,endSeq}`)。 */
|
|
70
|
+
export const LEGACY_FORMAT_MAX_VERSION = 2;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 当前应使用的词表(**必须显式传 version**)。
|
|
74
|
+
*
|
|
75
|
+
* 这里**不再有模块级可变全局**:旧实现 `let fileVersion` + `setFileFormatVersion()`(唯一写入点
|
|
76
|
+
* `validate.js`)会让同一进程里后调用的 prewrite 读到**别人文件**的版本——C2 实测:先
|
|
77
|
+
* validate(v3) 再 prewrite(v0),同一份合法 v0 输入的结论从 ok 翻成 S4+S8。改为参数后,
|
|
78
|
+
* 每个入口各自持有本次被检文件的版本。
|
|
79
|
+
*/
|
|
80
|
+
export function currentVocabulary(version) {
|
|
81
|
+
if (version <= 1) return V0_EVENT_TYPES;
|
|
82
|
+
if (version === 2) return V2_EVENT_TYPES;
|
|
83
|
+
return KNOWN_SESSION_EVENT_TYPES;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 当前可用的**终验**折叠:旧格式用本地等价实现,v3 用运行时导出。 */
|
|
87
|
+
export function currentFold(version) {
|
|
88
|
+
return isLegacyFormat(version) ? legacyFoldSurface : foldSurface;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 文件版本是否属于"旧格式"(v0/v1/v2:`{start,end}` replace + 允许 assistant/message 携带 sourceEventSeqs)。 */
|
|
92
|
+
export function isLegacyFormat(version) {
|
|
93
|
+
return version <= LEGACY_FORMAT_MAX_VERSION;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 从事件形状推断文件版本——调用方只给 `events`、拿不到 header 时的兜底(C2)。
|
|
98
|
+
* 判据(可靠性从高到低):
|
|
99
|
+
* 1. replace 的字段名:v3 `{startSeq,endSeq}` / v0–v2 `{start,end}`(最强,直接决定 S4/S8 路由);
|
|
100
|
+
* 2. `system/message`:官方 v3 才有的 surface 类型(V0/V2 词表均无);
|
|
101
|
+
* 3. `assistant/chunk`:顶层 chunk 行只在旧格式出现(v2 dispositions 已剔除它);
|
|
102
|
+
* 4. `assistant/attempt`:v2 与 v3 都有(v0 无)⇒ 只能推出"≥2"。
|
|
103
|
+
* 返回 3 / 2 / 0。**这是启发式**;有 `header.version` 或显式 `formatVersion` 时优先用它们。
|
|
104
|
+
*/
|
|
105
|
+
export function inferFormatVersion(events = []) {
|
|
106
|
+
let sawLegacyReplace = false;
|
|
107
|
+
let sawModernReplace = false;
|
|
108
|
+
let sawSystemMessage = false;
|
|
109
|
+
let sawChunk = false;
|
|
110
|
+
let sawAttempt = false;
|
|
111
|
+
for (const event of events) {
|
|
112
|
+
if (!event || typeof event !== 'object') continue;
|
|
113
|
+
const op = event.surfaceOp;
|
|
114
|
+
if (op && typeof op === 'object' && op.op === 'replace') {
|
|
115
|
+
if (Object.hasOwn(op, 'startSeq') || Object.hasOwn(op, 'endSeq')) sawModernReplace = true;
|
|
116
|
+
else if (Object.hasOwn(op, 'start') || Object.hasOwn(op, 'end')) sawLegacyReplace = true;
|
|
117
|
+
}
|
|
118
|
+
if (event.type === 'system/message') sawSystemMessage = true;
|
|
119
|
+
else if (event.type === 'assistant/chunk') sawChunk = true;
|
|
120
|
+
else if (event.type === 'assistant/attempt') sawAttempt = true;
|
|
121
|
+
}
|
|
122
|
+
if (sawModernReplace || sawSystemMessage) return 3;
|
|
123
|
+
if (sawLegacyReplace) return sawAttempt ? 2 : 0;
|
|
124
|
+
if (sawAttempt) return 2;
|
|
125
|
+
if (sawChunk) return 0;
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 解析本次校验应使用的文件版本:显式 `formatVersion` > `header.version` > 事件形状推断 > 0。
|
|
131
|
+
* @param {{formatVersion?:number, header?:object|null, events?:Array}} [input]
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
export function resolveFormatVersion({ formatVersion, header, events } = {}) {
|
|
135
|
+
if (Number.isSafeInteger(formatVersion) && formatVersion >= 0) return formatVersion;
|
|
136
|
+
if (header && Number.isSafeInteger(header.version) && header.version >= 0) return header.version;
|
|
137
|
+
return inferFormatVersion(events ?? []);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// 宿主能力闸(S2)——**被检文件版本 > 宿主支持的最大版本**时不能按本宿主语义判定。
|
|
142
|
+
// 一手依据:`@deepseek-ai/dsh-session` 导出 `SESSION_FORMAT_VERSION`(rc.7 = 0,0.1.5 = 3),
|
|
143
|
+
// 词表 `KNOWN_SESSION_EVENT_TYPES` 与 `foldSurface` 都随它变。rc.7 上拿 v3 文件跑本包的
|
|
144
|
+
// v3 语义检查会产出**假阳性**(实测 S8×1 + E3×40 / verdict=broken),危险在于用户会以为
|
|
145
|
+
// 日志坏了去跑 `fix --apply`。故此处显式暴露宿主能力,由入口输出"不可在本宿主评估"档。
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/** 本宿主支持的最大被检文件版本(= 运行时 `SESSION_FORMAT_VERSION`;缺失按 0)。 */
|
|
149
|
+
export const HOST_MAX_FILE_VERSION = Number.isSafeInteger(SESSION_FORMAT_VERSION) && SESSION_FORMAT_VERSION >= 0
|
|
150
|
+
? SESSION_FORMAT_VERSION
|
|
151
|
+
: 0;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 官方迁移链的目标格式版本(固定 3)——**不是宿主版本**。
|
|
155
|
+
* 依据:App 2.0.9 / `@deepseek-ai/dsh-session-format-*@0.1.5-rc.2` 的迁移链是 v0→v1→v2→v3,
|
|
156
|
+
* `SESSION_FORMAT_VERSION = 3`。迁移预检问的是"升到目标格式会不会被拒",与当前宿主能不能
|
|
157
|
+
* 评估 v3 **无关**(rc.7 宿主上同样可以预检一个 v0 文件将来能否升级)。
|
|
158
|
+
*/
|
|
159
|
+
export const MIGRATION_TARGET_VERSION = 3;
|
|
160
|
+
|
|
161
|
+
let hostVersionMemo;
|
|
162
|
+
/** 宿主 `@deepseek-ai/dsh-session` 的包版本(用于输出/报告;不可解析时给未知标记,不抛)。 */
|
|
163
|
+
export function hostPackageVersion() {
|
|
164
|
+
if (hostVersionMemo !== undefined) return hostVersionMemo;
|
|
165
|
+
try {
|
|
166
|
+
const req = createRequire(import.meta.url);
|
|
167
|
+
hostVersionMemo = String(req('@deepseek-ai/dsh-session/package.json').version);
|
|
168
|
+
} catch {
|
|
169
|
+
hostVersionMemo = `unknown (SESSION_FORMAT_VERSION=${HOST_MAX_FILE_VERSION})`;
|
|
170
|
+
}
|
|
171
|
+
return hostVersionMemo;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 被检文件版本能否在本宿主上按其自身语义评估。
|
|
175
|
+
*
|
|
176
|
+
* 判据(两层):
|
|
177
|
+
* - **v0/v1/v2**:本包自带 vendored 词表 + 本地等价折叠(`legacyFoldSurface`),**宿主无关** ⇒ 任何宿主都能评估;
|
|
178
|
+
* - **v3+**:词表用运行时 `KNOWN_SESSION_EVENT_TYPES`、折叠用运行时 `foldSurface` ⇒
|
|
179
|
+
* 需要宿主 `SESSION_FORMAT_VERSION >= 该版本`(rc.7 = 0 ⇒ v3 不可评估)。
|
|
180
|
+
*/
|
|
181
|
+
export function isAssessableFileVersion(version) {
|
|
182
|
+
if (!Number.isSafeInteger(version) || version < 0) return false;
|
|
183
|
+
if (version <= LEGACY_FORMAT_MAX_VERSION) return true;
|
|
184
|
+
return version <= HOST_MAX_FILE_VERSION;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 宿主能力描述(CLI/--json 共用)。 */
|
|
188
|
+
export function hostCapability() {
|
|
189
|
+
return {
|
|
190
|
+
hostPackage: hostPackageVersion(),
|
|
191
|
+
hostMaxFileVersion: HOST_MAX_FILE_VERSION,
|
|
192
|
+
};
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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.13",
|
|
5
|
+
"packageManager": "pnpm@11.7.0",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "lib/index.js",
|
|
7
8
|
"bin": {
|
|
@@ -27,6 +28,11 @@
|
|
|
27
28
|
"LICENSE",
|
|
28
29
|
"README.zh.md"
|
|
29
30
|
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"check": "node scripts/check-syntax.mjs",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"prepublishOnly": "node scripts/check-pkg-meta.mjs && pnpm check && pnpm test"
|
|
35
|
+
},
|
|
30
36
|
"keywords": [
|
|
31
37
|
"dsh",
|
|
32
38
|
"deepseek-harness",
|
|
@@ -53,15 +59,11 @@
|
|
|
53
59
|
"node": ">=22"
|
|
54
60
|
},
|
|
55
61
|
"peerDependencies": {
|
|
56
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.7"
|
|
62
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.7 || ^0.1.5-rc.1"
|
|
57
63
|
},
|
|
58
64
|
"devDependencies": {
|
|
59
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
65
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.1",
|
|
60
66
|
"esbuild": "0.28.2",
|
|
61
67
|
"vitest": "4.1.11"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"check": "node scripts/check-syntax.mjs",
|
|
65
|
-
"test": "vitest run"
|
|
66
68
|
}
|
|
67
|
-
}
|
|
69
|
+
}
|