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/checks.js
CHANGED
|
@@ -7,11 +7,20 @@
|
|
|
7
7
|
* 全部判定与 `@deepseek-ai/dsh-session@0.1.0-rc.7` 官方实现同语义,
|
|
8
8
|
* 每条违规都挂 `lib/contracts.js` 中的规则 id 与官方源码出处。
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { isSurfaceEligibleType } from '@deepseek-ai/dsh-session';
|
|
11
|
+
import { isJsonValue } from './compat.js';
|
|
12
|
+
import { currentVocabulary, currentFold, V0_EVENT_TYPES } from './vocab.js';
|
|
11
13
|
import { ruleById } from './contracts.js';
|
|
12
14
|
|
|
13
15
|
export const SURFACE_TYPES = new Set(['user/message', 'assistant/message', 'tool/result']);
|
|
16
|
+
/** v3 新增 `system/message`(官方 `SURFACE_EVENT_TYPES`,0.1.5 `lib/index.js:149-154`)。 */
|
|
17
|
+
export const MODERN_SURFACE_TYPES = new Set([...SURFACE_TYPES, 'system/message']);
|
|
14
18
|
export const CHUNK_ROW_TYPES = new Set(['text-chunks', 'reasoning-chunks', 'tool-call-chunks']);
|
|
19
|
+
|
|
20
|
+
/** 当前文件版本适用的 surface 候选类型集(v3 起含 system/message)。version = 被检文件版本。 */
|
|
21
|
+
export function currentSurfaceTypes(version) {
|
|
22
|
+
return version >= 3 ? MODERN_SURFACE_TYPES : SURFACE_TYPES;
|
|
23
|
+
}
|
|
15
24
|
export const MARKER_PREFIXES = ['retrace', 'message-editor'];
|
|
16
25
|
|
|
17
26
|
/** 构造一条违规记录。 */
|
|
@@ -35,15 +44,15 @@ export function isSafeInt(v) {
|
|
|
35
44
|
return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
/** 事件信封 shape:seq/type/time/data(E1/E3/E4/E5/E6)。 */
|
|
39
|
-
export function envelopeViolations(event, loc) {
|
|
47
|
+
/** 事件信封 shape:seq/type/time/data(E1/E3/E4/E5/E6)。version = 被检文件版本。 */
|
|
48
|
+
export function envelopeViolations(event, loc, version) {
|
|
40
49
|
const out = [];
|
|
41
50
|
if (!isSafeInt(event.seq)) {
|
|
42
51
|
out.push(violation('E1', loc, `事件 seq 缺失或非法(${String(event.seq)}),必须为非负安全整数`));
|
|
43
52
|
}
|
|
44
53
|
if (typeof event.type !== 'string') {
|
|
45
54
|
out.push(violation('E3', loc, `事件缺少 type 字符串(${String(event.type)})`));
|
|
46
|
-
} else if (!
|
|
55
|
+
} else if (!currentVocabulary(version).has(event.type) && event.ignorable !== true) {
|
|
47
56
|
out.push(violation('E3', loc, `type "${event.type}" 不在已知词汇表内且未带 ignorable 标记(可能由更新版本的 harness 写入)`));
|
|
48
57
|
}
|
|
49
58
|
if (!isJsonValue(event.data)) {
|
|
@@ -112,7 +121,7 @@ export function messageShapeViolations(event, loc) {
|
|
|
112
121
|
return out;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
/** replace
|
|
124
|
+
/** replace 操作数精确形状(镜像旧格式 isReplaceOp,lib/index.js:300-303)。 */
|
|
116
125
|
export function isReplaceOp(op) {
|
|
117
126
|
return (
|
|
118
127
|
typeof op === 'object' && op !== null &&
|
|
@@ -122,6 +131,29 @@ export function isReplaceOp(op) {
|
|
|
122
131
|
);
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
/**
|
|
135
|
+
* 按**被检文件版本**归一化 replace 操作数到内部 `{start,end}`。
|
|
136
|
+
*
|
|
137
|
+
* 一手依据:官方 replace 字段在 v3 改名——旧格式(v0/v1/v2)`{op,start,end}`,
|
|
138
|
+
* v3 `{op,startSeq,endSeq}`(`dsh-session-format-v2-to-v3/lib/index.js:361-371`)。
|
|
139
|
+
* 用错形状会得 null,由调用方归因为 S4(真实违规)。
|
|
140
|
+
*
|
|
141
|
+
* @param {unknown} op 待归一化的 surfaceOp
|
|
142
|
+
* @returns {{start:number,end:number}|null}
|
|
143
|
+
*/
|
|
144
|
+
export function normalizeReplaceOp(op, version) {
|
|
145
|
+
if (typeof op !== 'object' || op === null || Array.isArray(op)) return null;
|
|
146
|
+
if (Object.keys(op).length !== 3 || op.op !== 'replace') return null;
|
|
147
|
+
if (version >= 3) {
|
|
148
|
+
return Object.hasOwn(op, 'startSeq') && Object.hasOwn(op, 'endSeq') && isSafeInt(op.startSeq) && isSafeInt(op.endSeq)
|
|
149
|
+
? { start: op.startSeq, end: op.endSeq }
|
|
150
|
+
: null;
|
|
151
|
+
}
|
|
152
|
+
return Object.hasOwn(op, 'start') && Object.hasOwn(op, 'end') && isSafeInt(op.start) && isSafeInt(op.end)
|
|
153
|
+
? { start: op.start, end: op.end }
|
|
154
|
+
: null;
|
|
155
|
+
}
|
|
156
|
+
|
|
125
157
|
/**
|
|
126
158
|
* S9 —— 文件物理序 seq 单调(2026-08-30 事故固化;交接书 L2)。
|
|
127
159
|
*
|
|
@@ -157,14 +189,14 @@ export function physicalOrderViolations(rows) {
|
|
|
157
189
|
* 与官方同语义的 surface 增量重放,逐事件归因 S1–S7。
|
|
158
190
|
* @param {Array<{event:object, lineNo?:number}>} events 按日志顺序的事件(带 loc 包装)
|
|
159
191
|
*/
|
|
160
|
-
export function replaySurface(events) {
|
|
192
|
+
export function replaySurface(events, version) {
|
|
161
193
|
const violations = [];
|
|
162
194
|
const nodes = [];
|
|
163
195
|
let replaceGeneration = 0;
|
|
164
196
|
|
|
165
197
|
for (const { event, lineNo } of events) {
|
|
166
198
|
const loc = { seq: event.seq, lineNo, eventType: event.type };
|
|
167
|
-
const eligible =
|
|
199
|
+
const eligible = currentSurfaceTypes(version).has(event.type);
|
|
168
200
|
const op = event.surfaceOp;
|
|
169
201
|
const src = event.sourceEventSeqs;
|
|
170
202
|
|
|
@@ -185,18 +217,19 @@ export function replaySurface(events) {
|
|
|
185
217
|
continue;
|
|
186
218
|
}
|
|
187
219
|
|
|
188
|
-
|
|
189
|
-
|
|
220
|
+
const normalized = normalizeReplaceOp(op, version);
|
|
221
|
+
if (normalized === null) {
|
|
222
|
+
violations.push(violation('S4', loc, 'replace 操作数必须精确为 {op:"replace", start, end}(v0/v1/v2)或 {op:"replace", startSeq, endSeq}(v3),且端点为非负安全整数'));
|
|
190
223
|
continue;
|
|
191
224
|
}
|
|
192
|
-
const startIdx = nodes.indexOf(
|
|
193
|
-
const endIdx = nodes.indexOf(
|
|
225
|
+
const startIdx = nodes.indexOf(normalized.start);
|
|
226
|
+
const endIdx = nodes.indexOf(normalized.end);
|
|
194
227
|
if (startIdx === -1 || endIdx === -1) {
|
|
195
|
-
violations.push(violation('S4', loc, `replace 范围 ${
|
|
228
|
+
violations.push(violation('S4', loc, `replace 范围 ${normalized.start}..${normalized.end} 不在当前 surface 中(start 存在=${startIdx !== -1},end 存在=${endIdx !== -1})`));
|
|
196
229
|
continue;
|
|
197
230
|
}
|
|
198
231
|
if (startIdx > endIdx) {
|
|
199
|
-
violations.push(violation('S4', loc, `replace start ${
|
|
232
|
+
violations.push(violation('S4', loc, `replace start ${normalized.start}(index ${startIdx})在 end ${normalized.end}(index ${endIdx})之后`));
|
|
200
233
|
continue;
|
|
201
234
|
}
|
|
202
235
|
const shadowedSeqs = nodes.slice(startIdx, endIdx + 1);
|
|
@@ -287,10 +320,19 @@ export function pluginViolations(event, loc) {
|
|
|
287
320
|
return out;
|
|
288
321
|
}
|
|
289
322
|
|
|
290
|
-
/**
|
|
291
|
-
|
|
323
|
+
/** 折叠终验:**按文件版本**择路(1.3 第二半 + 2026-09-14 复核补强)。
|
|
324
|
+
*
|
|
325
|
+
* - v3 文件 → 运行时导出的官方 `foldSurface`(与运行时同语义);
|
|
326
|
+
* - v0/v1/v2 文件 → 本地 `legacyFoldSurface`(rc.7 `foldSurface` 逐条移植)。
|
|
327
|
+
*
|
|
328
|
+
* 为什么旧格式不能借用官方实现的 `violations` 字段:官方 `foldSurface` **只抛不报**,
|
|
329
|
+
* 旧口径下这里曾回退到 `replaySurface` 的 violations,但**没有任何调用方读它**
|
|
330
|
+
* (validate.js / prewrite.js 只看 `folded.error`)⇒ S8 在旧格式上永不触发——这正是
|
|
331
|
+
* 2026-09-14 六条失败里四条的真因。现在两条路径都是"抛错 = 拒绝"的同一种契约。 */
|
|
332
|
+
export function finalFold(events, version) {
|
|
333
|
+
const fold = currentFold(version);
|
|
292
334
|
try {
|
|
293
|
-
return { surface:
|
|
335
|
+
return { surface: fold(events) };
|
|
294
336
|
} catch (err) {
|
|
295
337
|
return { error: err };
|
|
296
338
|
}
|
|
@@ -405,11 +447,11 @@ function isKnownIgnorableType(type) {
|
|
|
405
447
|
if (KNOWN_IGNORABLE_CONSUMERS.has(type)) return true
|
|
406
448
|
return type.startsWith('message-editor/') || type.startsWith('retrace/')
|
|
407
449
|
}
|
|
408
|
-
export function ignorableTypeViolations(events) {
|
|
450
|
+
export function ignorableTypeViolations(events, version) {
|
|
409
451
|
const out = []
|
|
410
452
|
for (const { event, lineNo } of events) {
|
|
411
453
|
if (typeof event?.type !== 'string' || event.ignorable !== true) continue
|
|
412
|
-
if (
|
|
454
|
+
if (currentVocabulary(version).has(event.type)) continue
|
|
413
455
|
if (isKnownIgnorableType(event.type)) continue
|
|
414
456
|
out.push(violation('E7', { seq: event.seq, lineNo, eventType: event.type }, `type "${event.type}" 不在已知词汇表且带 ignorable 标记、无已知消费者——被读路径接纳但无人消费 = 静默垃圾(ignorable 后门;若你的插件消费它,登记到 KNOWN_IGNORABLE_CONSUMERS)`))
|
|
415
457
|
}
|
|
@@ -685,20 +727,21 @@ export function deriveWireMessage(event) {
|
|
|
685
727
|
* @param events - 展开后的完整事件流(含 chunk 展开)。
|
|
686
728
|
* @returns 违规列表。
|
|
687
729
|
*/
|
|
688
|
-
export function wireViolations(events) {
|
|
730
|
+
export function wireViolations(events, version) {
|
|
689
731
|
const out = [];
|
|
690
732
|
// 先折叠 surface(append 入列;replace 移除 [start..end] 并将 marker 自身入列)
|
|
691
733
|
const nodes = [];
|
|
692
734
|
const bySeq = new Map();
|
|
693
735
|
for (const { event, lineNo } of events) {
|
|
694
736
|
bySeq.set(event.seq, { event, lineNo });
|
|
695
|
-
if (!
|
|
737
|
+
if (!currentSurfaceTypes(version).has(event.type)) continue;
|
|
696
738
|
const op = event.surfaceOp;
|
|
697
739
|
if (op === 'append') {
|
|
698
740
|
nodes.push(event.seq);
|
|
699
|
-
} else if (op
|
|
700
|
-
const
|
|
701
|
-
const
|
|
741
|
+
} else if (op !== undefined) {
|
|
742
|
+
const normalized = normalizeReplaceOp(op, version);
|
|
743
|
+
const s = normalized === null ? -1 : nodes.indexOf(normalized.start);
|
|
744
|
+
const e = normalized === null ? -1 : nodes.indexOf(normalized.end);
|
|
702
745
|
if (s !== -1 && e !== -1 && s <= e) nodes.splice(s, e - s + 1, event.seq);
|
|
703
746
|
else nodes.push(event.seq);
|
|
704
747
|
}
|
|
@@ -740,3 +783,88 @@ export function wireViolations(events) {
|
|
|
740
783
|
}
|
|
741
784
|
return out;
|
|
742
785
|
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* G · 迁移预检(payload 层 + 类型层)——**官方迁移会不会拒**,与"本工具能否读取/折叠"是
|
|
789
|
+
* **两个独立维度**(见 contracts.js 的 LAYER.MIGRATION 段)。
|
|
790
|
+
*
|
|
791
|
+
* 覆盖(每条都有一手出处,官方 `@0.1.5-rc.2`):
|
|
792
|
+
* - **G1** `subagent/descriptor.data.version !== 3`
|
|
793
|
+
* - **G2** 事件类型不在官方 v0 dispositions 内(含 `ignorable:true`)
|
|
794
|
+
* - **G3** `session/title` / `session/title-llm-request` 的 `messageSeqs`(`v0-to-v1/lib/index.js:2543-2556`)
|
|
795
|
+
*
|
|
796
|
+
* 仍未覆盖(由 `migrationVerdict().coverage.uncovered` 显式列出,**不许**当通过):
|
|
797
|
+
* `turn/start`(闭合/预期轮)、继承切点、`assistant/attempt` 配对、`stored log corrupt`、
|
|
798
|
+
* v0→v1 对其余事件的形状拒绝。
|
|
799
|
+
*
|
|
800
|
+
* **为什么 `turn/start` 不做(第五轮实测结论)**:官方那个状态机(`v0-to-v1:2270` 的
|
|
801
|
+
* `assertReleasedArtifactRelationships`)**不是**在原始 v0 事件上跑的——它由 **v1→v2**
|
|
802
|
+
* 以 `RELEASED_V2_RELATIONSHIP_EXTENSIONS` 调用在**变换后的 v1/v2 artifact** 上
|
|
803
|
+
* (`v1-to-v2/lib/index.js:104`),并带 `cut`(继承切点)处理。第五轮实测:在原始 v0 上照抄该
|
|
804
|
+
* 状态机会在**已 seed 的会话**上狂报(样本 `session-62c5b531`:v0→v1 官方并不以该规则拒绝,
|
|
805
|
+
* 而原始 v0 上会报 19 条),属"规则文本对、应用对象错"。要忠实复现必须先把 v0→v1→v2 的
|
|
806
|
+
* 变换做出来 ⇒ 记未覆盖。
|
|
807
|
+
*
|
|
808
|
+
* 范围:三条都只约束 **v0 源文件**(官方 v0→v1 迁移的规则;v1/v2 源走别的迁移代码)。
|
|
809
|
+
* severity 固定 warning、layer 固定 migration ⇒ 不进 ok / loadable / resumable / compactable。
|
|
810
|
+
*
|
|
811
|
+
* @param {Array<{event:object, lineNo?:number}>} events 已展开事件(带 loc 包装)
|
|
812
|
+
* @param {number} version 被检文件版本
|
|
813
|
+
*/
|
|
814
|
+
export function migrationPrecheckViolations(events, version) {
|
|
815
|
+
const out = [];
|
|
816
|
+
if (version !== 0) return out;
|
|
817
|
+
for (const { event, lineNo } of events) {
|
|
818
|
+
if (!event || typeof event !== 'object') continue;
|
|
819
|
+
if (event.type === 'subagent/descriptor') {
|
|
820
|
+
const dv = event.data?.version;
|
|
821
|
+
if (dv !== 3) {
|
|
822
|
+
out.push(violation('G1', { seq: event.seq, lineNo, eventType: event.type },
|
|
823
|
+
`subagent/descriptor data.version=${JSON.stringify(dv)}(官方 v0→v1 迁移要求 === 3)—— 升级到当前格式时官方会拒绝:subagent/descriptor ${event.seq} uses unsupported descriptor version ${String(dv)}`));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (typeof event.type === 'string' && !V0_EVENT_TYPES.has(event.type)) {
|
|
827
|
+
// 官方对"unknown historical event"一律拒(原文 even when ignorable);E3 的 ignorable
|
|
828
|
+
// 豁免只作用于**读取路径**,这里不改它,只在迁移维度表达。
|
|
829
|
+
out.push(violation('G2', { seq: event.seq, lineNo, eventType: event.type },
|
|
830
|
+
`type "${event.type}" 不在官方 v0 dispositions 内${event.ignorable === true ? '(即使 ignorable:true)' : ''}—— 官方迁移会拒:format v0 contains unknown historical event type`));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// ── G3 · session/title 系列 messageSeqs(官方 v0→v1 `assertTitleSources`,:2543-2556)──
|
|
834
|
+
// 判据:每个被引 seq 必须解析到更早的 `user/message`,且 `data.source.kind === 'user'`;
|
|
835
|
+
// 另 `session/title` 的 `source.kind === 'user'` ⟺ `messageSeqs` 空。
|
|
836
|
+
//
|
|
837
|
+
// ⚠️ 应用面说明(第五轮实测,2026-09-15):官方 `assertTitleSources` 与 `turn/start` 状态机
|
|
838
|
+
// 同在 `assertReleasedArtifactRelationships` 里,而该函数被 **v1→v2** 以
|
|
839
|
+
// `RELEASED_V2_RELATIONSHIP_EXTENSIONS` 调用在**变换后的 v1/v2 artifact** 上
|
|
840
|
+
// (`dsh-session-format-v1-to-v2/lib/index.js:104`),不是原始 v0 事件。
|
|
841
|
+
// 对 `messageSeqs` 这类"按 seq 索引 + 事件类型/source 判定"的引用,变换保序保类型
|
|
842
|
+
// ⇒ 在原始 v0 上判是**必要条件的近似**;实测 281 个真实 v0 上 **0 误报**(round5/rule-probe)。
|
|
843
|
+
const bySeq = new Map();
|
|
844
|
+
for (const { event } of events) if (event && typeof event === 'object') bySeq.set(event.seq, event);
|
|
845
|
+
for (const { event, lineNo } of events) {
|
|
846
|
+
if (!event || typeof event !== 'object') continue;
|
|
847
|
+
if (event.type !== 'session/title' && event.type !== 'session/title-llm-request') continue;
|
|
848
|
+
const seqs = event.data?.messageSeqs;
|
|
849
|
+
if (!Array.isArray(seqs)) {
|
|
850
|
+
out.push(violation('G3', { seq: event.seq, lineNo, eventType: event.type },
|
|
851
|
+
`${event.type} ${event.seq} 的 messageSeqs 必须是数组 —— 官方迁移会拒`));
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (event.type === 'session/title') {
|
|
855
|
+
const kind = event.data?.source?.kind;
|
|
856
|
+
if ((seqs.length === 0) !== (kind === 'user')) {
|
|
857
|
+
out.push(violation('G3', { seq: event.seq, lineNo, eventType: event.type },
|
|
858
|
+
`session/title ${event.seq} messageSeqs must be empty exactly for a user title(官方原文;source.kind=${String(kind)},seqs=${seqs.length})`));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
for (const s of seqs) {
|
|
862
|
+
const src = bySeq.get(s);
|
|
863
|
+
if (!src || src.type !== 'user/message' || src.data?.source?.kind !== 'user') {
|
|
864
|
+
out.push(violation('G3', { seq: event.seq, lineNo, eventType: event.type },
|
|
865
|
+
`${event.type} ${event.seq} messageSeqs must cite earlier human user/message events(seq ${String(s)} 不是更早的人类 user/message)`));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}
|
package/lib/compat.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-log-contract · lib/compat.js —— 兼容层(任务 1.3 · 装即坏修复;2026-09-14 语义对齐重写)
|
|
3
|
+
*
|
|
4
|
+
* 背景:本包原先从 `@deepseek-ai/dsh-session` 直接导入 `decodeStorageRecord` 与 `isJsonValue`。
|
|
5
|
+
* 官方 0.1.5 树已**不再从包根导出**这两个符号(0.1.5-rc.1 `lib/index.js` 的 26 个导出里没有它们),
|
|
6
|
+
* 而本包是 plugin 的依赖 ⇒ 升级后"装即坏"(模块加载期报 does not provide an export named ...)。
|
|
7
|
+
*
|
|
8
|
+
* 本文件把这两个符号**本地化**,并把语义从"近似"改为**逐条移植**(原实现是宽松近似:
|
|
9
|
+
* `decodeStorageRecord` 对损坏 chunk 行返回 `[value]` 而非抛错,导致 R2 永不触发;
|
|
10
|
+
* `isJsonValue` 不拒 `-0`、不拒稀疏数组、不查原型——与官方 lossless-JSON 边界不一致)。
|
|
11
|
+
*
|
|
12
|
+
* 移植源(一手,2026-09-14 实测):
|
|
13
|
+
* - `decodeStorageRecord` / `validateRow` / `expandRow`:
|
|
14
|
+
* `@deepseek-ai/dsh-session@0.1.0-rc.7` `lib/index.js:922-1035`(该版本是最后一个把
|
|
15
|
+
* `decodeStorageRecord` 从包根导出的版本;与 App 2.0.9 内置的
|
|
16
|
+
* `dsh-session/lib/types/chunk-rows.js`(= 0.1.2-rc.1,sha256 前 16 位 `5724c4f798ed07e7`)
|
|
17
|
+
* 校验规则一致)。**fail-loud**:损坏的 chunk 行必须抛错——它是损坏存储,静默当普通事件
|
|
18
|
+
* 处理会丢掉整段 run。
|
|
19
|
+
* - `isJsonValue`:`@deepseek-ai/dsh-session@0.1.0-rc.7` `lib/index.js:74-205`
|
|
20
|
+
* `walkJsonValue(value, false)`(lossless-JSON 边界:拒绝稀疏数组、非有限数、`-0`、
|
|
21
|
+
* 循环引用、非普通原型、symbol/不可枚举键)。
|
|
22
|
+
*
|
|
23
|
+
* 仍从官方导入的符号(升级后保留):`foldSurface` / `isSurfaceEligibleType` /
|
|
24
|
+
* `KNOWN_SESSION_EVENT_TYPES`(见 `./vocab.js` / `./checks.js`)。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Whether a value is a JSON-visible record (object, not null, not array)(rc.7 `isRecord`)。 */
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Exact-key check: `value` has every key in `keys` and nothing else(rc.7 `hasExactKeys`)。 */
|
|
33
|
+
function hasExactKeys(value, keys) {
|
|
34
|
+
return Object.keys(value).length === keys.length && keys.every((k) => Object.hasOwn(value, k));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#region isJsonValue —— rc.7 walkJsonValue 的移植(不含 detach)
|
|
38
|
+
/** Whether a value is an object whose prototype is an intrinsic (plain) object prototype, across realms. */
|
|
39
|
+
function isIntrinsicObjectPrototype(prototype) {
|
|
40
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
41
|
+
}
|
|
42
|
+
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
|
43
|
+
function hasPlainObjectPrototype(value) {
|
|
44
|
+
const prototype = Object.getPrototypeOf(value);
|
|
45
|
+
return prototype === null || (typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype));
|
|
46
|
+
}
|
|
47
|
+
/** Whether an array is a plain array from any JavaScript realm (rejects subclasses/exotics). */
|
|
48
|
+
function hasPlainArrayPrototype(value) {
|
|
49
|
+
const prototype = Object.getPrototypeOf(value);
|
|
50
|
+
if (prototype === Array.prototype) return true;
|
|
51
|
+
return typeof prototype === 'object' && prototype !== null && Object.getPrototypeOf(prototype) === Object.prototype && Object.getPrototypeOf(Object.getPrototypeOf(prototype)) === null;
|
|
52
|
+
}
|
|
53
|
+
/** Return every JSON-visible object key, or reject own data JSON would discard. */
|
|
54
|
+
function enumerableStringKeys(value) {
|
|
55
|
+
const keys = Reflect.ownKeys(value);
|
|
56
|
+
if (keys.some((key) => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key))) return undefined;
|
|
57
|
+
return keys;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate lossless JSON iteratively(rc.7 `walkJsonValue(value, false)`)。
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function isJsonValue(value) {
|
|
66
|
+
const ancestors = new Set();
|
|
67
|
+
const tasks = [{ kind: 'visit', value }];
|
|
68
|
+
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
|
69
|
+
if (task.kind === 'leave') {
|
|
70
|
+
ancestors.delete(task.source);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (task.kind === 'array-item') {
|
|
74
|
+
if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return false;
|
|
75
|
+
tasks.push({ kind: 'visit', value: task.source[task.index] });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (task.kind === 'object-property') {
|
|
79
|
+
tasks.push({ kind: 'visit', value: task.source[task.key] });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const current = task.value;
|
|
83
|
+
if (current === null) continue;
|
|
84
|
+
if (typeof current === 'boolean' || typeof current === 'string') continue;
|
|
85
|
+
if (typeof current === 'number') {
|
|
86
|
+
if (!Number.isFinite(current) || Object.is(current, -0)) return false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (typeof current !== 'object') return false;
|
|
90
|
+
if (ancestors.has(current)) return false;
|
|
91
|
+
if (Array.isArray(current)) {
|
|
92
|
+
if (!hasPlainArrayPrototype(current)) return false;
|
|
93
|
+
const length = current.length;
|
|
94
|
+
if (Reflect.ownKeys(current).length !== length + 1) return false;
|
|
95
|
+
ancestors.add(current);
|
|
96
|
+
tasks.push({ kind: 'leave', source: current });
|
|
97
|
+
for (let index = length - 1; index >= 0; index--) tasks.push({ kind: 'array-item', source: current, index });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!hasPlainObjectPrototype(current)) return false;
|
|
101
|
+
const keys = enumerableStringKeys(current);
|
|
102
|
+
if (keys === undefined) return false;
|
|
103
|
+
ancestors.add(current);
|
|
104
|
+
tasks.push({ kind: 'leave', source: current });
|
|
105
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
106
|
+
const key = keys[index];
|
|
107
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
108
|
+
if (key === undefined) return false;
|
|
109
|
+
tasks.push({ kind: 'object-property', source: current, key });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
|
|
116
|
+
//#region decodeStorageRecord —— rc.7 validateRow/expandRow 的移植
|
|
117
|
+
/** Throw the uniform malformed-row diagnostic(rc.7 `malformed`)。 */
|
|
118
|
+
function malformed(tag, why) {
|
|
119
|
+
throw new Error(`malformed ${tag} storage row: ${why}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validate the shared run-data fields and the payload/dt arity; returns the member payload(rc.7 `validateRunData`)。 */
|
|
123
|
+
function validateRunData(tag, data, payloadKey) {
|
|
124
|
+
if (typeof data.turn !== 'number' || typeof data.step !== 'number' || typeof data.index !== 'number') malformed(tag, 'turn/step/index must be numbers');
|
|
125
|
+
const payload = data[payloadKey];
|
|
126
|
+
if (!Array.isArray(payload) || payload.length === 0 || payload.some((entry) => typeof entry !== 'string')) malformed(tag, `${payloadKey} must be a non-empty string array`);
|
|
127
|
+
const dt = data.dt;
|
|
128
|
+
if (!Array.isArray(dt) || dt.some((gap) => !Number.isSafeInteger(gap))) malformed(tag, 'dt must be an array of safe integers');
|
|
129
|
+
if (dt.length !== payload.length - 1) malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
|
|
130
|
+
return payload;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Validate a row-tagged parsed value's envelope and data, throwing on any malformation(rc.7 `validateRow`)。 */
|
|
134
|
+
function validateRow(value, tag) {
|
|
135
|
+
if (!hasExactKeys(value, ['type', 'seq0', 'time0', 'data'])) malformed(tag, 'envelope must be exactly {type, seq0, time0, data}');
|
|
136
|
+
if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0) malformed(tag, 'seq0 must be a non-negative safe integer');
|
|
137
|
+
if (!Number.isSafeInteger(value.time0)) malformed(tag, 'time0 must be a safe integer');
|
|
138
|
+
const data = value.data;
|
|
139
|
+
if (!isRecord(data)) malformed(tag, 'data must be an object');
|
|
140
|
+
let payload;
|
|
141
|
+
if (tag === 'tool-call-chunks') {
|
|
142
|
+
const withName = hasExactKeys(data, ['turn', 'step', 'index', 'id', 'name', 'dt', 'args']);
|
|
143
|
+
if (!withName && !hasExactKeys(data, ['turn', 'step', 'index', 'id', 'dt', 'args'])) malformed(tag, 'data must be exactly {turn, step, index, id, name?, dt, args}');
|
|
144
|
+
if (typeof data.id !== 'string' || (withName && typeof data.name !== 'string')) malformed(tag, 'id (and name when present) must be strings');
|
|
145
|
+
payload = validateRunData(tag, data, 'args');
|
|
146
|
+
} else {
|
|
147
|
+
if (!hasExactKeys(data, ['turn', 'step', 'index', 'dt', 'texts'])) malformed(tag, 'data must be exactly {turn, step, index, dt, texts}');
|
|
148
|
+
payload = validateRunData(tag, data, 'texts');
|
|
149
|
+
}
|
|
150
|
+
if (!Number.isSafeInteger(value.seq0 + payload.length - 1)) malformed(tag, 'member seqs must stay safe integers');
|
|
151
|
+
let time = value.time0;
|
|
152
|
+
for (const gap of data.dt) {
|
|
153
|
+
time += gap;
|
|
154
|
+
if (!Number.isSafeInteger(time)) malformed(tag, 'member times must stay safe integers');
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Expand a validated row back into its exact original events, in order(rc.7 `expandRow`)。 */
|
|
160
|
+
function expandRow(row) {
|
|
161
|
+
const members = row.type === 'tool-call-chunks' ? row.data.args : row.data.texts;
|
|
162
|
+
const events = [];
|
|
163
|
+
let time = row.time0;
|
|
164
|
+
for (let k = 0; k < members.length; k++) {
|
|
165
|
+
if (k > 0) time += row.data.dt[k - 1];
|
|
166
|
+
let chunk;
|
|
167
|
+
switch (row.type) {
|
|
168
|
+
case 'text-chunks':
|
|
169
|
+
chunk = { type: 'text-delta', index: row.data.index, text: members[k] };
|
|
170
|
+
break;
|
|
171
|
+
case 'reasoning-chunks':
|
|
172
|
+
chunk = { type: 'reasoning-delta', index: row.data.index, text: members[k] };
|
|
173
|
+
break;
|
|
174
|
+
case 'tool-call-chunks':
|
|
175
|
+
chunk = {
|
|
176
|
+
type: 'tool-call-delta',
|
|
177
|
+
index: row.data.index,
|
|
178
|
+
id: row.data.id,
|
|
179
|
+
...Object.hasOwn(row.data, 'name') ? { name: row.data.name } : {},
|
|
180
|
+
argumentsDelta: members[k],
|
|
181
|
+
};
|
|
182
|
+
break;
|
|
183
|
+
/* v8 ignore next 4 -- validateRow only returns the three row tags */
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(`chunk-rows received unsupported row ${String(row)}`);
|
|
186
|
+
}
|
|
187
|
+
events.push({ type: 'assistant/chunk', seq: row.seq0 + k, time, data: { turn: row.data.turn, step: row.data.step, chunk } });
|
|
188
|
+
}
|
|
189
|
+
return events;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 存储行解码:chunk 行校验后展开为完整事件序列,其他行原样返回(rc.7 `decodeStorageRecord`)。
|
|
194
|
+
* 损坏的 chunk 行**抛错**(fail-loud)——调用方(log-reader)据此落 `row.error` → R2。
|
|
195
|
+
* @param {unknown} value 已 JSON.parse 的一行
|
|
196
|
+
* @returns {unknown[]} 事件数组
|
|
197
|
+
*/
|
|
198
|
+
export function decodeStorageRecord(value) {
|
|
199
|
+
if (!isRecord(value)) return [value];
|
|
200
|
+
const tag = value.type;
|
|
201
|
+
if (tag !== 'text-chunks' && tag !== 'reasoning-chunks' && tag !== 'tool-call-chunks') return [value];
|
|
202
|
+
return expandRow(validateRow(value, tag));
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
|
|
206
|
+
//#region decodeSeqRanges —— v3 storage-form 区间编码解码(0.1.5 `lib/types/seq-ranges.js` 逐条移植)
|
|
207
|
+
// 为什么本地化:`decodeSeqRanges` 只在 0.1.5+ 从包根导出;`0.1.0-rc.7` 实测 `undefined`
|
|
208
|
+
// (见 vocab.js 的双宿主声明)。本包 peer 允许两个版本 ⇒ 必须在两种宿主上都能解码。
|
|
209
|
+
//
|
|
210
|
+
// 语义(官方 `seq-ranges.js:34-73`):`sourceEventSeqs` 的 JSON 存储形态是"数字 + 闭区间对"
|
|
211
|
+
// 混合数组,例 `[3, [174,176], 180, …]` ⇒ 内存形态 `[3,174,175,176,180,…]`。
|
|
212
|
+
// v3 持久化层对连续段用区间对压缩(App 2.0.9 真实 v3 会话实测:1 个 `user/message` replace
|
|
213
|
+
// 的 `sourceEventSeqs` 2251 个存储项展开为 2259 个 seq)。**不解码 = 拿物理未展开的序列
|
|
214
|
+
// 喂 S5/S6 与官方 foldSurface ⇒ 真实健康会话被误判 S5/S6/S8 + `--resume` broken。**
|
|
215
|
+
|
|
216
|
+
/** 非负安全整数(rc.7/0.1.5 `assertSeq`)。 */
|
|
217
|
+
function assertSeq(value) {
|
|
218
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError('sourceEventSeqs must contain non-negative safe integers');
|
|
219
|
+
}
|
|
220
|
+
/** 带 Session 序号 brand 的断言(官方 `SessionSeq`;额外拒绝 `-0`)。 */
|
|
221
|
+
function sessionSeq(value) {
|
|
222
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
|
|
223
|
+
throw new TypeError(`SessionSeq must be a non-negative safe integer, got ${String(value)}`);
|
|
224
|
+
}
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 展开 JSON 存储形态的 `sourceEventSeqs`(官方 `decodeSeqRanges` 逐条移植)。
|
|
230
|
+
* 任一形态非法即抛 `TypeError`(fail-loud:调用方据此报 S6/E4,不静默当稠密序列)。
|
|
231
|
+
* @param {unknown} value 已 JSON.parse 的 `sourceEventSeqs`
|
|
232
|
+
* @param {number} [maxEntries] 该事件允许的最大成员数
|
|
233
|
+
* @returns {number[]} 内存稠密序列
|
|
234
|
+
*/
|
|
235
|
+
export function decodeSeqRanges(value, maxEntries = Number.MAX_SAFE_INTEGER) {
|
|
236
|
+
if (!Array.isArray(value)) throw new TypeError('sourceEventSeqs must be an array');
|
|
237
|
+
const decoded = [];
|
|
238
|
+
let hasRange = false;
|
|
239
|
+
for (const entry of value) {
|
|
240
|
+
if (typeof entry === 'number') {
|
|
241
|
+
assertSeq(entry);
|
|
242
|
+
if (decoded.length >= maxEntries) throw new TypeError('sourceEventSeqs exceeds its event sequence');
|
|
243
|
+
decoded.push(sessionSeq(entry));
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
247
|
+
throw new TypeError('sourceEventSeqs range entries must be [start, end] pairs');
|
|
248
|
+
}
|
|
249
|
+
const start = entry[0];
|
|
250
|
+
const end = entry[1];
|
|
251
|
+
assertSeq(start);
|
|
252
|
+
assertSeq(end);
|
|
253
|
+
if (end < start) throw new TypeError('sourceEventSeqs ranges require start <= end');
|
|
254
|
+
if (end - start + 1 > maxEntries - decoded.length) {
|
|
255
|
+
throw new TypeError('sourceEventSeqs range exceeds its event sequence');
|
|
256
|
+
}
|
|
257
|
+
for (let seq = start; seq <= end; seq += 1) decoded.push(sessionSeq(seq));
|
|
258
|
+
hasRange = true;
|
|
259
|
+
}
|
|
260
|
+
if (hasRange && !decoded.every((v, i) => i === 0 || v > decoded[i - 1])) {
|
|
261
|
+
throw new TypeError('sourceEventSeqs ranges must be strictly increasing');
|
|
262
|
+
}
|
|
263
|
+
return decoded;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 把一个已解码事件的 storage-form `sourceEventSeqs` 归一成内存稠密序列。
|
|
268
|
+
* - 无 `sourceEventSeqs` / 非对象 → 原样返回(同一引用);
|
|
269
|
+
* - 解码成功 → 返回**新对象**(不修改入参;调用方须使用返回值);
|
|
270
|
+
* - 解码失败 → 原样返回,保留存储形态交由 S6/E4 报违规(读取层不崩、不吞错)。
|
|
271
|
+
*/
|
|
272
|
+
export function normalizeEventSeqRanges(event) {
|
|
273
|
+
if (!isRecord(event) || event.sourceEventSeqs === undefined) return event;
|
|
274
|
+
let expanded;
|
|
275
|
+
try {
|
|
276
|
+
expanded = decodeSeqRanges(event.sourceEventSeqs);
|
|
277
|
+
} catch {
|
|
278
|
+
return event;
|
|
279
|
+
}
|
|
280
|
+
return { ...event, sourceEventSeqs: expanded };
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
package/lib/contracts.js
CHANGED
|
@@ -21,6 +21,7 @@ export const LAYER = {
|
|
|
21
21
|
PLUGIN: 'plugin', // 插件语义层:marker 隐藏语义
|
|
22
22
|
CONCURRENCY: 'concurrency', // 并发/写入者假设
|
|
23
23
|
FRAMING: 'framing', // zstd 帧结构
|
|
24
|
+
MIGRATION: 'migration', // 迁移预检层:官方 v0/v1/v2 → 当前格式的升级路径会不会拒(**独立维度**)
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const SEVERITY = { ERROR: 'error', WARNING: 'warning', INFO: 'info' };
|
|
@@ -35,6 +36,7 @@ export const SEVERITY = { ERROR: 'error', WARNING: 'warning', INFO: 'info' };
|
|
|
35
36
|
* - P 插件 marker 语义层
|
|
36
37
|
* - C 并发 / 写入者假设
|
|
37
38
|
* - Z zstd 帧结构
|
|
39
|
+
* - G 迁移预检(migration gate,**独立维度**:官方迁移会不会拒;不进 ok/verdict)
|
|
38
40
|
*/
|
|
39
41
|
export const CONTRACT_RULES = [
|
|
40
42
|
// ── H · header ──────────────────────────────────────────────────────────
|
|
@@ -51,8 +53,8 @@ export const CONTRACT_RULES = [
|
|
|
51
53
|
title: 'header 版本与必填字段',
|
|
52
54
|
layer: LAYER.PERSISTENCE,
|
|
53
55
|
severity: SEVERITY.ERROR,
|
|
54
|
-
source: '@deepseek-ai/dsh-session lib/index.js:1110-1125',
|
|
55
|
-
description: 'header.version
|
|
56
|
+
source: '@deepseek-ai/dsh-session lib/index.js:1110-1125;已知格式版本 0/1/2/3(App 2.0.9 内置 v0→v1→v2→v3 迁移,SESSION_FORMAT_VERSION=3)',
|
|
57
|
+
description: 'header.version 必须为已知受支持版本(0/1/2/3);未知版本报 H2。id 为字符串;createdAt 为非负安全整数;cwd 若存在必须为绝对路径;origin 只能为 "subagent"。',
|
|
56
58
|
},
|
|
57
59
|
|
|
58
60
|
// ── R · 存储行 ──────────────────────────────────────────────────────────
|
|
@@ -209,8 +211,8 @@ export const CONTRACT_RULES = [
|
|
|
209
211
|
title: '整日志 foldSurface 可重放',
|
|
210
212
|
layer: LAYER.PERSISTENCE,
|
|
211
213
|
severity: SEVERITY.ERROR,
|
|
212
|
-
source: '@deepseek-ai/dsh-session lib/index.js:444-455 (foldSurface)
|
|
213
|
-
description: '
|
|
214
|
+
source: '@deepseek-ai/dsh-session lib/index.js:444-455 (foldSurface, v3);v0/v1/v2 用本地等价实现 lib/legacy-fold.js(rc.7 lib/index.js:229-455 逐条移植)',
|
|
215
|
+
description: '终验:按被检文件 header.version 选折叠器(v3 → 官方 foldSurface;v0/v1/v2 → 本地 legacyFoldSurface),不抛 = 持久化层通过。S1–S7 任何一条违反都会在此暴露。注意 0.1.5 的官方 foldSurface 是 v3 语义(replace 用 startSeq/endSeq、assistant/message 禁 sourceEventSeqs),对旧格式文件会误报,不可借用。',
|
|
214
216
|
},
|
|
215
217
|
|
|
216
218
|
{
|
|
@@ -358,6 +360,36 @@ export const CONTRACT_RULES = [
|
|
|
358
360
|
source: 'OpenAI 兼容端点对 tool 消息顺序的严格校验;DSH 序列化器将混合 user 消息展开为 text 在前、tool-result 在后',
|
|
359
361
|
description: '当仍有未满足的 assistant tool-call 时出现 user 文本消息,会产生 [assistant(tool_calls), user(text), tool] 序列,严格端点同样拒绝。',
|
|
360
362
|
},
|
|
363
|
+
|
|
364
|
+
// ── G · 迁移预检(migration precheck;**独立维度**,不是"可加载"判定)──────────
|
|
365
|
+
// 存在理由:官方把 v0/v1/v2 会话升到当前格式时会做一轮**迁移校验**,其规则比本工具的
|
|
366
|
+
// "读取/折叠"规则集更严(官方原文见各条 source)。本工具判"可加载"≠"可升级"。
|
|
367
|
+
// 这些规则只描述"官方迁移会不会拒",因此 severity 固定 warning、layer 固定 migration:
|
|
368
|
+
// 它们**不参与** ok / loadable / resumable / compactable,只喂 `migrationVerdict()`。
|
|
369
|
+
{
|
|
370
|
+
id: 'G1',
|
|
371
|
+
title: '迁移预检:v0 源文件的 subagent/descriptor.data.version 必须为 3',
|
|
372
|
+
layer: LAYER.MIGRATION,
|
|
373
|
+
severity: SEVERITY.WARNING,
|
|
374
|
+
source: '@deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 lib/index.js:1584-1586(assertReleasedEventPayload):data.version !== 3 且源版本 === 0 → SessionFormatUnsupportedMigrationError("uses unsupported descriptor version N");源版本 1/2 时官方提前 return(容忍)',
|
|
375
|
+
description: '文件版本 0 且事件类型为 subagent/descriptor 且 data.version !== 3 → 官方 v0→v1 迁移直接拒绝(消息形如 `subagent/descriptor <seq> uses unsupported descriptor version 2`);源版本 1/2 不受此条约束。',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
id: 'G2',
|
|
379
|
+
title: '迁移预检:v0 源文件不得含词表外的历史事件类型(含 ignorable)',
|
|
380
|
+
layer: LAYER.MIGRATION,
|
|
381
|
+
severity: SEVERITY.WARNING,
|
|
382
|
+
source: '@deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 lib/index.js:1580-1583(assertReleasedEventPayload):RELEASED_V0_EVENT_DISPOSITIONS 里没有该 type → "format v0 contains unknown historical event type … migration refuses unknown historical events even when ignorable"',
|
|
383
|
+
description: '文件版本 0 且事件 type 不在官方 v0 dispositions(本包 vendored 为 V0_EVENT_TYPES)内 → 官方迁移拒绝,**即使该事件带 ignorable:true**。E3 的 ignorable 豁免是**读取路径**语义(不改),本条只在迁移预检维度表达。',
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: 'G3',
|
|
387
|
+
title: '迁移预检:v0 源 session/title 系列的 messageSeqs 必须引用更早的人类 user/message',
|
|
388
|
+
layer: LAYER.MIGRATION,
|
|
389
|
+
severity: SEVERITY.WARNING,
|
|
390
|
+
source: '@deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 lib/index.js:2543-2556(assertTitleSources):`session/title` 的 messageSeqs 为空 ⟺ source.kind === "user";每个被引 seq 必须解析到 `user/message` 且其 `data.source.kind === "user"`,否则 "messageSeqs must cite earlier human user/message events" / "must be empty exactly for a user title"',
|
|
391
|
+
description: '文件版本 0 且事件为 session/title 或 session/title-llm-request:messageSeqs 必须是数组;session/title 的"空数组 ⟺ 用户标题"必须成立;每个被引 seq 必须是更早的 user/message 且 source.kind === "user"。违反 → 官方升级到当前格式时拒绝。**应用面说明**:官方 `assertTitleSources` 与 turn/start 状态机同在 `assertReleasedArtifactRelationships`,由 v1→v2 在**变换后的 v1/v2 artifact** 上调用(`dsh-session-format-v1-to-v2/lib/index.js:104`);对 messageSeqs 这类按 seq 索引 + 类型/source 判定的引用,变换保序保类型 ⇒ 在原始 v0 上判是必要条件的近似,实测 281 真实 v0 上 0 误报。',
|
|
392
|
+
},
|
|
361
393
|
];
|
|
362
394
|
|
|
363
395
|
/** 按 id 取规则。 */
|
package/lib/index.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* 公开 API:离线体检 + 写前校验 + 修复 + 契约目录。
|
|
6
6
|
*/
|
|
7
7
|
export { loadSessionLog, tailSeq, readSessionHeader } from './log-reader.js';
|
|
8
|
-
export { validateSessionLog, resumeVerdict } from './validate.js';
|
|
8
|
+
export { validateSessionLog, resumeVerdict, migrationVerdict, assessmentScope } 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 { HOST_MAX_FILE_VERSION, hostPackageVersion, hostCapability, isAssessableFileVersion } from './vocab.js';
|
|
12
13
|
export { tokenMeterViolations, tokenMeterSourceViolations, stepKeyViolations, nullTurnStepViolations, turnEndReasonViolations, ignorableTypeViolations, physicalOrderViolations, inboxReplayViolations } from './checks.js';
|
|
13
14
|
export { auditToolCalls, extractText, extractToolOutputs, indexToolCalls, toolCommandOf } from './archaeology.js';
|