dsh-log-contract 0.3.11 → 0.3.13
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 +32 -0
- package/README.zh.md +28 -0
- package/bin/dsh-log-contract.mjs +194 -64
- package/lib/checks.js +151 -23
- package/lib/compat.js +282 -0
- package/lib/contracts.js +36 -4
- package/lib/index.js +2 -1
- package/lib/legacy-fold.js +177 -0
- package/lib/log-reader.js +8 -4
- package/lib/prewrite.js +33 -15
- package/lib/repair.js +70 -31
- package/lib/validate.js +243 -13
- package/lib/vocab.js +193 -0
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -306,3 +306,35 @@ usually pre-fix bad samples. See the boundary note in
|
|
|
306
306
|
## License
|
|
307
307
|
|
|
308
308
|
MIT © OfferKuai Team
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## ⚠️ Upgrading to 0.3.12 — behaviour changes you must know
|
|
313
|
+
|
|
314
|
+
**1. New exit codes `3` / `4` — a defect fix that is also a breaking change.**
|
|
315
|
+
- `3` = **not assessable on this host**: the file's `header.version` is higher than the host supports
|
|
316
|
+
(e.g. checking a v3 log under `@deepseek-ai/dsh-session@0.1.0-rc.7`). Instead of reporting `broken`,
|
|
317
|
+
the tool now says the host lacks the capability, and `fix --apply` **refuses** the file (nothing written, no backup).
|
|
318
|
+
- `4` = **migration pre-check blocked**: the tool judges that the official upgrade chain will refuse the file.
|
|
319
|
+
- ⚠️ **Pipelines that treated `exit 0` as "fine" will now fail.** The old behaviour returned `0` even for files
|
|
320
|
+
the tool itself reported as refused by the official chain — a false negative where the exit code contradicted
|
|
321
|
+
the verdict. `--fail-on-migration` is an **explicit alias of the (now default) behaviour**; the **real opt-out
|
|
322
|
+
is `--no-fail-on-migration`**, which restores the 0.3.11 behaviour (structural green ⇒ `exit 0`). When both
|
|
323
|
+
flags are given, the opt-out wins. You can also ignore the exit code and read `assessmentScope` /
|
|
324
|
+
`migration.ready` from `--json` with your own threshold.
|
|
325
|
+
|
|
326
|
+
**2. `--json` gains `assessmentScope` (three values).**
|
|
327
|
+
|
|
328
|
+
| value | meaning |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `full` | file is already the host's target format (no migration) — coverage is complete(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
|
|
331
|
+
| `partial` | file needs migration ⇒ the migration pre-check covers **2 rules only; 6 classes are uncovered** (≠ pass; see `coverage.uncovered`) |
|
|
332
|
+
| `none` | **not assessable on this host** — no "usable / upgradable" claim is certified |
|
|
333
|
+
|
|
334
|
+
Under `partial` the CLI no longer prints an unconditional green: both the migration line and the last line say
|
|
335
|
+
the migration dimension is only partially covered.
|
|
336
|
+
|
|
337
|
+
**3. What "the health check passed" does *not* mean.**
|
|
338
|
+
The rule set is **not** the official upgrade chain's acceptance condition. `assessmentScope=partial` means
|
|
339
|
+
"the part this tool looked at is clean" — it does **not** mean the official upgrade will accept the file.
|
|
340
|
+
Read `coverage.uncovered` for the known gaps.
|
package/README.zh.md
CHANGED
|
@@ -263,3 +263,31 @@ node scripts/check-local-fossils.mjs # 扫描 ../ 下 backup-session-*.jsonl.z
|
|
|
263
263
|
## 许可
|
|
264
264
|
|
|
265
265
|
MIT © OfferKuai Team
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## ⚠️ 升级到 0.3.12 —— 必须知道的行为变更
|
|
270
|
+
|
|
271
|
+
**1. 新增退出码 `3` / `4`:本质是缺陷修复,形式上也是破坏性变更。**
|
|
272
|
+
- `3` = **不可在本宿主评估**:被检文件的 `header.version` 高于当前宿主支持的上限
|
|
273
|
+
(例如在 `@deepseek-ai/dsh-session@0.1.0-rc.7` 上检查 v3 日志)。此时**不再误报 `broken`**,
|
|
274
|
+
而是明确说明宿主能力不足;`fix --apply` 会**拒绝**该文件(不落盘、不写备份)。
|
|
275
|
+
- `4` = **迁移预检 blocked**:本工具判断该文件会被官方升级链拒绝。
|
|
276
|
+
- ⚠️ **把 `exit 0` 当"过"的流水线会由过转挂。** 旧行为在**工具自己就报告"官方会拒"**的文件上仍返回 `0`
|
|
277
|
+
—— 退出码与结论矛盾的假阴性。`--fail-on-migration` 是**默认已生效行为的显式别名**;
|
|
278
|
+
**真正的 opt-out 是 `--no-fail-on-migration`**,它恢复 0.3.11 的行为(结构绿即 `exit 0`)。
|
|
279
|
+
两者同时给时 **opt-out 优先**。也可改读 `--json` 的 `assessmentScope` / `migration.ready` 自定门槛。
|
|
280
|
+
|
|
281
|
+
**2. `--json` 新增 `assessmentScope`(三值)。**
|
|
282
|
+
|
|
283
|
+
| 值 | 含义 |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `full` | 被检文件已是当前宿主的目标格式(无需迁移),覆盖完整(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
|
|
286
|
+
| `partial` | 需要迁移 ⇒ 迁移预检**只覆盖 2 条规则,另有 6 类未覆盖**(≠ 通过;见 `coverage.uncovered`) |
|
|
287
|
+
| `none` | **不可在本宿主评估**(宿主能力不足),不认证任何"可用/可升级"结论 |
|
|
288
|
+
|
|
289
|
+
`partial` 时 CLI 不再给无条件绿:迁移行与末行都会写明"迁移维度仅部分覆盖"。
|
|
290
|
+
|
|
291
|
+
**3.「体检通过」到哪一步为止。**
|
|
292
|
+
本工具的规则集**不等于**官方升级链的接受条件。`assessmentScope=partial` 只表示"本工具看过的那部分没问题",
|
|
293
|
+
**不构成**"官方升级会接受"。已知未覆盖的 6 类见 `coverage.uncovered`。
|
package/bin/dsh-log-contract.mjs
CHANGED
|
@@ -13,17 +13,56 @@
|
|
|
13
13
|
* contracts 列出内置契约规则目录
|
|
14
14
|
*/
|
|
15
15
|
import fs from 'node:fs';
|
|
16
|
-
import { loadSessionLog, validateSessionLog, resumeVerdict, createPreWriter, repairSession, CONTRACT_RULES, ruleById, extractToolOutputs, auditToolCalls } from '../lib/index.js';
|
|
16
|
+
import { loadSessionLog, validateSessionLog, resumeVerdict, migrationVerdict, assessmentScope, createPreWriter, repairSession, readSessionHeader, CONTRACT_RULES, ruleById, extractToolOutputs, auditToolCalls, hostCapability, HOST_MAX_FILE_VERSION } from '../lib/index.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 同步写 fd(F4 修复)——`process.stdout.write` 在**管道**下是异步的,紧跟着的
|
|
20
|
+
* `process.exit()` 不会等待 flush:>~64KiB 的输出会丢尾,`--json` 因此变成非法 JSON
|
|
21
|
+
* (独立审核在 3000 事件夹具与真实 14.5MB v3 会话上复现,stdout 56648B 处 "Unterminated string")。
|
|
22
|
+
* 这里直接同步写 fd 1/2(非阻塞管道 EAGAIN 时自旋重试),做到"先写完再 exit"。
|
|
23
|
+
*/
|
|
24
|
+
function writeAllSync(fd, text) {
|
|
25
|
+
const buf = Buffer.from(String(text), 'utf8');
|
|
26
|
+
let offset = 0;
|
|
27
|
+
while (offset < buf.length) {
|
|
28
|
+
try {
|
|
29
|
+
offset += fs.writeSync(fd, buf, offset, buf.length - offset);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err.code === 'EAGAIN') continue;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const out = (text) => writeAllSync(1, text);
|
|
37
|
+
const errOut = (text) => writeAllSync(2, text);
|
|
17
38
|
|
|
18
39
|
const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log contract guard)
|
|
19
40
|
|
|
20
41
|
用法:
|
|
21
|
-
dsh-log-contract check <session-log> [--json] [--max-details N] [--resume]
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
dsh-log-contract check <session-log> [--json] [--max-details N] [--resume] [--fail-on-migration] [--no-fail-on-migration]
|
|
43
|
+
离线体检 + **迁移预检**(两个**独立维度**,互不蕴含)。session-log 支持 .jsonl 与 .jsonl.zstd。
|
|
44
|
+
⚠️ 口径边界:本工具只判定它实现的规则集。「ok」/「可加载」/「可继续」/「可压缩」
|
|
45
|
+
**不代表**会话"可用",**不代表**可安全编辑,也**不代表**官方升级路径会接受
|
|
46
|
+
(迁移是独立维度,覆盖边界见 contracts 输出与 lib/contracts.js 的 G 段)。
|
|
47
|
+
--json 输出机器可读 JSON 报告(含 host / assessable / assessmentScope / migration 字段)
|
|
24
48
|
--max-details N 每条违规最多列 N 个缺失 seq(默认 8,--json 忽略)
|
|
25
|
-
--resume
|
|
26
|
-
|
|
49
|
+
--resume 输出分层结论:可加载 / 可继续 / 可压缩(本工具规则集内)
|
|
50
|
+
+ 迁移预检(官方 v0/v1/v2→当前格式会不会拒);--json 时附带。
|
|
51
|
+
--fail-on-migration migration.ready===false(工具判"官方升级会拒")时退出码 4。
|
|
52
|
+
**注意:默认已经是这个行为**(第四轮把"报告与退出码不一致"的假阴性修掉了);
|
|
53
|
+
本开关是**显式别名**,便于 CI 把意图写进命令行。
|
|
54
|
+
--no-fail-on-migration **真 opt-out**:恢复 0.3.11 的行为——迁移 blocked 不再改退出码
|
|
55
|
+
(结构绿即 exit 0)。两者同时给时 **opt-out 优先**。若你的流水线依赖旧退出码,
|
|
56
|
+
用本开关;也可改读 --json 的 assessmentScope / migration.ready 自定门槛。
|
|
57
|
+
评估范围 assessmentScope(顶层字段,与 migration.assessmentScope 同值):
|
|
58
|
+
'full' = 被检文件**已是当前官方格式(version=3)、无需迁移** ⇒ 工具规则集对该文件是完整的;
|
|
59
|
+
'partial' = **需要迁移**(version<3):迁移维度只覆盖 2 条规则,另有 6 类未覆盖 ⇒
|
|
60
|
+
**不构成"官方升级会接受"**;'none' = 本宿主能力不足以评估(文件版本 > 宿主上限)。
|
|
61
|
+
机器应判 assessmentScope == "full" 才可把绿读作完整评估(partial/none 都不该)。
|
|
62
|
+
退出码:0 本工具认为可加载且无迁移阻断;1 有 error 级违规;3 不可在本宿主评估
|
|
63
|
+
(被检文件版本 > 宿主支持上限,如 rc.7 宿主的 v3 文件——**不是** broken);
|
|
64
|
+
4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false;可用
|
|
65
|
+
--no-fail-on-migration 退出该档)。
|
|
27
66
|
|
|
28
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]
|
|
29
68
|
诊断 + 修复(2026-08 事故固化方案)。先做严格 seq 连续扫描 + 契约体检
|
|
@@ -74,29 +113,73 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
|
|
|
74
113
|
`;
|
|
75
114
|
|
|
76
115
|
function fail(message, code = 1) {
|
|
77
|
-
|
|
116
|
+
errOut(`${message}\n`);
|
|
78
117
|
process.exit(code);
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
function printViolations(violations, maxDetails = 8) {
|
|
82
121
|
if (violations.length === 0) {
|
|
83
|
-
|
|
122
|
+
out(' ✔ 无违规\n');
|
|
84
123
|
return;
|
|
85
124
|
}
|
|
86
125
|
for (const v of violations) {
|
|
87
126
|
const loc = [v.seq !== null ? `seq ${v.seq}` : null, v.lineNo !== null ? `line ${v.lineNo}` : null]
|
|
88
127
|
.filter(Boolean).join(' / ');
|
|
89
128
|
const head = ` [${v.severity}] ${v.id} ${loc ? `@ ${loc}` : ''}${v.eventType ? ` (${v.eventType})` : ''}`;
|
|
90
|
-
|
|
129
|
+
out(`${head}\n ${v.message}\n`);
|
|
91
130
|
if (Array.isArray(v.missingSeqs) && v.missingSeqs.length > maxDetails) {
|
|
92
|
-
|
|
131
|
+
out(` …另有 ${v.missingSeqs.length - maxDetails} 个缺失 seq 未列出\n`);
|
|
93
132
|
}
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
135
|
|
|
136
|
+
/** S2 横幅:被检文件版本 > 宿主支持上限 ⇒ "不可在本宿主评估"(**不是** broken)。 */
|
|
137
|
+
function notAssessableBanner(result) {
|
|
138
|
+
const na = result.notAssessable ?? {};
|
|
139
|
+
return `\n⚠️ 不可在本宿主评估(not-assessable)\n`
|
|
140
|
+
+ ` 被检文件 version=${na.fileVersion} > 本宿主支持的最大文件版本 ${na.hostMaxFileVersion}\n`
|
|
141
|
+
+ ` 宿主 @deepseek-ai/dsh-session@${na.hostPackage}\n`
|
|
142
|
+
+ ` 原因:${na.reason}\n`
|
|
143
|
+
+ ` 已跳过规则:${(na.skippedRules ?? []).join(', ')}\n`
|
|
144
|
+
+ ` 已执行:结构层(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`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 迁移预检(独立维度)文本。
|
|
149
|
+
* @param {object} mig `migrationVerdict()` 结果
|
|
150
|
+
* @param {boolean} partial 待迁移文件(`migration.applies===true`)⇒ 迁移维度只部分覆盖,
|
|
151
|
+
* 不得给无条件绿(2026-09-14 最终复核规格 (i)+(iii))。
|
|
152
|
+
*/
|
|
153
|
+
function migrationLine(mig, partial = false) {
|
|
154
|
+
if (!mig) return '';
|
|
155
|
+
if (mig.assessable === false) return ` 迁移预检:不可评估(本宿主能力不足)\n`;
|
|
156
|
+
if (mig.applies === false) return ` 迁移预检:— 不适用(${mig.reason ?? ''})\n`;
|
|
157
|
+
const n = mig.blocked?.length ?? 0;
|
|
158
|
+
let s = mig.ready
|
|
159
|
+
? (partial
|
|
160
|
+
? ` 迁移预检(独立维度,≠"可用/可升级"):⚠️ 仅覆盖 ${mig.coverage.implemented.length} 条规则;另有 ${mig.coverage.uncovered.length} 类未覆盖(未覆盖 ≠ 通过)—— **不构成"升级会接受"**\n`
|
|
161
|
+
: ` 迁移预检(独立维度,≠"可用/可升级"):✅ 通过(仅限本工具覆盖范围)\n`)
|
|
162
|
+
: ` 迁移预检(独立维度,≠"可用/可升级"):❌ blocked(${n} 项:官方升级路径会拒)\n`;
|
|
163
|
+
for (const v of (mig.blocked ?? []).slice(0, 5)) {
|
|
164
|
+
s += ` [${v.id}] seq ${v.seq ?? '-'}${v.eventType ? ` (${v.eventType})` : ''} — ${String(v.message).slice(0, 130)}\n`;
|
|
165
|
+
}
|
|
166
|
+
if (n > 5) s += ` …另有 ${n - 5} 项\n`;
|
|
167
|
+
s += ` 覆盖边界:已覆盖 ${mig.coverage.implemented.length} 条;未覆盖 ${mig.coverage.uncovered.length} 类官方迁移规则(未覆盖 ≠ 通过;判据 ${mig.coverage.note})\n`;
|
|
168
|
+
// 规格 (iii):把未覆盖类名直接列出来,人不必开 --json 才知道边界。
|
|
169
|
+
if (partial) {
|
|
170
|
+
for (const [i, c] of (mig.coverage.uncovered ?? []).entries()) s += ` ${i + 1}. ${c}\n`;
|
|
171
|
+
}
|
|
172
|
+
return s;
|
|
173
|
+
}
|
|
174
|
+
|
|
97
175
|
function cmdCheck(args) {
|
|
98
176
|
const json = args.includes('--json');
|
|
99
177
|
const resume = args.includes('--resume');
|
|
178
|
+
// 第四轮/第五轮:`--fail-on-migration` 是**默认已生效行为**的显式别名;
|
|
179
|
+
// `--no-fail-on-migration` 是**真 opt-out**(恢复旧行为:迁移 blocked 不再改退出码)。
|
|
180
|
+
// 两者同时给 ⇒ opt-out 优先(help/README 写明)。
|
|
181
|
+
const failOnMigration = args.includes('--fail-on-migration');
|
|
182
|
+
const noFailOnMigration = args.includes('--no-fail-on-migration');
|
|
100
183
|
const maxDetailsIdx = args.indexOf('--max-details');
|
|
101
184
|
const maxDetails = maxDetailsIdx >= 0 && args[maxDetailsIdx + 1] ? Number(args[maxDetailsIdx + 1]) : 8;
|
|
102
185
|
const file = args.find((a) => !a.startsWith('-'));
|
|
@@ -110,43 +193,76 @@ function cmdCheck(args) {
|
|
|
110
193
|
}
|
|
111
194
|
const result = validateSessionLog(log);
|
|
112
195
|
const { summary, violations, ok } = result;
|
|
196
|
+
const notAssessable = result.assessable === false;
|
|
197
|
+
const migration = result.migration ?? migrationVerdict(result);
|
|
198
|
+
// 第四轮:机器可判的评估范围(partial = 待迁移文件,迁移维度只部分覆盖)。
|
|
199
|
+
const scope = result.assessmentScope ?? assessmentScope(result);
|
|
200
|
+
const partial = scope === 'partial';
|
|
201
|
+
const migBlocked = migration?.applies === true && migration.ready === false;
|
|
202
|
+
// 默认:`ready===false` ⇒ 非 0(第四轮修复的假阴性)。
|
|
203
|
+
// `--fail-on-migration` = 显式别名(同默认);`--no-fail-on-migration` = 真 opt-out(恢复旧行为)。
|
|
204
|
+
const migGate = !noFailOnMigration && (migBlocked || (failOnMigration && migration?.ready === false));
|
|
205
|
+
// 退出码:3 不可评估 > 1 结构 error > 4 迁移 blocked > 0。
|
|
206
|
+
const exitCode = notAssessable ? 3 : (!ok ? 1 : (migGate ? 4 : 0));
|
|
113
207
|
|
|
114
208
|
if (json) {
|
|
115
|
-
const payload = { file, ok, summary, violations };
|
|
209
|
+
const payload = { file, ok, assessable: !notAssessable, assessmentScope: scope, host: hostCapability(), migration, summary, violations };
|
|
210
|
+
if (notAssessable) payload.notAssessable = result.notAssessable;
|
|
116
211
|
if (resume) payload.resume = resumeVerdict(result);
|
|
117
|
-
|
|
118
|
-
process.exit(
|
|
212
|
+
out(JSON.stringify(payload, null, 2) + '\n');
|
|
213
|
+
process.exit(exitCode);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (notAssessable) {
|
|
217
|
+
out(notAssessableBanner(result));
|
|
218
|
+
printViolations(violations, maxDetails);
|
|
219
|
+
process.exit(3);
|
|
119
220
|
}
|
|
120
221
|
|
|
121
222
|
if (resume) {
|
|
122
223
|
const v = resumeVerdict(result);
|
|
123
224
|
const tier = v.verdict;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
225
|
+
// partial(待迁移文件)时不用 ✅:避免"全局绿"被读成"官方升级会接受"——用中性 ✔。
|
|
226
|
+
const passMark = partial ? '✔' : '✅';
|
|
227
|
+
const icons = { loadable: `${passMark} 可加载`, resumable: `${passMark} 可继续`, compactable: `${passMark} 可压缩`, broken: '❌ 不可用' };
|
|
228
|
+
out(`\n📋 dsh-log-contract check --resume —— ${file}\n`);
|
|
229
|
+
out(` 事件 ${summary.events} | surface 节点 ${summary.surfaceNodes} | replace 代数 ${summary.replaceGeneration} | 帧 ${summary.frames}(${(summary.compressedBytes / 1024).toFixed(1)}KiB → ${(summary.plaintextBytes / 1024).toFixed(1)}KiB)\n`);
|
|
230
|
+
out(` 违规 ${summary.total}(error ${summary.bySeverity.error} / warning ${summary.bySeverity.warning})\n\n`);
|
|
231
|
+
out(` 分层结论(**本工具规则集内**,不代表"可用",也不代表官方升级会接受):\n`);
|
|
129
232
|
const tiers = [
|
|
130
233
|
['可加载 loadable', v.loadable, '会话能被 DSH 读入(结构规则 S1-S9/E/W 全绿)', v.blocking.loadable],
|
|
131
234
|
['可继续 resumable', v.resumable, 'resume/followup 可用(结构 + I1 inbox 重放绿)', v.blocking.resumable],
|
|
132
235
|
['可压缩 compactable', v.compactable, '/compact 与压力测量可用(前两档 + T1/T2 token-meter 配对绿)', v.blocking.compactable],
|
|
133
236
|
];
|
|
134
237
|
for (const [name, pass, desc, blockers] of tiers) {
|
|
135
|
-
const mark = pass ?
|
|
136
|
-
|
|
137
|
-
if (blockers.length > 0)
|
|
238
|
+
const mark = pass ? passMark : '❌';
|
|
239
|
+
out(` ${mark} ${name} — ${desc}\n`);
|
|
240
|
+
if (blockers.length > 0) out(` 阻断: ${[...new Set(blockers)].join(', ')}\n`);
|
|
138
241
|
}
|
|
139
|
-
|
|
242
|
+
out(migrationLine(migration, partial));
|
|
243
|
+
out(`\n 结论: ${icons[tier]}${migBlocked
|
|
244
|
+
? '(本工具规则集内)—— 但**迁移预检 blocked**:不得据此宣称"可升级"'
|
|
245
|
+
: partial
|
|
246
|
+
? '(本工具规则集内)—— 但**迁移维度仅部分覆盖**(assessmentScope=partial):不得据此宣称"可升级"'
|
|
247
|
+
: v.verdict === 'compactable' ? ' —— 本工具规则集内可安全继续使用' : v.verdict === 'broken' ? ' —— 见上方违规明细(error 级 = 会话不可读/不可写)' : ' —— 部分能力受限'}\n\n`);
|
|
140
248
|
printViolations(violations, maxDetails);
|
|
141
|
-
process.exit(
|
|
249
|
+
process.exit(exitCode);
|
|
142
250
|
}
|
|
143
251
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
252
|
+
out(`\n📋 dsh-log-contract check —— ${file}\n`);
|
|
253
|
+
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
|
+
out(` 违规 ${summary.total}(error ${summary.bySeverity.error} / warning ${summary.bySeverity.warning})\n`);
|
|
255
|
+
out(migrationLine(migration, partial));
|
|
256
|
+
out('\n');
|
|
147
257
|
printViolations(violations, maxDetails);
|
|
148
|
-
|
|
149
|
-
|
|
258
|
+
out(`\n${ok
|
|
259
|
+
? (migBlocked
|
|
260
|
+
? '⚠️ 本工具认为可加载(结构层绿),但**迁移预检 blocked** —— 不得据此宣称"可用/可升级";详见上方迁移预检'
|
|
261
|
+
: partial
|
|
262
|
+
? '⚠️ 本工具规则集内未发现违规;**迁移维度仅部分覆盖**(assessmentScope=partial,不代表官方升级会接受)—— 详见上方迁移预检的覆盖边界'
|
|
263
|
+
: '✅ 通过(本工具规则集内:结构层绿)')
|
|
264
|
+
: '❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)'}\n\n`);
|
|
265
|
+
process.exit(exitCode);
|
|
150
266
|
}
|
|
151
267
|
|
|
152
268
|
function cmdPrewrite(args) {
|
|
@@ -170,12 +286,17 @@ function cmdPrewrite(args) {
|
|
|
170
286
|
fail(`会话日志读取失败:${err.message}`);
|
|
171
287
|
}
|
|
172
288
|
const baseline = validateSessionLog(log);
|
|
289
|
+
// S2:本宿主评估不了的文件**不能**给出写前结论(否则会拿假阳性去拦合法写入)
|
|
290
|
+
if (baseline.assessable === false) {
|
|
291
|
+
fail(`不可在本宿主评估该会话(${baseline.notAssessable?.reason ?? '宿主能力不足'})——写前校验无法给出可信结论;请用 0.1.5+ 宿主`, 3);
|
|
292
|
+
}
|
|
173
293
|
if (!baseline.ok) {
|
|
174
294
|
// 基线已坏:写前校验无法在坏基线上给出可信结论
|
|
175
295
|
fail(`基线会话已有 ${baseline.summary.bySeverity.error} 个 error 级违规,请先修复基线再校验写入(安全修复协议第 2 步:改前基线必须绿)`);
|
|
176
296
|
}
|
|
177
297
|
|
|
178
|
-
|
|
298
|
+
// C2:把被检文件的 header 显式交给 prewriter(版本不再靠模块级全局;缺省也能自行推断)
|
|
299
|
+
const prewriter = createPreWriter({ events: log.events.map((e) => e.event), header: log.header });
|
|
179
300
|
let result;
|
|
180
301
|
if (Object.hasOwn(plan, 'append')) {
|
|
181
302
|
result = prewriter.validateAppend(plan.append);
|
|
@@ -189,26 +310,26 @@ function cmdPrewrite(args) {
|
|
|
189
310
|
}
|
|
190
311
|
|
|
191
312
|
if (json) {
|
|
192
|
-
|
|
313
|
+
out(JSON.stringify({ file, op: result.op, ok: result.ok, bySeverity: result.bySeverity, violations: result.violations }, null, 2) + '\n');
|
|
193
314
|
process.exit(result.ok ? 0 : 1);
|
|
194
315
|
}
|
|
195
316
|
|
|
196
|
-
|
|
317
|
+
out(`\n✍️ dsh-log-contract prewrite —— ${file}(op: ${result.op},nextSeq: ${prewriter.nextSeq})\n\n`);
|
|
197
318
|
if (result.ok) {
|
|
198
|
-
|
|
199
|
-
|
|
319
|
+
out(' ✅ 写入安全:三层契约全绿(持久化 foldSurface 可重放 / 引擎层无崩溃风险 / 插件 marker 语义自洽)\n');
|
|
320
|
+
out(` 写入后 surface 节点 ${result.stateAfter.surfaceNodes} 个,nextSeq ${result.stateAfter.nextSeq}\n\n`);
|
|
200
321
|
process.exit(0);
|
|
201
322
|
}
|
|
202
|
-
|
|
323
|
+
out(' ❌ 写入会被拒:\n');
|
|
203
324
|
printViolations(result.violations);
|
|
204
|
-
|
|
325
|
+
out('\n');
|
|
205
326
|
process.exit(1);
|
|
206
327
|
}
|
|
207
328
|
|
|
208
329
|
function cmdContracts() {
|
|
209
|
-
|
|
330
|
+
out('dsh-log-contract 契约规则目录(spec:59 条审计发现 + 官方源码逐行核对)\n\n');
|
|
210
331
|
for (const r of CONTRACT_RULES) {
|
|
211
|
-
|
|
332
|
+
out(` ${r.id} [${r.severity}/${r.layer}] ${r.title}\n ${r.description}\n 出处: ${r.source}\n\n`);
|
|
212
333
|
}
|
|
213
334
|
}
|
|
214
335
|
|
|
@@ -240,26 +361,35 @@ function cmdFix(args) {
|
|
|
240
361
|
const file = args.find((a) => !a.startsWith('-'));
|
|
241
362
|
if (!file) fail(USAGE);
|
|
242
363
|
|
|
364
|
+
// S2 安全闸:本宿主评估不了的文件**不许**修。危害路径(独立复核指出):rc.7 上把健康 v3
|
|
365
|
+
// 日志报成 broken → 用户以为日志坏了去跑 `fix --apply` ⇒ 在健康日志上动手。这里直接拒绝。
|
|
366
|
+
const head = readSessionHeader(file);
|
|
367
|
+
const fileVersion = Number.isSafeInteger(head?.version) ? head.version : 0;
|
|
368
|
+
if (fileVersion > HOST_MAX_FILE_VERSION) {
|
|
369
|
+
fail(`拒绝修复:被检文件 version=${fileVersion} 高于本宿主 @deepseek-ai/dsh-session@${hostCapability().hostPackage} 支持的最大版本 ${HOST_MAX_FILE_VERSION}`
|
|
370
|
+
+ ` —— 本宿主评估不了该文件(不是"日志坏了")。请用 0.1.5+ 宿主修复。`, 3);
|
|
371
|
+
}
|
|
372
|
+
|
|
243
373
|
const result = repairSession(file, { removeMarkers, neutralize, clipCrossStep, dropFailedTurns, trimLast, trimBudget, compactLast, tailRenumberDelta, neutralizeOrphan, extractTurn, extractTurnTo, keepRanges, apply, backupDir });
|
|
244
374
|
if (json) {
|
|
245
|
-
|
|
375
|
+
out(JSON.stringify(result, null, 2) + '\n');
|
|
246
376
|
process.exit(result.ok ? 0 : 1);
|
|
247
377
|
}
|
|
248
378
|
|
|
249
|
-
|
|
250
|
-
|
|
379
|
+
out(`\n🔧 dsh-log-contract fix —— ${file}\n`);
|
|
380
|
+
out(` 诊断:${result.issues.length === 0 ? '无问题' : result.issues.map((i) => `[${i.kind}] ${i.detail}`).join('\n ')}\n`);
|
|
251
381
|
if (result.applied) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
382
|
+
out(` 已应用修复:移除 ${result.removed} 项,重编号 ${result.renumbered} 行,中和 ${result.neutralized} 个 turn-null marker,裁剪 ${result.clipped} 个跨 step 引用(seq ${(result.neutralizedSeqs ?? []).join(',')})\n`);
|
|
383
|
+
out(` 备份:${result.backupPath}\n`);
|
|
384
|
+
out(` 修复后体检:error ${result.check.summary?.bySeverity?.error ?? '?'} | surface ${result.check.summary?.surfaceNodes ?? '?'} 节点\n`);
|
|
255
385
|
} else if (apply && !result.ok) {
|
|
256
|
-
|
|
386
|
+
out(' ❌ 存在 error 级问题,拒绝应用(改前基线必须绿;先修基线或检查输出)\n');
|
|
257
387
|
} else if (apply) {
|
|
258
|
-
|
|
388
|
+
out(' (--apply 且无问题——无内容可修)\n');
|
|
259
389
|
} else {
|
|
260
|
-
|
|
390
|
+
out(` (干跑模式:${result.removed} 项可移除、${result.renumbered} 行待重编号、${result.neutralized} 个 turn-null marker 可中和、${result.clipped} 个跨 step 引用可裁剪;加 --apply 落盘,--remove-markers / --neutralize / --clip-crossstep / --drop-failed-turns / --trim-last N / --trim-budget N / --tail-renumber D / --neutralize-orphan / --extract-turn N / --keep-ranges a-b,c-d 启用于对应修复)\n`);
|
|
261
391
|
}
|
|
262
|
-
|
|
392
|
+
out('\n');
|
|
263
393
|
process.exit(result.ok ? 0 : 1);
|
|
264
394
|
}
|
|
265
395
|
|
|
@@ -277,11 +407,11 @@ function cmdExtract(args) {
|
|
|
277
407
|
const log = loadSessionLog(file);
|
|
278
408
|
const { pairs, total } = extractToolOutputs(log.events.map((e) => e.event), pattern, { minSize });
|
|
279
409
|
if (json) {
|
|
280
|
-
|
|
410
|
+
out(JSON.stringify({ file, pattern, matched: pairs.length, total, pairs: pairs.map((p) => ({ callId: p.callId, command: p.command, size: p.size })) }, null, 2) + '\n');
|
|
281
411
|
process.exit(0);
|
|
282
412
|
}
|
|
283
|
-
|
|
284
|
-
|
|
413
|
+
out(`\n🔍 dsh-log-contract extract —— ${file}\n`);
|
|
414
|
+
out(` 命令正则:/${pattern}/ | 匹配 ${pairs.length} 个输出(共 ${total} 个工具调用,min-size ${minSize})\n`);
|
|
285
415
|
if (outDir) {
|
|
286
416
|
fs.mkdirSync(outDir, { recursive: true });
|
|
287
417
|
let written = 0;
|
|
@@ -290,14 +420,14 @@ function cmdExtract(args) {
|
|
|
290
420
|
fs.writeFileSync(`${outDir}/${safe}.txt`, p.text);
|
|
291
421
|
written += 1;
|
|
292
422
|
}
|
|
293
|
-
|
|
423
|
+
out(` 已导出 ${written} 个输出到 ${outDir}\n`);
|
|
294
424
|
} else {
|
|
295
425
|
for (const p of pairs.slice(0, 3)) {
|
|
296
|
-
|
|
426
|
+
out(` - [${p.size}B] ${p.command.slice(0, 60)}… ${p.text.slice(0, 80).replace(/\n/g, ' ')}…\n`);
|
|
297
427
|
}
|
|
298
|
-
if (pairs.length > 3)
|
|
428
|
+
if (pairs.length > 3) out(` … 其余 ${pairs.length - 3} 个(加 --out DIR 全部导出)\n`);
|
|
299
429
|
}
|
|
300
|
-
|
|
430
|
+
out('\n');
|
|
301
431
|
process.exit(0);
|
|
302
432
|
}
|
|
303
433
|
|
|
@@ -308,30 +438,30 @@ function cmdAuditReport(args) {
|
|
|
308
438
|
const log = loadSessionLog(file);
|
|
309
439
|
const report = auditToolCalls(log.events.map((e) => e.event));
|
|
310
440
|
if (json) {
|
|
311
|
-
|
|
441
|
+
out(JSON.stringify({ file, ...report }, null, 2) + '\n');
|
|
312
442
|
process.exit(0);
|
|
313
443
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (report.largest)
|
|
318
|
-
|
|
444
|
+
out(`\n📊 dsh-log-contract audit-report —— ${file}\n`);
|
|
445
|
+
out(` 工具调用 ${report.calls} | 结果 ${report.results} | 孤儿 ${report.orphans} | 配对率 ${(report.pairingRate * 100).toFixed(1)}%\n`);
|
|
446
|
+
out(` 输出总字节 ${report.outputBytes}`);
|
|
447
|
+
if (report.largest) out(` | 最大 ${report.largest.size}B(${(report.largest.command || '?').slice(0, 40)})`);
|
|
448
|
+
out(`\n 命令分布(前 ${report.commands.top.length} 个去重):\n`);
|
|
319
449
|
for (const { command, count } of report.commands.top.slice(0, 8)) {
|
|
320
|
-
|
|
450
|
+
out(` ${String(count).padStart(4)} ${(command || '(no-command)').slice(0, 70)}\n`);
|
|
321
451
|
}
|
|
322
|
-
|
|
452
|
+
out('\n');
|
|
323
453
|
process.exit(0);
|
|
324
454
|
}
|
|
325
455
|
|
|
326
456
|
const args = process.argv.slice(2);
|
|
327
457
|
const cmd = args[0];
|
|
328
458
|
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
329
|
-
|
|
459
|
+
out(USAGE);
|
|
330
460
|
process.exit(0);
|
|
331
461
|
}
|
|
332
462
|
if (cmd === '--version' || cmd === '-v') {
|
|
333
463
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
334
|
-
|
|
464
|
+
out(`dsh-log-contract ${pkg.version}\n`);
|
|
335
465
|
process.exit(0);
|
|
336
466
|
}
|
|
337
467
|
if (cmd === 'check') cmdCheck(args.slice(1));
|