dsh-log-contract 0.3.14 → 0.3.16

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/lib/repair.js CHANGED
@@ -209,7 +209,7 @@ export function neutralizeMarkersText(text, { onlyLegacy = false } = {}) {
209
209
  if (v.data?.turn != null || v.data?.step != null) continue;
210
210
  const id = v.data?.message?.id;
211
211
  if (typeof id !== 'string' || !MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`))) continue;
212
- // R-C 一次性根治路径(`--neutralize-legacy-markers`):只动**历史载体**
212
+ // 一次性根治路径(`--neutralize-legacy-markers`):只动**历史载体**
213
213
  // (`assistant/message` + `data.editor`);新载体是 `user/message` + `data.id`,
214
214
  // 本来就不在这个分支里(类型不符),这里额外的判据是"必须有 editor",
215
215
  // 便于把"清历史债"与"泛化中和"区分开、也让报告口径可核对。
@@ -423,15 +423,14 @@ function renumberWithDrops(parts, dropPredicate, dropLinePredicate = null) {
423
423
  *
424
424
  * ⚠️ 与 renumberWithDrops 不同:本函数按「行是否保留」决定删除(双流交织时
425
425
  * 保留行与删除行可能共享 seq 值,按 seq 值判删会误删保留行——2026-08-31
426
- * 独立审查实测复现),且保留行**全量重映射** seq 从 0 连续(参考原工具
427
- * keep-ranges.mjs 的 seqMap 逻辑),同步 seq / seq0 / sourceEventSeqs /
426
+ * 实测复现),且保留行**全量重映射** seq 从 0 连续(与 `--keep-ranges` 同一 seqMap 逻辑),同步 seq / seq0 / sourceEventSeqs /
428
427
  * surfaceOp 范围。header 行(首行)恒保留且不改。
429
428
  * @param {string[]} parts - 按 \n 切分的行数组。
430
429
  * @param {(lineIdx: number) => boolean} keepLinePredicate - 行身份保留判定。
431
430
  * @returns {{ text: string, removed: number, renumbered: number }}
432
431
  */
433
432
  function renumberKeptLines(parts, keepLinePredicate) {
434
- // 第一遍:按物理序收集保留行的事件 seq(chunk 行展开,参考 keep-ranges 原工具)
433
+ // 第一遍:按物理序收集保留行的事件 seq(chunk 行展开,与 --keep-ranges 同口径)
435
434
  const keptLineIdx = [];
436
435
  for (let i = 0; i < parts.length; i++) {
437
436
  const raw = parts[i];
@@ -559,7 +558,7 @@ export function tailRenumberText(text, startSeq, delta) {
559
558
  const parts = text.split('\n');
560
559
  const out = [];
561
560
  let changed = 0;
562
- // ── seq 引用平移(2026-09-14 第五轮:把 round4 存档的未审补丁评审后纳入,并补两处同族缺口)──
561
+ // ── seq 引用平移(2026-09-14:并补两处同族缺口)──
563
562
  // 一手依据(官方 v0→v1 校验,@0.1.5-rc.2):
564
563
  // - `data.shadowedRange{start,end}` + `data.shadowedSeqs`:`dsh-session-format-v0-to-v1`
565
564
  // `lib/index.js:55-70`(compaction/prune、compaction/summary 的 dispositions)、
@@ -655,7 +654,7 @@ export function tailRenumberText(text, startSeq, delta) {
655
654
  * 重新排队 → UI 显示"待排队消息")。与 neutralize 同类:原地改
656
655
  * `removedCount → 0`(start/inserted 不变,seq/行数不变 → 附着力安全)。
657
656
  *
658
- * ⚠️ 判定(2026-08-31 独立审查修正):不能对"所有 removedCount>0 的
657
+ * ⚠️ 判定(2026-08-31 修正):不能对"所有 removedCount>0 的
659
658
  * next-turn spliced"下手——DSH 每个轮次消费消息都会写这种 spliced
660
659
  * (archive 实测 3069 处 removedCount>0,但真 I1 违规只 2 处)。只归零
661
660
  * **实际触发 I1 违规**的那条:按官方 inbox 重放(checks.js
@@ -724,10 +723,10 @@ export function neutralizeOrphanText(text) {
724
723
  export function extractTurnText(text, keepTurn, secondTurnTo = null) {
725
724
  const parts = text.split('\n');
726
725
  const KEEP_NULL_TYPES = new Set(['agent/inbox/spliced', 'user/message', 'request/header', 'session/end-seed', 'command/run', 'command/done']);
727
- // 第一轮:按行身份决定去留(保留 turn===keepTurn 的行 + 无 turn 系统事件
726
+ // 第一遍:按行身份决定去留(保留 turn===keepTurn 的行 + 无 turn 系统事件
728
727
  // 白名单),并就地改第二个同名轮次的 turn 号。
729
- // ⚠️ 用行索引而非 seq 值:双流交织时保留行与删除行可能共享 seq(独立审查
730
- // 实测复现内容静默丢失)——按行身份删除才安全。
728
+ // ⚠️ 用行索引而非 seq 值:双流交织时保留行与删除行可能共享 seq(实测
729
+ // 复现内容静默丢失)——按行身份删除才安全。
731
730
  const dropLines = new Set();
732
731
  let sawFirstTurnStart = false;
733
732
  let inRenumberTurn = false;
@@ -769,8 +768,7 @@ export function extractTurnText(text, keepTurn, secondTurnTo = null) {
769
768
  }
770
769
 
771
770
  /**
772
- * 只保留指定 seq 区间,其余删除 + 全量重编号(keep-ranges 收编,
773
- * 源:tools/keep-ranges.mjs)。
771
+ * 只保留指定 seq 区间,其余删除 + 全量重编号(`--keep-ranges`)。
774
772
  *
775
773
  * 从交织/污染文件中提取干净区段。区间为 1-based 行号(含端点),
776
774
  * 如 "10-20,40-50";区段外的行丢弃。header 行(首行)永远保留。
@@ -788,7 +786,7 @@ export function keepRangesText(text, rangesSpec) {
788
786
  return [a, b];
789
787
  });
790
788
  const parts = text.split('\n');
791
- // 按行身份删除(双流交织时保留行与删除行可能共享 seq——独立审查发现)
789
+ // 按行身份删除(双流交织时保留行与删除行可能共享 seq——实测发现)
792
790
  const dropLines = new Set();
793
791
  let keptLines = 0;
794
792
  for (let i = 0; i < parts.length; i++) {
@@ -1158,7 +1156,7 @@ export function repairSession(file, opts = {}) {
1158
1156
  let plain;
1159
1157
  if (isZstd) {
1160
1158
  try {
1161
- // R-B 边界:**写路径不吃撕裂尾帧**。`check`(只读)按宿主语义恢复撕裂尾帧;但
1159
+ // 边界:**写路径不吃撕裂尾帧**。`check`(只读)按宿主语义恢复撕裂尾帧;但
1162
1160
  // `fix` 会回写文件,若尾帧是**活动会话正在写入**的部分,回写会把它截掉 ——
1163
1161
  // 与宿主"由持有租约的会话自己截断"(:226-228)不同责。故这里显式 strict。
1164
1162
  plain = decompressZstd(buf, { allowTorn: false }).toString('utf8');
@@ -1245,10 +1243,10 @@ export function repairSession(file, opts = {}) {
1245
1243
  issues.push({ kind: 'trim-budget', detail: `估算 ${r.estimatedTokens} tokens ≤ 预算 ${opts.trimBudget}——无需裁剪(${r.kept} 条消息全保留)` });
1246
1244
  }
1247
1245
  }
1248
- // L4 新原语(2026-08-30 任务书 §L4 收编 tools/ 验证工具)
1246
+ // L4 新原语(2026-08-30 收编外部验证工具)
1249
1247
  if (typeof opts.tailRenumberDelta === 'number') {
1250
1248
  // 起点自动推导:从首个可平移的 seq 起(即所有事件都平移)。
1251
- // fix-tail 原工具是 <startLine> <delta> 双参;任务书 §L4 简化为单参 delta
1249
+ // fix-tail 原工具是 <startLine> <delta> 双参;L4 简化为单参 delta
1252
1250
  // (尾部全部平移)。若需部分平移,传 --tail-renumber 前先 --keep-ranges。
1253
1251
  const r = tailRenumberText(plain, 0, opts.tailRenumberDelta);
1254
1252
  if (r.error) {
package/lib/validate.js CHANGED
@@ -60,14 +60,14 @@ export function validateSessionLog(log, opts = {}) {
60
60
  // C1 防御(多入口一致):即使调用方手搓 log 对象、未经 `loadSessionLog`,也在体检入口
61
61
  // 归一一次 v3 区间编码。正常路径(loadSessionLog 已展开)下这是幂等的恒等映射。
62
62
  const events = (log.events ?? []).map((e) => ({ ...e, event: normalizeEventSeqRanges(e.event) }));
63
- // 1.3 第二半:词表/折叠路径按**被检文件自身版本**选择(不按运行时)。
63
+ // 词表/折叠路径按**被检文件自身版本**选择(不按运行时)。
64
64
  // C2:版本是**本次调用的局部量**,显式传给每条按版本择路的判定——不再写模块级全局
65
65
  // (旧实现 `setFileFormatVersion()` 会被同进程后调用的 prewrite 读到,造成结论翻转)。
66
66
  const formatVersion = Number.isSafeInteger(header?.version) && header.version >= 0 ? header.version : 0;
67
67
 
68
68
  // ── 宿主能力闸(S2·安全)──────────────────────────────────────────────────
69
69
  // 被检文件版本 > 本宿主支持的最大文件版本 ⇒ 本宿主没有该版本的词表/折叠语义。继续按
70
- // 本宿主语义判定会产出**假阳性**(独立复核实测:rc.7 上同一真实 v3 文件 →
70
+ // 本宿主语义判定会产出**假阳性**(实测:rc.7 上同一真实 v3 文件 →
71
71
  // `S8×1 + E3×40 / verdict=broken / loadable:false`),危险是用户以为日志坏了去跑
72
72
  // `fix --apply`。改为:只跑与版本无关的结构检查 + 显式"不可在本宿主评估"档。
73
73
  if (!isAssessableFileVersion(formatVersion)) {
@@ -106,7 +106,7 @@ export function validateSessionLog(log, opts = {}) {
106
106
  }
107
107
  }
108
108
 
109
- // ── Z3 · 空文件体检(2026-09-09 反向挑刺 T3 增量)──────────────────────────
109
+ // ── Z3 · 空文件体检(2026-09-09 T3 增量)──────────────────────────
110
110
  // 空态无覆盖:36 条规则全来自"有内容"事故,空会话文件应显式报(不管 DSH 编排层怎么处理)。
111
111
  // 有 header 但零事件 = 异常空会话(warning,不破坏 ok);连 header 都没有 → H1 已报。
112
112
  if (header !== null && events.length === 0) {
@@ -159,7 +159,7 @@ export function validateSessionLog(log, opts = {}) {
159
159
  violations.push(...nullTurnStepViolations(events));
160
160
  violations.push(...turnEndReasonViolations(events));
161
161
 
162
- // ── E7 · ignorable 未知 type 合法性(反向挑刺 T2)────────────────────
162
+ // ── E7 · ignorable 未知 type 合法性(T2)────────────────────
163
163
  violations.push(...ignorableTypeViolations(events, formatVersion));
164
164
 
165
165
  // ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
@@ -225,7 +225,7 @@ export function validateSessionLog(log, opts = {}) {
225
225
  // 版本支持面(2026-09-14 用户要求第 1 条):宿主版本 vs 测试基线 + 被检文件格式版本,
226
226
  // 给出 warnings/readOnly。只读档由入口(prewrite/fix)强制执行。
227
227
  result.support = detectSupport({ header, events });
228
- // R-D 漂移检测:行为探针(宿主自己的运行时函数当 oracle)+ R-F 出处/前提漂移清单。
228
+ // 漂移检测:行为探针(宿主自己的运行时函数当 oracle)+ 出处/前提漂移清单。
229
229
  result.probes = hostProbes();
230
230
  result.drift = driftOf(result.probes);
231
231
  result.migration = migrationVerdict(result);
@@ -353,11 +353,11 @@ function hostProbes() {
353
353
  }
354
354
 
355
355
  /**
356
- * R-D/R-F 合并出的**漂移报告**(`check` 抬头与 `--json` 共用)。
356
+ * 合并出的**漂移报告**(`check` 抬头与 `--json` 共用)。
357
357
  * · `unverifiedRules` —— 行为探针在本宿主上**不成立**的规则(宿主语义与规则假设不符);
358
- * · `driftedSources` —— 出处停在 rc.7、未在 0.1.5 上复核(R-F,机读清单);
359
- * · `premiseStale` —— 判定前提已不成立/无法判定(复核 §1);
360
- * · `fixApplyBlocked` —— 只要任一非空,就**不得据此跑 `fix --apply`**(R-E)。
358
+ * · `driftedSources` —— 出处停在 rc.7、未在 0.1.5 上复核(机读清单);
359
+ * · `premiseStale` —— 判定前提已不成立/无法判定;
360
+ * · `fixApplyBlocked` —— 只要任一非空,就**不得据此跑 `fix --apply`**。
361
361
  */
362
362
  export function driftOf(probes) {
363
363
  const p = probes ?? hostProbes();
@@ -371,7 +371,7 @@ export function driftOf(probes) {
371
371
  premiseStale,
372
372
  undecidable: [...SOURCE_DRIFT.undecidable],
373
373
  // `fix --apply` 的**硬前提**只看行为探针(宿主语义与规则假设不一致时才禁止写入类动作);
374
- // 出处/前提清单(R-F)是**已知文档债**,会显式告警但不阻断修复流程 —— 否则只要历史
374
+ // 出处/前提清单是**已知文档债**,会显式告警但不阻断修复流程 —— 否则只要历史
375
375
  // 清单非空,"修一个坏会话"这条主路径就永久不可用(见报告"我未照做之处")。
376
376
  fixApplyBlocked: unverifiedRules.length > 0,
377
377
  note: SOURCE_DRIFT.note,
@@ -397,11 +397,11 @@ export function migrationVerdict(result) {
397
397
  'G3 v0 源:session/title 系列 messageSeqs 必须引用更早的人类 user/message(含"空 ⟺ 用户标题")',
398
398
  ],
399
399
  uncovered: [
400
- // 2026-09-15 第五轮:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
400
+ // 2026-09-15:`turn/start` 试做后在**真实语料**上被判定为"应用对象错"——
401
401
  // 官方状态机(assertReleasedArtifactRelationships)由 v1→v2 在**变换后的 v1/v2 artifact**
402
402
  // 上调用(v1-to-v2/lib/index.js:104),带 cut/继承切点处理;在原始 v0 上照抄会在已 seed
403
403
  // 的会话上狂报(实测样本:0.1.5 链并不以该规则拒它,原始 v0 上会报 19 条)。
404
- // ⇒ 忠实复现需先做 v0→v1→v2 变换;本轮不做,保持未覆盖。
404
+ // ⇒ 忠实复现需先做 v0→v1→v2 变换;暂不做,保持未覆盖。
405
405
  'turn/start 闭合/预期轮(官方 v1→v2 在**变换后** artifact 上判:does not close the prior turn / does not open expected turn)',
406
406
  'assistant/attempt 配对/闭合(migration refuses the transformed artifact)',
407
407
  'Session inheritedEventCut / 继承切点(官方在变换后 artifact 上按 cut 判;header 只有 seedLength)',
@@ -428,7 +428,7 @@ export function migrationVerdict(result) {
428
428
  hostMaxFileVersion: HOST_MAX_FILE_VERSION,
429
429
  blocked,
430
430
  coverage,
431
- // 2026-09-14 第四轮(最终复核):待迁移文件(applies=true)**只**被 2 条 G 规则覆盖,
431
+ // 2026-09-14:待迁移文件(applies=true)**只**被 2 条 G 规则覆盖,
432
432
  // 另有 6 类官方迁移规则未覆盖 ⇒ 评估范围只能是 "partial"。工具无法逐文件知道
433
433
  // 本文件是否命中未覆盖类(那要先把 6 类实现出来或跑官方迁移),但它**知道**这件事。
434
434
  assessmentScope: applies ? 'partial' : 'full',
@@ -488,7 +488,7 @@ export function resumeVerdict(result) {
488
488
  violationsByTier: { structural: [], inbox: [], tokenMeter: [] },
489
489
  };
490
490
  }
491
- // 三档各自的「阻断规则集」——按任务书 §L3 档位定义:
491
+ // 三档各自的「阻断规则集」——按 L3 档位定义:
492
492
  // 可加载:结构层(PERSISTENCE/FRAMING)+ 引擎层非 I1/T1/T2/T3/T4 的 error;
493
493
  // 可继续:+ I1(inbox 重放);
494
494
  // 可压缩:+ T1/T2/T3/T4(token-meter 配对 + 渲染层节点 key/turn)。
@@ -500,7 +500,7 @@ export function resumeVerdict(result) {
500
500
  const has = (id) => (byId[id]?.length ?? 0) > 0;
501
501
 
502
502
  // 可加载阻断 = 除 I1/T1/T2/T3/T4 外的所有 error 违规(结构/信封/物理序/工具配对等)。
503
- // 注意:不能用 result.ok(它把 T1/T2/T3/T4/I1 也计为 error)——三档判定按任务书定义,
503
+ // 注意:不能用 result.ok(它把 T1/T2/T3/T4/I1 也计为 error)——三档判定按 L3 定义,
504
504
  // T1/T2/T3/T4 只影响「可压缩」档、I1 只影响「可继续」档。
505
505
  const loadableBlockers = violations.filter(
506
506
  (v) => v.severity === 'error' && !['I1', 'T1', 'T2', 'T3', 'T4', 'T5'].includes(v.id),
@@ -104,7 +104,7 @@ export function detectSupport({ header, events, formatVersion, hostVersion: host
104
104
  // ── 被检文件格式 ────────────────────────────────────────────────────────
105
105
  const resolved = resolveFormatVersionDetailed({ formatVersion, header, events });
106
106
  const unknownSource = resolved.source === 'default';
107
- // S4(独立核验):无法识别格式时**状态标签必须是 `unverified`**(README 就是这么写的:
107
+ // S4(实测):无法识别格式时**状态标签必须是 `unverified`**(README 就是这么写的:
108
108
  // "Format unrecognisable → same: "unverified format" + read-only")。旧实现让版本回落到默认 0
109
109
  // ⇒ 标签打印成 `legacy`,与 README 不一致(`readOnly` 本来就对)。
110
110
  const status = unknownSource ? 'unverified' : fileStatusOf(resolved.version);
package/lib/vocab.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**(1.3 第二半,内部评审裁定)
2
+ * dsh-log-contract · lib/vocab.js —— **按被检文件自身版本选词表与折叠路径**
3
3
  *
4
4
  * 背景(一手实测):同一健康会话,在**生产 0.1.1 解析**下 0 违规,在 **0.1.5 解析**下 3606 违规——
5
5
  * 根因是本包原先用**运行时导出**的 `KNOWN_SESSION_EVENT_TYPES` / `foldSurface` 去判**旧格式(v0)文件**:
6
6
  * 官方 0.1.5 的词汇表已不含 `assistant/chunk`(v0 词表 51 条里**有**),于是每个 chunk 事件被 E3 误报。
7
- * 裁定原文(内部评审 §二):**"判据必须来自文件自身的版本,不是判官(运行时)的版本"**。
7
+ * 判据:**"判据必须来自文件自身的版本,不是判官(运行时)的版本"**。
8
8
  *
9
- * 2026-09-14 复核补强(同一裁定的完整落地)——一手证据:
9
+ * 2026-09-14 补强(同一判据的完整落地)——一手证据:
10
10
  * 1. **0.1.5 的 `foldSurface` 不是"放宽",而是换成了 v3 语义**(对照 `dsh-session@0.1.0-rc.7`
11
11
  * 与 `0.1.5-rc.1` 的 `isReplaceOp` / `assertProvenance`):
12
12
  * - replace 字段改名:rc.7 `{op,start,end}`(`lib/index.js:300-303`)→ 0.1.5 `{op,startSeq,endSeq}`
@@ -59,7 +59,7 @@ export const V0_EVENT_TYPES = new Set([
59
59
  * v2 = retained ∪ {assistant/attempt, assistant/message, delivery-accepted, session/end-seed}
60
60
  * = v0 − assistant/chunk + assistant/attempt
61
61
  * ⇒ **v2 不含 `assistant/chunk`**(旧实现误用 `V0 ∪ {attempt}`,把 chunk 留在 v2 词表里 =
62
- * 宽松口径,会漏报 v2 里的顶层 chunk 行 ⇒ F9 修正)。
62
+ * 宽松口径,会漏报 v2 里的顶层 chunk 行 ⇒ 已修正)。
63
63
  */
64
64
  export const V2_EVENT_TYPES = new Set([
65
65
  ...[...V0_EVENT_TYPES].filter((t) => t !== 'assistant/chunk'),
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.14",
4
+ "version": "0.3.16",
5
5
  "packageManager": "pnpm@11.7.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -29,9 +29,9 @@
29
29
  "README.zh.md"
30
30
  ],
31
31
  "scripts": {
32
- "check": "node scripts/check-syntax.mjs",
32
+ "check": "node scripts/check-syntax.mjs && node scripts/gen-contracts-doc.mjs --check",
33
33
  "test": "vitest run",
34
- "prepublishOnly": "node scripts/check-pkg-meta.mjs && node scripts/check-publish-leaks.mjs && pnpm check && pnpm test"
34
+ "prepublishOnly": "node scripts/check-pkg-meta.mjs && node scripts/prepublish-gate.mjs && pnpm check && pnpm test"
35
35
  },
36
36
  "keywords": [
37
37
  "dsh",