@zhushanwen/pi-system-prompt-trace 0.2.4 → 0.2.5

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
@@ -13,7 +13,6 @@ appendEntry("taiji:system-prompt", {
13
13
  reason: "initial" | "resume" | "change";
14
14
  fullText: string; // 完整 system prompt
15
15
  charCount: number; // fullText.length
16
- parentVersionDiffSummary?: string; // 与上一版的行级 diff 摘要(有 parent 全文时)
17
16
  });
18
17
  ```
19
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-system-prompt-trace",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "System prompt trace extension: appends taiji:system-prompt entries to the session JSONL whenever the effective system prompt is established or changes",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -33,11 +33,11 @@
33
33
  "vitest": "^4.1.8"
34
34
  },
35
35
  "dependencies": {
36
- "@zhushanwen/pi-ext-guards": "0.5.0",
36
+ "@zhushanwen/pi-ext-guards": "0.5.1",
37
37
  "@zhushanwen/pi-extension-logger": "0.6.2"
38
38
  },
39
39
  "scripts": {
40
40
  "typecheck": "npx tsc --noEmit",
41
- "test": "vitest run"
41
+ "test": "vitest run --pool=threads"
42
42
  }
43
43
  }
@@ -10,7 +10,7 @@
10
10
  * 此链路无 switch 事件、reason=startup;文件未落盘 / 无留痕 → null)
11
11
  * 4. 兜底:三档都读不到 → 任意 reason 必写一条(startup/new→initial,resume/fork/reload→resume)
12
12
  *
13
- * 用真实临时目录 + 真实 fs 函数(非 mock 投影);appendEntry 同步模拟 pi appendCustomEntry
13
+ * 用真实临时目录 + 真实 fs 函数(非 mock 派生视图);appendEntry 同步模拟 pi appendCustomEntry
14
14
  * 的落盘形状(session-manager.ts)。
15
15
  */
16
16
  import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -152,7 +152,7 @@ describe("A12 hash 基线跨重启恢复", () => {
152
152
  expect(h.sessionLines()).toHaveLength(1);
153
153
  });
154
154
 
155
- it("路径 1(进程内 resume):基线 hash 变化 → 写 resume v2,diff 摘要的 parent 全文来自目标文件", () => {
155
+ it("路径 1(进程内 resume):基线 hash 变化 → 写 resume v2(version 自 stash 基线续接)", () => {
156
156
  const h = makeHarness(P1);
157
157
  h.logic.onSessionStart("startup", undefined, h.ctx);
158
158
  h.logic.onTurnStart(h.ctx); // v1 initial
@@ -164,8 +164,6 @@ describe("A12 hash 基线跨重启恢复", () => {
164
164
  logic2.onTurnStart(h.ctx);
165
165
  expect(h.entries).toHaveLength(2);
166
166
  expect(h.entries[1]).toMatchObject({ version: 2, reason: "resume", hash: computePromptHash(P2) });
167
- expect(typeof h.entries[1]?.parentVersionDiffSummary).toBe("string");
168
- expect(h.entries[1]?.parentVersionDiffSummary).toContain("+1 -0 lines");
169
167
  });
170
168
 
171
169
  it("直读档(app 重启直 spawn resume):getSessionFile() 目标文件命中且 hash 未变 → 不写(reason=startup、无 switch 事件)", () => {
@@ -183,7 +181,7 @@ describe("A12 hash 基线跨重启恢复", () => {
183
181
  expect(h.sessionLines()).toHaveLength(1);
184
182
  });
185
183
 
186
- it("直读档(app 重启直 spawn resume):基线命中但 prompt 已变 → 写 resume v2,diff 摘要来自直读留痕 fullText(persisted 时代的数据层缺口已修复)", () => {
184
+ it("直读档(app 重启直 spawn resume):基线命中但 prompt 已变 → 写 resume v2(版本续接直读基线,非 initial 重新计数)", () => {
187
185
  const h = makeHarness(P1);
188
186
  h.logic.onSessionStart("startup", undefined, h.ctx);
189
187
  h.logic.onTurnStart(h.ctx); // v1
@@ -195,8 +193,6 @@ describe("A12 hash 基线跨重启恢复", () => {
195
193
  expect(h.entries).toHaveLength(2);
196
194
  // 基线存在 → 该 session 已有历史版本 → resume(而非 initial 重新计数)
197
195
  expect(h.entries[1]).toMatchObject({ version: 2, reason: "resume", hash: computePromptHash(P2) });
198
- // 直读档基线含 fullText → parentVersionDiffSummary 不再缺省(F1 修复)
199
- expect(h.entries[1]?.parentVersionDiffSummary).toContain("+1 -0 lines");
200
196
  });
201
197
 
202
198
  it("路径 4(兜底):三档基线都读不到(getSessionFile 未落盘返回 undefined)且 reason=resume → 必写一条", () => {
@@ -260,7 +256,7 @@ describe("A12 hash 基线跨重启恢复", () => {
260
256
  expect(h.entries).toHaveLength(1);
261
257
  });
262
258
 
263
- it("fork(V6 第二步):fork 基线命中确立 current 后配置回变 → 写 change,version 自源留痕续接 +1,diff 相对源留痕 fullText", () => {
259
+ it("fork(V6 第二步):fork 基线命中确立 current 后配置回变 → 写 change,version 自源留痕续接 +1", () => {
264
260
  // 源 session 预置两条留痕:v1(配置 A prompt)→ v2(配置 B prompt)
265
261
  const h = makeHarness(P1);
266
262
  h.logic.onSessionStart("startup", undefined, h.ctx);
@@ -288,8 +284,6 @@ describe("A12 hash 基线跨重启恢复", () => {
288
284
  hash: computePromptHash(P1),
289
285
  fullText: P1,
290
286
  });
291
- // diff 相对源留痕 v2 的 fullText(B):P2 → P1 为删 1 行("+N -M lines" 头形态)
292
- expect(h.entries[2]?.parentVersionDiffSummary).toContain("+0 -1 lines");
293
287
  // fork 新文件恰好落盘这一条留痕
294
288
  expect(h.sessionLines()).toHaveLength(1);
295
289
  });
@@ -308,11 +302,10 @@ describe("A12 hash 基线跨重启恢复", () => {
308
302
  data: { version: 2, hash: "hash-v2", reason: "change", fullText: "new text", charCount: 8 },
309
303
  };
310
304
  writeFileSync(file, [JSON.stringify(entryV1), "{ broken json", JSON.stringify(entryV2), ""].join("\n"));
311
- expect(readLastPromptFromSessionFile(file)).toMatchObject({
312
- hash: "hash-v2",
313
- version: 2,
314
- fullText: "new text",
315
- });
305
+ expect(readLastPromptFromSessionFile(file)).toMatchObject({
306
+ hash: "hash-v2",
307
+ version: 2,
308
+ });
316
309
  expect(readLastPromptFromSessionFile(join(scanDir, "missing.jsonl"))).toBeNull();
317
310
  });
318
311
  });
@@ -157,7 +157,7 @@ describe("index.ts wiring SDK 契约", () => {
157
157
  expect(injected).toBe(h.pi);
158
158
  });
159
159
 
160
- it("startup(无 previousSessionFile、session 未落盘)→ 首 turn 写 initial v1(appendEntry 形状);prompt 变化写 change v2 带 diff 摘要", async () => {
160
+ it("startup(无 previousSessionFile、session 未落盘)→ 首 turn 写 initial v1(appendEntry 形状);prompt 变化写 change v2", async () => {
161
161
  const ext = await loadExtension();
162
162
  const h = createWiringHarness();
163
163
  ext(h.pi);
@@ -175,14 +175,13 @@ describe("index.ts wiring SDK 契约", () => {
175
175
  charCount: P1.length,
176
176
  hash: computePromptHash(P1),
177
177
  });
178
- // 自持久化基线小文件已随 persisted 子系统删除(设计目标 2):无 agentDir 落盘面,仅 session JSONL
178
+ // 自持久化基线小文件已随 persisted 子系统删除(设计目标 2):无 agentDir 写盘位置,仅 session JSONL
179
179
 
180
180
  prompt = P2;
181
181
  await emit(h, "turn_start", { type: "turn_start", turnIndex: 1, timestamp: 0 }, ctx);
182
182
  expect(h.entries).toHaveLength(2);
183
183
  const change = entryData(h, 1);
184
184
  expect(change).toMatchObject({ version: 2, reason: "change", hash: computePromptHash(P2) });
185
- expect(change.parentVersionDiffSummary).toContain("+1 -0 lines");
186
185
  });
187
186
 
188
187
  it("fork(previousSessionFile 为 string)→ 直读该文件最后留痕作基线(D2 v5 定案);hash 未变不写", async () => {
@@ -287,7 +286,7 @@ describe("index.ts wiring SDK 契约", () => {
287
286
  expect(newH.entries).toHaveLength(0);
288
287
  });
289
288
 
290
- it("stash 基线 hash 与当前不同 → 新 runtime 写 resume 续接版本,diff 摘要 parent 全文来自目标文件", async () => {
289
+ it("stash 基线 hash 与当前不同 → 新 runtime 写 resume 续接版本(version 自目标文件留痕 +1)", async () => {
291
290
  const targetFile = join(rootDir, "target-session.jsonl");
292
291
  writeSessionEntry(targetFile, {
293
292
  version: 2,
@@ -317,7 +316,6 @@ describe("index.ts wiring SDK 契约", () => {
317
316
  expect(newH.entries).toHaveLength(1);
318
317
  const entry = entryData(newH, 0);
319
318
  expect(entry).toMatchObject({ version: 3, reason: "resume", hash: computePromptHash(P2) });
320
- expect(entry.parentVersionDiffSummary).toContain("+1 -0 lines");
321
319
  });
322
320
 
323
321
  it("session_before_switch 无 targetSessionFile → 不读文件;resume 兜底必写 v1", async () => {
@@ -4,7 +4,7 @@
4
4
  * 覆盖:
5
5
  * - 写入时机:session_start 不写(emit 早于 resources_discover 的 prompt 重建,必误报——设计 D2 校正),
6
6
  * 首个 turn_start 写 initial/resume
7
- * - hash 去重:相同不重写;变化写 change 且 parentVersionDiffSummary 生成
7
+ * - hash 去重:相同不重写;变化写 change
8
8
  * - SessionStartEvent.reason 原生 5 值(startup/reload/new/resume/fork)的落盘映射:
9
9
  * initial←startup/new、resume←resume/fork/reload(设计 D2 v5 定案:fork 基线取源文件
10
10
  * 最后留痕、reload 为同 session 运行时重建,均属重开语义)
@@ -88,7 +88,6 @@ describe("A11 留痕时机与去重", () => {
88
88
  charCount: P1.length,
89
89
  hash: computePromptHash(P1),
90
90
  });
91
- expect(entry.parentVersionDiffSummary).toBeUndefined();
92
91
  });
93
92
 
94
93
  it("new 与 startup 同映射 initial(previousSessionFile 存在也不误作基线)", () => {
@@ -117,7 +116,7 @@ describe("A11 留痕时机与去重", () => {
117
116
  expect(h.entries).toHaveLength(1);
118
117
  });
119
118
 
120
- it("prompt 变化写 change v2 且 parentVersionDiffSummary 生成", () => {
119
+ it("prompt 变化写 change v2", () => {
121
120
  const h = makeHarness(P1);
122
121
  h.logic.onSessionStart("startup", undefined, h.ctx);
123
122
  h.logic.onTurnStart(h.ctx);
@@ -132,8 +131,6 @@ describe("A11 留痕时机与去重", () => {
132
131
  hash: computePromptHash(P2),
133
132
  charCount: P2.length,
134
133
  });
135
- expect(entry.parentVersionDiffSummary).toContain("+1 -0 lines");
136
- expect(entry.parentVersionDiffSummary).toContain("+ [Available Models]");
137
134
  });
138
135
 
139
136
  it("F1b 模式回落事实:env 携带 from/to → 本进程每条 entry 均带 presetFallback(resume 快照与 change 一致),既有字段不削弱", () => {
package/src/baseline.ts CHANGED
@@ -22,7 +22,7 @@ import type { PromptBaseline } from "./types.js";
22
22
  * pi 落盘形状:{"type":"custom","customType":"taiji:system-prompt","data":{...},...}
23
23
  * (pi 1.0.0 实装 session-manager.js:901 appendCustomEntry)。
24
24
  */
25
- function parseTraceEntryData(line: string): { hash: string; version: number; fullText: string } | null {
25
+ function parseTraceEntryData(line: string): { hash: string; version: number } | null {
26
26
  let parsed: unknown;
27
27
  try {
28
28
  parsed = JSON.parse(line);
@@ -33,7 +33,7 @@ function parseTraceEntryData(line: string): { hash: string; version: number; ful
33
33
  if (parsed["type"] !== "custom" || parsed["customType"] !== SYSTEM_PROMPT_CUSTOM_TYPE) return null;
34
34
  const data = parsed["data"];
35
35
  if (!isSystemPromptTraceEntryData(data)) return null;
36
- return { hash: data.hash, version: data.version, fullText: data.fullText };
36
+ return { hash: data.hash, version: data.version };
37
37
  }
38
38
 
39
39
  /**
@@ -53,7 +53,7 @@ export function readLastPromptFromSessionFile(sessionFilePath: string): PromptBa
53
53
  if (line === "") continue;
54
54
  const parsed = parseTraceEntryData(line);
55
55
  if (parsed !== null) {
56
- return { hash: parsed.hash, version: parsed.version, fullText: parsed.fullText };
56
+ return { hash: parsed.hash, version: parsed.version };
57
57
  }
58
58
  }
59
59
  return null;
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi extension wiring:把 ExtensionContext 适配进 trace.ts 状态机并订阅三个事件。
3
3
  *
4
- * 事件直接标注 SDK 具名类型(与同组 msg-id-mapper / system-prompt 的既有风格一致):
4
+ * 事件直接标注 SDK 具名类型(与同组 host-internals / system-prompt 的既有风格一致):
5
5
  * pi 的 on() 按事件名重载,handler 参数即 SDK 事件类型。SDK reason 枚举演进时
6
6
  * mapReasonForFirstWrite 的 switch 在编译期报 non-exhaustive(负防腐),不再有运行时静默归一。
7
7
  */
@@ -46,7 +46,7 @@ export default function systemPromptTraceExtension(pi: ExtensionAPI): void {
46
46
 
47
47
  const env: TraceEnv = {
48
48
  readLastPromptFromFile: (filePath) => readLastPromptFromSessionFile(filePath),
49
- // F1b(E4 trace 披露面):模式回落事实由 runtime 经 spawn 出站 env 携带。
49
+ // F1b(E4 trace 披露部分):模式回落事实由 runtime 经 spawn 出站 env 携带。
50
50
  // 工厂调用时读一次(进程 env 生命期内不变);未回落 → 空串 → undefined。
51
51
  getPresetFallback: () => readPresetFallbackFromEnv(),
52
52
  };
package/src/trace.ts CHANGED
@@ -17,7 +17,6 @@ import type { SessionStartEvent } from "@earendil-works/pi-coding-agent";
17
17
  import { getLogger } from "@zhushanwen/pi-extension-logger";
18
18
 
19
19
  const logger = getLogger("pi-system-prompt-trace");
20
- import { summarizePromptDiff } from "./diff.js";
21
20
  import { mapReasonForFirstWrite, SYSTEM_PROMPT_CUSTOM_TYPE } from "./types.js";
22
21
  import type {
23
22
  PresetFallbackFact,
@@ -40,7 +39,7 @@ export interface TraceContext {
40
39
  export interface TraceEnv {
41
40
  readLastPromptFromFile(filePath: string): PromptBaseline | null;
42
41
  /**
43
- * 本次 pi 进程启动时的模式回落事实(F1b,E4 trace 披露面);未回落 / 未注入 → undefined。
42
+ * 本次 pi 进程启动时的模式回落事实(F1b,E4 trace 披露部分);未回落 / 未注入 → undefined。
44
43
  *
45
44
  * 进程 env 在生命期内不变,故工厂构造时读一次即可;为空字符串(runtime 的「显式
46
45
  * 清除」形态)时同样返回 undefined——不得把空串当回落事实。
@@ -62,7 +61,6 @@ export interface SystemPromptTrace {
62
61
  interface CurrentPrompt {
63
62
  version: number;
64
63
  hash: string;
65
- fullText: string;
66
64
  }
67
65
 
68
66
  function computePromptHash(text: string): string {
@@ -73,19 +71,12 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
73
71
  let sessionStartReason: SessionStartEvent["reason"] | null = null;
74
72
  let baseline: PromptBaseline | null = null;
75
73
  let current: CurrentPrompt | null = null;
76
- // F1b(设计 `mode-system-composer-density` §7.5 E4 trace 披露面):
74
+ // F1b(设计 `mode-system-composer-density` §7.5 E4 trace 披露部分):
77
75
  // 本次 pi 进程的模式回落事实由 spawn 出站 env 携带,进程生命期内恒定,构造时读一次。
78
76
  // 未回落 / 未注入 → undefined,entry 的 presetFallback 字段不出现(既有形状向后兼容)。
79
77
  const presetFallback = env.getPresetFallback();
80
78
 
81
- const write = (
82
- ctx: TraceContext,
83
- text: string,
84
- hash: string,
85
- reason: TraceReason,
86
- version: number,
87
- parentFullText: string | undefined,
88
- ): void => {
79
+ const write = (ctx: TraceContext, text: string, hash: string, reason: TraceReason, version: number): void => {
89
80
  const data: SystemPromptTraceEntryData = {
90
81
  version,
91
82
  hash,
@@ -93,9 +84,6 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
93
84
  fullText: text,
94
85
  charCount: text.length,
95
86
  };
96
- if (parentFullText !== undefined) {
97
- data.parentVersionDiffSummary = summarizePromptDiff(parentFullText, text);
98
- }
99
87
  // 回落事实随本进程写出的每条 entry 携带:本次 pi 运行确实以回落档启动,
100
88
  // resume 快照与后续 change 同属该运行,披露一致(事件语义 = 本次运行口径)。
101
89
  if (presetFallback !== undefined) {
@@ -145,14 +133,14 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
145
133
  if (current !== null) {
146
134
  // 后续 turn:hash 对比去重(设计 D2),变化才写 change
147
135
  if (hash === current.hash) return;
148
- write(ctx, text, hash, "change", current.version + 1, current.fullText);
149
- current = { version: current.version + 1, hash, fullText: text };
136
+ write(ctx, text, hash, "change", current.version + 1);
137
+ current = { version: current.version + 1, hash };
150
138
  return;
151
139
  }
152
140
  // 本 session_start 周期的首个 turn_start
153
141
  if (baseline !== null && baseline.hash === hash) {
154
142
  // 跨重启基线命中且未变化:不写,但确立 current 供后续 turn 继续对比
155
- current = { version: baseline.version, hash, fullText: text };
143
+ current = { version: baseline.version, hash };
156
144
  return;
157
145
  }
158
146
  // 必写:有基线 → resume(该 session 已有历史版本,这是重开点的快照,version 续接);
@@ -160,9 +148,8 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
160
148
  const version = baseline === null ? 1 : baseline.version + 1;
161
149
  const reason: TraceReason =
162
150
  baseline === null ? mapReasonForFirstWrite(sessionStartReason ?? "startup") : "resume";
163
- const parentFullText = baseline === null ? undefined : baseline.fullText;
164
- write(ctx, text, hash, reason, version, parentFullText);
165
- current = { version, hash, fullText: text };
151
+ write(ctx, text, hash, reason, version);
152
+ current = { version, hash };
166
153
  } catch (e) {
167
154
  // 留痕是诊断性旁路:任何失败都不允许影响 agent 主流程(logger.error → appendEntry 持久化)
168
155
  logger.error("turn_start handler failed", { detail: String(e) });
package/src/types.ts CHANGED
@@ -16,7 +16,7 @@ export const SYSTEM_PROMPT_CUSTOM_TYPE = "taiji:system-prompt";
16
16
  * `PRESET_FALLBACK_ENV_KEYS`)。
17
17
  *
18
18
  * extension 独立发布体系不依赖 `@taiji/shared`,只能按字面量镜像——单侧改名即断链
19
- * (消息见 write 产出的 `presetFallback` 字段消失)。一致性由机器守卫断言:
19
+ * (消息见 write 产出的 `presetFallback` 字段消失)。一致性由机器检查断言:
20
20
  * `.githooks/check_env_whitelist_sync.py` 规则 3 A8(漂移即红)。写入方 =
21
21
  * `packages/runtime/src/services/session/launch-params.ts` 的 `buildPresetFallbackEnv`。
22
22
  */
@@ -27,10 +27,10 @@ export const PRESET_FALLBACK_ENV_KEYS = {
27
27
 
28
28
  /**
29
29
  * 模式回落事实(F1b,设计 `mode-system-composer-density` §7.5 E4
30
- * 的 trace 披露面)。
30
+ * 的 trace 披露部分)。
31
31
  *
32
32
  * 语义 = 本次 pi 进程启动时,sidecar 里的模式定义已不可得,runtime 已回落
33
- * `builtin:full` 启动(工具面从模式的受限面弹回全工具)——记录在 trace entry 里,
33
+ * `builtin:full` 启动(工具范围从模式的受限面弹回全工具)——记录在 trace entry 里,
34
34
  * 事后审计从 `taiji:system-prompt` 即可得知「提示词为何变了」。
35
35
  */
36
36
  export interface PresetFallbackFact {
@@ -72,10 +72,8 @@ export interface SystemPromptTraceEntryData {
72
72
  fullText: string;
73
73
  /** fullText.length(UTF-16 码元数)。 */
74
74
  charCount: number;
75
- /** 与上一版的行级 diff 摘要;首条留痕(无 parent)时缺省。 */
76
- parentVersionDiffSummary?: string;
77
75
  /**
78
- * 模式回落事实(F1b,E4 trace 披露面):本次 pi 进程以「模式已删除 → 回落
76
+ * 模式回落事实(F1b,E4 trace 披露部分):本次 pi 进程以「模式已删除 → 回落
79
77
  * builtin:full」启动时出现;未回落(正常启动)时**字段不出现**——既有 entry
80
78
  * 形状向后兼容。见 `PresetFallbackFact`。
81
79
  */
@@ -86,8 +84,6 @@ export interface SystemPromptTraceEntryData {
86
84
  export interface PromptBaseline {
87
85
  hash: string;
88
86
  version: number;
89
- /** 留痕 entry 直读恒有值(可生成 diff 摘要)。 */
90
- fullText?: string;
91
87
  }
92
88
 
93
89
  /**
@@ -1,107 +0,0 @@
1
- /**
2
- * diff.ts summarizePromptDiff 分支矩阵(Gate-1.6 覆盖缺口:lcsLineDiff 回溯分支 + multiset 降级路径)。
3
- *
4
- * 覆盖目标(对照 diff.ts):
5
- * - 全等 → header-only(samples 为空的 return 分支)
6
- * - 纯尾部增/删 → LCS 回溯的两个尾部循环(while i<m / while j<n)
7
- * - 中段替换 → 回溯 removed 分支;头部插入 → 回溯 added 分支(dp 上移 < 右移)
8
- * - 采样交替(added 优先)+ 8 条上限;removed 提前耗尽后 added 补满(采样循环 removed 条件的 false 分支)
9
- * - 单行 >80 字符截断(truncate)
10
- * - LCS 面积超限(2001×2001 > 4M cells)→ multisetLineDiff 降级:共享行计数抵消、
11
- * added 按新文本序、removed 按旧行插入序
12
- * - 空字符串输入(split 产生单空行的边界)
13
- */
14
- import { describe, expect, it } from "vitest";
15
-
16
- import { summarizePromptDiff } from "../diff.js";
17
-
18
- describe("summarizePromptDiff", () => {
19
- it("全等 → 仅 header,无采样行", () => {
20
- expect(summarizePromptDiff("a\nb\nc", "a\nb\nc")).toBe("+0 -0 lines");
21
- });
22
-
23
- it("纯尾部新增 → LCS 回溯走尾部 added 循环", () => {
24
- expect(summarizePromptDiff("keep", "keep\na1\na2")).toBe("+2 -0 lines\n+ a1\n+ a2");
25
- });
26
-
27
- it("纯尾部删除 → LCS 回溯走尾部 removed 循环", () => {
28
- expect(summarizePromptDiff("keep\nr1\nr2", "keep")).toBe("+0 -2 lines\n- r1\n- r2");
29
- });
30
-
31
- it("中段单行替换 → removed/added 各一,采样 added 在前", () => {
32
- expect(summarizePromptDiff("x\n1\ny", "x\nA\ny")).toBe("+1 -1 lines\n+ A\n- 1");
33
- });
34
-
35
- it("头部插入 → LCS 回溯走 added 分支(dp 取右移更优)", () => {
36
- expect(summarizePromptDiff("b\nc", "a\nb\nc")).toBe("+1 -0 lines\n+ a");
37
- });
38
-
39
- it("交错增删 → 采样 added/removed 交替且与文本顺序一致", () => {
40
- expect(summarizePromptDiff("x\n1\ny\n2", "x\nA\ny\nB")).toBe("+2 -2 lines\n+ A\n- 1\n+ B\n- 2");
41
- });
42
-
43
- it("采样上限 8 条:added/removed 交错时各取前 4,余量丢弃", () => {
44
- const old = ["keep", "r1", "r2", "r3", "r4", "r5", "r6"].join("\n");
45
- const next = ["keep", "a1", "a2", "a3", "a4", "a5", "a6"].join("\n");
46
- const lines = summarizePromptDiff(old, next).split("\n");
47
- expect(lines).toEqual([
48
- "+6 -6 lines",
49
- "+ a1",
50
- "- r1",
51
- "+ a2",
52
- "- r2",
53
- "+ a3",
54
- "- r3",
55
- "+ a4",
56
- "- r4",
57
- ]);
58
- });
59
-
60
- it("removed 提前耗尽后 added 补满 8 条(采样循环 removed 条件的 false 分支)", () => {
61
- const old = ["keep", "r1", "r2", "r3"].join("\n");
62
- const next = ["keep", "a1", "a2", "a3", "a4", "a5"].join("\n");
63
- const lines = summarizePromptDiff(old, next).split("\n");
64
- expect(lines).toEqual([
65
- "+5 -3 lines",
66
- "+ a1",
67
- "- r1",
68
- "+ a2",
69
- "- r2",
70
- "+ a3",
71
- "- r3",
72
- "+ a4",
73
- "+ a5",
74
- ]);
75
- });
76
-
77
- it("单行超 80 字符 → 截断为 79 字符 + 省略号", () => {
78
- const long = "L".repeat(120);
79
- const lines = summarizePromptDiff("keep", `keep\n${long}`).split("\n");
80
- expect(lines[0]).toBe("+1 -0 lines");
81
- expect(lines[1]).toBe(`+ ${"L".repeat(79)}…`);
82
- });
83
-
84
- it("空字符串输入 → 空行计入 removed(split 单空行边界)", () => {
85
- expect(summarizePromptDiff("", "a")).toBe("+1 -1 lines\n+ a\n- ");
86
- });
87
-
88
- it("LCS 面积超限(2001×2001 > 4M cells)→ multiset 降级:共享行抵消,added 按新文本序", () => {
89
- const oldLines = ["shared", ...Array.from({ length: 2000 }, (_, i) => `old-${i}`)];
90
- const newLines = ["shared", ...Array.from({ length: 2000 }, (_, i) => `new-${i}`)];
91
- const summary = summarizePromptDiff(oldLines.join("\n"), newLines.join("\n"));
92
- const lines = summary.split("\n");
93
- expect(lines[0]).toBe("+2000 -2000 lines");
94
- expect(lines.slice(1)).toEqual([
95
- "+ new-0",
96
- "- old-0",
97
- "+ new-1",
98
- "- old-1",
99
- "+ new-2",
100
- "- old-2",
101
- "+ new-3",
102
- "- old-3",
103
- ]);
104
- // shared 两边各出现一次 → multiset 计数抵消,不进 added/removed
105
- expect(summary).not.toContain("shared");
106
- });
107
- });
package/src/diff.ts DELETED
@@ -1,109 +0,0 @@
1
- /**
2
- * 行级 diff 摘要生成器(parentVersionDiffSummary 字段)。
3
- *
4
- * 设计定位:entry 内的摘要是对用户友好的「一眼看出改了什么」;Trace 视图 inspector 在渲染时
5
- * 会用相邻两条留痕 entry 的 fullText 重算完整 diff,这里只需轻量摘要,不追求 patch 级精确。
6
- */
7
-
8
- /** 摘要采样行数上限(防巨型 prompt 差异把 entry 撑爆)。 */
9
- const MAX_SAMPLE_LINES = 8;
10
- /** 单行截断长度(字符)。 */
11
- const MAX_SAMPLE_CHARS = 80;
12
- /** LCS DP 面积上限:超过退化为 multiset 计数(O(n·m) → O(n+m),防巨型 prompt 爆内存)。 */
13
- const MAX_LCS_CELLS = 4_000_000;
14
-
15
- /** 生成 oldText → newText 的行级 diff 摘要:"+A -R lines" 头 + 最多 8 条采样行。 */
16
- export function summarizePromptDiff(oldText: string, newText: string): string {
17
- const oldLines = oldText.split("\n");
18
- const newLines = newText.split("\n");
19
- const { removed, added } =
20
- oldLines.length * newLines.length <= MAX_LCS_CELLS
21
- ? lcsLineDiff(oldLines, newLines)
22
- : multisetLineDiff(oldLines, newLines);
23
-
24
- const header = `+${added.length} -${removed.length} lines`;
25
- const samples: string[] = [];
26
- // 交替取样(added 优先):注入类变化(只增不减)时全部展示新增行,混合变化时两类都可见
27
- let ai = 0;
28
- let ri = 0;
29
- while (samples.length < MAX_SAMPLE_LINES && (ai < added.length || ri < removed.length)) {
30
- if (ai < added.length) {
31
- samples.push(`+ ${truncate(added[ai])}`);
32
- ai++;
33
- }
34
- if (samples.length < MAX_SAMPLE_LINES && ri < removed.length) {
35
- samples.push(`- ${truncate(removed[ri])}`);
36
- ri++;
37
- }
38
- }
39
- return samples.length === 0 ? header : [header, ...samples].join("\n");
40
- }
41
-
42
- /** LCS 回溯行 diff:产出与文本顺序一致的 removed/added 序列(采样时更有可读性)。 */
43
- function lcsLineDiff(a: readonly string[], b: readonly string[]): { removed: string[]; added: string[] } {
44
- const m = a.length;
45
- const n = b.length;
46
- const width = n + 1;
47
- const dp = new Uint32Array((m + 1) * width);
48
- for (let i = m - 1; i >= 0; i--) {
49
- for (let j = n - 1; j >= 0; j--) {
50
- dp[i * width + j] =
51
- a[i] === b[j]
52
- ? dp[(i + 1) * width + j + 1] + 1
53
- : Math.max(dp[(i + 1) * width + j], dp[i * width + j + 1]);
54
- }
55
- }
56
- const removed: string[] = [];
57
- const added: string[] = [];
58
- let i = 0;
59
- let j = 0;
60
- while (i < m && j < n) {
61
- if (a[i] === b[j]) {
62
- i++;
63
- j++;
64
- } else if (dp[(i + 1) * width + j] >= dp[i * width + j + 1]) {
65
- removed.push(a[i]);
66
- i++;
67
- } else {
68
- added.push(b[j]);
69
- j++;
70
- }
71
- }
72
- while (i < m) {
73
- removed.push(a[i]);
74
- i++;
75
- }
76
- while (j < n) {
77
- added.push(b[j]);
78
- j++;
79
- }
80
- return { removed, added };
81
- }
82
-
83
- /** multiset 行计数 diff(LCS 面积超限的降级路径):行内容匹配抵消,剩余计入 added/removed。 */
84
- function multisetLineDiff(a: readonly string[], b: readonly string[]): { removed: string[]; added: string[] } {
85
- const counts = new Map<string, number>();
86
- for (const line of a) {
87
- counts.set(line, (counts.get(line) ?? 0) + 1);
88
- }
89
- const added: string[] = [];
90
- for (const line of b) {
91
- const c = counts.get(line) ?? 0;
92
- if (c > 0) {
93
- counts.set(line, c - 1);
94
- } else {
95
- added.push(line);
96
- }
97
- }
98
- const removed: string[] = [];
99
- for (const [line, c] of counts) {
100
- for (let k = 0; k < c; k++) {
101
- removed.push(line);
102
- }
103
- }
104
- return { removed, added };
105
- }
106
-
107
- function truncate(line: string): string {
108
- return line.length > MAX_SAMPLE_CHARS ? line.slice(0, MAX_SAMPLE_CHARS - 1) + "…" : line;
109
- }