dsh-log-contract 0.3.10 → 0.3.12

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/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,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.10",
4
+ "version": "0.3.12",
5
5
  "packageManager": "pnpm@11.7.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -31,7 +31,7 @@
31
31
  "scripts": {
32
32
  "check": "node scripts/check-syntax.mjs",
33
33
  "test": "vitest run",
34
- "prepublishOnly": "pnpm check && pnpm test"
34
+ "prepublishOnly": "node scripts/check-pkg-meta.mjs && pnpm check && pnpm test"
35
35
  },
36
36
  "keywords": [
37
37
  "dsh",
@@ -59,10 +59,10 @@
59
59
  "node": ">=22"
60
60
  },
61
61
  "peerDependencies": {
62
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7"
62
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.7 || ^0.1.5-rc.1"
63
63
  },
64
64
  "devDependencies": {
65
- "@deepseek-ai/dsh-session": "0.1.0-rc.7",
65
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
66
66
  "esbuild": "0.28.2",
67
67
  "vitest": "4.1.11"
68
68
  }