dsh-log-contract 0.3.12 → 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 CHANGED
@@ -318,15 +318,16 @@ MIT © OfferKuai Team
318
318
  - `4` = **migration pre-check blocked**: the tool judges that the official upgrade chain will refuse the file.
319
319
  - ⚠️ **Pipelines that treated `exit 0` as "fine" will now fail.** The old behaviour returned `0` even for files
320
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.
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.
324
325
 
325
326
  **2. `--json` gains `assessmentScope` (three values).**
326
327
 
327
328
  | value | meaning |
328
329
  |---|---|
329
- | `full` | file is already the host's target format (no migration) — coverage is complete |
330
+ | `full` | file is already the host's target format (no migration) — coverage is complete(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
330
331
  | `partial` | file needs migration ⇒ the migration pre-check covers **2 rules only; 6 classes are uncovered** (≠ pass; see `coverage.uncovered`) |
331
332
  | `none` | **not assessable on this host** — no "usable / upgradable" claim is certified |
332
333
 
package/README.zh.md CHANGED
@@ -274,14 +274,15 @@ MIT © OfferKuai Team
274
274
  而是明确说明宿主能力不足;`fix --apply` 会**拒绝**该文件(不落盘、不写备份)。
275
275
  - `4` = **迁移预检 blocked**:本工具判断该文件会被官方升级链拒绝。
276
276
  - ⚠️ **把 `exit 0` 当"过"的流水线会由过转挂。** 旧行为在**工具自己就报告"官方会拒"**的文件上仍返回 `0`
277
- —— 退出码与结论矛盾的假阴性。`--fail-on-migration` 是**默认已生效行为的显式别名**,不是开关,
278
- 且**没有 opt-out**;需要旧行为请读 `--json` `assessmentScope` / `migration.ready` 自定门槛。
277
+ —— 退出码与结论矛盾的假阴性。`--fail-on-migration` 是**默认已生效行为的显式别名**;
278
+ **真正的 opt-out `--no-fail-on-migration`**,它恢复 0.3.11 的行为(结构绿即 `exit 0`)。
279
+ 两者同时给时 **opt-out 优先**。也可改读 `--json` 的 `assessmentScope` / `migration.ready` 自定门槛。
279
280
 
280
281
  **2. `--json` 新增 `assessmentScope`(三值)。**
281
282
 
282
283
  | 值 | 含义 |
283
284
  |---|---|
284
- | `full` | 被检文件已是当前宿主的目标格式(无需迁移),覆盖完整 |
285
+ | `full` | 被检文件已是当前宿主的目标格式(无需迁移),覆盖完整(已是当前格式、**无需迁移**)(已是当前格式、**无需迁移**) |
285
286
  | `partial` | 需要迁移 ⇒ 迁移预检**只覆盖 2 条规则,另有 6 类未覆盖**(≠ 通过;见 `coverage.uncovered`) |
286
287
  | `none` | **不可在本宿主评估**(宿主能力不足),不认证任何"可用/可升级"结论 |
287
288
 
@@ -39,7 +39,7 @@ const errOut = (text) => writeAllSync(2, text);
39
39
  const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log contract guard)
40
40
 
41
41
  用法:
42
- dsh-log-contract check <session-log> [--json] [--max-details N] [--resume] [--fail-on-migration]
42
+ dsh-log-contract check <session-log> [--json] [--max-details N] [--resume] [--fail-on-migration] [--no-fail-on-migration]
43
43
  离线体检 + **迁移预检**(两个**独立维度**,互不蕴含)。session-log 支持 .jsonl 与 .jsonl.zstd。
44
44
  ⚠️ 口径边界:本工具只判定它实现的规则集。「ok」/「可加载」/「可继续」/「可压缩」
45
45
  **不代表**会话"可用",**不代表**可安全编辑,也**不代表**官方升级路径会接受
@@ -48,15 +48,21 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
48
48
  --max-details N 每条违规最多列 N 个缺失 seq(默认 8,--json 忽略)
49
49
  --resume 输出分层结论:可加载 / 可继续 / 可压缩(本工具规则集内)
50
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" 才可把绿读作完整评估。
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 都不该)。
57
62
  退出码:0 本工具认为可加载且无迁移阻断;1 有 error 级违规;3 不可在本宿主评估
58
63
  (被检文件版本 > 宿主支持上限,如 rc.7 宿主的 v3 文件——**不是** broken);
59
- 4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false)。
64
+ 4 迁移预检 blocked(assessmentScope=partial 且 migration.ready=false;可用
65
+ --no-fail-on-migration 退出该档)。
60
66
 
61
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]
62
68
  诊断 + 修复(2026-08 事故固化方案)。先做严格 seq 连续扫描 + 契约体检
@@ -169,9 +175,11 @@ function migrationLine(mig, partial = false) {
169
175
  function cmdCheck(args) {
170
176
  const json = args.includes('--json');
171
177
  const resume = args.includes('--resume');
172
- // 第四轮(最终复核 §六-2):显式开关。现行默认行为已是"ready===false ⇒ exit 4",
173
- // 本开关是显式别名,便于 CI 把意图写进命令行。
178
+ // 第四轮/第五轮:`--fail-on-migration` 是**默认已生效行为**的显式别名;
179
+ // `--no-fail-on-migration` 是**真 opt-out**(恢复旧行为:迁移 blocked 不再改退出码)。
180
+ // 两者同时给 ⇒ opt-out 优先(help/README 写明)。
174
181
  const failOnMigration = args.includes('--fail-on-migration');
182
+ const noFailOnMigration = args.includes('--no-fail-on-migration');
175
183
  const maxDetailsIdx = args.indexOf('--max-details');
176
184
  const maxDetails = maxDetailsIdx >= 0 && args[maxDetailsIdx + 1] ? Number(args[maxDetailsIdx + 1]) : 8;
177
185
  const file = args.find((a) => !a.startsWith('-'));
@@ -191,8 +199,9 @@ function cmdCheck(args) {
191
199
  const scope = result.assessmentScope ?? assessmentScope(result);
192
200
  const partial = scope === 'partial';
193
201
  const migBlocked = migration?.applies === true && migration.ready === false;
194
- // `--fail-on-migration` 与默认同效(显式别名):`ready===false` ⇒ 非 0
195
- const migGate = migBlocked || (failOnMigration && 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));
196
205
  // 退出码:3 不可评估 > 1 结构 error > 4 迁移 blocked > 0。
197
206
  const exitCode = notAssessable ? 3 : (!ok ? 1 : (migGate ? 4 : 0));
198
207
 
package/lib/checks.js CHANGED
@@ -788,11 +788,24 @@ export function wireViolations(events, version) {
788
788
  * G · 迁移预检(payload 层 + 类型层)——**官方迁移会不会拒**,与"本工具能否读取/折叠"是
789
789
  * **两个独立维度**(见 contracts.js 的 LAYER.MIGRATION 段)。
790
790
  *
791
- * 只覆盖已取得一手出处的两条(G1 descriptor version / G2 unknown historical type);
792
- * 官方迁移规则集更宽(turn/start 未闭合、继承切点、title messageSeqs、stored-log corrupt…),
793
- * 未覆盖部分由 `migrationVerdict().coverage.uncovered` 显式列出——**不许**把未覆盖当通过。
791
+ * 覆盖(每条都有一手出处,官方 `@0.1.5-rc.2`):
792
+ * - **G1** `subagent/descriptor.data.version !== 3`
793
+ * - **G2** 事件类型不在官方 v0 dispositions 内(含 `ignorable:true`)
794
+ * - **G3** `session/title` / `session/title-llm-request` 的 `messageSeqs`(`v0-to-v1/lib/index.js:2543-2556`)
794
795
  *
795
- * 范围:两条都只约束 **v0 源文件**(官方 v0→v1 迁移的规则;v1/v2 源走别的迁移代码)。
796
+ * 仍未覆盖(由 `migrationVerdict().coverage.uncovered` 显式列出,**不许**当通过):
797
+ * `turn/start`(闭合/预期轮)、继承切点、`assistant/attempt` 配对、`stored log corrupt`、
798
+ * v0→v1 对其余事件的形状拒绝。
799
+ *
800
+ * **为什么 `turn/start` 不做(第五轮实测结论)**:官方那个状态机(`v0-to-v1:2270` 的
801
+ * `assertReleasedArtifactRelationships`)**不是**在原始 v0 事件上跑的——它由 **v1→v2**
802
+ * 以 `RELEASED_V2_RELATIONSHIP_EXTENSIONS` 调用在**变换后的 v1/v2 artifact** 上
803
+ * (`v1-to-v2/lib/index.js:104`),并带 `cut`(继承切点)处理。第五轮实测:在原始 v0 上照抄该
804
+ * 状态机会在**已 seed 的会话**上狂报(样本 `session-62c5b531`:v0→v1 官方并不以该规则拒绝,
805
+ * 而原始 v0 上会报 19 条),属"规则文本对、应用对象错"。要忠实复现必须先把 v0→v1→v2 的
806
+ * 变换做出来 ⇒ 记未覆盖。
807
+ *
808
+ * 范围:三条都只约束 **v0 源文件**(官方 v0→v1 迁移的规则;v1/v2 源走别的迁移代码)。
796
809
  * severity 固定 warning、layer 固定 migration ⇒ 不进 ok / loadable / resumable / compactable。
797
810
  *
798
811
  * @param {Array<{event:object, lineNo?:number}>} events 已展开事件(带 loc 包装)
@@ -817,5 +830,41 @@ export function migrationPrecheckViolations(events, version) {
817
830
  `type "${event.type}" 不在官方 v0 dispositions 内${event.ignorable === true ? '(即使 ignorable:true)' : ''}—— 官方迁移会拒:format v0 contains unknown historical event type`));
818
831
  }
819
832
  }
833
+ // ── G3 · session/title 系列 messageSeqs(官方 v0→v1 `assertTitleSources`,:2543-2556)──
834
+ // 判据:每个被引 seq 必须解析到更早的 `user/message`,且 `data.source.kind === 'user'`;
835
+ // 另 `session/title` 的 `source.kind === 'user'` ⟺ `messageSeqs` 空。
836
+ //
837
+ // ⚠️ 应用面说明(第五轮实测,2026-09-15):官方 `assertTitleSources` 与 `turn/start` 状态机
838
+ // 同在 `assertReleasedArtifactRelationships` 里,而该函数被 **v1→v2** 以
839
+ // `RELEASED_V2_RELATIONSHIP_EXTENSIONS` 调用在**变换后的 v1/v2 artifact** 上
840
+ // (`dsh-session-format-v1-to-v2/lib/index.js:104`),不是原始 v0 事件。
841
+ // 对 `messageSeqs` 这类"按 seq 索引 + 事件类型/source 判定"的引用,变换保序保类型
842
+ // ⇒ 在原始 v0 上判是**必要条件的近似**;实测 281 个真实 v0 上 **0 误报**(round5/rule-probe)。
843
+ const bySeq = new Map();
844
+ for (const { event } of events) if (event && typeof event === 'object') bySeq.set(event.seq, event);
845
+ for (const { event, lineNo } of events) {
846
+ if (!event || typeof event !== 'object') continue;
847
+ if (event.type !== 'session/title' && event.type !== 'session/title-llm-request') continue;
848
+ const seqs = event.data?.messageSeqs;
849
+ if (!Array.isArray(seqs)) {
850
+ out.push(violation('G3', { seq: event.seq, lineNo, eventType: event.type },
851
+ `${event.type} ${event.seq} 的 messageSeqs 必须是数组 —— 官方迁移会拒`));
852
+ continue;
853
+ }
854
+ if (event.type === 'session/title') {
855
+ const kind = event.data?.source?.kind;
856
+ if ((seqs.length === 0) !== (kind === 'user')) {
857
+ out.push(violation('G3', { seq: event.seq, lineNo, eventType: event.type },
858
+ `session/title ${event.seq} messageSeqs must be empty exactly for a user title(官方原文;source.kind=${String(kind)},seqs=${seqs.length})`));
859
+ }
860
+ }
861
+ for (const s of seqs) {
862
+ const src = bySeq.get(s);
863
+ if (!src || src.type !== 'user/message' || src.data?.source?.kind !== 'user') {
864
+ out.push(violation('G3', { seq: event.seq, lineNo, eventType: event.type },
865
+ `${event.type} ${event.seq} messageSeqs must cite earlier human user/message events(seq ${String(s)} 不是更早的人类 user/message)`));
866
+ }
867
+ }
868
+ }
820
869
  return out;
821
870
  }
package/lib/contracts.js CHANGED
@@ -382,6 +382,14 @@ export const CONTRACT_RULES = [
382
382
  source: '@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"',
383
383
  description: '文件版本 0 且事件 type 不在官方 v0 dispositions(本包 vendored 为 V0_EVENT_TYPES)内 → 官方迁移拒绝,**即使该事件带 ignorable:true**。E3 的 ignorable 豁免是**读取路径**语义(不改),本条只在迁移预检维度表达。',
384
384
  },
385
+ {
386
+ id: 'G3',
387
+ title: '迁移预检:v0 源 session/title 系列的 messageSeqs 必须引用更早的人类 user/message',
388
+ layer: LAYER.MIGRATION,
389
+ severity: SEVERITY.WARNING,
390
+ source: '@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"',
391
+ description: '文件版本 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 误报。',
392
+ },
385
393
  ];
386
394
 
387
395
  /** 按 id 取规则。 */
package/lib/repair.js CHANGED
@@ -554,6 +554,48 @@ export function tailRenumberText(text, startSeq, delta) {
554
554
  const parts = text.split('\n');
555
555
  const out = [];
556
556
  let changed = 0;
557
+ // ── seq 引用平移(2026-09-14 第五轮:把 round4 存档的未审补丁评审后纳入,并补两处同族缺口)──
558
+ // 一手依据(官方 v0→v1 校验,@0.1.5-rc.2):
559
+ // - `data.shadowedRange{start,end}` + `data.shadowedSeqs`:`dsh-session-format-v0-to-v1`
560
+ // `lib/index.js:55-70`(compaction/prune、compaction/summary 的 dispositions)、
561
+ // `:1142-1146` `shadowedValue()`:start/end 必须是 `earlierSeq(, eventSeq)` 且与 shadowedSeqs 端点一致。
562
+ // - v3 的 `shadowedRange` 仍是 `{start,end}`(`dsh-session-format-v2-to-v3` `lib/index.js:634-640` `range()`),
563
+ // 而 v3 `surfaceOp` 是 `{startSeq,endSeq}`(同包 `:361-371`)⇒ 两种字段名都要覆盖。
564
+ // - `sourceEventSeqs` 在 v3 可为**区间对数组** `[[a,b],…]`(`decodeSeqRanges`);旧实现把区间对
565
+ // 当数字减 delta ⇒ `NaN` ⇒ `JSON.stringify` 成 `null`(实测破坏)。此处改为:两端点同侧才平移,
566
+ // **跨平移起点**的区间对直接报错(单区间无法忠实表达)。
567
+ // - `data.messageSeqs`(session/title、session/title-llm-request)同样是 seq 引用
568
+ // (`v0-to-v1/lib/index.js:2543-2556` `assertTitleSources` 用 `events[seq]` 取源事件)。
569
+ const shiftNum = (sv, label) => {
570
+ if (typeof sv !== 'number' || !Number.isSafeInteger(sv) || sv < startSeq) return { changed: false, value: sv };
571
+ const ns = sv - delta;
572
+ if (ns < 0) return { error: `${label} ${sv} 平移后为负(delta 过大或起点有误)` };
573
+ return { changed: true, value: ns };
574
+ };
575
+ const shiftSeqList = (list, label) => {
576
+ const mapped = [];
577
+ let local = false;
578
+ for (const item of list) {
579
+ if (Array.isArray(item)) {
580
+ const [a, b] = item;
581
+ if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b) || a > b) return { error: `${label} 区间对 [${String(a)},${String(b)}] 形状非法` };
582
+ if (b < startSeq) { mapped.push(item); continue; }
583
+ if (a < startSeq) return { error: `${label} 区间对 [${a},${b}] 跨平移起点 ${startSeq},无法用单区间表达(请先展开区间编码)` };
584
+ const na = a - delta;
585
+ const nb = b - delta;
586
+ if (na < 0) return { error: `${label} 区间对 [${a},${b}] 平移后为负` };
587
+ mapped.push([na, nb]);
588
+ local = true;
589
+ continue;
590
+ }
591
+ const r = shiftNum(item, label);
592
+ if (r.error) return { error: r.error };
593
+ mapped.push(r.value);
594
+ if (r.changed) local = true;
595
+ }
596
+ return { changed: local, value: mapped };
597
+ };
598
+
557
599
  for (const raw of parts) {
558
600
  if (!raw.trim()) {
559
601
  out.push(raw);
@@ -567,37 +609,34 @@ export function tailRenumberText(text, startSeq, delta) {
567
609
  continue;
568
610
  }
569
611
  let modified = false;
570
- const seq = v.seq;
571
- if (typeof seq === 'number' && seq >= startSeq) {
572
- const ns = seq - delta;
573
- if (ns < 0) return { text, changed: 0, startSeq, delta, error: `seq ${seq} 平移后为负(delta 过大或起点有误)` };
574
- v.seq = ns;
575
- modified = true;
576
- }
577
- if (typeof v.seq0 === 'number' && v.seq0 >= startSeq) {
578
- const ns = v.seq0 - delta;
579
- if (ns < 0) return { text, changed: 0, startSeq, delta, error: `seq0 ${v.seq0} 平移后为负(delta 过大或起点有误)` };
580
- v.seq0 = ns;
581
- modified = true;
582
- }
583
- if (Array.isArray(v.sourceEventSeqs)) {
584
- let mapped;
585
- try {
586
- mapped = v.sourceEventSeqs.map((s) => {
587
- if (s < startSeq) return s;
588
- const ns = s - delta;
589
- if (ns < 0) throw new Error(`sourceEventSeqs ${s} 平移后为负`);
590
- return ns;
591
- });
592
- } catch (err) {
593
- // seq/seq0 的优雅返回一致:错误走 error 字段而非未捕获异常
594
- return { text, changed: 0, startSeq, delta, error: err.message };
595
- }
596
- if (mapped.some((s, i) => s !== v.sourceEventSeqs[i])) {
597
- v.sourceEventSeqs = mapped;
598
- modified = true;
599
- }
600
- }
612
+ const applyNum = (obj, key, label) => {
613
+ if (!obj || typeof obj !== 'object') return null;
614
+ const r = shiftNum(obj[key], label);
615
+ if (r.error) return r.error;
616
+ if (r.changed) { obj[key] = r.value; modified = true; }
617
+ return null;
618
+ };
619
+ const applyList = (obj, key, label) => {
620
+ if (!obj || typeof obj !== 'object' || !Array.isArray(obj[key])) return null;
621
+ const r = shiftSeqList(obj[key], label);
622
+ if (r.error) return r.error;
623
+ if (r.changed) { obj[key] = r.value; modified = true; }
624
+ return null;
625
+ };
626
+
627
+ let err = applyNum(v, 'seq', 'seq');
628
+ if (!err) err = applyNum(v, 'seq0', 'seq0');
629
+ if (!err) err = applyList(v, 'sourceEventSeqs', 'sourceEventSeqs');
630
+ if (!err) err = applyNum(v.surfaceOp, 'start', 'surfaceOp.start');
631
+ if (!err) err = applyNum(v.surfaceOp, 'end', 'surfaceOp.end');
632
+ if (!err) err = applyNum(v.surfaceOp, 'startSeq', 'surfaceOp.startSeq');
633
+ if (!err) err = applyNum(v.surfaceOp, 'endSeq', 'surfaceOp.endSeq');
634
+ if (!err) err = applyNum(v.data?.shadowedRange, 'start', 'shadowedRange.start');
635
+ if (!err) err = applyNum(v.data?.shadowedRange, 'end', 'shadowedRange.end');
636
+ if (!err) err = applyList(v.data, 'shadowedSeqs', 'shadowedSeqs');
637
+ if (!err) err = applyList(v.data, 'messageSeqs', 'messageSeqs');
638
+ if (err) return { text, changed: 0, startSeq, delta, error: err };
639
+
601
640
  if (modified) changed++;
602
641
  out.push(JSON.stringify(v));
603
642
  }
package/lib/validate.js CHANGED
@@ -343,19 +343,22 @@ export function migrationVerdict(result) {
343
343
  implemented: [
344
344
  'G1 v0 源:subagent/descriptor.data.version !== 3',
345
345
  'G2 v0 源:事件类型不在官方 v0 dispositions 内(含 ignorable:true)',
346
+ 'G3 v0 源:session/title 系列 messageSeqs 必须引用更早的人类 user/message(含"空 ⟺ 用户标题")',
346
347
  ],
347
348
  uncovered: [
348
- // 2026-09-14 第四轮(最终复核实测):官方 v1→v2 的 turn/start 规则有**两个方向**——
349
- // "does not close the prior turn"(未闭合)与 "does not open expected turn"(未打开预期轮)。
350
- // 残余 27 例里有 7 例是后者;类文本原先只写"未闭合" ⇒ 标注与事实不符。此处扩类。
351
- 'turn/start 未闭合前一轮 / 未打开预期轮(官方:does not close the prior turn / does not open expected turn)',
349
+ // 2026-09-15 第五轮:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
350
+ // 官方状态机(assertReleasedArtifactRelationships)由 v1→v2 在**变换后的 v1/v2 artifact**
351
+ // 上调用(v1-to-v2/lib/index.js:104),带 cut/继承切点处理;在原始 v0 上照抄会在已 seed
352
+ // 的会话上狂报(样本 session-62c5b531:0.1.5 链并不以该规则拒它,原始 v0 上会报 19 条)。
353
+ // ⇒ 忠实复现需先做 v0→v1→v2 变换;本轮不做,保持未覆盖。
354
+ 'turn/start 闭合/预期轮(官方 v1→v2 在**变换后** artifact 上判:does not close the prior turn / does not open expected turn)',
352
355
  'assistant/attempt 配对/闭合(migration refuses the transformed artifact)',
353
- 'session/title messageSeqs 越界',
354
- 'Session inheritedEventCut / 继承切点',
356
+ 'Session inheritedEventCut / 继承切点(官方在变换后 artifact 上按 cut 判;header 只有 seedLength)',
355
357
  'stored log corrupt(SessionFormatError)',
356
358
  'v0→v1 对其余事件的形状拒绝',
359
+ 'turn/end 无匹配 open turn / open step 穿越 / 未解析 tool / open compaction 穿越(官方同一状态机的其余分支)',
357
360
  ],
358
- note: '未覆盖 ≠ 通过:本维度只对 implemented 两条负责。官方判据版本:@deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2',
361
+ note: '未覆盖 ≠ 通过:本维度只对 implemented 三条负责。官方判据版本:@deepseek-ai/dsh-session-format-v0-to-v1@0.1.5-rc.2 + v1-to-v2@0.1.5-rc.2',
359
362
  };
360
363
  if (result?.assessable === false) {
361
364
  return { assessable: false, applies: null, ready: null, fileVersion, blocked: [], coverage, assessmentScope: 'none', reason: result?.notAssessable?.reason ?? null };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-log-contract",
3
3
  "description": "日志契约守护 — DSH session log contract guard: offline health check (CLI) + pre-write validation for DeepSeek Harness session logs",
4
- "version": "0.3.12",
4
+ "version": "0.3.13",
5
5
  "packageManager": "pnpm@11.7.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",