dsh-log-contract 0.3.13 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -10
- package/README.zh.md +40 -9
- package/bin/dsh-log-contract.mjs +75 -7
- package/docs/CONTRACTS.md +67 -18
- package/lib/checks.js +69 -19
- package/lib/contracts.js +78 -10
- package/lib/host-probes.js +147 -0
- package/lib/index.js +1 -0
- package/lib/log-reader.js +15 -2
- package/lib/prewrite.js +59 -8
- package/lib/repair.js +22 -5
- package/lib/validate.js +52 -1
- package/lib/version-support.js +148 -0
- package/lib/vocab.js +31 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,10 +37,9 @@ plugin marker semantics).
|
|
|
37
37
|
|
|
38
38
|
## Where it sits in the business layer
|
|
39
39
|
|
|
40
|
-
> **dsh-log-contract is the core capability component of
|
|
41
|
-
>
|
|
42
|
-
>
|
|
43
|
-
> lands on a legal log and `/compact` never breaks.
|
|
40
|
+
> **dsh-log-contract is the core capability component of [dsh-retrace](https://github.com/yamingmou/dsh-retrace)** (the business layer's "doctor" module): it **checks and repairs** session logs so every recall/edit/rollback lands on a legal log — pre-write validation (`prewrite`) rejects error-level violations **before** anything is written.
|
|
41
|
+
>
|
|
42
|
+
> **About `/compact` (stated honestly)**: what this tool guarantees is that **new writes** stop creating token-meter pairing debt — since two-segment atomic pairs (marker + paired segment) landed, new markers pass T1 by construction. **Legacy** (single-segment) markers are still **known design debt**: pre-write validation downgrades their T1 violation to a warning (`isKnownMarkerCandidate` in `lib/prewrite.js`, which applies to **historical markers only**), so such sessions need **one check + cleanup before compaction** (`check` to locate → `fix --remove-markers`; the plugin side calls this "the doctor"). Otherwise the T1 self-check blocks `/compact`.
|
|
44
43
|
|
|
45
44
|
| Layer | What it is | Components |
|
|
46
45
|
|---|---|---|
|
|
@@ -107,6 +106,34 @@ omitted for privacy.
|
|
|
107
106
|
|
|
108
107
|
---
|
|
109
108
|
|
|
109
|
+
## Version support (host / session format)
|
|
110
|
+
|
|
111
|
+
**Tested baseline (the versions this rule set was verified against)**:
|
|
112
|
+
|
|
113
|
+
| Item | Baseline | Note |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| Host package | `@deepseek-ai/dsh-session@0.1.5-rc.1` | version used on the dev box and in CI; the four `prepublishOnly` steps are green on it |
|
|
116
|
+
| Session format | **v3** (`SESSION_FORMAT_VERSION = 3`) | v3 uses the runtime vocabulary + official `foldSurface` final check |
|
|
117
|
+
| Declared range | `peerDependencies: ^0.1.0-rc.7 || ^0.1.5-rc.1` | installable — a declared range is **not** a per-version verification |
|
|
118
|
+
| Known formats | 0 / 1 / 2 / 3 | 0–2 are supported by this package's **vendored** vocabulary + local equivalent fold (`legacyFoldSurface`), independent of the host |
|
|
119
|
+
|
|
120
|
+
**What happens on unverified versions (detected and reported at runtime — never silently judged by stale rules)**:
|
|
121
|
+
|
|
122
|
+
| Case | Behaviour |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Host **newer than the baseline** | explicit **warning** (`host … is newer than the tested baseline …`); the verdict is still produced but flagged as possibly not applicable |
|
|
125
|
+
| Host older than the baseline (still in peer range) | notice + the host-capability gate decides whether v3 is assessable (`SESSION_FORMAT_VERSION` too low ⇒ `not-assessable`, exit 3) |
|
|
126
|
+
| **Unknown** session format (not in 0/1/2/3) | reported as **unverified format** and handled **read-only**: `check` runs structure rules only; `prewrite` / `fix` **refuse** (exit 3) |
|
|
127
|
+
| Format **unrecognisable** (no `header.version` and no distinguishing event shapes) | same: "unverified format" + read-only |
|
|
128
|
+
| File version above the host's supported maximum | `not-assessable` (**not** broken), exit 3; skipped/executed rules are listed |
|
|
129
|
+
|
|
130
|
+
Implementation: `lib/version-support.js` (`TESTED_BASELINE` / `detectSupport()`, exported in the public API).
|
|
131
|
+
Both the human-readable `check` output and the `--json` `support` field carry the baseline summary and the warnings, e.g.:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
version support: host @deepseek-ai/dsh-session@0.1.5-rc.1 (baseline 0.1.5-rc.1) | file format v3 (header, baseline)
|
|
135
|
+
```
|
|
136
|
+
|
|
110
137
|
## Installation
|
|
111
138
|
|
|
112
139
|
```bash
|
|
@@ -144,7 +171,7 @@ Sample output (the CLI reports in Chinese — it is the tool's UI language):
|
|
|
144
171
|
事件 204754 | surface 节点 16 | replace 代数 5 | 帧 8620(3439.5KiB → 8191.3KiB)
|
|
145
172
|
违规 1(error 1 / warning 0)
|
|
146
173
|
|
|
147
|
-
[error] S5 @ seq
|
|
174
|
+
[error] S5 @ seq <seq> / line <line> (assistant/message)
|
|
148
175
|
surface replace: sourceEventSeqs 必须覆盖每个被替换节点;缺失 121774, 121779(共 2 个)
|
|
149
176
|
|
|
150
177
|
❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
|
|
@@ -276,10 +303,10 @@ node scripts/check-local-fossils.mjs # scans ../ for backup-session-*.jsonl.zs
|
|
|
276
303
|
|
|
277
304
|
Known truth table (updated 2026-08-31 — after 0.3.5 added I1, `spliced-orphan`
|
|
278
305
|
now FAILs; the old PASS row was stale):
|
|
279
|
-
-
|
|
280
|
-
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
306
|
+
- `<session>-corrupt` / `<session>-seqgap` / `<session>-rewritten-230542` → FAIL (seq gaps)
|
|
307
|
+
- `<session>-spliced-orphan` → **FAIL (0.3.5+)** (T1/I1: invalid inbox splice + turn-null)
|
|
308
|
+
- `<session>-pre-markerfix-20260825` → FAIL (pre-fix sample: turn-null markers remain)
|
|
309
|
+
- `<session>-pre-cleanstate-20260831` → PASS (a multi-thousand-span replace marker is data-legal;
|
|
283
310
|
official foldSurface replays cleanly — see the plugin ledger)
|
|
284
311
|
The truth table tracks rule evolution (0.3.5's I1 flipped spliced-orphan PASS→FAIL);
|
|
285
312
|
"repaired sessions PASS" must be verified on rebuilt samples — `pre-` backups are
|
|
@@ -309,7 +336,7 @@ MIT © OfferKuai Team
|
|
|
309
336
|
|
|
310
337
|
---
|
|
311
338
|
|
|
312
|
-
## ⚠️ Upgrading to 0.3.
|
|
339
|
+
## ⚠️ Upgrading to 0.3.14 — behaviour changes you must know (0.3.12 → 0.3.14)
|
|
313
340
|
|
|
314
341
|
**1. New exit codes `3` / `4` — a defect fix that is also a breaking change.**
|
|
315
342
|
- `3` = **not assessable on this host**: the file's `header.version` is higher than the host supports
|
package/README.zh.md
CHANGED
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
|
|
31
31
|
## 它在业务层里的位置
|
|
32
32
|
|
|
33
|
-
> **dsh-log-contract 是 [dsh-retrace](https://github.com/yamingmou/dsh-retrace)
|
|
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
|
|
152
|
+
[error] S5 @ seq <seq> / line <line> (assistant/message)
|
|
122
153
|
surface replace: sourceEventSeqs 必须覆盖每个被替换节点;缺失 121774, 121779(共 2 个)
|
|
123
154
|
|
|
124
155
|
❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
|
|
@@ -240,12 +271,12 @@ node scripts/check-local-fossils.mjs # 扫描 ../ 下 backup-session-*.jsonl.z
|
|
|
240
271
|
|
|
241
272
|
| 化石 | 判定 | 违规 |
|
|
242
273
|
|---|---|---|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
|
|
|
274
|
+
| `<session>-corrupt` | FAIL | S8/C1/T1/E2(seq 缺口 → 加载被拒) |
|
|
275
|
+
| `<session>-seqgap` / `<session>-recorrupt` | FAIL | S8/C1/T1/E2/S9(seq 缺口/倒退) |
|
|
276
|
+
| `<session>-rewritten-230542` | FAIL | S8/C1/T1/E2/I1(重写引入缺口) |
|
|
277
|
+
| `<session>-spliced-orphan` | **FAIL(0.3.5+)** | T1/I1(inbox splice 无效 + turn-null)——旧表 PASS 已过时 |
|
|
278
|
+
| `<session>-pre-markerfix-20260825` | FAIL | T1×5(修复前样本:turn-null marker 残留,非「修复后」) |
|
|
279
|
+
| `<session>-pre-cleanstate-20260831` | **PASS** | error 0(数千跨度的 replace marker 数据合规,官方 foldSurface 重放通过) |
|
|
249
280
|
|
|
250
281
|
> 说明:真值表随规则演进更新(0.3.5 新增 I1 后 spliced-orphan 从 PASS 变 FAIL);
|
|
251
282
|
> 「修复后会话 PASS」需用重建/修复后的样本验证,pre- 前缀备份多为修复前坏样本。
|
|
@@ -266,7 +297,7 @@ MIT © OfferKuai Team
|
|
|
266
297
|
|
|
267
298
|
---
|
|
268
299
|
|
|
269
|
-
## ⚠️ 升级到 0.3.
|
|
300
|
+
## ⚠️ 升级到 0.3.14 —— 必须知道的行为变更(0.3.12 → 0.3.14)
|
|
270
301
|
|
|
271
302
|
**1. 新增退出码 `3` / `4`:本质是缺陷修复,形式上也是破坏性变更。**
|
|
272
303
|
- `3` = **不可在本宿主评估**:被检文件的 `header.version` 高于当前宿主支持的上限
|
package/bin/dsh-log-contract.mjs
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
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');
|
|
@@ -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);
|
package/docs/CONTRACTS.md
CHANGED
|
@@ -8,13 +8,20 @@
|
|
|
8
8
|
> 与 `lib/prewrite.js`(写前校验)中有对应实现;离线体检(`lib/validate.js`)
|
|
9
9
|
> 逐条执行并在最后用官方 `foldSurface` 终验(S8)。
|
|
10
10
|
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
11
|
+
> 规则来源:早期内部审计发现(59 条)+ 三层契约事故复盘 + 官方源码逐行核对
|
|
12
|
+
> (`@deepseek-ai/dsh-session`,各条出处见下)。后续规则随官方版本演进追加:
|
|
13
|
+
> T3/T4 = 渲染层白屏事故复盘,T5 = malformed turn/end 事故复盘。
|
|
14
|
+
>
|
|
15
|
+
> **版本支持基线(本规则集验证过的版本;权威常量见 `lib/version-support.js` 的 TESTED_BASELINE)**:
|
|
16
|
+
> 宿主 `@deepseek-ai/dsh-session@0.1.5-rc.1` | 会话格式 **v3**(`SESSION_FORMAT_VERSION = 3`)|
|
|
17
|
+
> 声明范围 `^0.1.0-rc.7 || ^0.1.5-rc.1`(可安装 ≠ 逐个验证)| 已知格式 0/1/2/3
|
|
18
|
+
> (0–2 由本包自带 vendored 词表 + `legacyFoldSurface` 支持,与宿主版本无关)。
|
|
19
|
+
> **未验证版本**:宿主比基线新 → 运行时告警;文件格式未知/无法识别 → 报"未验证格式"并**按只读处理**
|
|
20
|
+
> (`prewrite`/`fix` 拒绝,退出码 3);文件版本 > 宿主上限 → `not-assessable`(不是 broken)。
|
|
14
21
|
>
|
|
15
22
|
> 严重度:**error** = 违反即会话不可加载/写入被拒(fail-loud);**warning** = 合法但可疑。
|
|
16
23
|
|
|
17
|
-
## 规则索引(共
|
|
24
|
+
## 规则索引(共 44 条)
|
|
18
25
|
|
|
19
26
|
| id | 严重度 | 层级 | 规则 |
|
|
20
27
|
|---|---|---|---|
|
|
@@ -45,6 +52,9 @@
|
|
|
45
52
|
| T4 | error | engine | step/消息本体 turn 缺失(null/undefined)→ 渲染死循环 |
|
|
46
53
|
| T5 | error | engine | turn/end 必须带 data.reason.kind |
|
|
47
54
|
| E7 | warning | persistence | ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者) |
|
|
55
|
+
| E8 | warning | persistence | 事件信封键白名单(多余键:seed/restore 路径会拒) |
|
|
56
|
+
| E9 | error | persistence | system/message 必须带 plugin source |
|
|
57
|
+
| E10 | error | persistence | request/header 的 data.header 字段约束 |
|
|
48
58
|
| Z3 | warning | framing | 空会话文件(有 header 无事件)显式报出 |
|
|
49
59
|
| P3 | warning | plugin | tool/call ↔ tool/result 配对完整性(考古 B1) |
|
|
50
60
|
| P4 | warning | plugin | tool/result 输出结构可解析(考古 B2) |
|
|
@@ -56,6 +66,9 @@
|
|
|
56
66
|
| Z2 | error | framing | zstd 帧解码失败 = 单帧全损 |
|
|
57
67
|
| W1 | error | engine | wire 流:tool 消息必须跟在带 tool-call 的 assistant 消息之后 |
|
|
58
68
|
| W2 | error | engine | wire 流:user 文本不得插在 tool_calls 与其 tool 结果之间 |
|
|
69
|
+
| G1 | warning | migration | 迁移预检:v0 源文件的 subagent/descriptor.data.version 必须为 3 |
|
|
70
|
+
| G2 | warning | migration | 迁移预检:v0 源文件不得含词表外的历史事件类型(含 ignorable) |
|
|
71
|
+
| G3 | warning | migration | 迁移预检:v0 源 session/title 系列的 messageSeqs 必须引用更早的人类 user/message |
|
|
59
72
|
|
|
60
73
|
## 详细规则
|
|
61
74
|
|
|
@@ -68,8 +81,8 @@
|
|
|
68
81
|
### H2 — header 版本与必填字段
|
|
69
82
|
|
|
70
83
|
- **层级**: persistence | **严重度**: error
|
|
71
|
-
- **出处**: @deepseek-ai/dsh-session lib/index.js:1110-1125
|
|
72
|
-
- **契约**: header.version
|
|
84
|
+
- **出处**: @deepseek-ai/dsh-session lib/index.js:1110-1125;已知格式版本 0/1/2/3(App 2.0.9 内置 v0→v1→v2→v3 迁移,SESSION_FORMAT_VERSION=3)
|
|
85
|
+
- **契约**: header.version 必须为已知受支持版本(0/1/2/3);未知版本报 H2。id 为字符串;createdAt 为非负安全整数;cwd 若存在必须为绝对路径;origin 只能为 "subagent"。
|
|
73
86
|
|
|
74
87
|
### R1 — 每行必须是合法 JSON
|
|
75
88
|
|
|
@@ -104,13 +117,13 @@
|
|
|
104
117
|
### S9 — 文件物理序 seq 单调(多写入者交织现场特征)
|
|
105
118
|
|
|
106
119
|
- **层级**: persistence | **严重度**: error
|
|
107
|
-
- **出处**: 2026-08-28
|
|
120
|
+
- **出处**: 2026-08-28 实锤(某真实会话):文件物理序出现回退(大→小→更大);单进程 appendCore 断言 seq==cursor+i 且按 id 串行化不可能写出
|
|
108
121
|
- **契约**: 按文件物理行序要求展开后事件 seq 严格单调递增。E2 在排序后检查(loadSessionLog 会 sort),物理序倒退被掩盖;S9 在排序前按行序检查,非单调 = 多写入者/旧光标回放交织的直接现场证据,加载会被拒。
|
|
109
122
|
|
|
110
123
|
### I1 — inbox seed 相对重放(fork 边界孤儿 spliced)
|
|
111
124
|
|
|
112
125
|
- **层级**: engine | **严重度**: error
|
|
113
|
-
- **出处**: @deepseek-ai/dsh-agent lib/types/inbox.js:155-178 (apply/validate);2026-08-28
|
|
126
|
+
- **出处**: @deepseek-ai/dsh-agent lib/types/inbox.js:155-178 (apply/validate);2026-08-28 实锤(某两个真实会话):fork 边界 removedCount=1 孤儿
|
|
114
127
|
- **契约**: 从 header.seedLength 起重放 agent/inbox/spliced,next-turn/next-step 双队列;start+removedCount 不得超过队列长、不得产生重复 pending id。fork 时"移除父待处理提示词"的 splice 假设父会话 inbox,子会话 seed 相对空 inbox 上非法 → resume 被拒(invalid persisted inbox splice)。
|
|
115
128
|
|
|
116
129
|
### E3 — type 必须在已知词汇表内(或带 ignorable 标记)
|
|
@@ -129,7 +142,7 @@
|
|
|
129
142
|
|
|
130
143
|
- **层级**: persistence | **严重度**: error
|
|
131
144
|
- **出处**: @deepseek-ai/dsh-session lib/index.js:1273-1277 (assertSupportedRequestHeader)
|
|
132
|
-
- **契约**: request/header-delta 与 reason=fallback 的 request/header
|
|
145
|
+
- **契约**: request/header-delta 与 reason=fallback 的 request/header 是已删除的遗留格式。注意(R-F 订正复核 §1 E5):宿主 Session.append/appendLines 不看 type ⇒ 写入会成功、下一次读取才炸(依据 dsh-session@0.1.5-rc.1 lib/index.js:1170-1210 / persistence-jsonl:3046-3073),不是写入即被拒。
|
|
133
146
|
|
|
134
147
|
### E6 — 消息类事件消息形状
|
|
135
148
|
|
|
@@ -182,8 +195,8 @@
|
|
|
182
195
|
### S8 — 整日志 foldSurface 可重放
|
|
183
196
|
|
|
184
197
|
- **层级**: persistence | **严重度**: error
|
|
185
|
-
- **出处**: @deepseek-ai/dsh-session lib/index.js:444-455 (foldSurface)
|
|
186
|
-
- **契约**:
|
|
198
|
+
- **出处**: @deepseek-ai/dsh-session lib/index.js:444-455 (foldSurface, v3);v0/v1/v2 用本地等价实现 lib/legacy-fold.js(rc.7 lib/index.js:229-455 逐条移植)
|
|
199
|
+
- **契约**: 终验:按被检文件 header.version 选折叠器(v3 → 官方 foldSurface;v0/v1/v2 → 本地 legacyFoldSurface),不抛 = 持久化层通过。S1–S7 任何一条违反都会在此暴露。注意 0.1.5 的官方 foldSurface 是 v3 语义(replace 用 startSeq/endSeq、assistant/message 禁 sourceEventSeqs),对旧格式文件会误报,不可借用。
|
|
187
200
|
|
|
188
201
|
### T1 — token-meter 配对:assistant/message 与 step/end 必须匹配当前打开的 step/start
|
|
189
202
|
|
|
@@ -195,25 +208,25 @@
|
|
|
195
208
|
|
|
196
209
|
- **层级**: engine | **严重度**: error
|
|
197
210
|
- **出处**: @deepseek-ai/dsh-token-meter lib/index.js:634-650 (_estimateProviderAssistant,:645 belongs to another step)
|
|
198
|
-
- **契约**: token meter 重建 provider 输出时,逐条检查 assistant/message 的 sourceEventSeqs:指向 assistant/chunk 的引用必须与消息同 turn/step,且 seq 更早、不重复;跨 step 引用 → 官方抛 belongs to another step → 每次事件追加都重抛(consumedEvents 不前进)→ 刷屏压垮 host(2026-08-30
|
|
211
|
+
- **契约**: token meter 重建 provider 输出时,逐条检查 assistant/message 的 sourceEventSeqs:指向 assistant/chunk 的引用必须与消息同 turn/step,且 seq 更早、不重复;跨 step 引用 → 官方抛 belongs to another step → 每次事件追加都重抛(consumedEvents 不前进)→ 刷屏压垮 host(2026-08-30 实测:某真实会话的 chunk 源引用跨 step 7/8/9)。T1 只查 step 配对不查源引用,此条补盲区;修复用 fix --clip-crossstep。
|
|
199
212
|
|
|
200
213
|
### T3 — step 节点 key 唯一(同 turn 内 step/start 的 step 号不得复用)
|
|
201
214
|
|
|
202
215
|
- **层级**: engine | **严重度**: error
|
|
203
|
-
- **出处**: 复盘 2026-09-02
|
|
204
|
-
- **契约**: 客户端渲染消息列表从事件流构建节点,节点 key = data.turn:data.step。同 turn 内两个 step/start 的 step 号相同 → key 冲突 → React 渲染死循环 →
|
|
216
|
+
- **出处**: 复盘 2026-09-02 渲染层白屏事故:客户端渲染节点 key = turn:step,冲突 → React 渲染死循环(由离线自查脚本判出)
|
|
217
|
+
- **契约**: 客户端渲染消息列表从事件流构建节点,节点 key = data.turn:data.step。同 turn 内两个 step/start 的 step 号相同 → key 冲突 → React 渲染死循环 → 白屏/不展示(实测:同一 turn 内两个 step/start 复用 step 95/1,一个来自正常轮、一个来自编辑块;后续全量扫描又发现多个同型冲突)。修复:同 turn 内 step 递增、整块重编号(含块内 chunk/tool/assistant)。
|
|
205
218
|
|
|
206
219
|
### T4 — step/消息本体 turn 缺失(null/undefined)→ 渲染死循环
|
|
207
220
|
|
|
208
221
|
- **层级**: engine | **严重度**: error
|
|
209
|
-
- **出处**: 复盘
|
|
210
|
-
- **契约**: 客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 →
|
|
222
|
+
- **出处**: 复盘 2026-09-01 D8 事故:retrace 0.4.17 编辑块 turn:null(离线自查脚本判致命)
|
|
223
|
+
- **契约**: 客户端渲染状态机对 turn=null 的 step/start|step/end|assistant/message 无法归属任何 turn → 渲染死循环 → 白屏「载入历史」(实测:编辑块的 step/start + marker + step/end 连续若干行 turn 全为 null)。user/message 天然无 turn 不查;chunk 坐标可缺失不查。step/消息本体必须带真实 turn 号。
|
|
211
224
|
|
|
212
225
|
### T5 — turn/end 必须带 data.reason.kind
|
|
213
226
|
|
|
214
227
|
- **层级**: engine | **严重度**: error
|
|
215
|
-
- **出处**: 官方 dsh-agent-loop lib/index.js:620(turn/end = {turn, reason:{kind}});
|
|
216
|
-
- **契约**: 官方 validation 强制 turn/end 的 data.reason.kind 存在(kind ∈ completed|max-tokens|blocked|aborted|error|interrupted)。缺失 = malformed → 官方 SessionPersistenceCorruptionError →
|
|
228
|
+
- **出处**: 官方 dsh-agent-loop lib/index.js:620(turn/end = {turn, reason:{kind}});malformed turn/end 事故(2026-09-02,由离线自查脚本判出)
|
|
229
|
+
- **契约**: 官方 validation 强制 turn/end 的 data.reason.kind 存在(kind ∈ completed|max-tokens|blocked|aborted|error|interrupted)。缺失 = malformed → 官方 SessionPersistenceCorruptionError → 会话加载失败。实测:retrace 情形③信封 turn/end 漏 reason → 每次编辑后加载失败(已修 0.4.18)。
|
|
217
230
|
|
|
218
231
|
### E7 — ignorable 未知 type 合法性(带被忽略标记的未知事件须有消费者)
|
|
219
232
|
|
|
@@ -221,6 +234,24 @@
|
|
|
221
234
|
- **出处**: 反向挑刺 2026-09-09 T2(E3 ignorable 无合法性校验 = 后门)
|
|
222
235
|
- **契约**: 未知 type + ignorable:true 被读路径接纳但无人消费 = 静默垃圾。排除已知消费者白名单(retrace/marker、retrace/goal-marker、message-editor/ 前缀等 retrace 客户端消费的插件 marker)后,其余 ignorable 未知事件报 warning。
|
|
223
236
|
|
|
237
|
+
### E8 — 事件信封键白名单(多余键:seed/restore 路径会拒)
|
|
238
|
+
|
|
239
|
+
- **层级**: persistence | **严重度**: warning
|
|
240
|
+
- **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:849-861(assertSessionEventEnvelope)+ :1063-1068(唯一调用点=seed 路径);load 路径容忍见行为探针 p5
|
|
241
|
+
- **契约**: 事件对象只允许 7 个信封键(type/seq/time/data/surfaceOp/sourceEventSeqs/ignorable)。独立复核变异 05 指出"宿主拒、旧契约 0 违规";R-D 行为探针进一步订正口径:**load 路径容忍、seed/restore 路径拒** ⇒ warning。
|
|
242
|
+
|
|
243
|
+
### E9 — system/message 必须带 plugin source
|
|
244
|
+
|
|
245
|
+
- **层级**: persistence | **严重度**: error
|
|
246
|
+
- **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:942-944("must have plugin source");角色表 :917-926
|
|
247
|
+
- **契约**: v3 新增的 system/message:role 必须为 system,source.kind 必须为 plugin 且 plugin 非空。实测(变异 03):source.kind='user' 宿主拒、旧契约 0 违规 ⇒ 漏检。
|
|
248
|
+
|
|
249
|
+
### E10 — request/header 的 data.header 字段约束
|
|
250
|
+
|
|
251
|
+
- **层级**: persistence | **严重度**: error
|
|
252
|
+
- **出处**: @deepseek-ai/dsh-session@0.1.5-rc.1 lib/index.js:231-248(validateSessionEventData:omit header.system / omit empty tools / omit empty adapterDefaults)
|
|
253
|
+
- **契约**: request/header 必须省略 header.system(系统提示改走 system/message)、空 tools、空 adapterDefaults。实测(变异 12):带 header.system 的写入宿主拒、旧契约 0 违规 ⇒ 漏检。
|
|
254
|
+
|
|
224
255
|
### Z3 — 空会话文件(有 header 无事件)显式报出
|
|
225
256
|
|
|
226
257
|
- **层级**: framing | **严重度**: warning
|
|
@@ -287,3 +318,21 @@
|
|
|
287
318
|
- **出处**: OpenAI 兼容端点对 tool 消息顺序的严格校验;DSH 序列化器将混合 user 消息展开为 text 在前、tool-result 在后
|
|
288
319
|
- **契约**: 当仍有未满足的 assistant tool-call 时出现 user 文本消息,会产生 [assistant(tool_calls), user(text), tool] 序列,严格端点同样拒绝。
|
|
289
320
|
|
|
321
|
+
### G1 — 迁移预检:v0 源文件的 subagent/descriptor.data.version 必须为 3
|
|
322
|
+
|
|
323
|
+
- **层级**: migration | **严重度**: warning
|
|
324
|
+
- **出处**: @deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 lib/index.js:1584-1586(assertReleasedEventPayload):data.version !== 3 且源版本 === 0 → SessionFormatUnsupportedMigrationError("uses unsupported descriptor version N");源版本 1/2 时官方提前 return(容忍)
|
|
325
|
+
- **契约**: 文件版本 0 且事件类型为 subagent/descriptor 且 data.version !== 3 → 官方 v0→v1 迁移直接拒绝(消息形如 `subagent/descriptor <seq> uses unsupported descriptor version 2`);源版本 1/2 不受此条约束。
|
|
326
|
+
|
|
327
|
+
### G2 — 迁移预检:v0 源文件不得含词表外的历史事件类型(含 ignorable)
|
|
328
|
+
|
|
329
|
+
- **层级**: migration | **严重度**: warning
|
|
330
|
+
- **出处**: @deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 lib/index.js:1580-1583(assertReleasedEventPayload):RELEASED_V0_EVENT_DISPOSITIONS 里没有该 type → "format v0 contains unknown historical event type … migration refuses unknown historical events even when ignorable"
|
|
331
|
+
- **契约**: 文件版本 0 且事件 type 不在官方 v0 dispositions(本包 vendored 为 V0_EVENT_TYPES)内 → 官方迁移拒绝,**即使该事件带 ignorable:true**。E3 的 ignorable 豁免是**读取路径**语义(不改),本条只在迁移预检维度表达。
|
|
332
|
+
|
|
333
|
+
### G3 — 迁移预检:v0 源 session/title 系列的 messageSeqs 必须引用更早的人类 user/message
|
|
334
|
+
|
|
335
|
+
- **层级**: migration | **严重度**: warning
|
|
336
|
+
- **出处**: @deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 lib/index.js:2543-2556(assertTitleSources):`session/title` 的 messageSeqs 为空 ⟺ source.kind === "user";每个被引 seq 必须解析到 `user/message` 且其 `data.source.kind === "user"`,否则 "messageSeqs must cite earlier human user/message events" / "must be empty exactly for a user title"
|
|
337
|
+
- **契约**: 文件版本 0 且事件为 session/title 或 session/title-llm-request:messageSeqs 必须是数组;session/title 的"空数组 ⟺ 用户标题"必须成立;每个被引 seq 必须是更早的 user/message 且 source.kind === "user"。违反 → 官方升级到当前格式时拒绝。**应用面说明**:官方 `assertTitleSources` 与 turn/start 状态机同在 `assertReleasedArtifactRelationships`,由 v1→v2 在**变换后的 v1/v2 artifact** 上调用(`dsh-session-format-v1-to-v2/lib/index.js:104`);对 messageSeqs 这类按 seq 索引 + 类型/source 判定的引用,变换保序保类型 ⇒ 在原始 v0 上判是必要条件的近似,实测 281 真实 v0 上 0 误报。
|
|
338
|
+
|