dsh-log-contract 0.3.15 → 0.3.16

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
@@ -8,7 +8,7 @@ offline health check + pre-write validation. The business layer's **doctor**.
8
8
  [![npm version](https://img.shields.io/npm/v/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
9
9
  [![npm downloads](https://img.shields.io/npm/dm/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
10
10
  [![License: MIT](https://img.shields.io/npm/l/dsh-log-contract)](https://github.com/yamingmou/dsh-log-contract/blob/main/LICENSE)
11
- [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4A90D9)](https://github.com/topics/dsh-plugin)
11
+ [![DSH ecosystem](https://img.shields.io/badge/DSH-ecosystem-4A90D9)](https://github.com/topics/dsh-plugin)
12
12
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/yamingmou/dsh-log-contract/pulls)
13
13
 
14
14
  **English** · [简体中文](./README.zh.md)
@@ -38,31 +38,33 @@ plugin marker semantics).
38
38
 
39
39
  > **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.
40
40
  >
41
- > **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`.
41
+ > **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 (`legacyMarkerKindOf` 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`.
42
42
 
43
43
  | Layer | What it is | Components |
44
44
  |---|---|---|
45
45
  | **Agent business layer (production-grade)** | Framework-agnostic core: session hygiene / retraceability / auditability / recoverability | Four modules: governance / retrospect / archaeology / **doctor** |
46
46
  | **dsh-retrace** | The business layer's DeepSeek Harness implementation | recall/edit/version/rollback/watchdog |
47
- | **dsh-log-contract** | dsh-retrace's core component = the business layer's **doctor** (check & repair) | check / prewrite / fix / extract / audit |
47
+ | **dsh-log-contract** | dsh-retrace's core component = the business layer's **doctor** (check & repair) | check / prewrite / fix / extract / audit-report |
48
48
 
49
49
  **Meaning**: dsh-log-contract is published standalone (for direct use or
50
50
  re-implementation), but it is first the "check & repair" capability of dsh-retrace —
51
51
  together they form the **Agent business layer (production-grade guarantees)** on
52
- DSH (see the [dsh-retrace roadmap](https://github.com/yamingmou/dsh-retrace/blob/main/docs/ROADMAP.md)).
52
+ DSH (see the [dsh-retrace repository](https://github.com/yamingmou/dsh-retrace)).
53
53
 
54
54
  ---
55
55
 
56
56
  ## Why it exists
57
57
 
58
- **#3632 "one log, two consumers, two verdicts"**: one log is consumed by both
58
+ **"one log, two consumers, two verdicts"**: one log is consumed by both
59
59
  humans and automated programs. Humans tolerate format drift; programs depend on
60
60
  strict contracts. Once the format drifts, humans see nothing wrong while programs
61
61
  crash or misreport.
62
62
 
63
- **Real incidents shaped every rule here** — see the [Incident log](#-incident-log)
64
- below. Each incident is a regression fixture: a corrupted session that this tool
65
- must flag, and a fixed session that it must pass.
63
+ **Real incidents shaped most of the rules here** — see the [Incident log](#-incident-log)
64
+ below. The incident-derived cases are pinned by regression tests: a corrupt log shape
65
+ this tool must flag, and the repaired shape it must pass. The remaining rules come from
66
+ line-by-line verification against the official Host source (each rule carries its
67
+ `source`).
66
68
 
67
69
  ---
68
70
 
@@ -86,21 +88,21 @@ must flag, and a fixed session that it must pass.
86
88
 
87
89
  ## ⚡ Incident log — why "production-grade" is not a slogan
88
90
 
89
- Every rule below was born from a **real incident** in our workspace. These are the
90
- sessions that made us build this tool. Dates and shapes are real; session ids are
91
- omitted for privacy.
91
+ Each entry below is a **real incident** from production sessions the cases that
92
+ made us build this tool. Dates and shapes are real; session ids and file names are
93
+ omitted.
92
94
 
93
95
  | # | Date | What happened | The rule / fix it produced |
94
96
  |---|---|---|---|
95
97
  | 1 | 2026-08-25 | A "restore hidden content" repair wrote a replace marker with **emptied `sourceEventSeqs`** → the session refused to load (`SessionPersistenceCorruptionError`); a second attempt changed the marker to **append** → the client engine crashed. Both were **violating writes that nothing caught**. | **S5** (sourceEventSeqs must cover shadowed nodes), **M1** (turn-null assistant/message can only be replace), pre-write validation |
96
98
  | 2 | 2026-08-27~28 | Interrupted/restarted turns replayed with a **stale in-memory cursor**, re-appending old seqs to the file tail (tail regression, duplicate batches); two writers interleaved → **file-physical order non-monotonic** (`734056 → 733539 → 735470`). Sessions failed to load with `seq gap`. | **S9** (physical-order monotonic), fix `--tail-renumber` |
97
99
  | 3 | 2026-08-27~28 | **Fork-boundary orphan splice**: the fork's "remove parent's pending prompt" splice assumed the parent's inbox; the child's seed-relative replay has an empty inbox → `resume failed: invalid persisted inbox splice`. | **I1** (inbox seed-relative replay), fix `--neutralize-orphan` |
98
- | 4 | 2026-08-28 | An oversized session (**1,052,557 tokens** vs the 1M window) could neither continue nor `/compact`; the trim budget estimator underpriced CJK by ~3.7×. | T1 (token-meter pairing) for compactability, `fix --trim` budget guidance |
100
+ | 4 | 2026-08-28 | An oversized session (**1,052,557 tokens** vs the 1M window) could neither continue nor `/compact`; the trim budget estimator underpriced CJK by ~3.7×. | T1 (token-meter pairing) for compactability, `fix --trim-budget` guidance |
99
101
  | 5 | 2026-08-29 | **W1/W2 wire violations**: markers shadowed an assistant with tool_calls but left the tool results dangling → strict endpoints (`INVALID_REQUEST`) reject the session's request stream. | **W1 / W2** (wire message flow) |
100
102
  | 6 | 2026-08-30 | A single **turn-null marker** made the token-meter listener throw on **every** appended event (`consumedEvents` never advanced → full-prefix re-fold per event) → **30s / 10,008 log lines**, host event loop crushed, all sessions locked. Same session also had a **cross-step sourceEventSeqs** (steps 7/8/9 mixed in one assistant message) — offline checks were green, the live meter crashed. | **T1** (turn/step pairing), **T2** (cross-step source refs), `fix --neutralize`, `fix --clip-crossstep` |
101
103
 
102
- > **Takeaway**: every rule in this tool is a scar from a real session — validated
103
- > against the actual corrupted-session fixtures, not synthetic theory. That is what
104
+ > **Takeaway**: these rules are not synthetic theory each one is pinned by a
105
+ > regression test that reproduces the corrupt shape it guards against. That is what
104
106
  > "production-grade" means here.
105
107
 
106
108
  ---
@@ -111,7 +113,7 @@ omitted for privacy.
111
113
 
112
114
  | Item | Baseline | Note |
113
115
  |---|---|---|
114
- | 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
+ | Host package | `@deepseek-ai/dsh-session@0.1.5-rc.1` | the version this rule set is developed and CI-tested against (CI runs `pnpm check` + `pnpm test`; the four `prepublishOnly` steps are green locally on it) |
115
117
  | Session format | **v3** (`SESSION_FORMAT_VERSION = 3`) | v3 uses the runtime vocabulary + official `foldSurface` final check |
116
118
  | Declared range | `peerDependencies: ^0.1.0-rc.7 || ^0.1.5-rc.1` | installable — a declared range is **not** a per-version verification |
117
119
  | Known formats | 0 / 1 / 2 / 3 | 0–2 are supported by this package's **vendored** vocabulary + local equivalent fold (`legacyFoldSurface`), independent of the host |
@@ -145,8 +147,8 @@ pnpm dlx dsh-log-contract --help
145
147
  > repair primitives) comes with the plugin automatically. This package is published
146
148
  > standalone for direct use or re-implementation.
147
149
  >
148
- > **Downloaded the repo as a ZIP?** `cd dsh-log-contract && npm install && npm run build`,
149
- > then `node bin/dsh-log-contract.mjs check <session-log>` — no global install needed.
150
+ > **Downloaded the repo as a ZIP?** `cd dsh-log-contract && npm install`, then
151
+ > `node bin/dsh-log-contract.mjs check <session-log>` — no build step and no global install needed.
150
152
 
151
153
  Dependencies: Node ≥ 22 (`node:zlib` has built-in zstd), `@deepseek-ai/dsh-session`
152
154
  (peer; validation/decode reuse the official implementation, so it stays in sync
@@ -176,7 +178,8 @@ Sample output (the CLI reports in Chinese — it is the tool's UI language):
176
178
  ❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
177
179
  ```
178
180
 
179
- Exit code: 0 = pass (no error-level violations); 1 = error-level violations exist.
181
+ Exit code: 0 = pass (no error-level violations); 1 = error-level violations exist;
182
+ 3 = not assessable on this host; 4 = the migration pre-check is blocked (see the upgrade notes below).
180
183
 
181
184
  `check` adds **W1/W2 wire-level checks** since 0.2.0: expand the model request
182
185
  stream in surface order and catch "dangling tool messages" (a tool result with no
@@ -246,7 +249,7 @@ audit asset. Read-only archaeology:
246
249
 
247
250
  ```sh
248
251
  # Export tool outputs matching a command regex (original text preserved)
249
- dsh-log-contract extract <session-log> --pattern "seed-scale" --min-size 50 --out ./found
252
+ dsh-log-contract extract <session-log> --pattern "build-report" --min-size 50 --out ./found
250
253
 
251
254
  # Archaeology audit report: call count / pairing rate / orphans / command distribution
252
255
  dsh-log-contract audit-report <session-log>
@@ -299,17 +302,21 @@ pnpm check && pnpm test # syntax check + contract-doc drift gate + all unit t
299
302
  > Local maintainer tooling is not part of the public repository, so `pnpm check` and `pnpm test`
300
303
  > work from a clean clone.
301
304
 
302
- Known truth table (updated 2026-08-31 after 0.3.5 added I1, `spliced-orphan`
303
- now FAILs; the old PASS row was stale):
304
- - `<session>-corrupt` / `<session>-seqgap` / `<session>-rewritten-230542` → FAIL (seq gaps)
305
- - `<session>-spliced-orphan` **FAIL (0.3.5+)** (T1/I1: invalid inbox splice + turn-null)
306
- - `<session>-pre-markerfix-20260825` → FAIL (pre-fix sample: turn-null markers remain)
307
- - `<session>-pre-cleanstate-20260831` PASS (a multi-thousand-span replace marker is data-legal;
308
- official foldSurface replays cleanly see the plugin ledger)
309
- The truth table tracks rule evolution (0.3.5's I1 flipped spliced-orphan PASS→FAIL);
310
- "repaired sessions PASS" must be verified on rebuilt samples `pre-` backups are
311
- usually pre-fix bad samples. See the boundary note in
312
- [docs/CONTRACTS.md](docs/CONTRACTS.md)).
305
+ Verdict by **log shape**what the tool must say about each defect class, not about
306
+ any particular file:
307
+
308
+ | Log shape | Verdict | Rules that fire |
309
+ |---|---|---|
310
+ | seq gap, or non-monotonic file-physical order | FAIL | S8 / C1 / T1 / E2 (plus S9 when seq goes backwards) |
311
+ | a rewrite that introduces a gap | FAIL | S8 / C1 / T1 / E2 / I1 |
312
+ | invalid inbox splice + turn-null marker | FAIL | T1 / I1 |
313
+ | pre-fix shape: legacy turn-null markers still present | FAIL | T1 |
314
+ | a multi-thousand-span replace marker with full coverage | PASS | error 0 — data-legal, official `foldSurface` replays cleanly |
315
+
316
+ Rule evolution moves rows: 0.3.5 added I1, which flipped the "invalid inbox splice"
317
+ shape from PASS to FAIL. And "a repaired session passes" holds only for the repaired
318
+ artifact — the pre-repair form of the same session is normally still a FAIL. See the
319
+ boundary note in [docs/CONTRACTS.md](docs/CONTRACTS.md).
313
320
 
314
321
  ---
315
322
 
@@ -323,6 +330,9 @@ usually pre-fix bad samples. See the boundary note in
323
330
  fold-position fix → 0.3.2 `tailSeq` → 0.3.3 `fix --neutralize` (in-place
324
331
  turn-null neutralization) → 0.3.4 `fix --clip-crossstep` (cross-step clipping) →
325
332
  0.3.5 **T2/S9/I1 rules** (cross-step source refs / physical order / inbox replay)
333
+ - [x] **0.3.6 → 0.3.15**: per-version rule routing for v0–v3 (T3/T4/T5, L3–L5, E8/E9/E10),
334
+ exit codes `3`/`4` for the host-capability and migration gates, and a version-support
335
+ report that says what was *not* verified (see [CHANGELOG.md](CHANGELOG.md))
326
336
  - [ ] Phase 2: runtime guard (subscribe to the session append stream, validate live,
327
337
  mark violations as `dsh/contract-violation`, policy configurable alert/block) —
328
338
  DSH plugin form
@@ -334,11 +344,13 @@ MIT © OfferKuai Team
334
344
 
335
345
  ---
336
346
 
337
- ## ⚠️ Upgrading to 0.3.15 — behaviour changes you must know (0.3.12 → 0.3.15)
347
+ ## ⚠️ Upgrading to 0.3.16 — behaviour changes you must know (0.3.12 → 0.3.16)
338
348
 
339
- > **0.3.15 itself changes no behaviour.** It removes two internal identifiers that had leaked into the
340
- > 0.3.14 published artifact (a README naming note and one source comment) and widens the pre-publish
341
- > leak gate vocabulary. Everything below applies unchanged when going from 0.3.12 to 0.3.15.
349
+ > **0.3.16 changes no behaviour.** It rewrites comments, test titles, a few diagnostic strings and the
350
+ > README so that they describe the product and its contract only. Every verdict, exit code and `--json`
351
+ > field is unchanged from 0.3.15, so everything below applies unchanged when going from 0.3.12 to 0.3.16.
352
+ >
353
+ > 0.3.15 was a documentation-only release as well.
342
354
 
343
355
  **1. New exit codes `3` / `4` — a defect fix that is also a breaking change.**
344
356
  - `3` = **not assessable on this host**: the file's `header.version` is higher than the host supports
@@ -356,7 +368,7 @@ MIT © OfferKuai Team
356
368
 
357
369
  | value | meaning |
358
370
  |---|---|
359
- | `full` | file is already the host's target format (no migration) — coverage is complete(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
371
+ | `full` | file is already the host's target format (no migration) — coverage is complete |
360
372
  | `partial` | file needs migration ⇒ the migration pre-check covers **2 rules only; 6 classes are uncovered** (≠ pass; see `coverage.uncovered`) |
361
373
  | `none` | **not assessable on this host** — no "usable / upgradable" claim is certified |
362
374
 
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![npm version](https://img.shields.io/npm/v/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
9
9
  [![License: MIT](https://img.shields.io/npm/l/dsh-log-contract)](https://github.com/yamingmou/dsh-log-contract/blob/main/LICENSE)
10
- [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4A90D9)](https://github.com/topics/dsh-plugin)
10
+ [![DSH ecosystem](https://img.shields.io/badge/DSH-ecosystem-4A90D9)](https://github.com/topics/dsh-plugin)
11
11
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/yamingmou/dsh-log-contract/pulls)
12
12
 
13
13
  [English](./README.md) · **简体中文**
@@ -32,25 +32,25 @@
32
32
 
33
33
  > **dsh-log-contract 是 [dsh-retrace](https://github.com/yamingmou/dsh-retrace) 的核心能力组件**(业务层的「医生」模块):负责会话日志的**体检与修复**——让每一次撤回/编辑/回退都落在合法日志上,写前校验(`prewrite`)会把 error 级违约**拦在落盘之前**。
34
34
  >
35
- > **关于 `/compact`(如实表述)**:本工具保证的是**新写入**不再制造 token-meter 配对债——自两段原子成对(marker + 配对段)起,新 marker 天然通过 T1。**历史**(单段)marker 仍属**已知设计债**:写前校验对它的 T1 违规降级为 warning(`lib/prewrite.js` 的 `isKnownMarkerCandidate` 白名单,**只适用于历史 marker**),因此这类会话**压缩(`/compact`)前需要先跑一次体检 + 清理**(`check` 定位 → `fix --remove-markers`;插件侧即「医生」),否则 T1 自检会拦住压缩。
35
+ > **关于 `/compact`(如实表述)**:本工具保证的是**新写入**不再制造 token-meter 配对债——自两段原子成对(marker + 配对段)起,新 marker 天然通过 T1。**历史**(单段)marker 仍属**已知设计债**:写前校验对它的 T1 违规降级为 warning(`lib/prewrite.js` 的 `legacyMarkerKindOf` 白名单,**只适用于历史 marker**),因此这类会话**压缩(`/compact`)前需要先跑一次体检 + 清理**(`check` 定位 → `fix --remove-markers`;插件侧即「医生」),否则 T1 自检会拦住压缩。
36
36
 
37
37
  | 层 | 是什么 | 组件 |
38
38
  |---|---|---|
39
- | **Agent 业务层(生产级保证)** | 抽象核心能力:会话卫生 / 可回溯 / 可审计 / 可恢复,与平台无关 | 四模块:治理 / / 考古 / **医生** |
39
+ | **Agent 业务层(生产级保证)** | 抽象核心能力:会话卫生 / 可回溯 / 可审计 / 可恢复,与平台无关 | 四模块:治理(governance)/ 回溯(retrospect)/ 考古(archaeology)/ **医生(doctor)** |
40
40
  | **dsh-retrace** | 业务层在 DeepSeek Harness 上的实现(生产级业务插件) | 撤回/编辑/版本/回退/看门狗 |
41
- | **dsh-log-contract** | dsh-retrace 的核心能力组件 = 业务层的**医生**(体检/修复) | check / prewrite / fix / extract / audit |
41
+ | **dsh-log-contract** | dsh-retrace 的核心能力组件 = 业务层的**医生**(体检/修复) | check / prewrite / fix / extract / audit-report |
42
42
 
43
43
  **含义**:dsh-log-contract 独立发布(供单独使用或二次开发),但它首先是
44
44
  dsh-retrace 的「日志体检与修复」能力——与 dsh-retrace 一起构成
45
- **Agent 业务层(生产级保证)** 在 DSH 上的落地(详见 [dsh-retrace 路线图](https://github.com/yamingmou/dsh-retrace/blob/main/docs/ROADMAP.md))。
45
+ **Agent 业务层(生产级保证)** 在 DSH 上的落地(详见 [dsh-retrace 仓库](https://github.com/yamingmou/dsh-retrace))。
46
46
 
47
47
  ---
48
48
 
49
49
  ## 为什么需要它
50
50
 
51
- **#3632「one log, two consumers, two verdicts」**:一条日志同时被人类与自动化程序消费,人眼容忍格式微调,程序解析依赖严格契约;格式一旦漂移,人看不出问题,程序直接崩溃或误报。
51
+ **「one log, two consumers, two verdicts」**:一条日志同时被人类与自动化程序消费,人眼容忍格式微调,程序解析依赖严格契约;格式一旦漂移,人看不出问题,程序直接崩溃或误报。
52
52
 
53
- **这里每一条规则都来自真实事故**——见下方 [⚡ 事故记录](#-事故记录)。每起事故都是一个回归夹具:损坏的会话本工具必须报出,修复后的会话必须通过。
53
+ **这里大多数规则都来自真实事故**——见下方 [⚡ 事故记录](#-事故记录)。事故派生的用例都有回归测试钉住:损坏的日志形态本工具必须报出,修复后的形态必须通过。其余规则来自对官方宿主源码的逐行核对(每条规则都带 `source`)。
54
54
 
55
55
  ---
56
56
 
@@ -71,18 +71,18 @@ dsh-retrace 的「日志体检与修复」能力——与 dsh-retrace 一起构
71
71
 
72
72
  ## ⚡ 事故记录 ——「生产级保证」不是口号
73
73
 
74
- 下面的每一条规则都来自我们工作区的一起**真实事故**。日期与形态真实,会话 id 为隐私省略。
74
+ 下面每一条都是一起来自生产会话的**真实事故**——正是这些案例促成了本工具。日期与形态真实,会话 id 与文件名省略。
75
75
 
76
76
  | # | 日期 | 发生了什么 | 产出的规则/修复 |
77
77
  |---|---|---|---|
78
78
  | 1 | 2026-08-25 | 一次「恢复被隐藏内容」的修复写了**清空 `sourceEventSeqs`** 的 replace marker → 会话加载被拒(`SessionPersistenceCorruptionError`);第二次尝试把 marker 改成 **append** → 客户端引擎崩溃。两次都是**违约写入没被拦**。 | **S5**(sourceEventSeqs 必须覆盖被替换节点)、**M1**(turn-null 的 assistant/message 只能 replace)、写前校验 |
79
79
  | 2 | 2026-08-27~28 | 中断/暂停的轮次恢复时按**过期内存光标**重放,把旧 seq 追加到文件尾(尾部回归、重复批次);两个写入者交织 → **文件物理序非单调**(`734056 → 733539 → 735470`)。会话 `seq gap` 加载失败。 | **S9**(物理序单调)、fix `--tail-renumber` |
80
80
  | 3 | 2026-08-27~28 | **fork 边界孤儿 spliced**:fork 的「移除父待处理提示词」splice 假设父会话 inbox;子会话 seed 相对重放里 inbox 为空 → `resume failed: invalid persisted inbox splice`。 | **I1**(inbox seed 相对重放)、fix `--neutralize-orphan` |
81
- | 4 | 2026-08-28 | 超限会话(**1,052,557 tokens** vs 1M 窗口)既无法继续也无法 `/compact`;裁剪预算估算对中文低估 ~3.7×。 | T1(token-meter 配对,保障可压缩)、`fix --trim` 预算指引 |
81
+ | 4 | 2026-08-28 | 超限会话(**1,052,557 tokens** vs 1M 窗口)既无法继续也无法 `/compact`;裁剪预算估算对中文低估 ~3.7×。 | T1(token-meter 配对,保障可压缩)、`fix --trim-budget` 预算指引 |
82
82
  | 5 | 2026-08-29 | **W1/W2 wire 违规**:marker 遮蔽了带 tool_calls 的 assistant 但漏盖 tool 结果 → 悬空 tool,严格端点 `INVALID_REQUEST` 拒绝请求流。 | **W1 / W2**(wire 消息流) |
83
83
  | 6 | 2026-08-30 | 单个 **turn-null marker** 让 token-meter 监听器在**每条**追加事件上抛错(`consumedEvents` 不前进 → 每事件全前缀重折)→ **30 秒 / 10,008 行日志**、host 事件循环被压垮、全部会话锁定。同会话还有**跨 step sourceEventSeqs**(step 7/8/9 混进一条 assistant 消息)——离线 check 全绿、实机 meter 崩溃。 | **T1**(turn/step 配对)、**T2**(跨 step 源引用)、`fix --neutralize`、`fix --clip-crossstep` |
84
84
 
85
- > **结论**:这个工具的每条规则都是一次真实会话留下的疤——用真实的损坏会话夹具验证过,不是合成理论。这就是这里「生产级」的含义。
85
+ > **结论**:这些规则不是合成理论——每一条都有回归测试复现它守护的那种损坏形态。这就是这里「生产级」的含义。
86
86
 
87
87
  ---
88
88
 
@@ -92,7 +92,7 @@ dsh-retrace 的「日志体检与修复」能力——与 dsh-retrace 一起构
92
92
 
93
93
  | 项 | 基线 | 说明 |
94
94
  |---|---|---|
95
- | 宿主包 | `@deepseek-ai/dsh-session@0.1.5-rc.1` | 开发机与 CI 实装版本;`prepublishOnly` 四步在此版本上全绿 |
95
+ | 宿主包 | `@deepseek-ai/dsh-session@0.1.5-rc.1` | 本规则集开发与 CI 验证所用的版本(CI 跑 `pnpm check` + `pnpm test`;`prepublishOnly` 四步在本机该版本上全绿) |
96
96
  | 会话格式 | **v3**(`SESSION_FORMAT_VERSION = 3`) | v3 走运行时词表 + 官方 `foldSurface` 终验 |
97
97
  | 声明范围 | `peerDependencies: ^0.1.0-rc.7 || ^0.1.5-rc.1` | 允许安装;**范围内不等于逐个验证过** |
98
98
  | 已知格式 | 0 / 1 / 2 / 3 | 0–2 由本包**自带** vendored 词表 + 本地等价折叠(`legacyFoldSurface`)支持,与宿主版本无关 |
@@ -126,8 +126,8 @@ pnpm dlx dsh-log-contract --help
126
126
  > 声明为依赖,装 retrace 时自动带好契约守护(体检/写前校验/修复原语全部随插件生效)。
127
127
  > 本包独立发布,供愿意单独使用或二次开发的用户直接引入。
128
128
  >
129
- > **从 GitHub 下载了 ZIP?** 解压后 `cd dsh-log-contract && npm install && npm run build`,
130
- > 然后 `node bin/dsh-log-contract.mjs check <session-log>` 即可使用(无需全局安装)。
129
+ > **从 GitHub 下载了 ZIP?** 解压后 `cd dsh-log-contract && npm install`,
130
+ > 然后 `node bin/dsh-log-contract.mjs check <session-log>` 即可使用(无需构建、无需全局安装)。
131
131
 
132
132
  依赖:Node ≥ 22(`node:zlib` 内置 zstd)、`@deepseek-ai/dsh-session`(peer,校验/解码复用官方实现,保证与 Harness 读路径同源)。
133
133
 
@@ -216,7 +216,7 @@ DSH 会话日志持久化了每次工具调用的完整输入输出——数据
216
216
 
217
217
  ```sh
218
218
  # 按命令正则导出工具输出(保留原始文本)
219
- dsh-log-contract extract <session-log> --pattern "seed-scale" --min-size 50 --out ./found
219
+ dsh-log-contract extract <session-log> --pattern "build-report" --min-size 50 --out ./found
220
220
 
221
221
  # 考古审计报告:调用数 / 配对率 / 孤儿数 / 命令分布
222
222
  dsh-log-contract audit-report <session-log>
@@ -265,19 +265,18 @@ pnpm check && pnpm test # 语法检查 + 契约文档漂移闸 + 全部单测
265
265
  > **发布包范围**:只含运行时代码与文档。维护者本地工具不在公开仓内,
266
266
  > 因此 `pnpm check` / `pnpm test` 在干净的克隆里即可通过。
267
267
 
268
- 已知真值表(2026-08-31 实测更新——0.3.5 加 I1 后,`spliced-orphan` 已可报错,旧表 PASS 过时):
268
+ 按**日志形态**给出的判定——本工具对每类缺陷该说什么,而不是对某个具体文件:
269
269
 
270
- | 化石 | 判定 | 违规 |
270
+ | 日志形态 | 判定 | 触发的规则 |
271
271
  |---|---|---|
272
- | `<session>-corrupt` | FAIL | S8/C1/T1/E2(seq 缺口 加载被拒) |
273
- | `<session>-seqgap` / `<session>-recorrupt` | FAIL | S8/C1/T1/E2/S9(seq 缺口/倒退) |
274
- | `<session>-rewritten-230542` | FAIL | S8/C1/T1/E2/I1(重写引入缺口) |
275
- | `<session>-spliced-orphan` | **FAIL(0.3.5+)** | T1/I1(inbox splice 无效 + turn-null)——旧表 PASS 已过时 |
276
- | `<session>-pre-markerfix-20260825` | FAIL | T1×5(修复前样本:turn-null marker 残留,非「修复后」) |
277
- | `<session>-pre-cleanstate-20260831` | **PASS** | error 0(数千跨度的 replace marker 数据合规,官方 foldSurface 重放通过) |
272
+ | seq 缺口,或文件物理序非单调 | FAIL | S8 / C1 / T1 / E2(seq 倒退时另加 S9) |
273
+ | 重写引入缺口 | FAIL | S8 / C1 / T1 / E2 / I1 |
274
+ | inbox splice 无效 + turn-null marker | FAIL | T1 / I1 |
275
+ | 修复前形态:遗留 turn-null marker | FAIL | T1 |
276
+ | 完整覆盖的数千跨度 replace marker | PASS | error 0——数据合规,官方 `foldSurface` 重放通过 |
278
277
 
279
- > 说明:真值表随规则演进更新(0.3.5 新增 I1 spliced-orphan PASS 变 FAIL);
280
- > 「修复后会话 PASS」需用重建/修复后的样本验证,pre- 前缀备份多为修复前坏样本。
278
+ > 规则演进会移动表中行:0.3.5 新增 I1 后,「inbox splice 无效」这一形态从 PASS 变 FAIL
279
+ > 而「修复后会话通过」只对被修复后的产物成立——同一会话修复前的形态通常仍是 FAIL。
281
280
 
282
281
  ---
283
282
 
@@ -286,6 +285,7 @@ pnpm check && pnpm test # 语法检查 + 契约文档漂移闸 + 全部单测
286
285
  - [x] **Phase 1(0.1.0)**:CLI 离线体检 + 写前校验 + 契约目录
287
286
  - [x] **Phase 1.5(0.2.0)**:`fix` 子命令(严格 seq 扫描 + W1/W2 wire 检查 + 移除 marker 重编号 + 官方帧格式重建);CI 集成(`dsh-log-contract check` 作为 Harness 会话目录的定时守护)
288
287
  - [x] **0.3.x(2026-08-30 事故固化)**:T1 token-meter 配对 → 0.3.1 W1/W2 折叠位置修复 → 0.3.2 `tailSeq` → 0.3.3 `fix --neutralize`(turn-null marker 原地中和)→ 0.3.4 `fix --clip-crossstep`(跨 step 引用裁剪)→ 0.3.5 **T2/S9/I1 规则**(跨 step 源引用 / 物理序单调 / inbox 重放)
288
+ - [x] **0.3.6 → 0.3.15**:按被检文件版本择路的 v0–v3 规则(T3/T4/T5、L3–L5、E8/E9/E10)、宿主能力与迁移预检的退出码 `3`/`4`,以及如实报告"哪些版本**未**验证"的版本支持说明(详见 [CHANGELOG.md](CHANGELOG.md))
289
289
  - [ ] Phase 2:运行时守护(订阅 session append 事件流实时校验,断裂即标记 `dsh/contract-violation` 事件,策略可配 告警/拦截)——DSH 插件形态
290
290
  - [ ] Phase 3:与 dsh-turn-guard / dsh-retrace 时间线联动
291
291
 
@@ -295,11 +295,13 @@ MIT © OfferKuai Team
295
295
 
296
296
  ---
297
297
 
298
- ## ⚠️ 升级到 0.3.15 —— 必须知道的行为变更(0.3.12 → 0.3.15
298
+ ## ⚠️ 升级到 0.3.16 —— 必须知道的行为变更(0.3.12 → 0.3.16
299
299
 
300
- > **0.3.15 本身不含行为变更。** 它只做两件事:清掉 0.3.14 已发布产物里的两处内部标识
301
- > (README 的定名说明 + 一处源码注释),并补齐发布前泄漏闸门的词表。
302
- > 0.3.12 升到 0.3.15 时,下面各条与原来完全一致。
300
+ > **0.3.16 不含行为变更。** 它只重写了注释、测试标题、少量诊断文案与 README,让它们
301
+ > 只描述产品与契约本身。所有判定结果、退出码与 `--json` 字段与 0.3.15 完全一致,
302
+ > 因此从 0.3.12 升到 0.3.16 时,下面各条与原来完全一致。
303
+ >
304
+ > 0.3.15 同样只是文档层面的发布。
303
305
 
304
306
  **1. 新增退出码 `3` / `4`:本质是缺陷修复,形式上也是破坏性变更。**
305
307
  - `3` = **不可在本宿主评估**:被检文件的 `header.version` 高于当前宿主支持的上限
@@ -16,9 +16,9 @@ import fs from 'node:fs';
16
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
- * 同步写 fd(F4 修复)——`process.stdout.write` 在**管道**下是异步的,紧跟着的
19
+ * 同步写 fd——`process.stdout.write` 在**管道**下是异步的,紧跟着的
20
20
  * `process.exit()` 不会等待 flush:>~64KiB 的输出会丢尾,`--json` 因此变成非法 JSON
21
- * (独立审核在 3000 事件夹具与真实 14.5MB v3 会话上复现,stdout 56648B 处 "Unterminated string")。
21
+ * (实测在 3000 事件夹具与真实 14.5MB v3 会话上复现,stdout 56648B 处 "Unterminated string")。
22
22
  * 这里直接同步写 fd 1/2(非阻塞管道 EAGAIN 时自旋重试),做到"先写完再 exit"。
23
23
  */
24
24
  function writeAllSync(fd, text) {
@@ -49,7 +49,7 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
49
49
  --resume 输出分层结论:可加载 / 可继续 / 可压缩(本工具规则集内)
50
50
  + 迁移预检(官方 v0/v1/v2→当前格式会不会拒);--json 时附带。
51
51
  --fail-on-migration migration.ready===false(工具判"官方升级会拒")时退出码 4。
52
- **注意:默认已经是这个行为**(第四轮把"报告与退出码不一致"的假阴性修掉了);
52
+ **注意:默认已经是这个行为**(已把"报告与退出码不一致"的假阴性修掉);
53
53
  本开关是**显式别名**,便于 CI 把意图写进命令行。
54
54
  --no-fail-on-migration **真 opt-out**:恢复 0.3.11 的行为——迁移 blocked 不再改退出码
55
55
  (结构绿即 exit 0)。两者同时给时 **opt-out 优先**。若你的流水线依赖旧退出码,
@@ -74,7 +74,7 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
74
74
  ignorable:true,删 surfaceOp/sourceEventSeqs,seq/行数不变)
75
75
  —— token-meter 不再刷屏,会话驻留也安全(2026-08-30 事故)
76
76
  --neutralize-legacy-markers
77
- **一次性根治历史载体债**(R-C):只中和
77
+ **一次性根治历史载体债**:只中和
78
78
  assistant/message + data.editor 形态的历史 retrace marker
79
79
  (新载体是 user/message + data.id,不在此列)。清理后,
80
80
  写前校验不再需要"≤v2 历史 marker T1 降级"那条豁免。
@@ -138,7 +138,7 @@ function printViolations(violations, maxDetails = 8) {
138
138
  }
139
139
  }
140
140
 
141
- /** R-E 抬头(2026-09-14 裁定):漂移清单 + 未复核清单 + "勿据此跑 fix --apply"。 */
141
+ /** 漂移抬头(2026-09-14):漂移清单 + 未复核清单 + "勿据此跑 fix --apply"。 */
142
142
  function driftLines(drift, probes) {
143
143
  if (!drift) return '';
144
144
  const host = probes ? `${probes.hostPackage}(SESSION_FORMAT_VERSION=${probes.sessionFormatVersion},词表 ${probes.knownTypes} 类)` : '未知';
@@ -150,8 +150,8 @@ function driftLines(drift, probes) {
150
150
  if (drift.unverifiedRules.length > 0) {
151
151
  out += ` ⛔ **UNVERIFIED**(探针与规则假设不一致,结论不可采信):${drift.unverifiedRules.join('、')}\n`;
152
152
  }
153
- out += ` 出处未复核(R-F,已知债,仅告警):${drift.driftedSources.join('、')}\n`;
154
- out += ` 判定前提存疑(复核 §1,仅告警):${drift.premiseStale.join('、')}${drift.undecidable.length ? `;无法判定:${drift.undecidable.join('、')}` : ''}\n`;
153
+ out += ` 出处未复核(已知债,仅告警):${drift.driftedSources.join('、')}\n`;
154
+ out += ` 判定前提存疑(仅告警):${drift.premiseStale.join('、')}${drift.undecidable.length ? `;无法判定:${drift.undecidable.join('、')}` : ''}\n`;
155
155
  if (drift.fixApplyBlocked) {
156
156
  out += ' ⛔ **禁止据此跑 `fix --apply`**(存在未验证规则:写入类动作不可逆)——先按 report 复核这些规则或显式确认。\n';
157
157
  }
@@ -186,7 +186,7 @@ function notAssessableBanner(result) {
186
186
  /** 迁移预检(独立维度)文本。
187
187
  * @param {object} mig `migrationVerdict()` 结果
188
188
  * @param {boolean} partial 待迁移文件(`migration.applies===true`)⇒ 迁移维度只部分覆盖,
189
- * 不得给无条件绿(2026-09-14 最终复核规格 (i)+(iii))。
189
+ * 不得给无条件绿(2026-09-14 规格 (i)+(iii))。
190
190
  */
191
191
  function migrationLine(mig, partial = false) {
192
192
  if (!mig) return '';
@@ -213,7 +213,7 @@ function migrationLine(mig, partial = false) {
213
213
  function cmdCheck(args) {
214
214
  const json = args.includes('--json');
215
215
  const resume = args.includes('--resume');
216
- // 第四轮/第五轮:`--fail-on-migration` 是**默认已生效行为**的显式别名;
216
+ // `--fail-on-migration` 是**默认已生效行为**的显式别名;
217
217
  // `--no-fail-on-migration` 是**真 opt-out**(恢复旧行为:迁移 blocked 不再改退出码)。
218
218
  // 两者同时给 ⇒ opt-out 优先(help/README 写明)。
219
219
  const failOnMigration = args.includes('--fail-on-migration');
@@ -233,11 +233,11 @@ function cmdCheck(args) {
233
233
  const { summary, violations, ok } = result;
234
234
  const notAssessable = result.assessable === false;
235
235
  const migration = result.migration ?? migrationVerdict(result);
236
- // 第四轮:机器可判的评估范围(partial = 待迁移文件,迁移维度只部分覆盖)。
236
+ // 机器可判的评估范围(partial = 待迁移文件,迁移维度只部分覆盖)。
237
237
  const scope = result.assessmentScope ?? assessmentScope(result);
238
238
  const partial = scope === 'partial';
239
239
  const migBlocked = migration?.applies === true && migration.ready === false;
240
- // 默认:`ready===false` ⇒ 非 0(第四轮修复的假阴性)。
240
+ // 默认:`ready===false` ⇒ 非 0(修掉的假阴性)。
241
241
  // `--fail-on-migration` = 显式别名(同默认);`--no-fail-on-migration` = 真 opt-out(恢复旧行为)。
242
242
  const migGate = !noFailOnMigration && (migBlocked || (failOnMigration && migration?.ready === false));
243
243
  // 退出码:3 不可评估 > 1 结构 error > 4 迁移 blocked > 0。
@@ -363,7 +363,7 @@ function cmdPrewrite(args) {
363
363
 
364
364
  out(`\n✍️ dsh-log-contract prewrite —— ${file}(op: ${result.op},nextSeq: ${prewriter.nextSeq})\n\n`);
365
365
  if (result.legacyMarkerDebt) {
366
- // R-C「降级可见」:历史 marker 债不只是 warning 里的一句话,这里显式打印并给出根治命令。
366
+ // 「降级可见」:历史 marker 债不只是 warning 里的一句话,这里显式打印并给出根治命令。
367
367
  const d = result.legacyMarkerDebt;
368
368
  out(` ⚠️ 检测到**历史 marker 载体**(${d.kind} 形态,id=${d.id},targetSeq=${d.targetSeq},seq=${d.seq}):\n`);
369
369
  out(` T1 已按 ≤v${d.formatVersion <= 2 ? 2 : d.formatVersion} 白名单降级为 warning(仅历史载体、仅旧格式文件)——\n`);
@@ -382,7 +382,7 @@ function cmdPrewrite(args) {
382
382
  }
383
383
 
384
384
  function cmdContracts() {
385
- out('dsh-log-contract 契约规则目录(spec:59 条审计发现 + 官方源码逐行核对)\n\n');
385
+ out('dsh-log-contract 契约规则目录(spec:官方源码逐行核对 + 实测事故固化)\n\n');
386
386
  for (const r of CONTRACT_RULES) {
387
387
  const flags = [
388
388
  r.candidate ? '候选规则' : null,
@@ -397,7 +397,7 @@ function cmdFix(args) {
397
397
  const json = args.includes('--json');
398
398
  const removeMarkers = args.includes('--remove-markers');
399
399
  const neutralize = args.includes('--neutralize');
400
- // R-C 一次性根治路径:只中和**历史载体**(assistant/message + data.editor)的 retrace marker
400
+ // 一次性根治路径:只中和**历史载体**(assistant/message + data.editor)的 retrace marker
401
401
  const neutralizeLegacyMarkers = args.includes('--neutralize-legacy-markers');
402
402
  const clipCrossStep = args.includes('--clip-crossstep');
403
403
  const dropFailedTurns = args.includes('--drop-failed-turns');
@@ -423,7 +423,7 @@ function cmdFix(args) {
423
423
  const file = args.find((a) => !a.startsWith('-'));
424
424
  if (!file) fail(USAGE);
425
425
 
426
- // S2 安全闸:本宿主评估不了的文件**不许**修。危害路径(独立复核指出):rc.7 上把健康 v3
426
+ // S2 安全闸:本宿主评估不了的文件**不许**修。危害路径(实测指出):rc.7 上把健康 v3
427
427
  // 日志报成 broken → 用户以为日志坏了去跑 `fix --apply` ⇒ 在健康日志上动手。这里直接拒绝。
428
428
  const head = readSessionHeader(file);
429
429
  const fileVersion = Number.isSafeInteger(head?.version) ? head.version : 0;
package/docs/CONTRACTS.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  > **自动生成**(2026-09-06 起):本文件由 `node scripts/gen-contracts-doc.mjs`
4
4
  > 从 `lib/contracts.js` 的 `CONTRACT_RULES` 注册表生成——**勿手改**,规则只增不减,
5
- > 新增规则后跑一次生成即同步(此前手工维护滞后 15+ 条,外部审计指出)。
5
+ > 新增规则后跑一次生成即同步(此前手工维护滞后 15+ 条)。
6
6
  > **漂移闸**:`pnpm check` 会跑 `--check` 逐字节比对,改了注册表没重生成 ⇒ 直接红。
7
7
  >
8
8
  > DSH 会话日志契约的**可执行 spec**。每条规则在 `lib/checks.js`(逐事件判定)
9
9
  > 与 `lib/prewrite.js`(写前校验)中有对应实现;离线体检(`lib/validate.js`)
10
10
  > 逐条执行并在最后用官方 `foldSurface` 终验(S8)。
11
11
  >
12
- > 规则来源:早期内部审计发现(59 条)+ 三层契约事故复盘 + 官方源码逐行核对
12
+ > 规则来源:历史会话日志的实测事故与契约缺口(持久化/客户端引擎/插件语义三层)+ 官方源码逐行核对
13
13
  > (`@deepseek-ai/dsh-session`,各条出处见下)。后续规则随官方版本演进追加:
14
14
  > T3/T4 = 渲染层白屏事故复盘,T5 = malformed turn/end 事故复盘。
15
15
  >
@@ -88,7 +88,7 @@
88
88
  ### R1 — 每行必须是合法 JSON
89
89
 
90
90
  - **层级**: persistence | **严重度**: error
91
- - **出处**: 审计方法论(scan-seq-gaps.mjs);dsh-session-persistence-jsonl 读路径
91
+ - **出处**: seq 缺口扫描;dsh-session-persistence-jsonl 读路径
92
92
  - **契约**: 非空行无法 JSON.parse = 损坏行。帧边界产生的空行是合法的(跳过)。
93
93
 
94
94
  ### R2 — chunk 行必须满足精确信封形状
@@ -112,7 +112,7 @@
112
112
  ### E2 — seq 严格连续(单写入者假设)
113
113
 
114
114
  - **层级**: persistence | **严重度**: error
115
- - **出处**: @deepseek-ai/dsh-session lib/index.js:398 (planSurfaceEvent "not contiguous");审计 S2/N6
115
+ - **出处**: @deepseek-ai/dsh-session lib/index.js:398 (planSurfaceEvent "not contiguous")
116
116
  - **契约**: seq 必须从 0(或窗口 baseSeq)严格连续递增。缺口/倒退 = 违反单写入者假设(多实例共享存储并发写的痕迹),加载时直接 throw。
117
117
 
118
118
  ### S9 — 文件物理序 seq 单调(多写入者交织现场特征)
@@ -143,7 +143,7 @@
143
143
 
144
144
  - **层级**: persistence | **严重度**: error
145
145
  - **出处**: @deepseek-ai/dsh-session lib/index.js:1273-1277 (assertSupportedRequestHeader)
146
- - **契约**: 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),不是写入即被拒。
146
+ - **契约**: request/header-delta 与 reason=fallback 的 request/header 是已删除的遗留格式。注意(订正 E5):宿主 Session.append/appendLines 不看 type ⇒ 写入会成功、下一次读取才炸(依据 dsh-session@0.1.5-rc.1 lib/index.js:1170-1210 / persistence-jsonl:3046-3073),不是写入即被拒。
147
147
 
148
148
  ### E6 — 消息类事件消息形状
149
149
 
@@ -178,8 +178,8 @@
178
178
  ### S5 — replace 的 sourceEventSeqs 必须完整覆盖被替换节点
179
179
 
180
180
  - **层级**: persistence | **严重度**: error
181
- - **出处**: @deepseek-ai/dsh-session lib/index.js:335-336 (assertProvenance);复盘事故第 1
182
- - **契约**: ★ 写前校验核心规则:sourceEventSeqs 必须包含每一个被替换(shadowed)的 surface 节点,缺一个 = 会话加载被拒(SessionPersistenceCorruptionError)。2026-08-25 事故第 1 轮(清空 sourceEventSeqs)正是违反此规则。
181
+ - **出处**: @deepseek-ai/dsh-session lib/index.js:335-336 (assertProvenance);复盘事故第 1 次尝试
182
+ - **契约**: ★ 写前校验核心规则:sourceEventSeqs 必须包含每一个被替换(shadowed)的 surface 节点,缺一个 = 会话加载被拒(SessionPersistenceCorruptionError)。2026-08-25 事故第 1 次尝试(清空 sourceEventSeqs)正是违反此规则。
183
183
 
184
184
  ### S6 — sourceEventSeqs 自身约束
185
185
 
@@ -232,31 +232,31 @@
232
232
  ### E7 — ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者)
233
233
 
234
234
  - **层级**: persistence | **严重度**: warning
235
- - **出处**: 对抗性复查 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)
235
+ - **出处**: 实测 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)
236
236
  - **契约**: 未知 type + ignorable:true 被读路径接纳但无人消费 = 静默垃圾。排除已知消费者白名单(retrace/marker、retrace/goal-marker、message-editor/ 前缀等 retrace 客户端消费的插件 marker)后,其余 ignorable 未知事件报 warning。
237
237
 
238
238
  ### E8 — 事件信封键白名单(多余键:seed/restore 路径会拒)
239
239
 
240
240
  - **层级**: persistence | **严重度**: warning
241
241
  - **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:849-861(assertSessionEventEnvelope)+ :1063-1068(唯一调用点=seed 路径);load 路径容忍见行为探针 p5
242
- - **契约**: 事件对象只允许 7 个信封键(type/seq/time/data/surfaceOp/sourceEventSeqs/ignorable)。独立复核变异 05 指出"宿主拒、旧契约 0 违规";R-D 行为探针进一步订正口径:**load 路径容忍、seed/restore 路径拒** ⇒ warning。
242
+ - **契约**: 事件对象只允许 7 个信封键(type/seq/time/data/surfaceOp/sourceEventSeqs/ignorable)。实测反例指出"宿主拒、旧契约 0 违规";行为探针进一步订正口径:**load 路径容忍、seed/restore 路径拒** ⇒ warning。
243
243
 
244
244
  ### E9 — system/message 必须带 plugin source
245
245
 
246
246
  - **层级**: persistence | **严重度**: error
247
247
  - **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:942-944("must have plugin source");角色表 :917-926
248
- - **契约**: v3 新增的 system/message:role 必须为 system,source.kind 必须为 plugin 且 plugin 非空。实测(变异 03):source.kind='user' 宿主拒、旧契约 0 违规 ⇒ 漏检。
248
+ - **契约**: v3 新增的 system/message:role 必须为 system,source.kind 必须为 plugin 且 plugin 非空。实测(反例):source.kind='user' 宿主拒、旧契约 0 违规 ⇒ 漏检。
249
249
 
250
250
  ### E10 — request/header 的 data.header 字段约束
251
251
 
252
252
  - **层级**: persistence | **严重度**: error
253
253
  - **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:231-248(validateSessionEventData:omit header.system / omit empty tools / omit empty adapterDefaults)
254
- - **契约**: request/header 必须省略 header.system(系统提示改走 system/message)、空 tools、空 adapterDefaults。实测(变异 12):带 header.system 的写入宿主拒、旧契约 0 违规 ⇒ 漏检。
254
+ - **契约**: request/header 必须省略 header.system(系统提示改走 system/message)、空 tools、空 adapterDefaults。实测(反例):带 header.system 的写入宿主拒、旧契约 0 违规 ⇒ 漏检。
255
255
 
256
256
  ### Z3 — 空会话文件(有 header 无事件)显式报出
257
257
 
258
258
  - **层级**: framing | **严重度**: warning
259
- - **出处**: 对抗性复查 2026-09-09 T3(36 条规则全来自有内容事故,空态无覆盖)
259
+ - **出处**: 实测 2026-09-09 T3(36 条规则全来自有内容事故,空态无覆盖)
260
260
  - **契约**: 有 header 但零事件 = 异常空会话(新建即空或写入未落盘)。空态不在任何有内容规则的覆盖下,显式 warning 供人判断。
261
261
 
262
262
  ### P3 — tool/call ↔ tool/result 配对完整性(考古 B1)
@@ -274,7 +274,7 @@
274
274
  ### M1 — turn/step 为 null 的 assistant/message 只能 replace,不能 append
275
275
 
276
276
  - **层级**: engine | **严重度**: error
277
- - **出处**: 复盘事故第 2 轮(rt.js:6816 崩溃);实证 data.turn/data.step:正常消息为数字、插件 marker 为 null
277
+ - **出处**: 复盘事故第 2 次尝试(rt.js:6816 崩溃);实证 data.turn/data.step:正常消息为数字、插件 marker 为 null
278
278
  - **契约**: data.turn/data.step 为 null 的 assistant/message(如插件 marker)只能以 replace 承载(走插件 marker 定义);作为 append 会落进核心 assistant-step 定义,因 turn=null 发布 location data 导致客户端引擎崩溃。
279
279
 
280
280
  ### P1 — marker id 前缀必须被识别
@@ -292,19 +292,19 @@
292
292
  ### C1 — seq 缺口/倒退提示多写入者
293
293
 
294
294
  - **层级**: concurrency | **严重度**: warning
295
- - **出处**: 审计 N6:dsh-session-persistence-jsonl appendLines 无锁(:1200-1227),全仓无会话级排他锁
295
+ - **出处**: dsh-session-persistence-jsonl appendLines 无锁(:1200-1227),全仓无会话级排他锁
296
296
  - **契约**: 离线体检无法直接观测跨进程竞态,但 E2 暴露的缺口/倒退即是"≥2 个 Host 进程共享同一 session 目录并发写"的后果。单实例部署不触发。
297
297
 
298
298
  ### Z1 — zstd 尾帧撕裂
299
299
 
300
300
  - **层级**: framing | **严重度**: warning
301
- - **出处**: 审计 N5 相关;帧扫描方法论
301
+ - **出处**: zstd 帧布局(dsh-session-persistence-jsonl 多帧)
302
302
  - **契约**: 尾帧不完整(torn):可能正在写入(in-flight)或文件被截断。若这是唯一异常,通常可等待写入完成;若持续存在则是截断证据。
303
303
 
304
304
  ### Z2 — zstd 帧解码失败 = 单帧全损
305
305
 
306
306
  - **层级**: framing | **严重度**: error
307
- - **出处**: 审计 N5:多帧单帧全损 → 整会话不可读
307
+ - **出处**: 多帧单帧全损 → 整会话不可读
308
308
  - **契约**: 任一帧解码失败(磁盘 bitrot / 传输截断 / 并发写撕裂)即整会话不可读;帧越多,单帧损坏下丢失概率线性上升。
309
309
 
310
310
  ### W1 — wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后
package/lib/checks.js CHANGED
@@ -70,10 +70,10 @@ export function envelopeViolations(event, loc, version) {
70
70
  if (event.type === 'request/header' && event.data?.reason === 'fallback') {
71
71
  out.push(violation('E5', loc, 'request/header 使用已删除的遗留 reason "fallback"'));
72
72
  }
73
- // ── R-G(2026-09-14 独立复核:3 例确证漏检补成候选规则)────────────────────
73
+ // ── 2026-09-14 实测:3 例确证漏检补成候选规则 ───────────────────────
74
74
  // E8 信封键白名单:宿主 `assertSessionEventEnvelope`(@deepseek-ai/dsh-session@0.1.5-rc.1
75
75
  // lib/index.js:852-861)逐键 switch,只认 7 个键,其余一律 `invalid event envelope`。
76
- // 实测变异 05:宿主拒、旧契约 0 违规。
76
+ // 实测反例:宿主拒、旧契约 0 违规。
77
77
  if (typeof event === 'object' && event !== null) {
78
78
  const extra = Object.keys(event).filter((k) => !ENVELOPE_ALLOWED_KEYS.has(k));
79
79
  if (extra.length > 0) {
@@ -82,7 +82,7 @@ export function envelopeViolations(event, loc, version) {
82
82
  }
83
83
  // E10 request/header 的 data 字段约束:宿主 `validateSessionEventData`
84
84
  // (dsh-session@0.1.5-rc.1 lib/index.js:231-248)——`header.system` 必须省略、
85
- // 空 tools / 空 adapterDefaults 必须省略。实测变异 12:宿主拒、旧契约 0 违规。
85
+ // 空 tools / 空 adapterDefaults 必须省略。实测反例:宿主拒、旧契约 0 违规。
86
86
  if (event.type === 'request/header') {
87
87
  const header = event.data?.header;
88
88
  if (typeof header !== 'object' || header === null || Array.isArray(header)) {
@@ -106,9 +106,9 @@ export const ENVELOPE_ALLOWED_KEYS = new Set(['type', 'seq', 'time', 'data', 'su
106
106
  /** 镜像官方 assertMessageEventShape(lib/index.js:1242-1266)。 */
107
107
  export function messageShapeViolations(event, loc) {
108
108
  const type = event.type;
109
- // R-G:把 v3 新增的 `system/message` 纳入形状检查(MESSAGE_ROLE_BY_TYPE:system/message → system,
109
+ // v3 新增的 `system/message` 纳入形状检查(MESSAGE_ROLE_BY_TYPE:system/message → system,
110
110
  // dsh-session@0.1.5-rc.1 lib/index.js:917-926;source 要求 :942-944)。旧实现只查 user/assistant/tool
111
- // ⇒ 实测变异 03(system/message 的 source.kind='user')宿主拒、旧契约 0 违规。
111
+ // ⇒ 实测反例(system/message 的 source.kind='user')宿主拒、旧契约 0 违规。
112
112
  if (type !== 'user/message' && type !== 'assistant/message' && type !== 'tool/result' && type !== 'system/message') return [];
113
113
  const out = [];
114
114
  const data = event.data;
@@ -139,7 +139,7 @@ export function messageShapeViolations(event, loc) {
139
139
  }
140
140
  }
141
141
  if (type === 'system/message') {
142
- // E9(R-G 候选规则):system/message 必须 plugin source(kind==='plugin' + plugin 非空)。
142
+ // E9(候选规则):system/message 必须 plugin source(kind==='plugin' + plugin 非空)。
143
143
  // 宿主依据:dsh-session@0.1.5-rc.1 lib/index.js:942-944("must have plugin source")。
144
144
  if (source?.kind !== 'plugin' || typeof source.plugin !== 'string' || source.plugin === '') {
145
145
  out.push(violation('E9', loc, `${shape()}:system/message 必须带 plugin source(kind==='plugin' 且 plugin 非空)——宿主 lib/index.js:942-944`));
@@ -241,13 +241,13 @@ export function replaySurface(events, version) {
241
241
 
242
242
  if (!eligible) {
243
243
  if (op !== undefined || src !== undefined) {
244
- // ── R-B(2026-09-14 独立复核,误报 S2)────────────────────────────────
244
+ // ── 2026-09-14 实测:这里的 S2 是误报 ───────────────────────────────
245
245
  // 宿主 `@deepseek-ai/dsh-session@0.1.5-rc.1` 是**刻意容忍**的:
246
246
  // lib/index.js:270 `if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable === true) return;`
247
247
  // 同一函数的契约注释 lib/index.js:305 —— "Unknown ignorable records retain opaque
248
248
  // metadata and never change the surface."
249
249
  // ⇒ **未知**类型且 `ignorable===true` 的事件带 surfaceOp/sourceEventSeqs 是合法的不透明
250
- // 元数据(宿主收;实测变异 06:宿主收、旧契约 S2/error)。这里不再报 S2。
250
+ // 元数据(宿主收;实测反例:宿主收、旧契约 S2/error)。这里不再报 S2。
251
251
  // "该未知类型有没有消费者"由 E7 以 **warning** 表达(策略层,非宿主契约)。
252
252
  const unknownIgnorable = !currentVocabulary(version).has(event.type) && event.ignorable === true;
253
253
  if (!unknownIgnorable) {
@@ -343,7 +343,7 @@ export function engineViolations(event, loc) {
343
343
  const out = [];
344
344
  if (event.type === 'assistant/message' && event.surfaceOp === 'append') {
345
345
  // turn/step 位于 event.data 层(实证:正常消息 data.turn/data.step 为数字,
346
- // 插件 marker data.turn/data.step 为 null —— 复盘事故第 2 轮)
346
+ // 插件 marker data.turn/data.step 为 null —— 复盘事故第 2 次尝试)
347
347
  const turn = event.data?.turn;
348
348
  const step = event.data?.step;
349
349
  if (turn == null || step == null) {
@@ -370,7 +370,7 @@ export function pluginViolations(event, loc) {
370
370
  return out;
371
371
  }
372
372
 
373
- /** 折叠终验:**按文件版本**择路(1.3 第二半 + 2026-09-14 复核补强)。
373
+ /** 折叠终验:**按文件版本**择路(2026-09-14 补强)。
374
374
  *
375
375
  * - v3 文件 → 运行时导出的官方 `foldSurface`(与运行时同语义);
376
376
  * - v0/v1/v2 文件 → 本地 `legacyFoldSurface`(rc.7 `foldSurface` 逐条移植)。
@@ -847,10 +847,10 @@ export function wireViolations(events, version) {
847
847
  * `turn/start`(闭合/预期轮)、继承切点、`assistant/attempt` 配对、`stored log corrupt`、
848
848
  * v0→v1 对其余事件的形状拒绝。
849
849
  *
850
- * **为什么 `turn/start` 不做(第五轮实测结论)**:官方那个状态机(`v0-to-v1:2270` 的
850
+ * **为什么 `turn/start` 不做(实测结论)**:官方那个状态机(`v0-to-v1:2270` 的
851
851
  * `assertReleasedArtifactRelationships`)**不是**在原始 v0 事件上跑的——它由 **v1→v2**
852
852
  * 以 `RELEASED_V2_RELATIONSHIP_EXTENSIONS` 调用在**变换后的 v1/v2 artifact** 上
853
- * (`v1-to-v2/lib/index.js:104`),并带 `cut`(继承切点)处理。第五轮实测:在原始 v0 上照抄该
853
+ * (`v1-to-v2/lib/index.js:104`),并带 `cut`(继承切点)处理。实测:在原始 v0 上照抄该
854
854
  * 状态机会在**已 seed 的会话**上狂报(样本(某真实会话):v0→v1 官方并不以该规则拒绝,
855
855
  * 而原始 v0 上会报 19 条),属"规则文本对、应用对象错"。要忠实复现必须先把 v0→v1→v2 的
856
856
  * 变换做出来 ⇒ 记未覆盖。
@@ -884,12 +884,12 @@ export function migrationPrecheckViolations(events, version) {
884
884
  // 判据:每个被引 seq 必须解析到更早的 `user/message`,且 `data.source.kind === 'user'`;
885
885
  // 另 `session/title` 的 `source.kind === 'user'` ⟺ `messageSeqs` 空。
886
886
  //
887
- // ⚠️ 应用面说明(第五轮实测,2026-09-15):官方 `assertTitleSources` 与 `turn/start` 状态机
887
+ // ⚠️ 应用面说明(实测 2026-09-15):官方 `assertTitleSources` 与 `turn/start` 状态机
888
888
  // 同在 `assertReleasedArtifactRelationships` 里,而该函数被 **v1→v2** 以
889
889
  // `RELEASED_V2_RELATIONSHIP_EXTENSIONS` 调用在**变换后的 v1/v2 artifact** 上
890
890
  // (`dsh-session-format-v1-to-v2/lib/index.js:104`),不是原始 v0 事件。
891
891
  // 对 `messageSeqs` 这类"按 seq 索引 + 事件类型/source 判定"的引用,变换保序保类型
892
- // ⇒ 在原始 v0 上判是**必要条件的近似**;实测 281 个真实 v0 上 **0 误报**(round5/rule-probe)。
892
+ // ⇒ 在原始 v0 上判是**必要条件的近似**;实测 281 个真实 v0 上 **0 误报**。
893
893
  const bySeq = new Map();
894
894
  for (const { event } of events) if (event && typeof event === 'object') bySeq.set(event.seq, event);
895
895
  for (const { event, lineNo } of events) {
package/lib/contracts.js CHANGED
@@ -4,8 +4,7 @@
4
4
  * DSH 会话日志契约规则目录(spec)。
5
5
  *
6
6
  * 规则集来源:
7
- * - 早期契约审计发现(59 / F1–F7 / N1–N6 / R1–R3)
8
- * - 会话修复事故复盘(三层契约:持久化 / 客户端引擎 / 插件语义)
7
+ * - 历史会话日志的实测事故与契约缺口(持久化 / 客户端引擎 / 插件语义三层)
9
8
  * - `@deepseek-ai/dsh-session@0.1.0-rc.7` 官方源码逐行核对(见每条 `source`)
10
9
  *
11
10
  * 每条规则只描述"契约是什么";具体判定逻辑在 `lib/validate.js`(离线体检)
@@ -39,25 +38,25 @@ export const SEVERITY = { ERROR: 'error', WARNING: 'warning', INFO: 'info' };
39
38
  * - G 迁移预检(migration gate,**独立维度**:官方迁移会不会拒;不进 ok/verdict)
40
39
  */
41
40
  /**
42
- * R-F/R-D(2026-09-14 独立复核)——**机读的漂移/未复核标注**。
41
+ * (2026-09-14 实测)——**机读的漂移/未复核标注**。
43
42
  *
44
- * 复核结论:规则的 `source` 大量停在 `@deepseek-ai/dsh-session@0.1.0-rc.7` 的行号上,
43
+ * 核对结论:规则的 `source` 大量停在 `@deepseek-ai/dsh-session@0.1.0-rc.7` 的行号上,
45
44
  * 且其中 4 条(T2/P1/P2/C1)的**判定前提**在真宿主 0.1.5 上已不成立——此前**没有任何机制
46
45
  * 能发现**(`source` 只是人读字符串)。这里把它们变成机读字段,并由 `check` 抬头点名。
47
46
  */
48
47
  export const SOURCE_DRIFT = Object.freeze({
49
- /** 出处停在 rc.7 行号、未在 0.1.5 上复核(复核报告 §1 已给清单)。 */
48
+ /** 出处停在 rc.7 行号、未在 0.1.5 上复核(清单见下)。 */
50
49
  drifted: Object.freeze(['T2', 'P1', 'P2', 'C1', 'I1', 'G3', 'R2', 'R3', 'E5', 'E6']),
51
50
  /**
52
- * **判定前提在 0.1.5 上不成立/无法判定**(复核 §1):
51
+ * **判定前提在 0.1.5 上不成立/无法判定**:
53
52
  * T2 token-meter 已改从 `event.data.stream` 重建(无 sourceEventSeqs/无 belongs to another step);
54
53
  * P1/P2 载体已换成 `user/message + data.id`(无 data.editor);C1 前提"无会话级排他锁"被
55
54
  * `session.lock` flock 租约证伪;T3/T4 半步需真机实验。⇒ 这些规则的结论**不可单独采信**。
56
55
  */
57
56
  premiseStale: Object.freeze(['T2', 'P1', 'P2', 'C1']),
58
- /** 复核明确"无法判定"、需要真机实验的条目(列出来是为了不假装覆盖)。 */
57
+ /** 明确"无法判定"、需要真机实验的条目(列出来是为了不假装覆盖)。 */
59
58
  undecidable: Object.freeze(['T3', 'T4']),
60
- note: '出处/前提漂移是**静默失真**:R-D 行为探针(lib/host-probes.js)负责机器可判的那部分,'
59
+ note: '出处/前提漂移是**静默失真**:行为探针(lib/host-probes.js)负责机器可判的那部分,'
61
60
  + '其余在此显式列出,结论抬头必须带漂移清单与未复核清单。',
62
61
  });
63
62
 
@@ -86,7 +85,7 @@ export const CONTRACT_RULES = [
86
85
  title: '每行必须是合法 JSON',
87
86
  layer: LAYER.PERSISTENCE,
88
87
  severity: SEVERITY.ERROR,
89
- source: '审计方法论(scan-seq-gaps.mjs);dsh-session-persistence-jsonl 读路径',
88
+ source: 'seq 缺口扫描;dsh-session-persistence-jsonl 读路径',
90
89
  description: '非空行无法 JSON.parse = 损坏行。帧边界产生的空行是合法的(跳过)。',
91
90
  },
92
91
  {
@@ -122,7 +121,7 @@ export const CONTRACT_RULES = [
122
121
  title: 'seq 严格连续(单写入者假设)',
123
122
  layer: LAYER.PERSISTENCE,
124
123
  severity: SEVERITY.ERROR,
125
- source: '@deepseek-ai/dsh-session lib/index.js:398 (planSurfaceEvent "not contiguous");审计 S2/N6',
124
+ source: '@deepseek-ai/dsh-session lib/index.js:398 (planSurfaceEvent "not contiguous")',
126
125
  description: 'seq 必须从 0(或窗口 baseSeq)严格连续递增。缺口/倒退 = 违反单写入者假设(多实例共享存储并发写的痕迹),加载时直接 throw。',
127
126
  },
128
127
  {
@@ -165,7 +164,7 @@ export const CONTRACT_RULES = [
165
164
  layer: LAYER.PERSISTENCE,
166
165
  severity: SEVERITY.ERROR,
167
166
  source: '@deepseek-ai/dsh-session lib/index.js:1273-1277 (assertSupportedRequestHeader)',
168
- description: '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),不是写入即被拒。',
167
+ description: 'request/header-delta 与 reason=fallback 的 request/header 是已删除的遗留格式。注意(订正 E5):宿主 Session.append/appendLines 不看 type ⇒ 写入会成功、下一次读取才炸(依据 dsh-session@0.1.5-rc.1 lib/index.js:1170-1210 / persistence-jsonl:3046-3073),不是写入即被拒。',
169
168
  },
170
169
  {
171
170
  id: 'E6',
@@ -215,8 +214,8 @@ export const CONTRACT_RULES = [
215
214
  title: 'replace 的 sourceEventSeqs 必须完整覆盖被替换节点',
216
215
  layer: LAYER.PERSISTENCE,
217
216
  severity: SEVERITY.ERROR,
218
- source: '@deepseek-ai/dsh-session lib/index.js:335-336 (assertProvenance);复盘事故第 1 ',
219
- description: '★ 写前校验核心规则:sourceEventSeqs 必须包含每一个被替换(shadowed)的 surface 节点,缺一个 = 会话加载被拒(SessionPersistenceCorruptionError)。2026-08-25 事故第 1 轮(清空 sourceEventSeqs)正是违反此规则。',
217
+ source: '@deepseek-ai/dsh-session lib/index.js:335-336 (assertProvenance);复盘事故第 1 次尝试',
218
+ description: '★ 写前校验核心规则:sourceEventSeqs 必须包含每一个被替换(shadowed)的 surface 节点,缺一个 = 会话加载被拒(SessionPersistenceCorruptionError)。2026-08-25 事故第 1 次尝试(清空 sourceEventSeqs)正是违反此规则。',
220
219
  },
221
220
  {
222
221
  id: 'S6',
@@ -290,20 +289,20 @@ export const CONTRACT_RULES = [
290
289
  title: 'ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者)',
291
290
  layer: LAYER.PERSISTENCE,
292
291
  severity: SEVERITY.WARNING,
293
- source: '对抗性复查 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)',
292
+ source: '实测 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)',
294
293
  description: '未知 type + ignorable:true 被读路径接纳但无人消费 = 静默垃圾。排除已知消费者白名单(retrace/marker、retrace/goal-marker、message-editor/ 前缀等 retrace 客户端消费的插件 marker)后,其余 ignorable 未知事件报 warning。',
295
294
  },
296
295
  {
297
296
  id: 'E8',
298
297
  title: '事件信封键白名单(多余键:seed/restore 路径会拒)',
299
298
  layer: LAYER.PERSISTENCE,
300
- // R-G 候选规则 + R-D 探针订正:宿主 `assertSessionEventEnvelope`(dsh-session@0.1.5-rc.1
299
+ // 候选规则 + 探针订正:宿主 `assertSessionEventEnvelope`(dsh-session@0.1.5-rc.1
301
300
  // lib/index.js:849-861)确实拒多余键,但其**唯一调用点是 Session 构造器的 seed 路径**
302
301
  // (:1063-1068);JSONL **load 路径**(`adoptSessionEvent`)实测**容忍**(探针 p5 钉住)。
303
302
  // 故本规则按 **warning** 报(不影响"可加载",但该日志作为 seed/restore 输入会被拒)。
304
303
  severity: SEVERITY.WARNING,
305
304
  source: '@deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:849-861(assertSessionEventEnvelope)+ :1063-1068(唯一调用点=seed 路径);load 路径容忍见行为探针 p5',
306
- description: '事件对象只允许 7 个信封键(type/seq/time/data/surfaceOp/sourceEventSeqs/ignorable)。独立复核变异 05 指出"宿主拒、旧契约 0 违规";R-D 行为探针进一步订正口径:**load 路径容忍、seed/restore 路径拒** ⇒ warning。',
305
+ description: '事件对象只允许 7 个信封键(type/seq/time/data/surfaceOp/sourceEventSeqs/ignorable)。实测反例指出"宿主拒、旧契约 0 违规";行为探针进一步订正口径:**load 路径容忍、seed/restore 路径拒** ⇒ warning。',
307
306
  candidate: true,
308
307
  },
309
308
  {
@@ -312,7 +311,7 @@ export const CONTRACT_RULES = [
312
311
  layer: LAYER.PERSISTENCE,
313
312
  severity: SEVERITY.ERROR,
314
313
  source: '@deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:942-944("must have plugin source");角色表 :917-926',
315
- description: 'v3 新增的 system/message:role 必须为 system,source.kind 必须为 plugin 且 plugin 非空。实测(变异 03):source.kind=\'user\' 宿主拒、旧契约 0 违规 ⇒ 漏检。',
314
+ description: 'v3 新增的 system/message:role 必须为 system,source.kind 必须为 plugin 且 plugin 非空。实测(反例):source.kind=\'user\' 宿主拒、旧契约 0 违规 ⇒ 漏检。',
316
315
  candidate: true,
317
316
  },
318
317
  {
@@ -321,7 +320,7 @@ export const CONTRACT_RULES = [
321
320
  layer: LAYER.PERSISTENCE,
322
321
  severity: SEVERITY.ERROR,
323
322
  source: '@deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:231-248(validateSessionEventData:omit header.system / omit empty tools / omit empty adapterDefaults)',
324
- description: 'request/header 必须省略 header.system(系统提示改走 system/message)、空 tools、空 adapterDefaults。实测(变异 12):带 header.system 的写入宿主拒、旧契约 0 违规 ⇒ 漏检。',
323
+ description: 'request/header 必须省略 header.system(系统提示改走 system/message)、空 tools、空 adapterDefaults。实测(反例):带 header.system 的写入宿主拒、旧契约 0 违规 ⇒ 漏检。',
325
324
  candidate: true,
326
325
  },
327
326
  {
@@ -329,7 +328,7 @@ export const CONTRACT_RULES = [
329
328
  title: '空会话文件(有 header 无事件)显式报出',
330
329
  layer: LAYER.FRAMING,
331
330
  severity: SEVERITY.WARNING,
332
- source: '对抗性复查 2026-09-09 T3(36 条规则全来自有内容事故,空态无覆盖)',
331
+ source: '实测 2026-09-09 T3(36 条规则全来自有内容事故,空态无覆盖)',
333
332
  description: '有 header 但零事件 = 异常空会话(新建即空或写入未落盘)。空态不在任何有内容规则的覆盖下,显式 warning 供人判断。',
334
333
  },
335
334
  {
@@ -354,7 +353,7 @@ export const CONTRACT_RULES = [
354
353
  title: 'turn/step 为 null 的 assistant/message 只能 replace,不能 append',
355
354
  layer: LAYER.ENGINE,
356
355
  severity: SEVERITY.ERROR,
357
- source: '复盘事故第 2 轮(rt.js:6816 崩溃);实证 data.turn/data.step:正常消息为数字、插件 marker 为 null',
356
+ source: '复盘事故第 2 次尝试(rt.js:6816 崩溃);实证 data.turn/data.step:正常消息为数字、插件 marker 为 null',
358
357
  description: 'data.turn/data.step 为 null 的 assistant/message(如插件 marker)只能以 replace 承载(走插件 marker 定义);作为 append 会落进核心 assistant-step 定义,因 turn=null 发布 location data 导致客户端引擎崩溃。',
359
358
  },
360
359
 
@@ -388,7 +387,7 @@ export const CONTRACT_RULES = [
388
387
  title: 'seq 缺口/倒退提示多写入者',
389
388
  layer: LAYER.CONCURRENCY,
390
389
  severity: SEVERITY.WARNING,
391
- source: '审计 N6:dsh-session-persistence-jsonl appendLines 无锁(:1200-1227),全仓无会话级排他锁',
390
+ source: 'dsh-session-persistence-jsonl appendLines 无锁(:1200-1227),全仓无会话级排他锁',
392
391
  description: '离线体检无法直接观测跨进程竞态,但 E2 暴露的缺口/倒退即是"≥2 个 Host 进程共享同一 session 目录并发写"的后果。单实例部署不触发。',
393
392
  },
394
393
 
@@ -398,7 +397,7 @@ export const CONTRACT_RULES = [
398
397
  title: 'zstd 尾帧撕裂',
399
398
  layer: LAYER.FRAMING,
400
399
  severity: SEVERITY.WARNING,
401
- source: '审计 N5 相关;帧扫描方法论',
400
+ source: 'zstd 帧布局(dsh-session-persistence-jsonl 多帧)',
402
401
  description: '尾帧不完整(torn):可能正在写入(in-flight)或文件被截断。若这是唯一异常,通常可等待写入完成;若持续存在则是截断证据。',
403
402
  },
404
403
  {
@@ -406,7 +405,7 @@ export const CONTRACT_RULES = [
406
405
  title: 'zstd 帧解码失败 = 单帧全损',
407
406
  layer: LAYER.FRAMING,
408
407
  severity: SEVERITY.ERROR,
409
- source: '审计 N5:多帧单帧全损 → 整会话不可读',
408
+ source: '多帧单帧全损 → 整会话不可读',
410
409
  description: '任一帧解码失败(磁盘 bitrot / 传输截断 / 并发写撕裂)即整会话不可读;帧越多,单帧损坏下丢失概率线性上升。',
411
410
  },
412
411
 
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * dsh-log-contract · lib/host-probes.js —— **行为探针**(漂移检测的正解)
3
- * (2026-09-14 独立复核 §4 + 裁定 R-D
3
+ * (2026-09-14)
4
4
  *
5
- * 为什么需要它(复核原话):本包"承重墙建错了地方"——**委托宿主运行时函数**的判据基本正确,
5
+ * 为什么需要它:本包"承重墙建错了地方"——**委托宿主运行时函数**的判据基本正确,
6
6
  * **作者手写镜像/自建模型**的判据成片出错,而 `hostPackageVersion()`/`hostCapability()`
7
7
  * **只打印不门禁**、规则的 `source` 是**不可机读的字符串** ⇒ 本次 5 类失准**没有任何机制能发现**。
8
8
  *
@@ -84,7 +84,7 @@ export const PROBES = [
84
84
  title: '信封多余键:load 路径(adoptSessionEvent)**容忍**,只在 seed 路径拒(E8 的口径)',
85
85
  rules: ['E8'],
86
86
  expect: 'adoptSessionEvent 不拒(宿主 load 路径容忍)',
87
- // 这条探针**修正了一条规则的过度声称**(R-D 的价值实证):
87
+ // 这条探针**修正了一条规则的过度声称**(探针的价值实证):
88
88
  // · 宿主 `assertSessionEventEnvelope`(dsh-session@0.1.5-rc.1 lib/index.js:849-861)确实拒多余键,
89
89
  // 但它的**唯一调用点**是 Session 构造器的 **seed 路径**(:1063-1068)——不是 JSONL load 路径;
90
90
  // · load 路径(`adoptSessionEvent`)实测**容忍**多余键。
package/lib/log-reader.js CHANGED
@@ -5,11 +5,10 @@
5
5
  * `decodeStorageRecord` 展开(chunk 行展开 / 损坏行报错)。
6
6
  *
7
7
  * 契约来源:
8
- * - 帧扫描/撕裂尾帧判定:复用本项目审计方法论(scan-seq-gaps.mjs),
9
- * 帧头布局对齐 zstd 规范(magic 0xFD2FB528、descriptor、block 头)。
8
+ * - 帧扫描/撕裂尾帧判定:按 zstd 规范逐帧扫(magic 0xFD2FB528、descriptor、block 头)。
10
9
  * - 行解码:本地兼容层 `./compat.js` 的 `decodeStorageRecord`(原官方导出于 0.1.5 移除)
11
10
  * (lib/index.js:1029,validateRow :922 / expandRow :973)。
12
- * - 损坏语义:R2 —— chunk 行损坏 = 整段 run 丢失且加载失败(dsh-session
11
+ * - 损坏语义:chunk 行损坏 = 整段 run 丢失且加载失败(dsh-session
13
12
  * lib/index.js:1022-1024 注释明示 fail-loud,无跳过逃生舱)。
14
13
  */
15
14
  import fs from 'node:fs';
@@ -78,7 +77,7 @@ export function scanZstdFrames(buf) {
78
77
  /** 把 zstd 多帧拼成完整明文;任一帧解码失败即抛错(N5 单帧全损语义)。 */
79
78
  export function decompressZstd(buf, { allowTorn = true } = {}) {
80
79
  const { frames, torn } = scanZstdFrames(buf);
81
- // ── R-B(2026-09-14 独立复核,误报 Z2)────────────────────────────────────
80
+ // ── 2026-09-14 实测:这里的 Z2 是误报 ───────────────────────────────────
82
81
  // 撕裂尾帧**不是**损坏:宿主 `dsh-session-persistence-jsonl@0.1.5-rc.1`
83
82
  // · `readZstdPrefix`(lib/index.js:2791-2849)——"Decode complete frames and retain
84
83
  // complete JSONL records from a torn final frame",恢复并返回 `tornTruncateTo`(:2847);
package/lib/prewrite.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ★ 写前校验(pre-write validation)——本工具的第一公民。
5
5
  *
6
- * 复盘事故(2026-08-25)第 1 轮失败就是"违约写入没被拦":surface-replace
6
+ * 复盘事故(2026-08-25)第 1 次尝试失败就是"违约写入没被拦":surface-replace
7
7
  * 的 `sourceEventSeqs` 被清空后写入,会话加载直接抛
8
8
  * `SessionPersistenceCorruptionError`。如果写入前先校验,会话根本不会被改坏。
9
9
  *
@@ -28,10 +28,10 @@ import { normalizeEventSeqRanges } from './compat.js';
28
28
  const LEGACY_MARKER_ID_PREFIXES = ['retrace', 'message-editor'];
29
29
 
30
30
  /**
31
- * **可识别的历史 retrace 载体**(R-C,2026-09-14 独立复核:白名单收窄)。
31
+ * **可识别的历史 retrace 载体**(2026-09-14 实测:白名单收窄)。
32
32
  *
33
33
  * 旧实现只判"任意 `data.editor` 存在" ⇒ **任何**写 `assistant/message` replace + `data.editor`
34
- * 的第三方插件都能领 T1 豁免(复核判定:"设计粗糙的豁口")。收窄为四条同时成立:
34
+ * 的第三方插件都能领 T1 豁免(实测判定:"设计粗糙的豁口")。收窄为四条同时成立:
35
35
  * ① 类型/操作:`assistant/message` + replace;
36
36
  * ② 载体标记:`data.editor !== undefined`;
37
37
  * ③ **身份**:`data.message.id` 带 retrace 历史 marker 前缀(`retrace-*` / `message-editor-*`);
@@ -66,9 +66,9 @@ function normalizeCandidate(candidate, nextSeq) {
66
66
  * **格式版本(C2)**:`formatVersion` > `header.version` > 事件形状推断 > 0,在**本次
67
67
  * `createPreWriter` 调用内固定**并显式传给每条按版本择路的判定。**不再读写模块级全局**
68
68
  * `fileVersion`——旧实现下同一进程"先 validate(v3) 再 prewrite(v0)"会把同一份合法 v0 输入
69
- * 的结论翻成 S4+S8(独立审核复现 B)。下游 `dsh-retrace` 正是只传 `events` 直接调用
69
+ * 的结论翻成 S4+S8(实测复现)。下游 `dsh-retrace` 正是只传 `events` 直接调用
70
70
  * (`lib/prewrite-guard.js:166`),所以缺省时必须能自行推断,不能把 v3 输入按 0 处理
71
- * (否则首次调用即 10 条 E3/S2/S8 误报,复现 C)。
71
+ * (否则首次调用即 10 条 E3/S2/S8 误报)。
72
72
  *
73
73
  * @param {{ events: Array<object>, baseSeq?: number, formatVersion?: number, header?: object|null }} input
74
74
  * 当前日志的已解码事件(按日志顺序;无 seq 字段的事件按位置补 seq,用于窗口校验)。
@@ -136,7 +136,7 @@ export function createPreWriter(input = {}) {
136
136
  // (check),不在这里重复拦截(否则历史 marker 会让后续编辑全部被拒)。
137
137
  const lastCandidate = candidateEvents[candidateEvents.length - 1];
138
138
  const legacyKind = legacyMarkerKindOf(lastCandidate);
139
- // R-C 版本门(2026-09-14 独立复核):降级**只对 ≤v2 文件**。
139
+ // 版本门(2026-09-14):降级**只对 ≤v2 文件**。
140
140
  // 依据:新载体(`user/message` + `data.id`,retrace 0.4.26)根本不带 `data.editor`,在 v3 上
141
141
  // 降级**救不回任何写入**(v3 禁 assistant/message 带 provenance ⇒ S8 兜住) —— 留在 v3 上
142
142
  // 只会掩盖 T1 的真实原因。v0/v1/v2 才是有价值的作用域(回放/重写历史形态 marker)。
@@ -144,7 +144,7 @@ export function createPreWriter(input = {}) {
144
144
  for (const t1 of tokenMeterViolations(candidateEvents.map((event) => ({ event })))) {
145
145
  if (t1.id !== 'T1' || t1.seq !== lastCandidate?.seq) continue;
146
146
  if (legacyKind && legacyDowngradeAllowed) {
147
- // 降级**可见**(R-C):违规里带 markerKind/id/targetSeq,并由结果字段 `legacyMarkerDebt`
147
+ // 降级**可见**:违规里带 markerKind/id/targetSeq,并由结果字段 `legacyMarkerDebt`
148
148
  // 显式带出;入口(CLI)打印"压缩前需一次性清理",不再"记了没人看"。
149
149
  violations.push({
150
150
  ...t1,
@@ -179,7 +179,7 @@ export function createPreWriter(input = {}) {
179
179
  }
180
180
  const bySeverity = { error: 0, warning: 0, info: 0 };
181
181
  for (const v of violations) bySeverity[v.severity] = (bySeverity[v.severity] ?? 0) + 1;
182
- // R-C「让降级可见」:把"本次写入沿用了历史 marker 形态(债)"作为**结构化字段**带出,
182
+ // 「让降级可见」:把"本次写入沿用了历史 marker 形态(债)"作为**结构化字段**带出,
183
183
  // 入口据此打印"压缩前需一次性清理"。降级不再只是 violations 里的一句 warning。
184
184
  const legacyDebt = legacyKind && legacyDowngradeAllowed && bySeverity.error === 0
185
185
  ? { kind: legacyKind.prefix, id: legacyKind.id, targetSeq: legacyKind.targetSeq, seq: lastCandidate?.seq ?? null, formatVersion }
package/lib/repair.js CHANGED
@@ -209,7 +209,7 @@ export function neutralizeMarkersText(text, { onlyLegacy = false } = {}) {
209
209
  if (v.data?.turn != null || v.data?.step != null) continue;
210
210
  const id = v.data?.message?.id;
211
211
  if (typeof id !== 'string' || !MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`))) continue;
212
- // R-C 一次性根治路径(`--neutralize-legacy-markers`):只动**历史载体**
212
+ // 一次性根治路径(`--neutralize-legacy-markers`):只动**历史载体**
213
213
  // (`assistant/message` + `data.editor`);新载体是 `user/message` + `data.id`,
214
214
  // 本来就不在这个分支里(类型不符),这里额外的判据是"必须有 editor",
215
215
  // 便于把"清历史债"与"泛化中和"区分开、也让报告口径可核对。
@@ -423,15 +423,14 @@ function renumberWithDrops(parts, dropPredicate, dropLinePredicate = null) {
423
423
  *
424
424
  * ⚠️ 与 renumberWithDrops 不同:本函数按「行是否保留」决定删除(双流交织时
425
425
  * 保留行与删除行可能共享 seq 值,按 seq 值判删会误删保留行——2026-08-31
426
- * 独立审查实测复现),且保留行**全量重映射** seq 从 0 连续(参考原工具
427
- * keep-ranges.mjs 的 seqMap 逻辑),同步 seq / seq0 / sourceEventSeqs /
426
+ * 实测复现),且保留行**全量重映射** seq 从 0 连续(与 `--keep-ranges` 同一 seqMap 逻辑),同步 seq / seq0 / sourceEventSeqs /
428
427
  * surfaceOp 范围。header 行(首行)恒保留且不改。
429
428
  * @param {string[]} parts - 按 \n 切分的行数组。
430
429
  * @param {(lineIdx: number) => boolean} keepLinePredicate - 行身份保留判定。
431
430
  * @returns {{ text: string, removed: number, renumbered: number }}
432
431
  */
433
432
  function renumberKeptLines(parts, keepLinePredicate) {
434
- // 第一遍:按物理序收集保留行的事件 seq(chunk 行展开,参考 keep-ranges 原工具)
433
+ // 第一遍:按物理序收集保留行的事件 seq(chunk 行展开,与 --keep-ranges 同口径)
435
434
  const keptLineIdx = [];
436
435
  for (let i = 0; i < parts.length; i++) {
437
436
  const raw = parts[i];
@@ -559,7 +558,7 @@ export function tailRenumberText(text, startSeq, delta) {
559
558
  const parts = text.split('\n');
560
559
  const out = [];
561
560
  let changed = 0;
562
- // ── seq 引用平移(2026-09-14 第五轮:把 round4 存档的未审补丁评审后纳入,并补两处同族缺口)──
561
+ // ── seq 引用平移(2026-09-14:并补两处同族缺口)──
563
562
  // 一手依据(官方 v0→v1 校验,@0.1.5-rc.2):
564
563
  // - `data.shadowedRange{start,end}` + `data.shadowedSeqs`:`dsh-session-format-v0-to-v1`
565
564
  // `lib/index.js:55-70`(compaction/prune、compaction/summary 的 dispositions)、
@@ -655,7 +654,7 @@ export function tailRenumberText(text, startSeq, delta) {
655
654
  * 重新排队 → UI 显示"待排队消息")。与 neutralize 同类:原地改
656
655
  * `removedCount → 0`(start/inserted 不变,seq/行数不变 → 附着力安全)。
657
656
  *
658
- * ⚠️ 判定(2026-08-31 独立审查修正):不能对"所有 removedCount>0 的
657
+ * ⚠️ 判定(2026-08-31 修正):不能对"所有 removedCount>0 的
659
658
  * next-turn spliced"下手——DSH 每个轮次消费消息都会写这种 spliced
660
659
  * (archive 实测 3069 处 removedCount>0,但真 I1 违规只 2 处)。只归零
661
660
  * **实际触发 I1 违规**的那条:按官方 inbox 重放(checks.js
@@ -724,10 +723,10 @@ export function neutralizeOrphanText(text) {
724
723
  export function extractTurnText(text, keepTurn, secondTurnTo = null) {
725
724
  const parts = text.split('\n');
726
725
  const KEEP_NULL_TYPES = new Set(['agent/inbox/spliced', 'user/message', 'request/header', 'session/end-seed', 'command/run', 'command/done']);
727
- // 第一轮:按行身份决定去留(保留 turn===keepTurn 的行 + 无 turn 系统事件
726
+ // 第一遍:按行身份决定去留(保留 turn===keepTurn 的行 + 无 turn 系统事件
728
727
  // 白名单),并就地改第二个同名轮次的 turn 号。
729
- // ⚠️ 用行索引而非 seq 值:双流交织时保留行与删除行可能共享 seq(独立审查
730
- // 实测复现内容静默丢失)——按行身份删除才安全。
728
+ // ⚠️ 用行索引而非 seq 值:双流交织时保留行与删除行可能共享 seq(实测
729
+ // 复现内容静默丢失)——按行身份删除才安全。
731
730
  const dropLines = new Set();
732
731
  let sawFirstTurnStart = false;
733
732
  let inRenumberTurn = false;
@@ -769,8 +768,7 @@ export function extractTurnText(text, keepTurn, secondTurnTo = null) {
769
768
  }
770
769
 
771
770
  /**
772
- * 只保留指定 seq 区间,其余删除 + 全量重编号(keep-ranges 收编,
773
- * 源:tools/keep-ranges.mjs)。
771
+ * 只保留指定 seq 区间,其余删除 + 全量重编号(`--keep-ranges`)。
774
772
  *
775
773
  * 从交织/污染文件中提取干净区段。区间为 1-based 行号(含端点),
776
774
  * 如 "10-20,40-50";区段外的行丢弃。header 行(首行)永远保留。
@@ -788,7 +786,7 @@ export function keepRangesText(text, rangesSpec) {
788
786
  return [a, b];
789
787
  });
790
788
  const parts = text.split('\n');
791
- // 按行身份删除(双流交织时保留行与删除行可能共享 seq——独立审查发现)
789
+ // 按行身份删除(双流交织时保留行与删除行可能共享 seq——实测发现)
792
790
  const dropLines = new Set();
793
791
  let keptLines = 0;
794
792
  for (let i = 0; i < parts.length; i++) {
@@ -1158,7 +1156,7 @@ export function repairSession(file, opts = {}) {
1158
1156
  let plain;
1159
1157
  if (isZstd) {
1160
1158
  try {
1161
- // R-B 边界:**写路径不吃撕裂尾帧**。`check`(只读)按宿主语义恢复撕裂尾帧;但
1159
+ // 边界:**写路径不吃撕裂尾帧**。`check`(只读)按宿主语义恢复撕裂尾帧;但
1162
1160
  // `fix` 会回写文件,若尾帧是**活动会话正在写入**的部分,回写会把它截掉 ——
1163
1161
  // 与宿主"由持有租约的会话自己截断"(:226-228)不同责。故这里显式 strict。
1164
1162
  plain = decompressZstd(buf, { allowTorn: false }).toString('utf8');
package/lib/validate.js CHANGED
@@ -60,14 +60,14 @@ export function validateSessionLog(log, opts = {}) {
60
60
  // C1 防御(多入口一致):即使调用方手搓 log 对象、未经 `loadSessionLog`,也在体检入口
61
61
  // 归一一次 v3 区间编码。正常路径(loadSessionLog 已展开)下这是幂等的恒等映射。
62
62
  const events = (log.events ?? []).map((e) => ({ ...e, event: normalizeEventSeqRanges(e.event) }));
63
- // 1.3 第二半:词表/折叠路径按**被检文件自身版本**选择(不按运行时)。
63
+ // 词表/折叠路径按**被检文件自身版本**选择(不按运行时)。
64
64
  // C2:版本是**本次调用的局部量**,显式传给每条按版本择路的判定——不再写模块级全局
65
65
  // (旧实现 `setFileFormatVersion()` 会被同进程后调用的 prewrite 读到,造成结论翻转)。
66
66
  const formatVersion = Number.isSafeInteger(header?.version) && header.version >= 0 ? header.version : 0;
67
67
 
68
68
  // ── 宿主能力闸(S2·安全)──────────────────────────────────────────────────
69
69
  // 被检文件版本 > 本宿主支持的最大文件版本 ⇒ 本宿主没有该版本的词表/折叠语义。继续按
70
- // 本宿主语义判定会产出**假阳性**(独立复核实测:rc.7 上同一真实 v3 文件 →
70
+ // 本宿主语义判定会产出**假阳性**(实测:rc.7 上同一真实 v3 文件 →
71
71
  // `S8×1 + E3×40 / verdict=broken / loadable:false`),危险是用户以为日志坏了去跑
72
72
  // `fix --apply`。改为:只跑与版本无关的结构检查 + 显式"不可在本宿主评估"档。
73
73
  if (!isAssessableFileVersion(formatVersion)) {
@@ -225,7 +225,7 @@ export function validateSessionLog(log, opts = {}) {
225
225
  // 版本支持面(2026-09-14 用户要求第 1 条):宿主版本 vs 测试基线 + 被检文件格式版本,
226
226
  // 给出 warnings/readOnly。只读档由入口(prewrite/fix)强制执行。
227
227
  result.support = detectSupport({ header, events });
228
- // R-D 漂移检测:行为探针(宿主自己的运行时函数当 oracle)+ R-F 出处/前提漂移清单。
228
+ // 漂移检测:行为探针(宿主自己的运行时函数当 oracle)+ 出处/前提漂移清单。
229
229
  result.probes = hostProbes();
230
230
  result.drift = driftOf(result.probes);
231
231
  result.migration = migrationVerdict(result);
@@ -353,11 +353,11 @@ function hostProbes() {
353
353
  }
354
354
 
355
355
  /**
356
- * R-D/R-F 合并出的**漂移报告**(`check` 抬头与 `--json` 共用)。
356
+ * 合并出的**漂移报告**(`check` 抬头与 `--json` 共用)。
357
357
  * · `unverifiedRules` —— 行为探针在本宿主上**不成立**的规则(宿主语义与规则假设不符);
358
- * · `driftedSources` —— 出处停在 rc.7、未在 0.1.5 上复核(R-F,机读清单);
359
- * · `premiseStale` —— 判定前提已不成立/无法判定(复核 §1);
360
- * · `fixApplyBlocked` —— 只要任一非空,就**不得据此跑 `fix --apply`**(R-E)。
358
+ * · `driftedSources` —— 出处停在 rc.7、未在 0.1.5 上复核(机读清单);
359
+ * · `premiseStale` —— 判定前提已不成立/无法判定;
360
+ * · `fixApplyBlocked` —— 只要任一非空,就**不得据此跑 `fix --apply`**。
361
361
  */
362
362
  export function driftOf(probes) {
363
363
  const p = probes ?? hostProbes();
@@ -371,7 +371,7 @@ export function driftOf(probes) {
371
371
  premiseStale,
372
372
  undecidable: [...SOURCE_DRIFT.undecidable],
373
373
  // `fix --apply` 的**硬前提**只看行为探针(宿主语义与规则假设不一致时才禁止写入类动作);
374
- // 出处/前提清单(R-F)是**已知文档债**,会显式告警但不阻断修复流程 —— 否则只要历史
374
+ // 出处/前提清单是**已知文档债**,会显式告警但不阻断修复流程 —— 否则只要历史
375
375
  // 清单非空,"修一个坏会话"这条主路径就永久不可用(见报告"我未照做之处")。
376
376
  fixApplyBlocked: unverifiedRules.length > 0,
377
377
  note: SOURCE_DRIFT.note,
@@ -397,11 +397,11 @@ export function migrationVerdict(result) {
397
397
  'G3 v0 源:session/title 系列 messageSeqs 必须引用更早的人类 user/message(含"空 ⟺ 用户标题")',
398
398
  ],
399
399
  uncovered: [
400
- // 2026-09-15 第五轮:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
400
+ // 2026-09-15:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
401
401
  // 官方状态机(assertReleasedArtifactRelationships)由 v1→v2 在**变换后的 v1/v2 artifact**
402
402
  // 上调用(v1-to-v2/lib/index.js:104),带 cut/继承切点处理;在原始 v0 上照抄会在已 seed
403
403
  // 的会话上狂报(实测样本:0.1.5 链并不以该规则拒它,原始 v0 上会报 19 条)。
404
- // ⇒ 忠实复现需先做 v0→v1→v2 变换;本轮不做,保持未覆盖。
404
+ // ⇒ 忠实复现需先做 v0→v1→v2 变换;暂不做,保持未覆盖。
405
405
  'turn/start 闭合/预期轮(官方 v1→v2 在**变换后** artifact 上判:does not close the prior turn / does not open expected turn)',
406
406
  'assistant/attempt 配对/闭合(migration refuses the transformed artifact)',
407
407
  'Session inheritedEventCut / 继承切点(官方在变换后 artifact 上按 cut 判;header 只有 seedLength)',
@@ -428,7 +428,7 @@ export function migrationVerdict(result) {
428
428
  hostMaxFileVersion: HOST_MAX_FILE_VERSION,
429
429
  blocked,
430
430
  coverage,
431
- // 2026-09-14 第四轮(最终复核):待迁移文件(applies=true)**只**被 2 条 G 规则覆盖,
431
+ // 2026-09-14:待迁移文件(applies=true)**只**被 2 条 G 规则覆盖,
432
432
  // 另有 6 类官方迁移规则未覆盖 ⇒ 评估范围只能是 "partial"。工具无法逐文件知道
433
433
  // 本文件是否命中未覆盖类(那要先把 6 类实现出来或跑官方迁移),但它**知道**这件事。
434
434
  assessmentScope: applies ? 'partial' : 'full',
@@ -104,7 +104,7 @@ export function detectSupport({ header, events, formatVersion, hostVersion: host
104
104
  // ── 被检文件格式 ────────────────────────────────────────────────────────
105
105
  const resolved = resolveFormatVersionDetailed({ formatVersion, header, events });
106
106
  const unknownSource = resolved.source === 'default';
107
- // S4(独立核验):无法识别格式时**状态标签必须是 `unverified`**(README 就是这么写的:
107
+ // S4(实测):无法识别格式时**状态标签必须是 `unverified`**(README 就是这么写的:
108
108
  // "Format unrecognisable → same: "unverified format" + read-only")。旧实现让版本回落到默认 0
109
109
  // ⇒ 标签打印成 `legacy`,与 README 不一致(`readOnly` 本来就对)。
110
110
  const status = unknownSource ? 'unverified' : fileStatusOf(resolved.version);
package/lib/vocab.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**(1.3 第二半,内部评审裁定)
2
+ * dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**
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
- * 2026-09-14 复核补强(同一裁定的完整落地)——一手证据:
9
+ * 2026-09-14 补强(同一判据的完整落地)——一手证据:
10
10
  * 1. **0.1.5 的 `foldSurface` 不是"放宽",而是换成了 v3 语义**(对照 `dsh-session@0.1.0-rc.7`
11
11
  * 与 `0.1.5-rc.1` 的 `isReplaceOp` / `assertProvenance`):
12
12
  * - replace 字段改名:rc.7 `{op,start,end}`(`lib/index.js:300-303`)→ 0.1.5 `{op,startSeq,endSeq}`
@@ -59,7 +59,7 @@ export const V0_EVENT_TYPES = new Set([
59
59
  * v2 = retained ∪ {assistant/attempt, assistant/message, delivery-accepted, session/end-seed}
60
60
  * = v0 − assistant/chunk + assistant/attempt
61
61
  * ⇒ **v2 不含 `assistant/chunk`**(旧实现误用 `V0 ∪ {attempt}`,把 chunk 留在 v2 词表里 =
62
- * 宽松口径,会漏报 v2 里的顶层 chunk 行 ⇒ F9 修正)。
62
+ * 宽松口径,会漏报 v2 里的顶层 chunk 行 ⇒ 已修正)。
63
63
  */
64
64
  export const V2_EVENT_TYPES = new Set([
65
65
  ...[...V0_EVENT_TYPES].filter((t) => t !== 'assistant/chunk'),
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.15",
4
+ "version": "0.3.16",
5
5
  "packageManager": "pnpm@11.7.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",