dsh-log-contract 0.3.12 → 0.3.14

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 CHANGED
@@ -37,10 +37,9 @@ plugin marker semantics).
37
37
 
38
38
  ## Where it sits in the business layer
39
39
 
40
- > **dsh-log-contract is the core capability component of
41
- > [dsh-retrace](https://github.com/yamingmou/dsh-retrace)** — the "doctor" module
42
- > of the business layer: session-log **check & repair**, so every recall/edit/rewind
43
- > lands on a legal log and `/compact` never breaks.
40
+ > **dsh-log-contract is the core capability component of [dsh-retrace](https://github.com/yamingmou/dsh-retrace)** (the business layer's "doctor" module): it **checks and repairs** session logs so every recall/edit/rollback lands on a legal log — pre-write validation (`prewrite`) rejects error-level violations **before** anything is written.
41
+ >
42
+ > **About `/compact` (stated honestly)**: what this tool guarantees is that **new writes** stop creating token-meter pairing debt — since two-segment atomic pairs (marker + paired segment) landed, new markers pass T1 by construction. **Legacy** (single-segment) markers are still **known design debt**: pre-write validation downgrades their T1 violation to a warning (`isKnownMarkerCandidate` in `lib/prewrite.js`, which applies to **historical markers only**), so such sessions need **one check + cleanup before compaction** (`check` to locate → `fix --remove-markers`; the plugin side calls this "the doctor"). Otherwise the T1 self-check blocks `/compact`.
44
43
 
45
44
  | Layer | What it is | Components |
46
45
  |---|---|---|
@@ -107,6 +106,34 @@ omitted for privacy.
107
106
 
108
107
  ---
109
108
 
109
+ ## Version support (host / session format)
110
+
111
+ **Tested baseline (the versions this rule set was verified against)**:
112
+
113
+ | Item | Baseline | Note |
114
+ |---|---|---|
115
+ | Host package | `@deepseek-ai/dsh-session@0.1.5-rc.1` | version used on the dev box and in CI; the four `prepublishOnly` steps are green on it |
116
+ | Session format | **v3** (`SESSION_FORMAT_VERSION = 3`) | v3 uses the runtime vocabulary + official `foldSurface` final check |
117
+ | Declared range | `peerDependencies: ^0.1.0-rc.7 || ^0.1.5-rc.1` | installable — a declared range is **not** a per-version verification |
118
+ | Known formats | 0 / 1 / 2 / 3 | 0–2 are supported by this package's **vendored** vocabulary + local equivalent fold (`legacyFoldSurface`), independent of the host |
119
+
120
+ **What happens on unverified versions (detected and reported at runtime — never silently judged by stale rules)**:
121
+
122
+ | Case | Behaviour |
123
+ |---|---|
124
+ | Host **newer than the baseline** | explicit **warning** (`host … is newer than the tested baseline …`); the verdict is still produced but flagged as possibly not applicable |
125
+ | Host older than the baseline (still in peer range) | notice + the host-capability gate decides whether v3 is assessable (`SESSION_FORMAT_VERSION` too low ⇒ `not-assessable`, exit 3) |
126
+ | **Unknown** session format (not in 0/1/2/3) | reported as **unverified format** and handled **read-only**: `check` runs structure rules only; `prewrite` / `fix` **refuse** (exit 3) |
127
+ | Format **unrecognisable** (no `header.version` and no distinguishing event shapes) | same: "unverified format" + read-only |
128
+ | File version above the host's supported maximum | `not-assessable` (**not** broken), exit 3; skipped/executed rules are listed |
129
+
130
+ Implementation: `lib/version-support.js` (`TESTED_BASELINE` / `detectSupport()`, exported in the public API).
131
+ Both the human-readable `check` output and the `--json` `support` field carry the baseline summary and the warnings, e.g.:
132
+
133
+ ```
134
+ version support: host @deepseek-ai/dsh-session@0.1.5-rc.1 (baseline 0.1.5-rc.1) | file format v3 (header, baseline)
135
+ ```
136
+
110
137
  ## Installation
111
138
 
112
139
  ```bash
@@ -144,7 +171,7 @@ Sample output (the CLI reports in Chinese — it is the tool's UI language):
144
171
  事件 204754 | surface 节点 16 | replace 代数 5 | 帧 8620(3439.5KiB → 8191.3KiB)
145
172
  违规 1(error 1 / warning 0)
146
173
 
147
- [error] S5 @ seq 156425 / line 778 (assistant/message)
174
+ [error] S5 @ seq <seq> / line <line> (assistant/message)
148
175
  surface replace: sourceEventSeqs 必须覆盖每个被替换节点;缺失 121774, 121779(共 2 个)
149
176
 
150
177
  ❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
@@ -276,10 +303,10 @@ node scripts/check-local-fossils.mjs # scans ../ for backup-session-*.jsonl.zs
276
303
 
277
304
  Known truth table (updated 2026-08-31 — after 0.3.5 added I1, `spliced-orphan`
278
305
  now FAILs; the old PASS row was stale):
279
- - `2c3f87d4-corrupt` / `b7713ea1-seqgap` / `rewritten-230542` → FAIL (seq gaps)
280
- - `2c3f87d4-spliced-orphan` → **FAIL (0.3.5+)** (T1/I1: invalid inbox splice + turn-null)
281
- - `e61d70da-pre-markerfix-20260825` → FAIL (pre-fix sample: turn-null markers remain)
282
- - `c2d05ce9-pre-cleansession-20260831` → PASS (3256-span replace marker is data-legal;
306
+ - `<session>-corrupt` / `<session>-seqgap` / `<session>-rewritten-230542` → FAIL (seq gaps)
307
+ - `<session>-spliced-orphan` → **FAIL (0.3.5+)** (T1/I1: invalid inbox splice + turn-null)
308
+ - `<session>-pre-markerfix-20260825` → FAIL (pre-fix sample: turn-null markers remain)
309
+ - `<session>-pre-cleanstate-20260831` → PASS (a multi-thousand-span replace marker is data-legal;
283
310
  official foldSurface replays cleanly — see the plugin ledger)
284
311
  The truth table tracks rule evolution (0.3.5's I1 flipped spliced-orphan PASS→FAIL);
285
312
  "repaired sessions PASS" must be verified on rebuilt samples — `pre-` backups are
@@ -309,7 +336,7 @@ MIT © OfferKuai Team
309
336
 
310
337
  ---
311
338
 
312
- ## ⚠️ Upgrading to 0.3.12 — behaviour changes you must know
339
+ ## ⚠️ Upgrading to 0.3.14 — behaviour changes you must know (0.3.12 → 0.3.14)
313
340
 
314
341
  **1. New exit codes `3` / `4` — a defect fix that is also a breaking change.**
315
342
  - `3` = **not assessable on this host**: the file's `header.version` is higher than the host supports
@@ -318,15 +345,16 @@ MIT © OfferKuai Team
318
345
  - `4` = **migration pre-check blocked**: the tool judges that the official upgrade chain will refuse the file.
319
346
  - ⚠️ **Pipelines that treated `exit 0` as "fine" will now fail.** The old behaviour returned `0` even for files
320
347
  the tool itself reported as refused by the official chain — a false negative where the exit code contradicted
321
- the verdict. `--fail-on-migration` is an **explicit alias of the (now default) behaviour**, not a toggle, and
322
- there is **no opt-out**; for the old behaviour read `assessmentScope` / `migration.ready` from `--json` and
323
- apply your own threshold.
348
+ the verdict. `--fail-on-migration` is an **explicit alias of the (now default) behaviour**; the **real opt-out
349
+ is `--no-fail-on-migration`**, which restores the 0.3.11 behaviour (structural green `exit 0`). When both
350
+ flags are given, the opt-out wins. You can also ignore the exit code and read `assessmentScope` /
351
+ `migration.ready` from `--json` with your own threshold.
324
352
 
325
353
  **2. `--json` gains `assessmentScope` (three values).**
326
354
 
327
355
  | value | meaning |
328
356
  |---|---|
329
- | `full` | file is already the host's target format (no migration) — coverage is complete |
357
+ | `full` | file is already the host's target format (no migration) — coverage is complete(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
330
358
  | `partial` | file needs migration ⇒ the migration pre-check covers **2 rules only; 6 classes are uncovered** (≠ pass; see `coverage.uncovered`) |
331
359
  | `none` | **not assessable on this host** — no "usable / upgradable" claim is certified |
332
360
 
package/README.zh.md CHANGED
@@ -30,7 +30,9 @@
30
30
 
31
31
  ## 它在业务层里的位置
32
32
 
33
- > **dsh-log-contract 是 [dsh-retrace](https://github.com/yamingmou/dsh-retrace) 的核心能力组件**(业务层的「医生」模块):负责会话日志的**体检与修复**——让每一次撤回/编辑/回退都落在合法日志上,让 /compact 永不失效。
33
+ > **dsh-log-contract 是 [dsh-retrace](https://github.com/yamingmou/dsh-retrace) 的核心能力组件**(业务层的「医生」模块):负责会话日志的**体检与修复**——让每一次撤回/编辑/回退都落在合法日志上,写前校验(`prewrite`)会把 error 级违约**拦在落盘之前**。
34
+ >
35
+ > **关于 `/compact`(如实表述)**:本工具保证的是**新写入**不再制造 token-meter 配对债——自两段原子成对(marker + 配对段)起,新 marker 天然通过 T1。**历史**(单段)marker 仍属**已知设计债**:写前校验对它的 T1 违规降级为 warning(`lib/prewrite.js` 的 `isKnownMarkerCandidate` 白名单,**只适用于历史 marker**),因此这类会话**压缩(`/compact`)前需要先跑一次体检 + 清理**(`check` 定位 → `fix --remove-markers`;插件侧即「医生」),否则 T1 自检会拦住压缩。
34
36
 
35
37
  | 层 | 是什么 | 组件 |
36
38
  |---|---|---|
@@ -84,6 +86,35 @@ dsh-retrace 的「日志体检与修复」能力——与 dsh-retrace 一起构
84
86
 
85
87
  ---
86
88
 
89
+ ## 版本支持(宿主 / 会话格式)
90
+
91
+ **测试基线(本规则集验证过的版本)**:
92
+
93
+ | 项 | 基线 | 说明 |
94
+ |---|---|---|
95
+ | 宿主包 | `@deepseek-ai/dsh-session@0.1.5-rc.1` | 开发机与 CI 实装版本;`prepublishOnly` 四步在此版本上全绿 |
96
+ | 会话格式 | **v3**(`SESSION_FORMAT_VERSION = 3`) | v3 走运行时词表 + 官方 `foldSurface` 终验 |
97
+ | 声明范围 | `peerDependencies: ^0.1.0-rc.7 || ^0.1.5-rc.1` | 允许安装;**范围内不等于逐个验证过** |
98
+ | 已知格式 | 0 / 1 / 2 / 3 | 0–2 由本包**自带** vendored 词表 + 本地等价折叠(`legacyFoldSurface`)支持,与宿主版本无关 |
99
+
100
+ **未验证的版本会发生什么(运行时检测并报告,不闷着按旧规则判)**:
101
+
102
+ | 情形 | 行为 |
103
+ |---|---|
104
+ | 宿主**比基线新** | 明确**告警**(`⚠️ 宿主 … 比测试基线 … 新 —— 本规则集未在该宿主上验证…`);结论仍给出,但标注可能不适用 |
105
+ | 宿主比基线旧(仍在 peer 内) | 提示 + 由宿主能力闸决定能否评估 v3(`SESSION_FORMAT_VERSION` 不足 ⇒ `not-assessable`,退出码 3) |
106
+ | 被检文件格式**未知**(不在 0/1/2/3) | 报「**未验证格式**」并**按只读处理**:`check` 只跑结构层,`prewrite` / `fix` **直接拒绝**(退出码 3) |
107
+ | 格式**无法识别**(无 `header.version` 且事件形状无判别特征) | 同上:报「未验证格式」+ 只读 |
108
+ | 文件版本 > 宿主支持上限 | `not-assessable`(**不是** broken),退出码 3;显式列出跳过/执行的规则 |
109
+
110
+ 检测实现:`lib/version-support.js`(`TESTED_BASELINE` / `detectSupport()`,导出到公开 API)。
111
+ `check` 的人类可读输出与 `--json` 的 `support` 字段都带基线摘要与逐条告警,例如:
112
+
113
+ ```
114
+ 版本支持:宿主 @deepseek-ai/dsh-session@0.1.5-rc.1(基线 0.1.5-rc.1,baseline) | 文件格式 v3(header,baseline)
115
+ 测试基线:@deepseek-ai/dsh-session@0.1.5-rc.1 | 会话格式 v3(已知 0/1/2/3;peer ^0.1.0-rc.7 || ^0.1.5-rc.1)
116
+ ```
117
+
87
118
  ## 安装
88
119
 
89
120
  ```bash
@@ -118,7 +149,7 @@ dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd --json # 机器可读
118
149
  事件 204754 | surface 节点 16 | replace 代数 5 | 帧 8620(3439.5KiB → 8191.3KiB)
119
150
  违规 1(error 1 / warning 0)
120
151
 
121
- [error] S5 @ seq 156425 / line 778 (assistant/message)
152
+ [error] S5 @ seq <seq> / line <line> (assistant/message)
122
153
  surface replace: sourceEventSeqs 必须覆盖每个被替换节点;缺失 121774, 121779(共 2 个)
123
154
 
124
155
  ❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
@@ -240,12 +271,12 @@ node scripts/check-local-fossils.mjs # 扫描 ../ 下 backup-session-*.jsonl.z
240
271
 
241
272
  | 化石 | 判定 | 违规 |
242
273
  |---|---|---|
243
- | `2c3f87d4-corrupt` | FAIL | S8/C1/T1/E2(seq 缺口 → 加载被拒) |
244
- | `b7713ea1-seqgap` / `recorrupt` | FAIL | S8/C1/T1/E2/S9(seq 缺口/倒退) |
245
- | `2c3f87d4-rewritten-230542` | FAIL | S8/C1/T1/E2/I1(重写引入缺口) |
246
- | `2c3f87d4-spliced-orphan` | **FAIL(0.3.5+)** | T1/I1(inbox splice 无效 + turn-null)——旧表 PASS 已过时 |
247
- | `e61d70da-pre-markerfix-20260825` | FAIL | T1×5(修复前样本:turn-null marker 残留,非「修复后」) |
248
- | `c2d05ce9-pre-cleansession-20260831` | **PASS** | error 0(3256 跨度 replace marker 数据合规,官方 foldSurface 重放通过——见插件任务台账) |
274
+ | `<session>-corrupt` | FAIL | S8/C1/T1/E2(seq 缺口 → 加载被拒) |
275
+ | `<session>-seqgap` / `<session>-recorrupt` | FAIL | S8/C1/T1/E2/S9(seq 缺口/倒退) |
276
+ | `<session>-rewritten-230542` | FAIL | S8/C1/T1/E2/I1(重写引入缺口) |
277
+ | `<session>-spliced-orphan` | **FAIL(0.3.5+)** | T1/I1(inbox splice 无效 + turn-null)——旧表 PASS 已过时 |
278
+ | `<session>-pre-markerfix-20260825` | FAIL | T1×5(修复前样本:turn-null marker 残留,非「修复后」) |
279
+ | `<session>-pre-cleanstate-20260831` | **PASS** | error 0(数千跨度的 replace marker 数据合规,官方 foldSurface 重放通过) |
249
280
 
250
281
  > 说明:真值表随规则演进更新(0.3.5 新增 I1 后 spliced-orphan 从 PASS 变 FAIL);
251
282
  > 「修复后会话 PASS」需用重建/修复后的样本验证,pre- 前缀备份多为修复前坏样本。
@@ -266,7 +297,7 @@ MIT © OfferKuai Team
266
297
 
267
298
  ---
268
299
 
269
- ## ⚠️ 升级到 0.3.12 —— 必须知道的行为变更
300
+ ## ⚠️ 升级到 0.3.14 —— 必须知道的行为变更(0.3.12 → 0.3.14)
270
301
 
271
302
  **1. 新增退出码 `3` / `4`:本质是缺陷修复,形式上也是破坏性变更。**
272
303
  - `3` = **不可在本宿主评估**:被检文件的 `header.version` 高于当前宿主支持的上限
@@ -274,14 +305,15 @@ MIT © OfferKuai Team
274
305
  而是明确说明宿主能力不足;`fix --apply` 会**拒绝**该文件(不落盘、不写备份)。
275
306
  - `4` = **迁移预检 blocked**:本工具判断该文件会被官方升级链拒绝。
276
307
  - ⚠️ **把 `exit 0` 当"过"的流水线会由过转挂。** 旧行为在**工具自己就报告"官方会拒"**的文件上仍返回 `0`
277
- —— 退出码与结论矛盾的假阴性。`--fail-on-migration` 是**默认已生效行为的显式别名**,不是开关,
278
- 且**没有 opt-out**;需要旧行为请读 `--json` `assessmentScope` / `migration.ready` 自定门槛。
308
+ —— 退出码与结论矛盾的假阴性。`--fail-on-migration` 是**默认已生效行为的显式别名**;
309
+ **真正的 opt-out `--no-fail-on-migration`**,它恢复 0.3.11 的行为(结构绿即 `exit 0`)。
310
+ 两者同时给时 **opt-out 优先**。也可改读 `--json` 的 `assessmentScope` / `migration.ready` 自定门槛。
279
311
 
280
312
  **2. `--json` 新增 `assessmentScope`(三值)。**
281
313
 
282
314
  | 值 | 含义 |
283
315
  |---|---|
284
- | `full` | 被检文件已是当前宿主的目标格式(无需迁移),覆盖完整 |
316
+ | `full` | 被检文件已是当前宿主的目标格式(无需迁移),覆盖完整(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
285
317
  | `partial` | 需要迁移 ⇒ 迁移预检**只覆盖 2 条规则,另有 6 类未覆盖**(≠ 通过;见 `coverage.uncovered`) |
286
318
  | `none` | **不可在本宿主评估**(宿主能力不足),不认证任何"可用/可升级"结论 |
287
319
 
@@ -13,7 +13,7 @@
13
13
  * contracts 列出内置契约规则目录
14
14
  */
15
15
  import fs from 'node:fs';
16
- import { loadSessionLog, validateSessionLog, resumeVerdict, migrationVerdict, assessmentScope, createPreWriter, repairSession, readSessionHeader, CONTRACT_RULES, ruleById, extractToolOutputs, auditToolCalls, hostCapability, HOST_MAX_FILE_VERSION } from '../lib/index.js';
16
+ import { loadSessionLog, validateSessionLog, resumeVerdict, migrationVerdict, assessmentScope, createPreWriter, repairSession, readSessionHeader, CONTRACT_RULES, ruleById, extractToolOutputs, auditToolCalls, hostCapability, HOST_MAX_FILE_VERSION, detectSupport } from '../lib/index.js';
17
17
 
18
18
  /**
19
19
  * 同步写 fd(F4 修复)——`process.stdout.write` 在**管道**下是异步的,紧跟着的
@@ -39,7 +39,7 @@ const errOut = (text) => writeAllSync(2, text);
39
39
  const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log contract guard)
40
40
 
41
41
  用法:
42
- dsh-log-contract check <session-log> [--json] [--max-details N] [--resume] [--fail-on-migration]
42
+ dsh-log-contract check <session-log> [--json] [--max-details N] [--resume] [--fail-on-migration] [--no-fail-on-migration]
43
43
  离线体检 + **迁移预检**(两个**独立维度**,互不蕴含)。session-log 支持 .jsonl 与 .jsonl.zstd。
44
44
  ⚠️ 口径边界:本工具只判定它实现的规则集。「ok」/「可加载」/「可继续」/「可压缩」
45
45
  **不代表**会话"可用",**不代表**可安全编辑,也**不代表**官方升级路径会接受
@@ -48,17 +48,23 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
48
48
  --max-details N 每条违规最多列 N 个缺失 seq(默认 8,--json 忽略)
49
49
  --resume 输出分层结论:可加载 / 可继续 / 可压缩(本工具规则集内)
50
50
  + 迁移预检(官方 v0/v1/v2→当前格式会不会拒);--json 时附带。
51
- --fail-on-migration migration.ready===false(工具判"官方升级会拒")时退出码 4。
52
- **注意:现行默认行为已经是"ready===false ⇒ 退出码 4"**;本开关是显式别名,
53
- 便于 CI 把意图写进命令行(也与将来若放宽默认值时保留 opt-in 口径)。
54
- 评估范围 assessmentScope(顶层字段):'partial' = 待迁移文件(迁移维度只覆盖 2 条规则,
55
- 另有 6 类未覆盖)⇒ **不构成"官方升级会接受"**;'full' = 无需迁移(已是当前格式);
56
- 'none' = 本宿主能力不足以评估。机器应判 assessmentScope == "full" 才可把绿读作完整评估。
51
+ --fail-on-migration migration.ready===false(工具判"官方升级会拒")时退出码 4。
52
+ **注意:默认已经是这个行为**(第四轮把"报告与退出码不一致"的假阴性修掉了);
53
+ 本开关是**显式别名**,便于 CI 把意图写进命令行。
54
+ --no-fail-on-migration **真 opt-out**:恢复 0.3.11 的行为——迁移 blocked 不再改退出码
55
+ (结构绿即 exit 0)。两者同时给时 **opt-out 优先**。若你的流水线依赖旧退出码,
56
+ 用本开关;也可改读 --json assessmentScope / migration.ready 自定门槛。
57
+ 评估范围 assessmentScope(顶层字段,与 migration.assessmentScope 同值):
58
+ 'full' = 被检文件**已是当前官方格式(version=3)、无需迁移** ⇒ 工具规则集对该文件是完整的;
59
+ 'partial' = **需要迁移**(version<3):迁移维度只覆盖 2 条规则,另有 6 类未覆盖 ⇒
60
+ **不构成"官方升级会接受"**;'none' = 本宿主能力不足以评估(文件版本 > 宿主上限)。
61
+ 机器应判 assessmentScope == "full" 才可把绿读作完整评估(partial/none 都不该)。
57
62
  退出码:0 本工具认为可加载且无迁移阻断;1 有 error 级违规;3 不可在本宿主评估
58
63
  (被检文件版本 > 宿主支持上限,如 rc.7 宿主的 v3 文件——**不是** broken);
59
- 4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false)。
64
+ 4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false;可用
65
+ --no-fail-on-migration 退出该档)。
60
66
 
61
- dsh-log-contract fix <session-log> [--remove-markers] [--neutralize] [--clip-crossstep] [--drop-failed-turns] [--trim-last N] [--compact-last N] [--tail-renumber D] [--neutralize-orphan] [--extract-turn N] [--keep-ranges a-b,c-d] [--apply] [--backup-dir DIR] [--json]
67
+ dsh-log-contract fix <session-log> [--remove-markers] [--neutralize] [--neutralize-legacy-markers] [--clip-crossstep] [--drop-failed-turns] [--trim-last N] [--compact-last N] [--tail-renumber D] [--neutralize-orphan] [--extract-turn N] [--keep-ranges a-b,c-d] [--apply] [--backup-dir DIR] [--json]
62
68
  诊断 + 修复(2026-08 事故固化方案)。先做严格 seq 连续扫描 + 契约体检
63
69
  (含 W1/W2 wire 级悬空 tool 检查),再按需修复:
64
70
  --remove-markers 移除 retrace/message-editor marker 并全量重编号
@@ -67,6 +73,11 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
67
73
  --neutralize 原地中和 turn-null marker(type→retrace/marker +
68
74
  ignorable:true,删 surfaceOp/sourceEventSeqs,seq/行数不变)
69
75
  —— token-meter 不再刷屏,会话驻留也安全(2026-08-30 事故)
76
+ --neutralize-legacy-markers
77
+ **一次性根治历史载体债**(R-C):只中和
78
+ assistant/message + data.editor 形态的历史 retrace marker
79
+ (新载体是 user/message + data.id,不在此列)。清理后,
80
+ 写前校验不再需要"≤v2 历史 marker T1 降级"那条豁免。
70
81
  --drop-failed-turns 删除"本轮运行失败"的轮次(清失败报错气泡)
71
82
  --trim-last N 裁剪到最近 N 条 append 消息(保留所在 turn 结构)
72
83
  --trim-budget N 按 token 预算裁剪(L5):自动选保留消息数使估算 ≤ N
@@ -127,6 +138,38 @@ function printViolations(violations, maxDetails = 8) {
127
138
  }
128
139
  }
129
140
 
141
+ /** R-E 抬头(2026-09-14 裁定):漂移清单 + 未复核清单 + "勿据此跑 fix --apply"。 */
142
+ function driftLines(drift, probes) {
143
+ if (!drift) return '';
144
+ const host = probes ? `${probes.hostPackage}(SESSION_FORMAT_VERSION=${probes.sessionFormatVersion},词表 ${probes.knownTypes} 类)` : '未知';
145
+ // 注:这里**刻意不用 ✅/⚠️ 之外的"通过"符号**——待迁移文件(assessmentScope=partial)的
146
+ // 输出被测试钉死为"不得出现无条件绿的 ✅",漂移行不能破坏那条纪律。
147
+ const probeOk = (probes?.probes ?? []).filter((x) => x.ok).length;
148
+ const probeTotal = (probes?.probes ?? []).length;
149
+ let out = ` 漂移检测:宿主 ${host} | 行为探针 ${drift.probeVerified ? `全部与规则假设一致(${probeOk}/${probeTotal})` : `${drift.unverifiedRules.length} 组不一致(${probeOk}/${probeTotal} 一致)`}\n`;
150
+ if (drift.unverifiedRules.length > 0) {
151
+ out += ` ⛔ **UNVERIFIED**(探针与规则假设不一致,结论不可采信):${drift.unverifiedRules.join('、')}\n`;
152
+ }
153
+ out += ` 出处未复核(R-F,已知债,仅告警):${drift.driftedSources.join('、')}\n`;
154
+ out += ` 判定前提存疑(复核 §1,仅告警):${drift.premiseStale.join('、')}${drift.undecidable.length ? `;无法判定:${drift.undecidable.join('、')}` : ''}\n`;
155
+ if (drift.fixApplyBlocked) {
156
+ out += ' ⛔ **禁止据此跑 `fix --apply`**(存在未验证规则:写入类动作不可逆)——先按 report 复核这些规则或显式确认。\n';
157
+ }
158
+ return out;
159
+ }
160
+
161
+ /** 版本支持面(2026-09-14 用户要求第 1 条):一行摘要 + 逐条告警。
162
+ * `readOnly` = 未验证格式 ⇒ 只读(check 照跑结构层;prewrite/fix 直接拒绝)。 */
163
+ function supportLines(support) {
164
+ if (!support) return '';
165
+ let s = ` 版本支持:${support.summary}\n`;
166
+ s += ` 测试基线:${support.baseline.hostPackage}@${support.baseline.hostVersion} | 会话格式 v${support.baseline.sessionFormatVersion}`
167
+ + `(已知 ${support.baseline.knownFormatVersions.join('/')};peer ${support.baseline.peerRange})\n`;
168
+ for (const w of support.warnings ?? []) s += ` ⚠️ ${w}\n`;
169
+ if (support.readOnly) s += ' ⛔ 未验证格式 ⇒ **只读**:不给出写入/修复结论(版本检测见 lib/version-support.js)\n';
170
+ return s;
171
+ }
172
+
130
173
  /** S2 横幅:被检文件版本 > 宿主支持上限 ⇒ "不可在本宿主评估"(**不是** broken)。 */
131
174
  function notAssessableBanner(result) {
132
175
  const na = result.notAssessable ?? {};
@@ -136,7 +179,8 @@ function notAssessableBanner(result) {
136
179
  + ` 原因:${na.reason}\n`
137
180
  + ` 已跳过规则:${(na.skippedRules ?? []).join(', ')}\n`
138
181
  + ` 已执行:结构层(Z/H/R/E1·E2·E4·E5·E6/S9/M/P/P3·P4/I1);结构层结论 ${result.structuralOk ? '绿' : '有 error(见上)'}\n`
139
- + ` ⚠️ 这不表示日志损坏,也**不要**据此运行 fix --apply —— 请用 0.1.5+ 宿主评估。\n`;
182
+ + ` ⚠️ 这不表示日志损坏,也**不要**据此运行 fix --apply —— 请用 0.1.5+ 宿主评估。\n`
183
+ + supportLines(result.support);
140
184
  }
141
185
 
142
186
  /** 迁移预检(独立维度)文本。
@@ -169,9 +213,11 @@ function migrationLine(mig, partial = false) {
169
213
  function cmdCheck(args) {
170
214
  const json = args.includes('--json');
171
215
  const resume = args.includes('--resume');
172
- // 第四轮(最终复核 §六-2):显式开关。现行默认行为已是"ready===false ⇒ exit 4",
173
- // 本开关是显式别名,便于 CI 把意图写进命令行。
216
+ // 第四轮/第五轮:`--fail-on-migration` 是**默认已生效行为**的显式别名;
217
+ // `--no-fail-on-migration` 是**真 opt-out**(恢复旧行为:迁移 blocked 不再改退出码)。
218
+ // 两者同时给 ⇒ opt-out 优先(help/README 写明)。
174
219
  const failOnMigration = args.includes('--fail-on-migration');
220
+ const noFailOnMigration = args.includes('--no-fail-on-migration');
175
221
  const maxDetailsIdx = args.indexOf('--max-details');
176
222
  const maxDetails = maxDetailsIdx >= 0 && args[maxDetailsIdx + 1] ? Number(args[maxDetailsIdx + 1]) : 8;
177
223
  const file = args.find((a) => !a.startsWith('-'));
@@ -191,13 +237,14 @@ function cmdCheck(args) {
191
237
  const scope = result.assessmentScope ?? assessmentScope(result);
192
238
  const partial = scope === 'partial';
193
239
  const migBlocked = migration?.applies === true && migration.ready === false;
194
- // `--fail-on-migration` 与默认同效(显式别名):`ready===false` ⇒ 非 0
195
- const migGate = migBlocked || (failOnMigration && migration?.ready === false);
240
+ // 默认:`ready===false` ⇒ 非 0(第四轮修复的假阴性)。
241
+ // `--fail-on-migration` = 显式别名(同默认);`--no-fail-on-migration` = opt-out(恢复旧行为)。
242
+ const migGate = !noFailOnMigration && (migBlocked || (failOnMigration && migration?.ready === false));
196
243
  // 退出码:3 不可评估 > 1 结构 error > 4 迁移 blocked > 0。
197
244
  const exitCode = notAssessable ? 3 : (!ok ? 1 : (migGate ? 4 : 0));
198
245
 
199
246
  if (json) {
200
- const payload = { file, ok, assessable: !notAssessable, assessmentScope: scope, host: hostCapability(), migration, summary, violations };
247
+ const payload = { file, ok, assessable: !notAssessable, assessmentScope: scope, host: hostCapability(), support: result.support ?? null, probes: result.probes ?? null, drift: result.drift ?? null, migration, summary, violations };
201
248
  if (notAssessable) payload.notAssessable = result.notAssessable;
202
249
  if (resume) payload.resume = resumeVerdict(result);
203
250
  out(JSON.stringify(payload, null, 2) + '\n');
@@ -230,6 +277,8 @@ function cmdCheck(args) {
230
277
  out(` ${mark} ${name} — ${desc}\n`);
231
278
  if (blockers.length > 0) out(` 阻断: ${[...new Set(blockers)].join(', ')}\n`);
232
279
  }
280
+ out(supportLines(result.support));
281
+ out(driftLines(result.drift, result.probes));
233
282
  out(migrationLine(migration, partial));
234
283
  out(`\n 结论: ${icons[tier]}${migBlocked
235
284
  ? '(本工具规则集内)—— 但**迁移预检 blocked**:不得据此宣称"可升级"'
@@ -243,6 +292,8 @@ function cmdCheck(args) {
243
292
  out(`\n📋 dsh-log-contract check —— ${file}\n`);
244
293
  out(` 事件 ${summary.events} | surface 节点 ${summary.surfaceNodes} | replace 代数 ${summary.replaceGeneration} | 帧 ${summary.frames}(${(summary.compressedBytes / 1024).toFixed(1)}KiB → ${(summary.plaintextBytes / 1024).toFixed(1)}KiB)\n`);
245
294
  out(` 违规 ${summary.total}(error ${summary.bySeverity.error} / warning ${summary.bySeverity.warning})\n`);
295
+ out(supportLines(result.support));
296
+ out(driftLines(result.drift, result.probes));
246
297
  out(migrationLine(migration, partial));
247
298
  out('\n');
248
299
  printViolations(violations, maxDetails);
@@ -277,6 +328,11 @@ function cmdPrewrite(args) {
277
328
  fail(`会话日志读取失败:${err.message}`);
278
329
  }
279
330
  const baseline = validateSessionLog(log);
331
+ // 版本支持面(2026-09-14 用户要求第 1 条):**未验证格式 ⇒ 只读**,不得给出写入结论。
332
+ if (baseline.support?.readOnly) {
333
+ fail(`未验证的会话格式(来源 ${baseline.support.file.source},版本 v${baseline.support.file.version})——按只读处理,写前校验无法给出可信结论。`
334
+ + ` ${baseline.support.warnings?.[0] ?? ''}`, 3);
335
+ }
280
336
  // S2:本宿主评估不了的文件**不能**给出写前结论(否则会拿假阳性去拦合法写入)
281
337
  if (baseline.assessable === false) {
282
338
  fail(`不可在本宿主评估该会话(${baseline.notAssessable?.reason ?? '宿主能力不足'})——写前校验无法给出可信结论;请用 0.1.5+ 宿主`, 3);
@@ -301,11 +357,19 @@ function cmdPrewrite(args) {
301
357
  }
302
358
 
303
359
  if (json) {
304
- out(JSON.stringify({ file, op: result.op, ok: result.ok, bySeverity: result.bySeverity, violations: result.violations }, null, 2) + '\n');
360
+ out(JSON.stringify({ file, op: result.op, ok: result.ok, bySeverity: result.bySeverity, legacyMarkerDebt: result.legacyMarkerDebt ?? null, violations: result.violations }, null, 2) + '\n');
305
361
  process.exit(result.ok ? 0 : 1);
306
362
  }
307
363
 
308
364
  out(`\n✍️ dsh-log-contract prewrite —— ${file}(op: ${result.op},nextSeq: ${prewriter.nextSeq})\n\n`);
365
+ if (result.legacyMarkerDebt) {
366
+ // R-C「降级可见」:历史 marker 债不只是 warning 里的一句话,这里显式打印并给出根治命令。
367
+ const d = result.legacyMarkerDebt;
368
+ out(` ⚠️ 检测到**历史 marker 载体**(${d.kind} 形态,id=${d.id},targetSeq=${d.targetSeq},seq=${d.seq}):\n`);
369
+ out(` T1 已按 ≤v${d.formatVersion <= 2 ? 2 : d.formatVersion} 白名单降级为 warning(仅历史载体、仅旧格式文件)——\n`);
370
+ out(' 但**压缩(/compact)前仍需清理**,否则 token-meter 自检会拦:一次性根治 →\n');
371
+ out(` \`dsh-log-contract fix <log> --neutralize-legacy-markers --apply\`(备份后原地中和,seq/行数不变)\n\n`);
372
+ }
309
373
  if (result.ok) {
310
374
  out(' ✅ 写入安全:三层契约全绿(持久化 foldSurface 可重放 / 引擎层无崩溃风险 / 插件 marker 语义自洽)\n');
311
375
  out(` 写入后 surface 节点 ${result.stateAfter.surfaceNodes} 个,nextSeq ${result.stateAfter.nextSeq}\n\n`);
@@ -320,7 +384,12 @@ function cmdPrewrite(args) {
320
384
  function cmdContracts() {
321
385
  out('dsh-log-contract 契约规则目录(spec:59 条审计发现 + 官方源码逐行核对)\n\n');
322
386
  for (const r of CONTRACT_RULES) {
323
- out(` ${r.id} [${r.severity}/${r.layer}] ${r.title}\n ${r.description}\n 出处: ${r.source}\n\n`);
387
+ const flags = [
388
+ r.candidate ? '候选规则' : null,
389
+ r.sourceVerified === false ? '出处未复核' : null,
390
+ r.premiseStale ? '判定前提存疑' : null,
391
+ ].filter(Boolean).join(' / ');
392
+ out(` ${r.id} [${r.severity}/${r.layer}] ${r.title}${flags ? ` ⟪${flags}⟫` : ''}\n ${r.description}\n 出处: ${r.source}\n\n`);
324
393
  }
325
394
  }
326
395
 
@@ -328,6 +397,8 @@ function cmdFix(args) {
328
397
  const json = args.includes('--json');
329
398
  const removeMarkers = args.includes('--remove-markers');
330
399
  const neutralize = args.includes('--neutralize');
400
+ // R-C 一次性根治路径:只中和**历史载体**(assistant/message + data.editor)的 retrace marker
401
+ const neutralizeLegacyMarkers = args.includes('--neutralize-legacy-markers');
331
402
  const clipCrossStep = args.includes('--clip-crossstep');
332
403
  const dropFailedTurns = args.includes('--drop-failed-turns');
333
404
  const trimIdx = args.indexOf('--trim-last');
@@ -356,12 +427,18 @@ function cmdFix(args) {
356
427
  // 日志报成 broken → 用户以为日志坏了去跑 `fix --apply` ⇒ 在健康日志上动手。这里直接拒绝。
357
428
  const head = readSessionHeader(file);
358
429
  const fileVersion = Number.isSafeInteger(head?.version) ? head.version : 0;
430
+ // 版本支持面:未验证格式 ⇒ 只读(不得修),与 S2 宿主能力闸并列。
431
+ const support = detectSupport({ header: head });
432
+ if (support.readOnly) {
433
+ fail(`未验证的会话格式(来源 ${support.file.source},版本 v${support.file.version})——按只读处理,拒绝修复。`
434
+ + ` ${support.warnings?.[0] ?? ''}`, 3);
435
+ }
359
436
  if (fileVersion > HOST_MAX_FILE_VERSION) {
360
437
  fail(`拒绝修复:被检文件 version=${fileVersion} 高于本宿主 @deepseek-ai/dsh-session@${hostCapability().hostPackage} 支持的最大版本 ${HOST_MAX_FILE_VERSION}`
361
438
  + ` —— 本宿主评估不了该文件(不是"日志坏了")。请用 0.1.5+ 宿主修复。`, 3);
362
439
  }
363
440
 
364
- const result = repairSession(file, { removeMarkers, neutralize, clipCrossStep, dropFailedTurns, trimLast, trimBudget, compactLast, tailRenumberDelta, neutralizeOrphan, extractTurn, extractTurnTo, keepRanges, apply, backupDir });
441
+ const result = repairSession(file, { removeMarkers, neutralize, neutralizeLegacyMarkers, clipCrossStep, dropFailedTurns, trimLast, trimBudget, compactLast, tailRenumberDelta, neutralizeOrphan, extractTurn, extractTurnTo, keepRanges, apply, backupDir });
365
442
  if (json) {
366
443
  out(JSON.stringify(result, null, 2) + '\n');
367
444
  process.exit(result.ok ? 0 : 1);
package/docs/CONTRACTS.md CHANGED
@@ -8,13 +8,20 @@
8
8
  > 与 `lib/prewrite.js`(写前校验)中有对应实现;离线体检(`lib/validate.js`)
9
9
  > 逐条执行并在最后用官方 `foldSurface` 终验(S8)。
10
10
  >
11
- > 规则来源:`dsh-scale-audit-疑点记录.md`(59 条审计发现)+ `复盘-会话修复事故-20260825.md`
12
- > (三层契约)+ `@deepseek-ai/dsh-session@0.1.0-rc.7` 官方源码逐行核对
13
- > (后续规则随官方版本演进追加:T3/T4 渲染层 = 1e99e1ff 复盘,T5 = 1f4d986e malformed)。
11
+ > 规则来源:早期内部审计发现(59 条)+ 三层契约事故复盘 + 官方源码逐行核对
12
+ > (`@deepseek-ai/dsh-session`,各条出处见下)。后续规则随官方版本演进追加:
13
+ > T3/T4 = 渲染层白屏事故复盘,T5 = malformed turn/end 事故复盘。
14
+ >
15
+ > **版本支持基线(本规则集验证过的版本;权威常量见 `lib/version-support.js` 的 TESTED_BASELINE)**:
16
+ > 宿主 `@deepseek-ai/dsh-session@0.1.5-rc.1` | 会话格式 **v3**(`SESSION_FORMAT_VERSION = 3`)|
17
+ > 声明范围 `^0.1.0-rc.7 || ^0.1.5-rc.1`(可安装 ≠ 逐个验证)| 已知格式 0/1/2/3
18
+ > (0–2 由本包自带 vendored 词表 + `legacyFoldSurface` 支持,与宿主版本无关)。
19
+ > **未验证版本**:宿主比基线新 → 运行时告警;文件格式未知/无法识别 → 报"未验证格式"并**按只读处理**
20
+ > (`prewrite`/`fix` 拒绝,退出码 3);文件版本 > 宿主上限 → `not-assessable`(不是 broken)。
14
21
  >
15
22
  > 严重度:**error** = 违反即会话不可加载/写入被拒(fail-loud);**warning** = 合法但可疑。
16
23
 
17
- ## 规则索引(共 38 条)
24
+ ## 规则索引(共 44 条)
18
25
 
19
26
  | id | 严重度 | 层级 | 规则 |
20
27
  |---|---|---|---|
@@ -45,6 +52,9 @@
45
52
  | T4 | error | engine | step/消息本体 turn 缺失(null/undefined)→ 渲染死循环 |
46
53
  | T5 | error | engine | turn/end 必须带 data.reason.kind |
47
54
  | E7 | warning | persistence | ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者) |
55
+ | E8 | warning | persistence | 事件信封键白名单(多余键:seed/restore 路径会拒) |
56
+ | E9 | error | persistence | system/message 必须带 plugin source |
57
+ | E10 | error | persistence | request/header 的 data.header 字段约束 |
48
58
  | Z3 | warning | framing | 空会话文件(有 header 无事件)显式报出 |
49
59
  | P3 | warning | plugin | tool/call ↔ tool/result 配对完整性(考古 B1) |
50
60
  | P4 | warning | plugin | tool/result 输出结构可解析(考古 B2) |
@@ -56,6 +66,9 @@
56
66
  | Z2 | error | framing | zstd 帧解码失败 = 单帧全损 |
57
67
  | W1 | error | engine | wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后 |
58
68
  | W2 | error | engine | wire 流:user 文本不得插在 tool_calls 与其 tool 结果之间 |
69
+ | G1 | warning | migration | 迁移预检:v0 源文件的 subagent/descriptor.data.version 必须为 3 |
70
+ | G2 | warning | migration | 迁移预检:v0 源文件不得含词表外的历史事件类型(含 ignorable) |
71
+ | G3 | warning | migration | 迁移预检:v0 源 session/title 系列的 messageSeqs 必须引用更早的人类 user/message |
59
72
 
60
73
  ## 详细规则
61
74
 
@@ -68,8 +81,8 @@
68
81
  ### H2 — header 版本与必填字段
69
82
 
70
83
  - **层级**: persistence | **严重度**: error
71
- - **出处**: @deepseek-ai/dsh-session lib/index.js:1110-1125
72
- - **契约**: header.version 必须为 0;id 为字符串;createdAt 为非负安全整数;cwd 若存在必须为绝对路径;origin 只能为 "subagent"。
84
+ - **出处**: @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)
85
+ - **契约**: header.version 必须为已知受支持版本(0/1/2/3);未知版本报 H2。id 为字符串;createdAt 为非负安全整数;cwd 若存在必须为绝对路径;origin 只能为 "subagent"。
73
86
 
74
87
  ### R1 — 每行必须是合法 JSON
75
88
 
@@ -104,13 +117,13 @@
104
117
  ### S9 — 文件物理序 seq 单调(多写入者交织现场特征)
105
118
 
106
119
  - **层级**: persistence | **严重度**: error
107
- - **出处**: 2026-08-28 实锤:526f1835 文件物理序 734056→733539→735470;单进程 appendCore 断言 seq==cursor+i 且按 id 串行化不可能写出
120
+ - **出处**: 2026-08-28 实锤(某真实会话):文件物理序出现回退(大→小→更大);单进程 appendCore 断言 seq==cursor+i 且按 id 串行化不可能写出
108
121
  - **契约**: 按文件物理行序要求展开后事件 seq 严格单调递增。E2 在排序后检查(loadSessionLog 会 sort),物理序倒退被掩盖;S9 在排序前按行序检查,非单调 = 多写入者/旧光标回放交织的直接现场证据,加载会被拒。
109
122
 
110
123
  ### I1 — inbox seed 相对重放(fork 边界孤儿 spliced)
111
124
 
112
125
  - **层级**: engine | **严重度**: error
113
- - **出处**: @deepseek-ai/dsh-agent lib/types/inbox.js:155-178 (apply/validate);2026-08-28 实锤:62c5b531/73ed35d8 fork 边界 removedCount=1 孤儿
126
+ - **出处**: @deepseek-ai/dsh-agent lib/types/inbox.js:155-178 (apply/validate);2026-08-28 实锤(某两个真实会话):fork 边界 removedCount=1 孤儿
114
127
  - **契约**: 从 header.seedLength 起重放 agent/inbox/spliced,next-turn/next-step 双队列;start+removedCount 不得超过队列长、不得产生重复 pending id。fork 时"移除父待处理提示词"的 splice 假设父会话 inbox,子会话 seed 相对空 inbox 上非法 → resume 被拒(invalid persisted inbox splice)。
115
128
 
116
129
  ### E3 — type 必须在已知词汇表内(或带 ignorable 标记)
@@ -129,7 +142,7 @@
129
142
 
130
143
  - **层级**: persistence | **严重度**: error
131
144
  - **出处**: @deepseek-ai/dsh-session lib/index.js:1273-1277 (assertSupportedRequestHeader)
132
- - **契约**: request/header-delta 与 reason=fallback 的 request/header 是已删除的遗留格式,写入即被拒。
145
+ - **契约**: request/header-delta 与 reason=fallback 的 request/header 是已删除的遗留格式。注意(R-F 订正复核 §1 E5):宿主 Session.append/appendLines 不看 type ⇒ 写入会成功、下一次读取才炸(依据 dsh-session@0.1.5-rc.1 lib/index.js:1170-1210 / persistence-jsonl:3046-3073),不是写入即被拒。
133
146
 
134
147
  ### E6 — 消息类事件消息形状
135
148
 
@@ -182,8 +195,8 @@
182
195
  ### S8 — 整日志 foldSurface 可重放
183
196
 
184
197
  - **层级**: persistence | **严重度**: error
185
- - **出处**: @deepseek-ai/dsh-session lib/index.js:444-455 (foldSurface);复盘"官方 foldSurface 不抛 = 通过"
186
- - **契约**: 终验:把全部事件按序喂给官方 foldSurface,不抛 = 持久化层通过。S1–S7 任何一条违反都会在此暴露。
198
+ - **出处**: @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 逐条移植)
199
+ - **契约**: 终验:按被检文件 header.version 选折叠器(v3 → 官方 foldSurface;v0/v1/v2 → 本地 legacyFoldSurface),不抛 = 持久化层通过。S1–S7 任何一条违反都会在此暴露。注意 0.1.5 的官方 foldSurface 是 v3 语义(replace 用 startSeq/endSeq、assistant/message 禁 sourceEventSeqs),对旧格式文件会误报,不可借用。
187
200
 
188
201
  ### T1 — token-meter 配对:assistant/message 与 step/end 必须匹配当前打开的 step/start
189
202
 
@@ -195,25 +208,25 @@
195
208
 
196
209
  - **层级**: engine | **严重度**: error
197
210
  - **出处**: @deepseek-ai/dsh-token-meter lib/index.js:634-650 (_estimateProviderAssistant,:645 belongs to another step)
198
- - **契约**: token meter 重建 provider 输出时,逐条检查 assistant/message 的 sourceEventSeqs:指向 assistant/chunk 的引用必须与消息同 turn/step,且 seq 更早、不重复;跨 step 引用 → 官方抛 belongs to another step → 每次事件追加都重抛(consumedEvents 不前进)→ 刷屏压垮 host(2026-08-30 实测 526f1835 seq 936047 跨 step 7/8/9)。T1 只查 step 配对不查源引用,此条补盲区;修复用 fix --clip-crossstep。
211
+ - **契约**: token meter 重建 provider 输出时,逐条检查 assistant/message 的 sourceEventSeqs:指向 assistant/chunk 的引用必须与消息同 turn/step,且 seq 更早、不重复;跨 step 引用 → 官方抛 belongs to another step → 每次事件追加都重抛(consumedEvents 不前进)→ 刷屏压垮 host(2026-08-30 实测:某真实会话的 chunk 源引用跨 step 7/8/9)。T1 只查 step 配对不查源引用,此条补盲区;修复用 fix --clip-crossstep。
199
212
 
200
213
  ### T3 — step 节点 key 唯一(同 turn 内 step/start 的 step 号不得复用)
201
214
 
202
215
  - **层级**: engine | **严重度**: error
203
- - **出处**: 复盘 2026-09-02 1e99e1ff 白屏(修复线 session-3f9e4f12):客户端渲染节点 key = turn:step,冲突 → React 渲染死循环;工具 tools/check-step-keys.mjs
204
- - **契约**: 客户端渲染消息列表从事件流构建节点,节点 key = data.turn:data.step。同 turn 内两个 step/start 的 step 号相同 → key 冲突 → React 渲染死循环 → 白屏/不展示(1e99e1ff:580034 step 95/1 vs 580037 编辑块 step 95/1;6924781d/97786207/4b149a4a 同型)。修复:同 turn 内 step 递增、整块重编号(含块内 chunk/tool/assistant)。
216
+ - **出处**: 复盘 2026-09-02 渲染层白屏事故:客户端渲染节点 key = turn:step,冲突 → React 渲染死循环(由离线自查脚本判出)
217
+ - **契约**: 客户端渲染消息列表从事件流构建节点,节点 key = data.turn:data.step。同 turn 内两个 step/start 的 step 号相同 → key 冲突 → React 渲染死循环 → 白屏/不展示(实测:同一 turn 内两个 step/start 复用 step 95/1,一个来自正常轮、一个来自编辑块;后续全量扫描又发现多个同型冲突)。修复:同 turn 内 step 递增、整块重编号(含块内 chunk/tool/assistant)。
205
218
 
206
219
  ### T4 — step/消息本体 turn 缺失(null/undefined)→ 渲染死循环
207
220
 
208
221
  - **层级**: engine | **严重度**: error
209
- - **出处**: 复盘 D8 1e99e1ff(2026-09-01):retrace 0.4.17 编辑块 turn:null;修复线 tools/check-null-turn.mjs 判致命
210
- - **契约**: 客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 → 白屏「载入历史」(1e99e1ff seq 580037-580039)。user/message 天然无 turn 不查;chunk 坐标可缺失不查。step/消息本体必须带真实 turn 号。
222
+ - **出处**: 复盘 2026-09-01 D8 事故:retrace 0.4.17 编辑块 turn:null(离线自查脚本判致命)
223
+ - **契约**: 客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 → 白屏「载入历史」(实测:编辑块的 step/start + marker + step/end 连续若干行 turn 全为 null)。user/message 天然无 turn 不查;chunk 坐标可缺失不查。step/消息本体必须带真实 turn 号。
211
224
 
212
225
  ### T5 — turn/end 必须带 data.reason.kind
213
226
 
214
227
  - **层级**: engine | **严重度**: error
215
- - **出处**: 官方 dsh-agent-loop lib/index.js:620(turn/end = {turn, reason:{kind}});1f4d986e malformed turn/end 事故(2026-09-02,修复线 check-turn-end-reason.mjs)
216
- - **契约**: 官方 validation 强制 turn/end 的 data.reason.kind 存在(kind ∈ completed|max-tokens|blocked|aborted|error|interrupted)。缺失 = malformed → 官方 SessionPersistenceCorruptionError → 会话加载失败。1f4d986e:retrace 情形③信封 turn/end 漏 reason → 每次编辑后加载失败(已修 0.4.18)。
228
+ - **出处**: 官方 dsh-agent-loop lib/index.js:620(turn/end = {turn, reason:{kind}});malformed turn/end 事故(2026-09-02,由离线自查脚本判出)
229
+ - **契约**: 官方 validation 强制 turn/end 的 data.reason.kind 存在(kind ∈ completed|max-tokens|blocked|aborted|error|interrupted)。缺失 = malformed → 官方 SessionPersistenceCorruptionError → 会话加载失败。实测:retrace 情形③信封 turn/end 漏 reason → 每次编辑后加载失败(已修 0.4.18)。
217
230
 
218
231
  ### E7 — ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者)
219
232
 
@@ -221,6 +234,24 @@
221
234
  - **出处**: 反向挑刺 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)
222
235
  - **契约**: 未知 type + ignorable:true 被读路径接纳但无人消费 = 静默垃圾。排除已知消费者白名单(retrace/marker、retrace/goal-marker、message-editor/ 前缀等 retrace 客户端消费的插件 marker)后,其余 ignorable 未知事件报 warning。
223
236
 
237
+ ### E8 — 事件信封键白名单(多余键:seed/restore 路径会拒)
238
+
239
+ - **层级**: persistence | **严重度**: warning
240
+ - **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:849-861(assertSessionEventEnvelope)+ :1063-1068(唯一调用点=seed 路径);load 路径容忍见行为探针 p5
241
+ - **契约**: 事件对象只允许 7 个信封键(type/seq/time/data/surfaceOp/sourceEventSeqs/ignorable)。独立复核变异 05 指出"宿主拒、旧契约 0 违规";R-D 行为探针进一步订正口径:**load 路径容忍、seed/restore 路径拒** ⇒ warning。
242
+
243
+ ### E9 — system/message 必须带 plugin source
244
+
245
+ - **层级**: persistence | **严重度**: error
246
+ - **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:942-944("must have plugin source");角色表 :917-926
247
+ - **契约**: v3 新增的 system/message:role 必须为 system,source.kind 必须为 plugin 且 plugin 非空。实测(变异 03):source.kind='user' 宿主拒、旧契约 0 违规 ⇒ 漏检。
248
+
249
+ ### E10 — request/header 的 data.header 字段约束
250
+
251
+ - **层级**: persistence | **严重度**: error
252
+ - **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:231-248(validateSessionEventData:omit header.system / omit empty tools / omit empty adapterDefaults)
253
+ - **契约**: request/header 必须省略 header.system(系统提示改走 system/message)、空 tools、空 adapterDefaults。实测(变异 12):带 header.system 的写入宿主拒、旧契约 0 违规 ⇒ 漏检。
254
+
224
255
  ### Z3 — 空会话文件(有 header 无事件)显式报出
225
256
 
226
257
  - **层级**: framing | **严重度**: warning
@@ -287,3 +318,21 @@
287
318
  - **出处**: OpenAI 兼容端点对 tool 消息顺序的严格校验;DSH 序列化器将混合 user 消息展开为 text 在前、tool-result 在后
288
319
  - **契约**: 当仍有未满足的 assistant tool-call 时出现 user 文本消息,会产生 [assistant(tool_calls), user(text), tool] 序列,严格端点同样拒绝。
289
320
 
321
+ ### G1 — 迁移预检:v0 源文件的 subagent/descriptor.data.version 必须为 3
322
+
323
+ - **层级**: migration | **严重度**: warning
324
+ - **出处**: @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(容忍)
325
+ - **契约**: 文件版本 0 且事件类型为 subagent/descriptor 且 data.version !== 3 → 官方 v0→v1 迁移直接拒绝(消息形如 `subagent/descriptor <seq> uses unsupported descriptor version 2`);源版本 1/2 不受此条约束。
326
+
327
+ ### G2 — 迁移预检:v0 源文件不得含词表外的历史事件类型(含 ignorable)
328
+
329
+ - **层级**: migration | **严重度**: warning
330
+ - **出处**: @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"
331
+ - **契约**: 文件版本 0 且事件 type 不在官方 v0 dispositions(本包 vendored 为 V0_EVENT_TYPES)内 → 官方迁移拒绝,**即使该事件带 ignorable:true**。E3 的 ignorable 豁免是**读取路径**语义(不改),本条只在迁移预检维度表达。
332
+
333
+ ### G3 — 迁移预检:v0 源 session/title 系列的 messageSeqs 必须引用更早的人类 user/message
334
+
335
+ - **层级**: migration | **严重度**: warning
336
+ - **出处**: @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"
337
+ - **契约**: 文件版本 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 误报。
338
+