dsh-log-contract 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,8 +15,7 @@ offline health check + pre-write validation. The business layer's **doctor**.
15
15
 
16
16
  </div>
17
17
 
18
- Formerly `log-contract-validator` (candidate #2); named **`dsh-log-contract`**
19
- per the OfferKuai three-piece plan.
18
+ Formerly `log-contract-validator`; now **`dsh-log-contract`**.
20
19
 
21
20
  A fuse for DSH session logs (`*.jsonl` / `*.jsonl.zstd`): format drift that humans
22
21
  cannot see but parsers crash on is caught and reported here. It does **not** judge
@@ -37,10 +36,9 @@ plugin marker semantics).
37
36
 
38
37
  ## Where it sits in the business layer
39
38
 
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.
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
+ >
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`.
44
42
 
45
43
  | Layer | What it is | Components |
46
44
  |---|---|---|
@@ -107,6 +105,34 @@ omitted for privacy.
107
105
 
108
106
  ---
109
107
 
108
+ ## Version support (host / session format)
109
+
110
+ **Tested baseline (the versions this rule set was verified against)**:
111
+
112
+ | Item | Baseline | Note |
113
+ |---|---|---|
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 |
115
+ | Session format | **v3** (`SESSION_FORMAT_VERSION = 3`) | v3 uses the runtime vocabulary + official `foldSurface` final check |
116
+ | Declared range | `peerDependencies: ^0.1.0-rc.7 || ^0.1.5-rc.1` | installable — a declared range is **not** a per-version verification |
117
+ | Known formats | 0 / 1 / 2 / 3 | 0–2 are supported by this package's **vendored** vocabulary + local equivalent fold (`legacyFoldSurface`), independent of the host |
118
+
119
+ **What happens on unverified versions (detected and reported at runtime — never silently judged by stale rules)**:
120
+
121
+ | Case | Behaviour |
122
+ |---|---|
123
+ | 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 |
124
+ | 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) |
125
+ | **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) |
126
+ | Format **unrecognisable** (no `header.version` and no distinguishing event shapes) | same: "unverified format" + read-only |
127
+ | File version above the host's supported maximum | `not-assessable` (**not** broken), exit 3; skipped/executed rules are listed |
128
+
129
+ Implementation: `lib/version-support.js` (`TESTED_BASELINE` / `detectSupport()`, exported in the public API).
130
+ Both the human-readable `check` output and the `--json` `support` field carry the baseline summary and the warnings, e.g.:
131
+
132
+ ```
133
+ version support: host @deepseek-ai/dsh-session@0.1.5-rc.1 (baseline 0.1.5-rc.1) | file format v3 (header, baseline)
134
+ ```
135
+
110
136
  ## Installation
111
137
 
112
138
  ```bash
@@ -144,7 +170,7 @@ Sample output (the CLI reports in Chinese — it is the tool's UI language):
144
170
  事件 204754 | surface 节点 16 | replace 代数 5 | 帧 8620(3439.5KiB → 8191.3KiB)
145
171
  违规 1(error 1 / warning 0)
146
172
 
147
- [error] S5 @ seq 156425 / line 778 (assistant/message)
173
+ [error] S5 @ seq <seq> / line <line> (assistant/message)
148
174
  surface replace: sourceEventSeqs 必须覆盖每个被替换节点;缺失 121774, 121779(共 2 个)
149
175
 
150
176
  ❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
@@ -262,24 +288,23 @@ if (!verdict.ok) {
262
288
  ## Tests
263
289
 
264
290
  ```bash
265
- pnpm check && pnpm test # syntax check + 79 unit tests (incl. incident regressions)
291
+ pnpm check && pnpm test # syntax check + contract-doc drift gate + all unit tests
266
292
  ```
267
293
 
268
294
  - **Synthetic fixtures** (in-repo): legal session / seq gap / empty sourceEventSeqs
269
295
  / turn-null append / unknown type / bad chunk row / torn tail frame / unknown
270
296
  marker prefix / self-shadowing etc.
271
- - **Real fossils** (not in-repo, contain user data): run locally
272
297
 
273
- ```bash
274
- node scripts/check-local-fossils.mjs # scans ../ for backup-session-*.jsonl.zstd
275
- ```
298
+ > **Scope of the published package**: it contains the runtime code and documentation only.
299
+ > Local maintainer tooling is not part of the public repository, so `pnpm check` and `pnpm test`
300
+ > work from a clean clone.
276
301
 
277
302
  Known truth table (updated 2026-08-31 — after 0.3.5 added I1, `spliced-orphan`
278
303
  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;
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;
283
308
  official foldSurface replays cleanly — see the plugin ledger)
284
309
  The truth table tracks rule evolution (0.3.5's I1 flipped spliced-orphan PASS→FAIL);
285
310
  "repaired sessions PASS" must be verified on rebuilt samples — `pre-` backups are
@@ -309,7 +334,11 @@ MIT © OfferKuai Team
309
334
 
310
335
  ---
311
336
 
312
- ## ⚠️ Upgrading to 0.3.12 — behaviour changes you must know
337
+ ## ⚠️ Upgrading to 0.3.15 — behaviour changes you must know (0.3.12 → 0.3.15)
338
+
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.
313
342
 
314
343
  **1. New exit codes `3` / `4` — a defect fix that is also a breaking change.**
315
344
  - `3` = **not assessable on this host**: the file's `header.version` is higher than the host supports
package/README.zh.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  > DSH(DeepSeek Harness)会话日志的**结构契约保险丝**:离线体检 + 写前校验。
21
- > 原名 `log-contract-validator`(候选二号),按 Offer快 三件套规划定名 **`dsh-log-contract`**。
21
+ > 本包名 **`dsh-log-contract`**;早期工作名 `log-contract-validator`。
22
22
 
23
23
  给 DSH 会话日志(`*.jsonl` / `*.jsonl.zstd`)装一条保险丝:人眼看不出、程序解析会崩的日志格式漂移,在它这里被拦下并告警。它不判断日志**内容**对不对,只守护日志**结构**是否破坏了下游消费者(Harness 读路径、客户端引擎、插件 marker 语义)的预期。
24
24
 
@@ -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 级 = 会话不可读/不可写)
@@ -226,26 +257,24 @@ if (!verdict.ok) {
226
257
  ## 测试
227
258
 
228
259
  ```bash
229
- pnpm check && pnpm test # 语法检查 + 79 个单测(含事故回归用例)
260
+ pnpm check && pnpm test # 语法检查 + 契约文档漂移闸 + 全部单测(含事故回归用例)
230
261
  ```
231
262
 
232
263
  - **合成夹具**(入库):合法会话 / seq 缺口 / 空 sourceEventSeqs / turn=null append / 未知 type / 坏 chunk 行 / 撕裂尾帧 / 未知 marker 前缀 / 自指 shadowed 等。
233
- - **真实化石**(不入库,含用户隐私):本地跑
234
264
 
235
- ```bash
236
- node scripts/check-local-fossils.mjs # 扫描 ../ backup-session-*.jsonl.zstd
237
- ```
265
+ > **发布包范围**:只含运行时代码与文档。维护者本地工具不在公开仓内,
266
+ > 因此 `pnpm check` / `pnpm test` 在干净的克隆里即可通过。
238
267
 
239
268
  已知真值表(2026-08-31 实测更新——0.3.5 加 I1 后,`spliced-orphan` 已可报错,旧表 PASS 过时):
240
269
 
241
270
  | 化石 | 判定 | 违规 |
242
271
  |---|---|---|
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 重放通过——见插件任务台账) |
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 重放通过) |
249
278
 
250
279
  > 说明:真值表随规则演进更新(0.3.5 新增 I1 后 spliced-orphan 从 PASS 变 FAIL);
251
280
  > 「修复后会话 PASS」需用重建/修复后的样本验证,pre- 前缀备份多为修复前坏样本。
@@ -266,7 +295,11 @@ MIT © OfferKuai Team
266
295
 
267
296
  ---
268
297
 
269
- ## ⚠️ 升级到 0.3.12 —— 必须知道的行为变更
298
+ ## ⚠️ 升级到 0.3.15 —— 必须知道的行为变更(0.3.12 → 0.3.15)
299
+
300
+ > **0.3.15 本身不含行为变更。** 它只做两件事:清掉 0.3.14 已发布产物里的两处内部标识
301
+ > (README 的定名说明 + 一处源码注释),并补齐发布前泄漏闸门的词表。
302
+ > 从 0.3.12 升到 0.3.15 时,下面各条与原来完全一致。
270
303
 
271
304
  **1. 新增退出码 `3` / `4`:本质是缺陷修复,形式上也是破坏性变更。**
272
305
  - `3` = **不可在本宿主评估**:被检文件的 `header.version` 高于当前宿主支持的上限
@@ -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` 在**管道**下是异步的,紧跟着的
@@ -64,7 +64,7 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
64
64
  4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false;可用
65
65
  --no-fail-on-migration 退出该档)。
66
66
 
67
- 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]
68
68
  诊断 + 修复(2026-08 事故固化方案)。先做严格 seq 连续扫描 + 契约体检
69
69
  (含 W1/W2 wire 级悬空 tool 检查),再按需修复:
70
70
  --remove-markers 移除 retrace/message-editor marker 并全量重编号
@@ -73,6 +73,11 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
73
73
  --neutralize 原地中和 turn-null marker(type→retrace/marker +
74
74
  ignorable:true,删 surfaceOp/sourceEventSeqs,seq/行数不变)
75
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 降级"那条豁免。
76
81
  --drop-failed-turns 删除"本轮运行失败"的轮次(清失败报错气泡)
77
82
  --trim-last N 裁剪到最近 N 条 append 消息(保留所在 turn 结构)
78
83
  --trim-budget N 按 token 预算裁剪(L5):自动选保留消息数使估算 ≤ N
@@ -101,7 +106,7 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
101
106
 
102
107
  dsh-log-contract extract <session-log> --pattern <regex> [--out DIR] [--min-size N] [--json]
103
108
  考古提取:按命令正则导出工具输出(只读)。--out 写到目录(保留原始文本),
104
- 否则打印前 3 条摘要。--min-size 过滤小输出(默认 50,任务书口径)。
109
+ 否则打印前 3 条摘要。--min-size 过滤小输出(默认 50)。
105
110
 
106
111
  dsh-log-contract audit-report <session-log> [--json]
107
112
  考古审计报告:调用数 / 配对率 / 孤儿数 / 命令分布。
@@ -133,6 +138,38 @@ function printViolations(violations, maxDetails = 8) {
133
138
  }
134
139
  }
135
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
+
136
173
  /** S2 横幅:被检文件版本 > 宿主支持上限 ⇒ "不可在本宿主评估"(**不是** broken)。 */
137
174
  function notAssessableBanner(result) {
138
175
  const na = result.notAssessable ?? {};
@@ -142,7 +179,8 @@ function notAssessableBanner(result) {
142
179
  + ` 原因:${na.reason}\n`
143
180
  + ` 已跳过规则:${(na.skippedRules ?? []).join(', ')}\n`
144
181
  + ` 已执行:结构层(Z/H/R/E1·E2·E4·E5·E6/S9/M/P/P3·P4/I1);结构层结论 ${result.structuralOk ? '绿' : '有 error(见上)'}\n`
145
- + ` ⚠️ 这不表示日志损坏,也**不要**据此运行 fix --apply —— 请用 0.1.5+ 宿主评估。\n`;
182
+ + ` ⚠️ 这不表示日志损坏,也**不要**据此运行 fix --apply —— 请用 0.1.5+ 宿主评估。\n`
183
+ + supportLines(result.support);
146
184
  }
147
185
 
148
186
  /** 迁移预检(独立维度)文本。
@@ -206,7 +244,7 @@ function cmdCheck(args) {
206
244
  const exitCode = notAssessable ? 3 : (!ok ? 1 : (migGate ? 4 : 0));
207
245
 
208
246
  if (json) {
209
- 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 };
210
248
  if (notAssessable) payload.notAssessable = result.notAssessable;
211
249
  if (resume) payload.resume = resumeVerdict(result);
212
250
  out(JSON.stringify(payload, null, 2) + '\n');
@@ -239,6 +277,8 @@ function cmdCheck(args) {
239
277
  out(` ${mark} ${name} — ${desc}\n`);
240
278
  if (blockers.length > 0) out(` 阻断: ${[...new Set(blockers)].join(', ')}\n`);
241
279
  }
280
+ out(supportLines(result.support));
281
+ out(driftLines(result.drift, result.probes));
242
282
  out(migrationLine(migration, partial));
243
283
  out(`\n 结论: ${icons[tier]}${migBlocked
244
284
  ? '(本工具规则集内)—— 但**迁移预检 blocked**:不得据此宣称"可升级"'
@@ -252,6 +292,8 @@ function cmdCheck(args) {
252
292
  out(`\n📋 dsh-log-contract check —— ${file}\n`);
253
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`);
254
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));
255
297
  out(migrationLine(migration, partial));
256
298
  out('\n');
257
299
  printViolations(violations, maxDetails);
@@ -286,6 +328,11 @@ function cmdPrewrite(args) {
286
328
  fail(`会话日志读取失败:${err.message}`);
287
329
  }
288
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
+ }
289
336
  // S2:本宿主评估不了的文件**不能**给出写前结论(否则会拿假阳性去拦合法写入)
290
337
  if (baseline.assessable === false) {
291
338
  fail(`不可在本宿主评估该会话(${baseline.notAssessable?.reason ?? '宿主能力不足'})——写前校验无法给出可信结论;请用 0.1.5+ 宿主`, 3);
@@ -310,11 +357,19 @@ function cmdPrewrite(args) {
310
357
  }
311
358
 
312
359
  if (json) {
313
- 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');
314
361
  process.exit(result.ok ? 0 : 1);
315
362
  }
316
363
 
317
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
+ }
318
373
  if (result.ok) {
319
374
  out(' ✅ 写入安全:三层契约全绿(持久化 foldSurface 可重放 / 引擎层无崩溃风险 / 插件 marker 语义自洽)\n');
320
375
  out(` 写入后 surface 节点 ${result.stateAfter.surfaceNodes} 个,nextSeq ${result.stateAfter.nextSeq}\n\n`);
@@ -329,7 +384,12 @@ function cmdPrewrite(args) {
329
384
  function cmdContracts() {
330
385
  out('dsh-log-contract 契约规则目录(spec:59 条审计发现 + 官方源码逐行核对)\n\n');
331
386
  for (const r of CONTRACT_RULES) {
332
- 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`);
333
393
  }
334
394
  }
335
395
 
@@ -337,6 +397,8 @@ function cmdFix(args) {
337
397
  const json = args.includes('--json');
338
398
  const removeMarkers = args.includes('--remove-markers');
339
399
  const neutralize = args.includes('--neutralize');
400
+ // R-C 一次性根治路径:只中和**历史载体**(assistant/message + data.editor)的 retrace marker
401
+ const neutralizeLegacyMarkers = args.includes('--neutralize-legacy-markers');
340
402
  const clipCrossStep = args.includes('--clip-crossstep');
341
403
  const dropFailedTurns = args.includes('--drop-failed-turns');
342
404
  const trimIdx = args.indexOf('--trim-last');
@@ -345,7 +407,7 @@ function cmdFix(args) {
345
407
  const trimBudget = budgetIdx >= 0 && args[budgetIdx + 1] ? Number(args[budgetIdx + 1]) : undefined;
346
408
  const compactIdx = args.indexOf('--compact-last');
347
409
  const compactLast = compactIdx >= 0 && args[compactIdx + 1] ? Number(args[compactIdx + 1]) : undefined;
348
- // L4 新原语(2026-08-30 任务书 §L4 收编 tools/)
410
+ // L4 新原语(2026-08-30 收编外部验证工具)
349
411
  const tailIdx = args.indexOf('--tail-renumber');
350
412
  const tailRenumberDelta = tailIdx >= 0 && args[tailIdx + 1] ? Number(args[tailIdx + 1]) : undefined;
351
413
  const neutralizeOrphan = args.includes('--neutralize-orphan');
@@ -365,12 +427,18 @@ function cmdFix(args) {
365
427
  // 日志报成 broken → 用户以为日志坏了去跑 `fix --apply` ⇒ 在健康日志上动手。这里直接拒绝。
366
428
  const head = readSessionHeader(file);
367
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
+ }
368
436
  if (fileVersion > HOST_MAX_FILE_VERSION) {
369
437
  fail(`拒绝修复:被检文件 version=${fileVersion} 高于本宿主 @deepseek-ai/dsh-session@${hostCapability().hostPackage} 支持的最大版本 ${HOST_MAX_FILE_VERSION}`
370
438
  + ` —— 本宿主评估不了该文件(不是"日志坏了")。请用 0.1.5+ 宿主修复。`, 3);
371
439
  }
372
440
 
373
- 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 });
374
442
  if (json) {
375
443
  out(JSON.stringify(result, null, 2) + '\n');
376
444
  process.exit(result.ok ? 0 : 1);