dsh-log-contract 0.3.13 → 0.3.15
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 +46 -17
- package/README.zh.md +48 -15
- package/bin/dsh-log-contract.mjs +77 -9
- package/docs/CONTRACTS.md +72 -22
- package/lib/archaeology.js +2 -2
- package/lib/checks.js +74 -24
- package/lib/contracts.js +84 -16
- package/lib/host-probes.js +147 -0
- package/lib/index.js +1 -0
- package/lib/log-reader.js +17 -4
- package/lib/prewrite.js +59 -8
- package/lib/repair.js +24 -7
- package/lib/validate.js +56 -5
- package/lib/version-support.js +148 -0
- package/lib/vocab.js +31 -3
- package/package.json +3 -3
package/lib/validate.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
import { ruleById } from './contracts.js';
|
|
13
13
|
import { normalizeEventSeqRanges } from './compat.js';
|
|
14
14
|
import { HOST_MAX_FILE_VERSION, MIGRATION_TARGET_VERSION, hostCapability, isAssessableFileVersion } from './vocab.js';
|
|
15
|
+
import { detectSupport } from './version-support.js';
|
|
16
|
+
import { runHostProbes } from './host-probes.js';
|
|
17
|
+
import { SOURCE_DRIFT } from './contracts.js';
|
|
15
18
|
import {
|
|
16
19
|
CHUNK_ROW_TYPES,
|
|
17
20
|
envelopeViolations,
|
|
@@ -103,7 +106,7 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
// ── Z3 · 空文件体检(2026-09-09
|
|
109
|
+
// ── Z3 · 空文件体检(2026-09-09 T3 增量)──────────────────────────
|
|
107
110
|
// 空态无覆盖:36 条规则全来自"有内容"事故,空会话文件应显式报(不管 DSH 编排层怎么处理)。
|
|
108
111
|
// 有 header 但零事件 = 异常空会话(warning,不破坏 ok);连 header 都没有 → H1 已报。
|
|
109
112
|
if (header !== null && events.length === 0) {
|
|
@@ -156,7 +159,7 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
156
159
|
violations.push(...nullTurnStepViolations(events));
|
|
157
160
|
violations.push(...turnEndReasonViolations(events));
|
|
158
161
|
|
|
159
|
-
// ── E7 · ignorable 未知 type
|
|
162
|
+
// ── E7 · ignorable 未知 type 合法性(T2)────────────────────
|
|
160
163
|
violations.push(...ignorableTypeViolations(events, formatVersion));
|
|
161
164
|
|
|
162
165
|
// ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
|
|
@@ -219,6 +222,12 @@ export function validateSessionLog(log, opts = {}) {
|
|
|
219
222
|
summary,
|
|
220
223
|
surface: folded.surface ?? { nodes: replay.nodes, replacements: [] },
|
|
221
224
|
};
|
|
225
|
+
// 版本支持面(2026-09-14 用户要求第 1 条):宿主版本 vs 测试基线 + 被检文件格式版本,
|
|
226
|
+
// 给出 warnings/readOnly。只读档由入口(prewrite/fix)强制执行。
|
|
227
|
+
result.support = detectSupport({ header, events });
|
|
228
|
+
// R-D 漂移检测:行为探针(宿主自己的运行时函数当 oracle)+ R-F 出处/前提漂移清单。
|
|
229
|
+
result.probes = hostProbes();
|
|
230
|
+
result.drift = driftOf(result.probes);
|
|
222
231
|
result.migration = migrationVerdict(result);
|
|
223
232
|
result.assessmentScope = result.migration.assessmentScope;
|
|
224
233
|
return result;
|
|
@@ -322,11 +331,53 @@ function unassessableResult({ header, headerLine, rows, events, frameInfo, baseS
|
|
|
322
331
|
},
|
|
323
332
|
surface: null,
|
|
324
333
|
};
|
|
334
|
+
result.support = detectSupport({ header, events });
|
|
335
|
+
result.probes = hostProbes();
|
|
336
|
+
result.drift = driftOf(result.probes);
|
|
325
337
|
result.migration = migrationVerdict(result);
|
|
326
338
|
result.assessmentScope = result.migration.assessmentScope;
|
|
327
339
|
return result;
|
|
328
340
|
}
|
|
329
341
|
|
|
342
|
+
/** 行为探针只跑一次(宿主在同一进程内不变)。 */
|
|
343
|
+
let probesMemo;
|
|
344
|
+
function hostProbes() {
|
|
345
|
+
if (probesMemo === undefined) {
|
|
346
|
+
try {
|
|
347
|
+
probesMemo = runHostProbes();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
probesMemo = { hostPackage: 'unknown', sessionFormatVersion: null, knownTypes: 0, verified: false, probes: [], unverifiedRules: ['<probe-run-failed>'], error: String(err?.message ?? err) };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return probesMemo;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* R-D/R-F 合并出的**漂移报告**(`check` 抬头与 `--json` 共用)。
|
|
357
|
+
* · `unverifiedRules` —— 行为探针在本宿主上**不成立**的规则(宿主语义与规则假设不符);
|
|
358
|
+
* · `driftedSources` —— 出处停在 rc.7、未在 0.1.5 上复核(R-F,机读清单);
|
|
359
|
+
* · `premiseStale` —— 判定前提已不成立/无法判定(复核 §1);
|
|
360
|
+
* · `fixApplyBlocked` —— 只要任一非空,就**不得据此跑 `fix --apply`**(R-E)。
|
|
361
|
+
*/
|
|
362
|
+
export function driftOf(probes) {
|
|
363
|
+
const p = probes ?? hostProbes();
|
|
364
|
+
const unverifiedRules = [...new Set(p.unverifiedRules ?? [])];
|
|
365
|
+
const driftedSources = [...SOURCE_DRIFT.drifted];
|
|
366
|
+
const premiseStale = [...SOURCE_DRIFT.premiseStale];
|
|
367
|
+
return {
|
|
368
|
+
probeVerified: p.verified === true,
|
|
369
|
+
unverifiedRules,
|
|
370
|
+
driftedSources,
|
|
371
|
+
premiseStale,
|
|
372
|
+
undecidable: [...SOURCE_DRIFT.undecidable],
|
|
373
|
+
// `fix --apply` 的**硬前提**只看行为探针(宿主语义与规则假设不一致时才禁止写入类动作);
|
|
374
|
+
// 出处/前提清单(R-F)是**已知文档债**,会显式告警但不阻断修复流程 —— 否则只要历史
|
|
375
|
+
// 清单非空,"修一个坏会话"这条主路径就永久不可用(见报告"我未照做之处")。
|
|
376
|
+
fixApplyBlocked: unverifiedRules.length > 0,
|
|
377
|
+
note: SOURCE_DRIFT.note,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
/**
|
|
331
382
|
* 迁移预检结论(S1)——**独立维度**:官方把 v0/v1/v2 升到当前格式会不会拒。
|
|
332
383
|
*
|
|
@@ -349,7 +400,7 @@ export function migrationVerdict(result) {
|
|
|
349
400
|
// 2026-09-15 第五轮:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
|
|
350
401
|
// 官方状态机(assertReleasedArtifactRelationships)由 v1→v2 在**变换后的 v1/v2 artifact**
|
|
351
402
|
// 上调用(v1-to-v2/lib/index.js:104),带 cut/继承切点处理;在原始 v0 上照抄会在已 seed
|
|
352
|
-
//
|
|
403
|
+
// 的会话上狂报(实测样本:0.1.5 链并不以该规则拒它,原始 v0 上会报 19 条)。
|
|
353
404
|
// ⇒ 忠实复现需先做 v0→v1→v2 变换;本轮不做,保持未覆盖。
|
|
354
405
|
'turn/start 闭合/预期轮(官方 v1→v2 在**变换后** artifact 上判:does not close the prior turn / does not open expected turn)',
|
|
355
406
|
'assistant/attempt 配对/闭合(migration refuses the transformed artifact)',
|
|
@@ -437,7 +488,7 @@ export function resumeVerdict(result) {
|
|
|
437
488
|
violationsByTier: { structural: [], inbox: [], tokenMeter: [] },
|
|
438
489
|
};
|
|
439
490
|
}
|
|
440
|
-
//
|
|
491
|
+
// 三档各自的「阻断规则集」——按 L3 档位定义:
|
|
441
492
|
// 可加载:结构层(PERSISTENCE/FRAMING)+ 引擎层非 I1/T1/T2/T3/T4 的 error;
|
|
442
493
|
// 可继续:+ I1(inbox 重放);
|
|
443
494
|
// 可压缩:+ T1/T2/T3/T4(token-meter 配对 + 渲染层节点 key/turn)。
|
|
@@ -449,7 +500,7 @@ export function resumeVerdict(result) {
|
|
|
449
500
|
const has = (id) => (byId[id]?.length ?? 0) > 0;
|
|
450
501
|
|
|
451
502
|
// 可加载阻断 = 除 I1/T1/T2/T3/T4 外的所有 error 违规(结构/信封/物理序/工具配对等)。
|
|
452
|
-
// 注意:不能用 result.ok(它把 T1/T2/T3/T4/I1 也计为 error
|
|
503
|
+
// 注意:不能用 result.ok(它把 T1/T2/T3/T4/I1 也计为 error)——三档判定按 L3 定义,
|
|
453
504
|
// T1/T2/T3/T4 只影响「可压缩」档、I1 只影响「可继续」档。
|
|
454
505
|
const loadableBlockers = violations.filter(
|
|
455
506
|
(v) => v.severity === 'error' && !['I1', 'T1', 'T2', 'T3', 'T4', 'T5'].includes(v.id),
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-log-contract · lib/version-support.js —— **版本支持面**:测试基线 + 运行时检测/告警
|
|
3
|
+
* (2026-09-14 用户要求第 1 条:把"哪些版本被验证过"写成明文,并在运行时检测报告)。
|
|
4
|
+
*
|
|
5
|
+
* 背景(外部质疑 + 事实核对):
|
|
6
|
+
* - 事实①:本包 `peerDependencies` = `^0.1.0-rc.7 || ^0.1.5-rc.1`,devDependency 与开发机
|
|
7
|
+
* 宿主实装都是 `0.1.5-rc.1` ⇒ **覆盖到了**;且本规则集在 v3 会话上实测有效
|
|
8
|
+
* (在真实 v3 文件上准确拒绝了伪造的 `seq: 0`,见 E2/S6/S8 用例)。
|
|
9
|
+
* - 事实②:"**哪些版本被验证过**"此前只散落在注释与 CHANGELOG 里,**没有一处明文**,
|
|
10
|
+
* 外部无法核对 ⇒ 本模块把**测试基线**变成一个可读、可比对、会告警的常量。
|
|
11
|
+
*
|
|
12
|
+
* 三条运行时规则("不闷着按旧规则判"):
|
|
13
|
+
* 1. 宿主 `@deepseek-ai/dsh-session` 版本**比基线新** ⇒ 明确告警(规则集未在该版本上验证);
|
|
14
|
+
* 2. 被检**会话文件格式版本**不在已知集合 {0,1,2,3} 内 ⇒ 报"未验证格式"并**按只读处理**
|
|
15
|
+
* (不做写入前判定、不做修复);
|
|
16
|
+
* 3. 两者都可解析时,给出一行可核对的摘要(宿主 / 文件格式 / 基线)。
|
|
17
|
+
*/
|
|
18
|
+
import { hostPackageVersion, resolveFormatVersionDetailed, HOST_MAX_FILE_VERSION } from './vocab.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* **测试基线**(唯一权威):本规则集在下列宿主 / 会话格式版本上被验证。
|
|
22
|
+
* 改这里必须同步 README(中英)与 docs/CONTRACTS.md 头部(生成器模板)。
|
|
23
|
+
*/
|
|
24
|
+
export const TESTED_BASELINE = Object.freeze({
|
|
25
|
+
hostPackage: '@deepseek-ai/dsh-session',
|
|
26
|
+
/** 开发机与 CI 实装宿主版本(devDependency 同值)。 */
|
|
27
|
+
hostVersion: '0.1.5-rc.1',
|
|
28
|
+
/** 声明的 peer 范围(允许安装,但未逐一验证)。 */
|
|
29
|
+
peerRange: '^0.1.0-rc.7 || ^0.1.5-rc.1',
|
|
30
|
+
/** 会话格式版本基线(= 宿主 SESSION_FORMAT_VERSION)。 */
|
|
31
|
+
sessionFormatVersion: 3,
|
|
32
|
+
/** 已知/受支持的会话格式版本(0/1/2 由本包 vendored 词表 + 本地等价折叠支持)。 */
|
|
33
|
+
knownFormatVersions: Object.freeze([0, 1, 2, 3]),
|
|
34
|
+
note: '旧格式(0/1/2)由本包自带的 vendored 词表与 legacyFoldSurface 支持,与宿主版本无关;'
|
|
35
|
+
+ 'v3 用运行时词表/官方 foldSurface ⇒ 需要宿主 SESSION_FORMAT_VERSION ≥ 3。',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 语义化版本比较(只处理 `x.y.z[-pre[.n]]`;不认识 → null = 不可比较)。
|
|
40
|
+
* 预发布版 < 同号正式版(0.1.5-rc.1 < 0.1.5)。
|
|
41
|
+
* @returns {-1|0|1|null}
|
|
42
|
+
*/
|
|
43
|
+
export function compareVersions(a, b) {
|
|
44
|
+
const parse = (v) => {
|
|
45
|
+
const m = /^\s*(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(String(v ?? ''));
|
|
46
|
+
if (!m) return null;
|
|
47
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split('.') : [] };
|
|
48
|
+
};
|
|
49
|
+
const pa = parse(a);
|
|
50
|
+
const pb = parse(b);
|
|
51
|
+
if (!pa || !pb) return null;
|
|
52
|
+
for (let i = 0; i < 3; i++) if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] < pb.nums[i] ? -1 : 1;
|
|
53
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
54
|
+
if (pa.pre.length === 0) return 1;
|
|
55
|
+
if (pb.pre.length === 0) return -1;
|
|
56
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
57
|
+
const x = pa.pre[i];
|
|
58
|
+
const y = pb.pre[i];
|
|
59
|
+
if (x === undefined) return -1;
|
|
60
|
+
if (y === undefined) return 1;
|
|
61
|
+
const nx = /^\d+$/.test(x);
|
|
62
|
+
const ny = /^\d+$/.test(y);
|
|
63
|
+
if (nx && ny) { if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1; continue; }
|
|
64
|
+
if (nx !== ny) return nx ? -1 : 1;
|
|
65
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 被检文件格式版本 → 支持档(`baseline` = 与测试基线同版;`legacy` = 旧格式,由本包自带实现支持)。 */
|
|
71
|
+
function fileStatusOf(version) {
|
|
72
|
+
if (!TESTED_BASELINE.knownFormatVersions.includes(version)) return 'unverified';
|
|
73
|
+
return version === TESTED_BASELINE.sessionFormatVersion ? 'baseline' : 'legacy';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* **运行时版本检测**:宿主版本 + 被检文件格式版本,与测试基线对比。
|
|
78
|
+
*
|
|
79
|
+
* @param {{header?:object|null, events?:Array, formatVersion?:number}} [input]
|
|
80
|
+
* @returns {{baseline:object, host:object, file:object, warnings:string[], readOnly:boolean, summary:string}}
|
|
81
|
+
* `readOnly === true` ⇒ 调用方**不得**给出写入结论(prewrite/fix 必须拒绝)——
|
|
82
|
+
* 触发条件:被检文件格式版本未知(不在 knownFormatVersions 内),或格式完全无法识别。
|
|
83
|
+
*/
|
|
84
|
+
export function detectSupport({ header, events, formatVersion, hostVersion: hostOverride } = {}) {
|
|
85
|
+
const warnings = [];
|
|
86
|
+
// ── 宿主 ────────────────────────────────────────────────────────────────
|
|
87
|
+
// `hostVersion` 仅用于测试/演示(注入"比基线新的宿主"场景),生产路径不传。
|
|
88
|
+
const hostVersion = hostOverride ?? hostPackageVersion();
|
|
89
|
+
const hostCmp = compareVersions(hostVersion, TESTED_BASELINE.hostVersion);
|
|
90
|
+
let hostStatus;
|
|
91
|
+
if (hostCmp === null) {
|
|
92
|
+
hostStatus = 'unknown';
|
|
93
|
+
warnings.push(`宿主 ${TESTED_BASELINE.hostPackage} 版本不可解析(${hostVersion})——无法与测试基线 ${TESTED_BASELINE.hostVersion} 比对;结论按"未验证"对待。`);
|
|
94
|
+
} else if (hostCmp > 0) {
|
|
95
|
+
hostStatus = 'newer-than-baseline';
|
|
96
|
+
warnings.push(`宿主 ${TESTED_BASELINE.hostPackage}@${hostVersion} **比测试基线 ${TESTED_BASELINE.hostVersion} 新** —— 本规则集未在该宿主上验证,官方语义可能已变(词表/折叠/replace 字段);结论可能不适用,请先核对上游变更或升级本包基线。`);
|
|
97
|
+
} else if (hostCmp < 0) {
|
|
98
|
+
hostStatus = 'older-than-baseline';
|
|
99
|
+
warnings.push(`宿主 ${TESTED_BASELINE.hostPackage}@${hostVersion} 比测试基线 ${TESTED_BASELINE.hostVersion} 旧(仍在声明的 peer 范围 ${TESTED_BASELINE.peerRange} 内)—— v3 文件需要宿主能提供对应词表/折叠(本宿主 SESSION_FORMAT_VERSION=${HOST_MAX_FILE_VERSION})。`);
|
|
100
|
+
} else {
|
|
101
|
+
hostStatus = 'baseline';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 被检文件格式 ────────────────────────────────────────────────────────
|
|
105
|
+
const resolved = resolveFormatVersionDetailed({ formatVersion, header, events });
|
|
106
|
+
const unknownSource = resolved.source === 'default';
|
|
107
|
+
// S4(独立核验):无法识别格式时**状态标签必须是 `unverified`**(README 就是这么写的:
|
|
108
|
+
// "Format unrecognisable → same: "unverified format" + read-only")。旧实现让版本回落到默认 0
|
|
109
|
+
// ⇒ 标签打印成 `legacy`,与 README 不一致(`readOnly` 本来就对)。
|
|
110
|
+
const status = unknownSource ? 'unverified' : fileStatusOf(resolved.version);
|
|
111
|
+
const readOnly = unknownSource || status === 'unverified';
|
|
112
|
+
if (unknownSource) {
|
|
113
|
+
warnings.push('被检文件格式版本**无法识别**(无 header.version,且事件形状无判别特征:S4 replace 字段名 / system/message / assistant/chunk / assistant/attempt 皆未见)——按"未验证格式"处理,仅只读。');
|
|
114
|
+
} else if (status === 'unverified') {
|
|
115
|
+
warnings.push(`被检文件格式版本 ${resolved.version} **不在已知集合 {${TESTED_BASELINE.knownFormatVersions.join(',')}}** 内(来源 ${resolved.source})——按"未验证格式"处理,仅只读;不得据此判定可写/可修。`);
|
|
116
|
+
}
|
|
117
|
+
const file = {
|
|
118
|
+
version: resolved.version,
|
|
119
|
+
source: resolved.source, // explicit | header | inferred | default
|
|
120
|
+
status, // baseline | legacy | unverified
|
|
121
|
+
known: status !== 'unverified' && !unknownSource,
|
|
122
|
+
evidence: resolved.evidence ?? null,
|
|
123
|
+
};
|
|
124
|
+
// 无法识别时版本号是占位 0 —— 显示成 `?`(否则"文件格式 v0(default,unverified)"自相矛盾)
|
|
125
|
+
const versionLabel = unknownSource ? '?' : `v${file.version}`;
|
|
126
|
+
const summary = `宿主 ${TESTED_BASELINE.hostPackage}@${hostVersion}(基线 ${TESTED_BASELINE.hostVersion},${hostStatus})`
|
|
127
|
+
+ ` | 文件格式 ${versionLabel}(${file.source},${status}${readOnly ? ',只读' : ''})`;
|
|
128
|
+
return {
|
|
129
|
+
baseline: TESTED_BASELINE,
|
|
130
|
+
host: {
|
|
131
|
+
package: TESTED_BASELINE.hostPackage,
|
|
132
|
+
version: hostVersion,
|
|
133
|
+
sessionFormatVersion: HOST_MAX_FILE_VERSION,
|
|
134
|
+
status: hostStatus,
|
|
135
|
+
tested: hostStatus === 'baseline',
|
|
136
|
+
newerThanBaseline: hostStatus === 'newer-than-baseline',
|
|
137
|
+
},
|
|
138
|
+
file,
|
|
139
|
+
warnings,
|
|
140
|
+
readOnly,
|
|
141
|
+
summary,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 从 `loadSessionLog()` 的结果算支持面(CLI/调用方共用)。 */
|
|
146
|
+
export function supportOfLog(log) {
|
|
147
|
+
return detectSupport({ header: log?.header ?? null, events: (log?.events ?? []).map((e) => e.event ?? e) });
|
|
148
|
+
}
|
package/lib/vocab.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**(1.3
|
|
2
|
+
* dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**(1.3 第二半,内部评审裁定)
|
|
3
3
|
*
|
|
4
4
|
* 背景(一手实测):同一健康会话,在**生产 0.1.1 解析**下 0 违规,在 **0.1.5 解析**下 3606 违规——
|
|
5
5
|
* 根因是本包原先用**运行时导出**的 `KNOWN_SESSION_EVENT_TYPES` / `foldSurface` 去判**旧格式(v0)文件**:
|
|
6
6
|
* 官方 0.1.5 的词汇表已不含 `assistant/chunk`(v0 词表 51 条里**有**),于是每个 chunk 事件被 E3 误报。
|
|
7
|
-
*
|
|
7
|
+
* 裁定原文(内部评审 §二):**"判据必须来自文件自身的版本,不是判官(运行时)的版本"**。
|
|
8
8
|
*
|
|
9
9
|
* 2026-09-14 复核补强(同一裁定的完整落地)——一手证据:
|
|
10
10
|
* 1. **0.1.5 的 `foldSurface` 不是"放宽",而是换成了 v3 语义**(对照 `dsh-session@0.1.0-rc.7`
|
|
@@ -103,6 +103,15 @@ export function isLegacyFormat(version) {
|
|
|
103
103
|
* 返回 3 / 2 / 0。**这是启发式**;有 `header.version` 或显式 `formatVersion` 时优先用它们。
|
|
104
104
|
*/
|
|
105
105
|
export function inferFormatVersion(events = []) {
|
|
106
|
+
return inferFormatVersionDetailed(events) ?? 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 同上,但**没有判别证据时返回 `null`**(而不是假装 0)——供版本支持面报"未验证格式"
|
|
111
|
+
* (2026-09-14 用户要求第 1 条:无法识别格式 ⇒ 报未验证并按只读处理)。
|
|
112
|
+
* @returns {3|2|0|null} 版本号;null = 事件里没有任何判别特征
|
|
113
|
+
*/
|
|
114
|
+
export function inferFormatVersionDetailed(events = []) {
|
|
106
115
|
let sawLegacyReplace = false;
|
|
107
116
|
let sawModernReplace = false;
|
|
108
117
|
let sawSystemMessage = false;
|
|
@@ -123,7 +132,26 @@ export function inferFormatVersion(events = []) {
|
|
|
123
132
|
if (sawLegacyReplace) return sawAttempt ? 2 : 0;
|
|
124
133
|
if (sawAttempt) return 2;
|
|
125
134
|
if (sawChunk) return 0;
|
|
126
|
-
return
|
|
135
|
+
return null; // 无证据:调用方(版本支持面)据此报"未验证格式"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 解析版本 + **来源**(版本支持面用):显式 `formatVersion` > `header.version` > 事件形状推断 > 无证据。
|
|
140
|
+
* @param {{formatVersion?:number, header?:object|null, events?:Array}} [input]
|
|
141
|
+
* @returns {{version:number, source:'explicit'|'header'|'inferred'|'default', evidence:string|null}}
|
|
142
|
+
*/
|
|
143
|
+
export function resolveFormatVersionDetailed({ formatVersion, header, events } = {}) {
|
|
144
|
+
if (Number.isSafeInteger(formatVersion) && formatVersion >= 0) {
|
|
145
|
+
return { version: formatVersion, source: 'explicit', evidence: null };
|
|
146
|
+
}
|
|
147
|
+
if (header && Number.isSafeInteger(header.version) && header.version >= 0) {
|
|
148
|
+
return { version: header.version, source: 'header', evidence: null };
|
|
149
|
+
}
|
|
150
|
+
const inferred = inferFormatVersionDetailed(events ?? []);
|
|
151
|
+
if (inferred !== null) {
|
|
152
|
+
return { version: inferred, source: 'inferred', evidence: 'replace 字段名/system/message/assistant/{chunk,attempt} 形状' };
|
|
153
|
+
}
|
|
154
|
+
return { version: 0, source: 'default', evidence: null };
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
/**
|
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.15",
|
|
5
5
|
"packageManager": "pnpm@11.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"README.zh.md"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"check": "node scripts/check-syntax.mjs",
|
|
32
|
+
"check": "node scripts/check-syntax.mjs && node scripts/gen-contracts-doc.mjs --check",
|
|
33
33
|
"test": "vitest run",
|
|
34
|
-
"prepublishOnly": "node scripts/check-pkg-meta.mjs && pnpm check && pnpm test"
|
|
34
|
+
"prepublishOnly": "node scripts/check-pkg-meta.mjs && node scripts/prepublish-gate.mjs && pnpm check && pnpm test"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"dsh",
|