dsh-log-contract 0.3.10 → 0.3.12
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 +31 -0
- package/README.zh.md +27 -0
- package/bin/dsh-log-contract.mjs +185 -64
- package/docs/CONTRACTS.md +100 -20
- package/lib/checks.js +139 -23
- package/lib/compat.js +282 -0
- package/lib/contracts.js +44 -4
- package/lib/index.js +3 -2
- package/lib/legacy-fold.js +177 -0
- package/lib/log-reader.js +8 -4
- package/lib/prewrite.js +33 -15
- package/lib/validate.js +250 -12
- package/lib/vocab.js +193 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -306,3 +306,34 @@ 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**, not a toggle, and
|
|
322
|
+
there is **no opt-out**; for the old behaviour read `assessmentScope` / `migration.ready` from `--json` and
|
|
323
|
+
apply your own threshold.
|
|
324
|
+
|
|
325
|
+
**2. `--json` gains `assessmentScope` (three values).**
|
|
326
|
+
|
|
327
|
+
| value | meaning |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `full` | file is already the host's target format (no migration) — coverage is complete |
|
|
330
|
+
| `partial` | file needs migration ⇒ the migration pre-check covers **2 rules only; 6 classes are uncovered** (≠ pass; see `coverage.uncovered`) |
|
|
331
|
+
| `none` | **not assessable on this host** — no "usable / upgradable" claim is certified |
|
|
332
|
+
|
|
333
|
+
Under `partial` the CLI no longer prints an unconditional green: both the migration line and the last line say
|
|
334
|
+
the migration dimension is only partially covered.
|
|
335
|
+
|
|
336
|
+
**3. What "the health check passed" does *not* mean.**
|
|
337
|
+
The rule set is **not** the official upgrade chain's acceptance condition. `assessmentScope=partial` means
|
|
338
|
+
"the part this tool looked at is clean" — it does **not** mean the official upgrade will accept the file.
|
|
339
|
+
Read `coverage.uncovered` for the known gaps.
|
package/README.zh.md
CHANGED
|
@@ -263,3 +263,30 @@ 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**;需要旧行为请读 `--json` 的 `assessmentScope` / `migration.ready` 自定门槛。
|
|
279
|
+
|
|
280
|
+
**2. `--json` 新增 `assessmentScope`(三值)。**
|
|
281
|
+
|
|
282
|
+
| 值 | 含义 |
|
|
283
|
+
|---|---|
|
|
284
|
+
| `full` | 被检文件已是当前宿主的目标格式(无需迁移),覆盖完整 |
|
|
285
|
+
| `partial` | 需要迁移 ⇒ 迁移预检**只覆盖 2 条规则,另有 6 类未覆盖**(≠ 通过;见 `coverage.uncovered`) |
|
|
286
|
+
| `none` | **不可在本宿主评估**(宿主能力不足),不认证任何"可用/可升级"结论 |
|
|
287
|
+
|
|
288
|
+
`partial` 时 CLI 不再给无条件绿:迁移行与末行都会写明"迁移维度仅部分覆盖"。
|
|
289
|
+
|
|
290
|
+
**3.「体检通过」到哪一步为止。**
|
|
291
|
+
本工具的规则集**不等于**官方升级链的接受条件。`assessmentScope=partial` 只表示"本工具看过的那部分没问题",
|
|
292
|
+
**不构成**"官方升级会接受"。已知未覆盖的 6 类见 `coverage.uncovered`。
|
package/bin/dsh-log-contract.mjs
CHANGED
|
@@ -13,17 +13,50 @@
|
|
|
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]
|
|
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
|
+
**注意:现行默认行为已经是"ready===false ⇒ 退出码 4"**;本开关是显式别名,
|
|
53
|
+
便于 CI 把意图写进命令行(也与将来若放宽默认值时保留 opt-in 口径)。
|
|
54
|
+
评估范围 assessmentScope(顶层字段):'partial' = 待迁移文件(迁移维度只覆盖 2 条规则,
|
|
55
|
+
另有 6 类未覆盖)⇒ **不构成"官方升级会接受"**;'full' = 无需迁移(已是当前格式);
|
|
56
|
+
'none' = 本宿主能力不足以评估。机器应判 assessmentScope == "full" 才可把绿读作完整评估。
|
|
57
|
+
退出码:0 本工具认为可加载且无迁移阻断;1 有 error 级违规;3 不可在本宿主评估
|
|
58
|
+
(被检文件版本 > 宿主支持上限,如 rc.7 宿主的 v3 文件——**不是** broken);
|
|
59
|
+
4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false)。
|
|
27
60
|
|
|
28
61
|
dsh-log-contract fix <session-log> [--remove-markers] [--neutralize] [--clip-crossstep] [--drop-failed-turns] [--trim-last N] [--compact-last N] [--tail-renumber D] [--neutralize-orphan] [--extract-turn N] [--keep-ranges a-b,c-d] [--apply] [--backup-dir DIR] [--json]
|
|
29
62
|
诊断 + 修复(2026-08 事故固化方案)。先做严格 seq 连续扫描 + 契约体检
|
|
@@ -74,29 +107,71 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
|
|
|
74
107
|
`;
|
|
75
108
|
|
|
76
109
|
function fail(message, code = 1) {
|
|
77
|
-
|
|
110
|
+
errOut(`${message}\n`);
|
|
78
111
|
process.exit(code);
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
function printViolations(violations, maxDetails = 8) {
|
|
82
115
|
if (violations.length === 0) {
|
|
83
|
-
|
|
116
|
+
out(' ✔ 无违规\n');
|
|
84
117
|
return;
|
|
85
118
|
}
|
|
86
119
|
for (const v of violations) {
|
|
87
120
|
const loc = [v.seq !== null ? `seq ${v.seq}` : null, v.lineNo !== null ? `line ${v.lineNo}` : null]
|
|
88
121
|
.filter(Boolean).join(' / ');
|
|
89
122
|
const head = ` [${v.severity}] ${v.id} ${loc ? `@ ${loc}` : ''}${v.eventType ? ` (${v.eventType})` : ''}`;
|
|
90
|
-
|
|
123
|
+
out(`${head}\n ${v.message}\n`);
|
|
91
124
|
if (Array.isArray(v.missingSeqs) && v.missingSeqs.length > maxDetails) {
|
|
92
|
-
|
|
125
|
+
out(` …另有 ${v.missingSeqs.length - maxDetails} 个缺失 seq 未列出\n`);
|
|
93
126
|
}
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
|
|
130
|
+
/** S2 横幅:被检文件版本 > 宿主支持上限 ⇒ "不可在本宿主评估"(**不是** broken)。 */
|
|
131
|
+
function notAssessableBanner(result) {
|
|
132
|
+
const na = result.notAssessable ?? {};
|
|
133
|
+
return `\n⚠️ 不可在本宿主评估(not-assessable)\n`
|
|
134
|
+
+ ` 被检文件 version=${na.fileVersion} > 本宿主支持的最大文件版本 ${na.hostMaxFileVersion}\n`
|
|
135
|
+
+ ` 宿主 @deepseek-ai/dsh-session@${na.hostPackage}\n`
|
|
136
|
+
+ ` 原因:${na.reason}\n`
|
|
137
|
+
+ ` 已跳过规则:${(na.skippedRules ?? []).join(', ')}\n`
|
|
138
|
+
+ ` 已执行:结构层(Z/H/R/E1·E2·E4·E5·E6/S9/M/P/P3·P4/I1);结构层结论 ${result.structuralOk ? '绿' : '有 error(见上)'}\n`
|
|
139
|
+
+ ` ⚠️ 这不表示日志损坏,也**不要**据此运行 fix --apply —— 请用 0.1.5+ 宿主评估。\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 迁移预检(独立维度)文本。
|
|
143
|
+
* @param {object} mig `migrationVerdict()` 结果
|
|
144
|
+
* @param {boolean} partial 待迁移文件(`migration.applies===true`)⇒ 迁移维度只部分覆盖,
|
|
145
|
+
* 不得给无条件绿(2026-09-14 最终复核规格 (i)+(iii))。
|
|
146
|
+
*/
|
|
147
|
+
function migrationLine(mig, partial = false) {
|
|
148
|
+
if (!mig) return '';
|
|
149
|
+
if (mig.assessable === false) return ` 迁移预检:不可评估(本宿主能力不足)\n`;
|
|
150
|
+
if (mig.applies === false) return ` 迁移预检:— 不适用(${mig.reason ?? ''})\n`;
|
|
151
|
+
const n = mig.blocked?.length ?? 0;
|
|
152
|
+
let s = mig.ready
|
|
153
|
+
? (partial
|
|
154
|
+
? ` 迁移预检(独立维度,≠"可用/可升级"):⚠️ 仅覆盖 ${mig.coverage.implemented.length} 条规则;另有 ${mig.coverage.uncovered.length} 类未覆盖(未覆盖 ≠ 通过)—— **不构成"升级会接受"**\n`
|
|
155
|
+
: ` 迁移预检(独立维度,≠"可用/可升级"):✅ 通过(仅限本工具覆盖范围)\n`)
|
|
156
|
+
: ` 迁移预检(独立维度,≠"可用/可升级"):❌ blocked(${n} 项:官方升级路径会拒)\n`;
|
|
157
|
+
for (const v of (mig.blocked ?? []).slice(0, 5)) {
|
|
158
|
+
s += ` [${v.id}] seq ${v.seq ?? '-'}${v.eventType ? ` (${v.eventType})` : ''} — ${String(v.message).slice(0, 130)}\n`;
|
|
159
|
+
}
|
|
160
|
+
if (n > 5) s += ` …另有 ${n - 5} 项\n`;
|
|
161
|
+
s += ` 覆盖边界:已覆盖 ${mig.coverage.implemented.length} 条;未覆盖 ${mig.coverage.uncovered.length} 类官方迁移规则(未覆盖 ≠ 通过;判据 ${mig.coverage.note})\n`;
|
|
162
|
+
// 规格 (iii):把未覆盖类名直接列出来,人不必开 --json 才知道边界。
|
|
163
|
+
if (partial) {
|
|
164
|
+
for (const [i, c] of (mig.coverage.uncovered ?? []).entries()) s += ` ${i + 1}. ${c}\n`;
|
|
165
|
+
}
|
|
166
|
+
return s;
|
|
167
|
+
}
|
|
168
|
+
|
|
97
169
|
function cmdCheck(args) {
|
|
98
170
|
const json = args.includes('--json');
|
|
99
171
|
const resume = args.includes('--resume');
|
|
172
|
+
// 第四轮(最终复核 §六-2):显式开关。现行默认行为已是"ready===false ⇒ exit 4",
|
|
173
|
+
// 本开关是显式别名,便于 CI 把意图写进命令行。
|
|
174
|
+
const failOnMigration = args.includes('--fail-on-migration');
|
|
100
175
|
const maxDetailsIdx = args.indexOf('--max-details');
|
|
101
176
|
const maxDetails = maxDetailsIdx >= 0 && args[maxDetailsIdx + 1] ? Number(args[maxDetailsIdx + 1]) : 8;
|
|
102
177
|
const file = args.find((a) => !a.startsWith('-'));
|
|
@@ -110,43 +185,75 @@ function cmdCheck(args) {
|
|
|
110
185
|
}
|
|
111
186
|
const result = validateSessionLog(log);
|
|
112
187
|
const { summary, violations, ok } = result;
|
|
188
|
+
const notAssessable = result.assessable === false;
|
|
189
|
+
const migration = result.migration ?? migrationVerdict(result);
|
|
190
|
+
// 第四轮:机器可判的评估范围(partial = 待迁移文件,迁移维度只部分覆盖)。
|
|
191
|
+
const scope = result.assessmentScope ?? assessmentScope(result);
|
|
192
|
+
const partial = scope === 'partial';
|
|
193
|
+
const migBlocked = migration?.applies === true && migration.ready === false;
|
|
194
|
+
// `--fail-on-migration` 与默认同效(显式别名):`ready===false` ⇒ 非 0。
|
|
195
|
+
const migGate = migBlocked || (failOnMigration && migration?.ready === false);
|
|
196
|
+
// 退出码:3 不可评估 > 1 结构 error > 4 迁移 blocked > 0。
|
|
197
|
+
const exitCode = notAssessable ? 3 : (!ok ? 1 : (migGate ? 4 : 0));
|
|
113
198
|
|
|
114
199
|
if (json) {
|
|
115
|
-
const payload = { file, ok, summary, violations };
|
|
200
|
+
const payload = { file, ok, assessable: !notAssessable, assessmentScope: scope, host: hostCapability(), migration, summary, violations };
|
|
201
|
+
if (notAssessable) payload.notAssessable = result.notAssessable;
|
|
116
202
|
if (resume) payload.resume = resumeVerdict(result);
|
|
117
|
-
|
|
118
|
-
process.exit(
|
|
203
|
+
out(JSON.stringify(payload, null, 2) + '\n');
|
|
204
|
+
process.exit(exitCode);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (notAssessable) {
|
|
208
|
+
out(notAssessableBanner(result));
|
|
209
|
+
printViolations(violations, maxDetails);
|
|
210
|
+
process.exit(3);
|
|
119
211
|
}
|
|
120
212
|
|
|
121
213
|
if (resume) {
|
|
122
214
|
const v = resumeVerdict(result);
|
|
123
215
|
const tier = v.verdict;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
216
|
+
// partial(待迁移文件)时不用 ✅:避免"全局绿"被读成"官方升级会接受"——用中性 ✔。
|
|
217
|
+
const passMark = partial ? '✔' : '✅';
|
|
218
|
+
const icons = { loadable: `${passMark} 可加载`, resumable: `${passMark} 可继续`, compactable: `${passMark} 可压缩`, broken: '❌ 不可用' };
|
|
219
|
+
out(`\n📋 dsh-log-contract check --resume —— ${file}\n`);
|
|
220
|
+
out(` 事件 ${summary.events} | surface 节点 ${summary.surfaceNodes} | replace 代数 ${summary.replaceGeneration} | 帧 ${summary.frames}(${(summary.compressedBytes / 1024).toFixed(1)}KiB → ${(summary.plaintextBytes / 1024).toFixed(1)}KiB)\n`);
|
|
221
|
+
out(` 违规 ${summary.total}(error ${summary.bySeverity.error} / warning ${summary.bySeverity.warning})\n\n`);
|
|
222
|
+
out(` 分层结论(**本工具规则集内**,不代表"可用",也不代表官方升级会接受):\n`);
|
|
129
223
|
const tiers = [
|
|
130
224
|
['可加载 loadable', v.loadable, '会话能被 DSH 读入(结构规则 S1-S9/E/W 全绿)', v.blocking.loadable],
|
|
131
225
|
['可继续 resumable', v.resumable, 'resume/followup 可用(结构 + I1 inbox 重放绿)', v.blocking.resumable],
|
|
132
226
|
['可压缩 compactable', v.compactable, '/compact 与压力测量可用(前两档 + T1/T2 token-meter 配对绿)', v.blocking.compactable],
|
|
133
227
|
];
|
|
134
228
|
for (const [name, pass, desc, blockers] of tiers) {
|
|
135
|
-
const mark = pass ?
|
|
136
|
-
|
|
137
|
-
if (blockers.length > 0)
|
|
229
|
+
const mark = pass ? passMark : '❌';
|
|
230
|
+
out(` ${mark} ${name} — ${desc}\n`);
|
|
231
|
+
if (blockers.length > 0) out(` 阻断: ${[...new Set(blockers)].join(', ')}\n`);
|
|
138
232
|
}
|
|
139
|
-
|
|
233
|
+
out(migrationLine(migration, partial));
|
|
234
|
+
out(`\n 结论: ${icons[tier]}${migBlocked
|
|
235
|
+
? '(本工具规则集内)—— 但**迁移预检 blocked**:不得据此宣称"可升级"'
|
|
236
|
+
: partial
|
|
237
|
+
? '(本工具规则集内)—— 但**迁移维度仅部分覆盖**(assessmentScope=partial):不得据此宣称"可升级"'
|
|
238
|
+
: v.verdict === 'compactable' ? ' —— 本工具规则集内可安全继续使用' : v.verdict === 'broken' ? ' —— 见上方违规明细(error 级 = 会话不可读/不可写)' : ' —— 部分能力受限'}\n\n`);
|
|
140
239
|
printViolations(violations, maxDetails);
|
|
141
|
-
process.exit(
|
|
240
|
+
process.exit(exitCode);
|
|
142
241
|
}
|
|
143
242
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
243
|
+
out(`\n📋 dsh-log-contract check —— ${file}\n`);
|
|
244
|
+
out(` 事件 ${summary.events} | surface 节点 ${summary.surfaceNodes} | replace 代数 ${summary.replaceGeneration} | 帧 ${summary.frames}(${(summary.compressedBytes / 1024).toFixed(1)}KiB → ${(summary.plaintextBytes / 1024).toFixed(1)}KiB)\n`);
|
|
245
|
+
out(` 违规 ${summary.total}(error ${summary.bySeverity.error} / warning ${summary.bySeverity.warning})\n`);
|
|
246
|
+
out(migrationLine(migration, partial));
|
|
247
|
+
out('\n');
|
|
147
248
|
printViolations(violations, maxDetails);
|
|
148
|
-
|
|
149
|
-
|
|
249
|
+
out(`\n${ok
|
|
250
|
+
? (migBlocked
|
|
251
|
+
? '⚠️ 本工具认为可加载(结构层绿),但**迁移预检 blocked** —— 不得据此宣称"可用/可升级";详见上方迁移预检'
|
|
252
|
+
: partial
|
|
253
|
+
? '⚠️ 本工具规则集内未发现违规;**迁移维度仅部分覆盖**(assessmentScope=partial,不代表官方升级会接受)—— 详见上方迁移预检的覆盖边界'
|
|
254
|
+
: '✅ 通过(本工具规则集内:结构层绿)')
|
|
255
|
+
: '❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)'}\n\n`);
|
|
256
|
+
process.exit(exitCode);
|
|
150
257
|
}
|
|
151
258
|
|
|
152
259
|
function cmdPrewrite(args) {
|
|
@@ -170,12 +277,17 @@ function cmdPrewrite(args) {
|
|
|
170
277
|
fail(`会话日志读取失败:${err.message}`);
|
|
171
278
|
}
|
|
172
279
|
const baseline = validateSessionLog(log);
|
|
280
|
+
// S2:本宿主评估不了的文件**不能**给出写前结论(否则会拿假阳性去拦合法写入)
|
|
281
|
+
if (baseline.assessable === false) {
|
|
282
|
+
fail(`不可在本宿主评估该会话(${baseline.notAssessable?.reason ?? '宿主能力不足'})——写前校验无法给出可信结论;请用 0.1.5+ 宿主`, 3);
|
|
283
|
+
}
|
|
173
284
|
if (!baseline.ok) {
|
|
174
285
|
// 基线已坏:写前校验无法在坏基线上给出可信结论
|
|
175
286
|
fail(`基线会话已有 ${baseline.summary.bySeverity.error} 个 error 级违规,请先修复基线再校验写入(安全修复协议第 2 步:改前基线必须绿)`);
|
|
176
287
|
}
|
|
177
288
|
|
|
178
|
-
|
|
289
|
+
// C2:把被检文件的 header 显式交给 prewriter(版本不再靠模块级全局;缺省也能自行推断)
|
|
290
|
+
const prewriter = createPreWriter({ events: log.events.map((e) => e.event), header: log.header });
|
|
179
291
|
let result;
|
|
180
292
|
if (Object.hasOwn(plan, 'append')) {
|
|
181
293
|
result = prewriter.validateAppend(plan.append);
|
|
@@ -189,26 +301,26 @@ function cmdPrewrite(args) {
|
|
|
189
301
|
}
|
|
190
302
|
|
|
191
303
|
if (json) {
|
|
192
|
-
|
|
304
|
+
out(JSON.stringify({ file, op: result.op, ok: result.ok, bySeverity: result.bySeverity, violations: result.violations }, null, 2) + '\n');
|
|
193
305
|
process.exit(result.ok ? 0 : 1);
|
|
194
306
|
}
|
|
195
307
|
|
|
196
|
-
|
|
308
|
+
out(`\n✍️ dsh-log-contract prewrite —— ${file}(op: ${result.op},nextSeq: ${prewriter.nextSeq})\n\n`);
|
|
197
309
|
if (result.ok) {
|
|
198
|
-
|
|
199
|
-
|
|
310
|
+
out(' ✅ 写入安全:三层契约全绿(持久化 foldSurface 可重放 / 引擎层无崩溃风险 / 插件 marker 语义自洽)\n');
|
|
311
|
+
out(` 写入后 surface 节点 ${result.stateAfter.surfaceNodes} 个,nextSeq ${result.stateAfter.nextSeq}\n\n`);
|
|
200
312
|
process.exit(0);
|
|
201
313
|
}
|
|
202
|
-
|
|
314
|
+
out(' ❌ 写入会被拒:\n');
|
|
203
315
|
printViolations(result.violations);
|
|
204
|
-
|
|
316
|
+
out('\n');
|
|
205
317
|
process.exit(1);
|
|
206
318
|
}
|
|
207
319
|
|
|
208
320
|
function cmdContracts() {
|
|
209
|
-
|
|
321
|
+
out('dsh-log-contract 契约规则目录(spec:59 条审计发现 + 官方源码逐行核对)\n\n');
|
|
210
322
|
for (const r of CONTRACT_RULES) {
|
|
211
|
-
|
|
323
|
+
out(` ${r.id} [${r.severity}/${r.layer}] ${r.title}\n ${r.description}\n 出处: ${r.source}\n\n`);
|
|
212
324
|
}
|
|
213
325
|
}
|
|
214
326
|
|
|
@@ -240,26 +352,35 @@ function cmdFix(args) {
|
|
|
240
352
|
const file = args.find((a) => !a.startsWith('-'));
|
|
241
353
|
if (!file) fail(USAGE);
|
|
242
354
|
|
|
355
|
+
// S2 安全闸:本宿主评估不了的文件**不许**修。危害路径(独立复核指出):rc.7 上把健康 v3
|
|
356
|
+
// 日志报成 broken → 用户以为日志坏了去跑 `fix --apply` ⇒ 在健康日志上动手。这里直接拒绝。
|
|
357
|
+
const head = readSessionHeader(file);
|
|
358
|
+
const fileVersion = Number.isSafeInteger(head?.version) ? head.version : 0;
|
|
359
|
+
if (fileVersion > HOST_MAX_FILE_VERSION) {
|
|
360
|
+
fail(`拒绝修复:被检文件 version=${fileVersion} 高于本宿主 @deepseek-ai/dsh-session@${hostCapability().hostPackage} 支持的最大版本 ${HOST_MAX_FILE_VERSION}`
|
|
361
|
+
+ ` —— 本宿主评估不了该文件(不是"日志坏了")。请用 0.1.5+ 宿主修复。`, 3);
|
|
362
|
+
}
|
|
363
|
+
|
|
243
364
|
const result = repairSession(file, { removeMarkers, neutralize, clipCrossStep, dropFailedTurns, trimLast, trimBudget, compactLast, tailRenumberDelta, neutralizeOrphan, extractTurn, extractTurnTo, keepRanges, apply, backupDir });
|
|
244
365
|
if (json) {
|
|
245
|
-
|
|
366
|
+
out(JSON.stringify(result, null, 2) + '\n');
|
|
246
367
|
process.exit(result.ok ? 0 : 1);
|
|
247
368
|
}
|
|
248
369
|
|
|
249
|
-
|
|
250
|
-
|
|
370
|
+
out(`\n🔧 dsh-log-contract fix —— ${file}\n`);
|
|
371
|
+
out(` 诊断:${result.issues.length === 0 ? '无问题' : result.issues.map((i) => `[${i.kind}] ${i.detail}`).join('\n ')}\n`);
|
|
251
372
|
if (result.applied) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
373
|
+
out(` 已应用修复:移除 ${result.removed} 项,重编号 ${result.renumbered} 行,中和 ${result.neutralized} 个 turn-null marker,裁剪 ${result.clipped} 个跨 step 引用(seq ${(result.neutralizedSeqs ?? []).join(',')})\n`);
|
|
374
|
+
out(` 备份:${result.backupPath}\n`);
|
|
375
|
+
out(` 修复后体检:error ${result.check.summary?.bySeverity?.error ?? '?'} | surface ${result.check.summary?.surfaceNodes ?? '?'} 节点\n`);
|
|
255
376
|
} else if (apply && !result.ok) {
|
|
256
|
-
|
|
377
|
+
out(' ❌ 存在 error 级问题,拒绝应用(改前基线必须绿;先修基线或检查输出)\n');
|
|
257
378
|
} else if (apply) {
|
|
258
|
-
|
|
379
|
+
out(' (--apply 且无问题——无内容可修)\n');
|
|
259
380
|
} else {
|
|
260
|
-
|
|
381
|
+
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
382
|
}
|
|
262
|
-
|
|
383
|
+
out('\n');
|
|
263
384
|
process.exit(result.ok ? 0 : 1);
|
|
264
385
|
}
|
|
265
386
|
|
|
@@ -277,11 +398,11 @@ function cmdExtract(args) {
|
|
|
277
398
|
const log = loadSessionLog(file);
|
|
278
399
|
const { pairs, total } = extractToolOutputs(log.events.map((e) => e.event), pattern, { minSize });
|
|
279
400
|
if (json) {
|
|
280
|
-
|
|
401
|
+
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
402
|
process.exit(0);
|
|
282
403
|
}
|
|
283
|
-
|
|
284
|
-
|
|
404
|
+
out(`\n🔍 dsh-log-contract extract —— ${file}\n`);
|
|
405
|
+
out(` 命令正则:/${pattern}/ | 匹配 ${pairs.length} 个输出(共 ${total} 个工具调用,min-size ${minSize})\n`);
|
|
285
406
|
if (outDir) {
|
|
286
407
|
fs.mkdirSync(outDir, { recursive: true });
|
|
287
408
|
let written = 0;
|
|
@@ -290,14 +411,14 @@ function cmdExtract(args) {
|
|
|
290
411
|
fs.writeFileSync(`${outDir}/${safe}.txt`, p.text);
|
|
291
412
|
written += 1;
|
|
292
413
|
}
|
|
293
|
-
|
|
414
|
+
out(` 已导出 ${written} 个输出到 ${outDir}\n`);
|
|
294
415
|
} else {
|
|
295
416
|
for (const p of pairs.slice(0, 3)) {
|
|
296
|
-
|
|
417
|
+
out(` - [${p.size}B] ${p.command.slice(0, 60)}… ${p.text.slice(0, 80).replace(/\n/g, ' ')}…\n`);
|
|
297
418
|
}
|
|
298
|
-
if (pairs.length > 3)
|
|
419
|
+
if (pairs.length > 3) out(` … 其余 ${pairs.length - 3} 个(加 --out DIR 全部导出)\n`);
|
|
299
420
|
}
|
|
300
|
-
|
|
421
|
+
out('\n');
|
|
301
422
|
process.exit(0);
|
|
302
423
|
}
|
|
303
424
|
|
|
@@ -308,30 +429,30 @@ function cmdAuditReport(args) {
|
|
|
308
429
|
const log = loadSessionLog(file);
|
|
309
430
|
const report = auditToolCalls(log.events.map((e) => e.event));
|
|
310
431
|
if (json) {
|
|
311
|
-
|
|
432
|
+
out(JSON.stringify({ file, ...report }, null, 2) + '\n');
|
|
312
433
|
process.exit(0);
|
|
313
434
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (report.largest)
|
|
318
|
-
|
|
435
|
+
out(`\n📊 dsh-log-contract audit-report —— ${file}\n`);
|
|
436
|
+
out(` 工具调用 ${report.calls} | 结果 ${report.results} | 孤儿 ${report.orphans} | 配对率 ${(report.pairingRate * 100).toFixed(1)}%\n`);
|
|
437
|
+
out(` 输出总字节 ${report.outputBytes}`);
|
|
438
|
+
if (report.largest) out(` | 最大 ${report.largest.size}B(${(report.largest.command || '?').slice(0, 40)})`);
|
|
439
|
+
out(`\n 命令分布(前 ${report.commands.top.length} 个去重):\n`);
|
|
319
440
|
for (const { command, count } of report.commands.top.slice(0, 8)) {
|
|
320
|
-
|
|
441
|
+
out(` ${String(count).padStart(4)} ${(command || '(no-command)').slice(0, 70)}\n`);
|
|
321
442
|
}
|
|
322
|
-
|
|
443
|
+
out('\n');
|
|
323
444
|
process.exit(0);
|
|
324
445
|
}
|
|
325
446
|
|
|
326
447
|
const args = process.argv.slice(2);
|
|
327
448
|
const cmd = args[0];
|
|
328
449
|
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
329
|
-
|
|
450
|
+
out(USAGE);
|
|
330
451
|
process.exit(0);
|
|
331
452
|
}
|
|
332
453
|
if (cmd === '--version' || cmd === '-v') {
|
|
333
454
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
334
|
-
|
|
455
|
+
out(`dsh-log-contract ${pkg.version}\n`);
|
|
335
456
|
process.exit(0);
|
|
336
457
|
}
|
|
337
458
|
if (cmd === 'check') cmdCheck(args.slice(1));
|