@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 +0 -1
- package/package.json +3 -3
- package/src/__tests__/baseline.a12.test.ts +8 -15
- package/src/__tests__/index-wiring.test.ts +3 -5
- package/src/__tests__/trace.a11.test.ts +2 -5
- package/src/baseline.ts +3 -3
- package/src/index.ts +2 -2
- package/src/trace.ts +8 -21
- package/src/types.ts +4 -8
- package/src/__tests__/diff.test.ts +0 -107
- package/src/diff.ts +0 -109
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-system-prompt-trace",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 具名类型(与同组
|
|
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
|
|
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
|
|
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
|
|
149
|
-
current = { version: current.version + 1, hash
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
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`
|
|
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
|
|
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
|
-
}
|