pi-smart-compact 9.2.1 → 9.3.0
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/ARCHITECTURE.md +2 -2
- package/CHANGELOG.md +95 -0
- package/dist/app/steps/extract.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/app/steps/verify.d.ts.map +1 -1
- package/dist/constants.d.ts +1 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/domain/scrub.d.ts.map +1 -1
- package/dist/domain/summary-parse.d.ts +5 -0
- package/dist/domain/summary-parse.d.ts.map +1 -1
- package/dist/index.js +715 -152
- package/dist/infra/llm-client.d.ts +1 -1
- package/dist/infra/llm-client.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts +3 -5
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/phases/verify.d.ts +2 -1
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/provider-eval.js +392 -58
- package/dist/provider-scenario-eval.js +800 -249
- package/dist/telemetry-report.js +364 -30
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/file-needles.d.ts.map +1 -1
- package/dist/utils/file-ref-detect.d.ts.map +1 -1
- package/dist/utils/state.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -4,172 +4,8 @@ var __require = import.meta.require;
|
|
|
4
4
|
// scripts/provider-scenario-eval.ts
|
|
5
5
|
import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
-
// src/infra/llm-client.ts
|
|
8
|
-
var _complete = null;
|
|
9
|
-
var _completeSimple = null;
|
|
10
|
-
var _stream = null;
|
|
11
|
-
var _streamSimple = null;
|
|
12
|
-
async function resolveComplete() {
|
|
13
|
-
if (_complete)
|
|
14
|
-
return _complete;
|
|
15
|
-
const mod = await import("@earendil-works/pi-ai/compat");
|
|
16
|
-
const fn = mod.complete;
|
|
17
|
-
if (typeof fn !== "function")
|
|
18
|
-
throw new Error("smart-compact: pi-ai /compat did not export complete()");
|
|
19
|
-
_complete = fn;
|
|
20
|
-
return fn;
|
|
21
|
-
}
|
|
22
|
-
async function resolveCompleteSimple() {
|
|
23
|
-
if (_completeSimple)
|
|
24
|
-
return _completeSimple;
|
|
25
|
-
const mod = await import("@earendil-works/pi-ai/compat");
|
|
26
|
-
const fn = mod.completeSimple;
|
|
27
|
-
if (typeof fn !== "function")
|
|
28
|
-
throw new Error("smart-compact: pi-ai /compat did not export completeSimple()");
|
|
29
|
-
_completeSimple = fn;
|
|
30
|
-
return fn;
|
|
31
|
-
}
|
|
32
|
-
async function resolveStream() {
|
|
33
|
-
if (_stream)
|
|
34
|
-
return _stream;
|
|
35
|
-
const mod = await import("@earendil-works/pi-ai/compat");
|
|
36
|
-
if (typeof mod.stream !== "function")
|
|
37
|
-
throw new Error("smart-compact: pi-ai /compat did not export stream()");
|
|
38
|
-
_stream = mod.stream;
|
|
39
|
-
return _stream;
|
|
40
|
-
}
|
|
41
|
-
async function resolveStreamSimple() {
|
|
42
|
-
if (_streamSimple)
|
|
43
|
-
return _streamSimple;
|
|
44
|
-
const mod = await import("@earendil-works/pi-ai/compat");
|
|
45
|
-
if (typeof mod.streamSimple !== "function")
|
|
46
|
-
throw new Error("smart-compact: pi-ai /compat did not export streamSimple()");
|
|
47
|
-
_streamSimple = mod.streamSimple;
|
|
48
|
-
return _streamSimple;
|
|
49
|
-
}
|
|
50
|
-
function isChatGptCodex(model) {
|
|
51
|
-
if (model.api !== "openai-codex-responses")
|
|
52
|
-
return false;
|
|
53
|
-
return !model.baseUrl || model.baseUrl.includes("chatgpt.com");
|
|
54
|
-
}
|
|
55
|
-
function withCodexWireLimit(model, opts) {
|
|
56
|
-
if (model.api !== "openai-codex-responses" || isChatGptCodex(model) || !opts.maxTokens)
|
|
57
|
-
return opts;
|
|
58
|
-
const previous = opts.onPayload;
|
|
59
|
-
return {
|
|
60
|
-
...opts,
|
|
61
|
-
onPayload: async (payload, requestModel) => {
|
|
62
|
-
const transformed = await previous?.(payload, requestModel);
|
|
63
|
-
const body = transformed ?? payload;
|
|
64
|
-
return body && typeof body === "object" ? { ...body, max_output_tokens: opts.maxTokens } : body;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function resolveCodexWatchdogMs(maxTokens, configuredMs = 0) {
|
|
69
|
-
if (configuredMs > 0)
|
|
70
|
-
return configuredMs;
|
|
71
|
-
return Math.min(90000, Math.max(15000, 1e4 + (maxTokens ?? 4096) * 8));
|
|
72
|
-
}
|
|
73
|
-
function streamedChars(event) {
|
|
74
|
-
if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") {
|
|
75
|
-
return event.delta.length;
|
|
76
|
-
}
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
function assertSuccessful(message) {
|
|
80
|
-
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
81
|
-
throw new Error(message.errorMessage || "LLM request failed");
|
|
82
|
-
}
|
|
83
|
-
return message;
|
|
84
|
-
}
|
|
85
|
-
async function withProviderDeadline(opts, invoke) {
|
|
86
|
-
if (opts.signal?.aborted)
|
|
87
|
-
throw new Error("LLM request aborted before dispatch");
|
|
88
|
-
const controller = new AbortController;
|
|
89
|
-
const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
|
|
90
|
-
const abort = Promise.withResolvers();
|
|
91
|
-
const abortFromCaller = () => {
|
|
92
|
-
controller.abort(opts.signal?.reason);
|
|
93
|
-
abort.reject(new Error("LLM request aborted by caller"));
|
|
94
|
-
};
|
|
95
|
-
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
96
|
-
const timeout = Promise.withResolvers();
|
|
97
|
-
const timer = setTimeout(() => {
|
|
98
|
-
controller.abort("provider-watchdog");
|
|
99
|
-
timeout.reject(new Error("Provider watchdog stopped generation after " + watchdogMs + "ms"));
|
|
100
|
-
}, watchdogMs);
|
|
101
|
-
if (typeof timer === "object" && "unref" in timer)
|
|
102
|
-
timer.unref();
|
|
103
|
-
try {
|
|
104
|
-
return await Promise.race([
|
|
105
|
-
invoke({ ...opts, signal: controller.signal }),
|
|
106
|
-
abort.promise,
|
|
107
|
-
timeout.promise
|
|
108
|
-
]);
|
|
109
|
-
} finally {
|
|
110
|
-
clearTimeout(timer);
|
|
111
|
-
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
async function completeChatGptCodex(model, body, opts) {
|
|
115
|
-
const controller = new AbortController;
|
|
116
|
-
const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
|
|
117
|
-
let watchdogReason = null;
|
|
118
|
-
let visibleChars = 0;
|
|
119
|
-
const abortFromCaller = () => controller.abort(opts.signal?.reason);
|
|
120
|
-
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
121
|
-
if (opts.signal?.aborted)
|
|
122
|
-
abortFromCaller();
|
|
123
|
-
const timer = setTimeout(() => {
|
|
124
|
-
watchdogReason = "time";
|
|
125
|
-
controller.abort("codex-watchdog");
|
|
126
|
-
}, watchdogMs);
|
|
127
|
-
timer.unref?.();
|
|
128
|
-
try {
|
|
129
|
-
const limited = { ...opts, signal: controller.signal };
|
|
130
|
-
const events = opts.reasoning === undefined ? (await resolveStream())(model, body, limited) : (await resolveStreamSimple())(model, body, limited);
|
|
131
|
-
let final;
|
|
132
|
-
for await (const event of events) {
|
|
133
|
-
visibleChars += streamedChars(event);
|
|
134
|
-
if (!watchdogReason && opts.maxTokens && visibleChars > opts.maxTokens * 3) {
|
|
135
|
-
watchdogReason = "visible-output";
|
|
136
|
-
controller.abort("codex-visible-output-cap");
|
|
137
|
-
}
|
|
138
|
-
if (event.type === "done")
|
|
139
|
-
final = event.message;
|
|
140
|
-
else if (event.type === "error")
|
|
141
|
-
final = event.error;
|
|
142
|
-
}
|
|
143
|
-
if (watchdogReason) {
|
|
144
|
-
throw new Error("Codex " + watchdogReason + " watchdog stopped generation after " + watchdogMs + "ms / " + visibleChars + " streamed chars");
|
|
145
|
-
}
|
|
146
|
-
if (!final)
|
|
147
|
-
throw new Error("Codex stream ended without a final message");
|
|
148
|
-
return assertSuccessful(final);
|
|
149
|
-
} finally {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
var rawLlmClient = {
|
|
155
|
-
complete: async (model, body, originalOpts) => {
|
|
156
|
-
const opts = withCodexWireLimit(model, originalOpts);
|
|
157
|
-
return withProviderDeadline(opts, async (bounded) => {
|
|
158
|
-
if (isChatGptCodex(model))
|
|
159
|
-
return completeChatGptCodex(model, body, bounded);
|
|
160
|
-
const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
|
|
161
|
-
return assertSuccessful(response);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
var defaultLlmClient = rawLlmClient;
|
|
166
|
-
var _client = defaultLlmClient;
|
|
167
|
-
function getLlmClient() {
|
|
168
|
-
return _client;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
7
|
// src/constants.ts
|
|
172
|
-
var VERSION = "9.
|
|
8
|
+
var VERSION = "9.3.0";
|
|
173
9
|
var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
|
|
174
10
|
var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
|
|
175
11
|
var PROFILES = {
|
|
@@ -391,9 +227,6 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
|
|
|
391
227
|
` + `After exploration, output ONLY a JSON object (no markdown):
|
|
392
228
|
` + '{"boundaries":[{"afterIndex":N,"topic":"...","priority":"critical|high|normal|low","confidence":0.0-1.0}],"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","enrichedConstraints":[...],"crossReferences":[...],"statusAssessment":{"done":[...],"inProgress":[...],"blocked":[...]},"criticalContext":[...],"keyDecisions":[...]}';
|
|
393
229
|
|
|
394
|
-
// src/utils/cache.ts
|
|
395
|
-
import fs2 from "fs";
|
|
396
|
-
|
|
397
230
|
// src/utils/lru.ts
|
|
398
231
|
function lruGet(m, key) {
|
|
399
232
|
if (!m.has(key))
|
|
@@ -416,6 +249,188 @@ function lruSet(m, key, value, max) {
|
|
|
416
249
|
}
|
|
417
250
|
|
|
418
251
|
// src/utils/tokens.ts
|
|
252
|
+
var PROVIDER_MAP = {
|
|
253
|
+
"zai-anthropic": {
|
|
254
|
+
maxOutputTokens: 8192,
|
|
255
|
+
supportsTools: "probe",
|
|
256
|
+
jsonReliability: "high",
|
|
257
|
+
instructionFollowing: "high",
|
|
258
|
+
tokenRatioEstimate: 3.5,
|
|
259
|
+
concurrencyLimit: 3,
|
|
260
|
+
cacheStrategy: "anthropic",
|
|
261
|
+
timeoutMultiplier: 1.2,
|
|
262
|
+
singlePassTokenMultiplier: 1,
|
|
263
|
+
multimodal: "metadata-only"
|
|
264
|
+
},
|
|
265
|
+
"kimi-coding": {
|
|
266
|
+
maxOutputTokens: 8192,
|
|
267
|
+
supportsTools: "probe",
|
|
268
|
+
jsonReliability: "high",
|
|
269
|
+
instructionFollowing: "high",
|
|
270
|
+
tokenRatioEstimate: 3.5,
|
|
271
|
+
concurrencyLimit: 2,
|
|
272
|
+
cacheStrategy: "anthropic",
|
|
273
|
+
timeoutMultiplier: 1.5,
|
|
274
|
+
singlePassTokenMultiplier: 0.95,
|
|
275
|
+
multimodal: "metadata-only"
|
|
276
|
+
},
|
|
277
|
+
anthropic: {
|
|
278
|
+
maxOutputTokens: 8192,
|
|
279
|
+
supportsTools: true,
|
|
280
|
+
jsonReliability: "high",
|
|
281
|
+
instructionFollowing: "high",
|
|
282
|
+
tokenRatioEstimate: 3.5,
|
|
283
|
+
concurrencyLimit: 3,
|
|
284
|
+
cacheStrategy: "anthropic",
|
|
285
|
+
timeoutMultiplier: 1.2,
|
|
286
|
+
singlePassTokenMultiplier: 1,
|
|
287
|
+
multimodal: "native"
|
|
288
|
+
},
|
|
289
|
+
openai: {
|
|
290
|
+
maxOutputTokens: 16384,
|
|
291
|
+
supportsTools: true,
|
|
292
|
+
jsonReliability: "high",
|
|
293
|
+
instructionFollowing: "high",
|
|
294
|
+
tokenRatioEstimate: 4,
|
|
295
|
+
concurrencyLimit: 5,
|
|
296
|
+
cacheStrategy: "openai",
|
|
297
|
+
timeoutMultiplier: 1,
|
|
298
|
+
singlePassTokenMultiplier: 1.15,
|
|
299
|
+
multimodal: "native"
|
|
300
|
+
},
|
|
301
|
+
google: {
|
|
302
|
+
maxOutputTokens: 8192,
|
|
303
|
+
supportsTools: true,
|
|
304
|
+
jsonReliability: "high",
|
|
305
|
+
instructionFollowing: "high",
|
|
306
|
+
tokenRatioEstimate: 3.8,
|
|
307
|
+
concurrencyLimit: 3,
|
|
308
|
+
cacheStrategy: "openai",
|
|
309
|
+
timeoutMultiplier: 1.15,
|
|
310
|
+
singlePassTokenMultiplier: 1.1,
|
|
311
|
+
multimodal: "native"
|
|
312
|
+
},
|
|
313
|
+
deepseek: {
|
|
314
|
+
maxOutputTokens: 8192,
|
|
315
|
+
supportsTools: true,
|
|
316
|
+
jsonReliability: "medium",
|
|
317
|
+
instructionFollowing: "medium",
|
|
318
|
+
tokenRatioEstimate: 3.6,
|
|
319
|
+
concurrencyLimit: 2,
|
|
320
|
+
cacheStrategy: "none",
|
|
321
|
+
timeoutMultiplier: 1.5,
|
|
322
|
+
singlePassTokenMultiplier: 0.85,
|
|
323
|
+
multimodal: "metadata-only"
|
|
324
|
+
},
|
|
325
|
+
minimax: {
|
|
326
|
+
maxOutputTokens: 4096,
|
|
327
|
+
supportsTools: "probe",
|
|
328
|
+
jsonReliability: "medium",
|
|
329
|
+
instructionFollowing: "medium",
|
|
330
|
+
tokenRatioEstimate: 3.8,
|
|
331
|
+
concurrencyLimit: 2,
|
|
332
|
+
cacheStrategy: "anthropic",
|
|
333
|
+
timeoutMultiplier: 1.6,
|
|
334
|
+
singlePassTokenMultiplier: 0.8,
|
|
335
|
+
multimodal: "metadata-only"
|
|
336
|
+
},
|
|
337
|
+
"xiaomi-token-plan": {
|
|
338
|
+
maxOutputTokens: 8192,
|
|
339
|
+
supportsTools: "probe",
|
|
340
|
+
jsonReliability: "medium",
|
|
341
|
+
instructionFollowing: "medium",
|
|
342
|
+
tokenRatioEstimate: 3.3,
|
|
343
|
+
concurrencyLimit: 2,
|
|
344
|
+
cacheStrategy: "openai",
|
|
345
|
+
timeoutMultiplier: 1.35,
|
|
346
|
+
singlePassTokenMultiplier: 0.9,
|
|
347
|
+
multimodal: "metadata-only"
|
|
348
|
+
},
|
|
349
|
+
"xiaomi-mimo": {
|
|
350
|
+
maxOutputTokens: 8192,
|
|
351
|
+
supportsTools: "probe",
|
|
352
|
+
jsonReliability: "medium",
|
|
353
|
+
instructionFollowing: "medium",
|
|
354
|
+
tokenRatioEstimate: 3.3,
|
|
355
|
+
concurrencyLimit: 2,
|
|
356
|
+
cacheStrategy: "anthropic",
|
|
357
|
+
timeoutMultiplier: 1.35,
|
|
358
|
+
singlePassTokenMultiplier: 0.9,
|
|
359
|
+
multimodal: "metadata-only"
|
|
360
|
+
},
|
|
361
|
+
crofai: {
|
|
362
|
+
maxOutputTokens: 8192,
|
|
363
|
+
supportsTools: "probe",
|
|
364
|
+
jsonReliability: "medium",
|
|
365
|
+
instructionFollowing: "medium",
|
|
366
|
+
tokenRatioEstimate: 3.8,
|
|
367
|
+
concurrencyLimit: 3,
|
|
368
|
+
cacheStrategy: "none",
|
|
369
|
+
timeoutMultiplier: 1.2,
|
|
370
|
+
singlePassTokenMultiplier: 0.95,
|
|
371
|
+
multimodal: "metadata-only"
|
|
372
|
+
},
|
|
373
|
+
mistral: {
|
|
374
|
+
maxOutputTokens: 8192,
|
|
375
|
+
supportsTools: true,
|
|
376
|
+
jsonReliability: "high",
|
|
377
|
+
instructionFollowing: "high",
|
|
378
|
+
tokenRatioEstimate: 3.5,
|
|
379
|
+
concurrencyLimit: 3,
|
|
380
|
+
cacheStrategy: "openai",
|
|
381
|
+
timeoutMultiplier: 1.2,
|
|
382
|
+
singlePassTokenMultiplier: 1,
|
|
383
|
+
multimodal: "metadata-only"
|
|
384
|
+
},
|
|
385
|
+
xai: {
|
|
386
|
+
maxOutputTokens: 8192,
|
|
387
|
+
supportsTools: true,
|
|
388
|
+
jsonReliability: "medium",
|
|
389
|
+
instructionFollowing: "high",
|
|
390
|
+
tokenRatioEstimate: 3.8,
|
|
391
|
+
concurrencyLimit: 3,
|
|
392
|
+
cacheStrategy: "openai",
|
|
393
|
+
timeoutMultiplier: 1.2,
|
|
394
|
+
singlePassTokenMultiplier: 1,
|
|
395
|
+
multimodal: "native"
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var PROVIDER_ALIASES = [
|
|
399
|
+
{ pattern: /anthropic/i, provider: "anthropic" },
|
|
400
|
+
{ pattern: /kimi/i, provider: "kimi-coding" },
|
|
401
|
+
{ pattern: /zai/i, provider: "zai-anthropic" },
|
|
402
|
+
{ pattern: /openai/i, provider: "openai" },
|
|
403
|
+
{ pattern: /gpt/i, provider: "openai" },
|
|
404
|
+
{ pattern: /google|gemini/i, provider: "google" },
|
|
405
|
+
{ pattern: /deepseek/i, provider: "deepseek" },
|
|
406
|
+
{ pattern: /minimax/i, provider: "minimax" },
|
|
407
|
+
{ pattern: /xiaomi-mimo/i, provider: "xiaomi-mimo" },
|
|
408
|
+
{ pattern: /xiaomi/i, provider: "xiaomi-token-plan" },
|
|
409
|
+
{ pattern: /crofai/i, provider: "crofai" },
|
|
410
|
+
{ pattern: /mistral/i, provider: "mistral" },
|
|
411
|
+
{ pattern: /xai|grok/i, provider: "xai" }
|
|
412
|
+
];
|
|
413
|
+
var DEFAULT_CAPS = {
|
|
414
|
+
maxOutputTokens: 8192,
|
|
415
|
+
supportsTools: "probe",
|
|
416
|
+
jsonReliability: "medium",
|
|
417
|
+
instructionFollowing: "medium",
|
|
418
|
+
tokenRatioEstimate: 3.8,
|
|
419
|
+
concurrencyLimit: 2,
|
|
420
|
+
cacheStrategy: "none",
|
|
421
|
+
timeoutMultiplier: 1.35,
|
|
422
|
+
singlePassTokenMultiplier: 0.9,
|
|
423
|
+
multimodal: "metadata-only"
|
|
424
|
+
};
|
|
425
|
+
function getProviderCaps(provider) {
|
|
426
|
+
if (PROVIDER_MAP[provider])
|
|
427
|
+
return PROVIDER_MAP[provider];
|
|
428
|
+
for (const { pattern, provider: key } of PROVIDER_ALIASES) {
|
|
429
|
+
if (pattern.test(provider))
|
|
430
|
+
return PROVIDER_MAP[key] ?? DEFAULT_CAPS;
|
|
431
|
+
}
|
|
432
|
+
return DEFAULT_CAPS;
|
|
433
|
+
}
|
|
419
434
|
class TokenCalibrationStore {
|
|
420
435
|
maxEntries;
|
|
421
436
|
factors = new Map;
|
|
@@ -452,6 +467,177 @@ function calibrationKey(provider, model) {
|
|
|
452
467
|
return model ? provider + "/" + model : provider + "/*";
|
|
453
468
|
}
|
|
454
469
|
|
|
470
|
+
// src/infra/llm-client.ts
|
|
471
|
+
var _complete = null;
|
|
472
|
+
var _completeSimple = null;
|
|
473
|
+
var _stream = null;
|
|
474
|
+
var _streamSimple = null;
|
|
475
|
+
async function resolveComplete() {
|
|
476
|
+
if (_complete)
|
|
477
|
+
return _complete;
|
|
478
|
+
const mod = await import("@earendil-works/pi-ai/compat");
|
|
479
|
+
const fn = mod.complete;
|
|
480
|
+
if (typeof fn !== "function")
|
|
481
|
+
throw new Error("smart-compact: pi-ai /compat did not export complete()");
|
|
482
|
+
_complete = fn;
|
|
483
|
+
return fn;
|
|
484
|
+
}
|
|
485
|
+
async function resolveCompleteSimple() {
|
|
486
|
+
if (_completeSimple)
|
|
487
|
+
return _completeSimple;
|
|
488
|
+
const mod = await import("@earendil-works/pi-ai/compat");
|
|
489
|
+
const fn = mod.completeSimple;
|
|
490
|
+
if (typeof fn !== "function")
|
|
491
|
+
throw new Error("smart-compact: pi-ai /compat did not export completeSimple()");
|
|
492
|
+
_completeSimple = fn;
|
|
493
|
+
return fn;
|
|
494
|
+
}
|
|
495
|
+
async function resolveStream() {
|
|
496
|
+
if (_stream)
|
|
497
|
+
return _stream;
|
|
498
|
+
const mod = await import("@earendil-works/pi-ai/compat");
|
|
499
|
+
if (typeof mod.stream !== "function")
|
|
500
|
+
throw new Error("smart-compact: pi-ai /compat did not export stream()");
|
|
501
|
+
_stream = mod.stream;
|
|
502
|
+
return _stream;
|
|
503
|
+
}
|
|
504
|
+
async function resolveStreamSimple() {
|
|
505
|
+
if (_streamSimple)
|
|
506
|
+
return _streamSimple;
|
|
507
|
+
const mod = await import("@earendil-works/pi-ai/compat");
|
|
508
|
+
if (typeof mod.streamSimple !== "function")
|
|
509
|
+
throw new Error("smart-compact: pi-ai /compat did not export streamSimple()");
|
|
510
|
+
_streamSimple = mod.streamSimple;
|
|
511
|
+
return _streamSimple;
|
|
512
|
+
}
|
|
513
|
+
function isChatGptCodex(model) {
|
|
514
|
+
if (model.api !== "openai-codex-responses")
|
|
515
|
+
return false;
|
|
516
|
+
return !model.baseUrl || model.baseUrl.includes("chatgpt.com");
|
|
517
|
+
}
|
|
518
|
+
function withCodexWireLimit(model, opts) {
|
|
519
|
+
if (model.api !== "openai-codex-responses" || isChatGptCodex(model) || !opts.maxTokens)
|
|
520
|
+
return opts;
|
|
521
|
+
const previous = opts.onPayload;
|
|
522
|
+
return {
|
|
523
|
+
...opts,
|
|
524
|
+
onPayload: async (payload, requestModel) => {
|
|
525
|
+
const transformed = await previous?.(payload, requestModel);
|
|
526
|
+
const body = transformed ?? payload;
|
|
527
|
+
return body && typeof body === "object" ? {
|
|
528
|
+
...body,
|
|
529
|
+
max_output_tokens: opts.maxTokens
|
|
530
|
+
} : body;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function resolveCodexWatchdogMs(maxTokens, configuredMs = 0) {
|
|
535
|
+
if (configuredMs > 0)
|
|
536
|
+
return configuredMs;
|
|
537
|
+
return Math.min(90000, Math.max(15000, 1e4 + (maxTokens ?? 4096) * 8));
|
|
538
|
+
}
|
|
539
|
+
function streamedChars(event) {
|
|
540
|
+
if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") {
|
|
541
|
+
return event.delta.length;
|
|
542
|
+
}
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
function assertSuccessful(message) {
|
|
546
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
547
|
+
throw new Error(message.errorMessage || "LLM request failed");
|
|
548
|
+
}
|
|
549
|
+
return message;
|
|
550
|
+
}
|
|
551
|
+
async function withProviderDeadline(opts, invoke, modelId) {
|
|
552
|
+
if (opts.signal?.aborted)
|
|
553
|
+
throw new Error("LLM request aborted before dispatch");
|
|
554
|
+
const controller = new AbortController;
|
|
555
|
+
const multiplier = modelId && !((opts.codexWatchdogMs ?? 0) > 0) ? getProviderCaps(modelId).timeoutMultiplier : 1;
|
|
556
|
+
const watchdogMs = Math.round(resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs) * multiplier);
|
|
557
|
+
const abort = Promise.withResolvers();
|
|
558
|
+
const abortFromCaller = () => {
|
|
559
|
+
controller.abort(opts.signal?.reason);
|
|
560
|
+
abort.reject(new Error("LLM request aborted by caller"));
|
|
561
|
+
};
|
|
562
|
+
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
563
|
+
const timeout = Promise.withResolvers();
|
|
564
|
+
const timer = setTimeout(() => {
|
|
565
|
+
controller.abort("provider-watchdog");
|
|
566
|
+
timeout.reject(new Error("Provider watchdog stopped generation after " + watchdogMs + "ms"));
|
|
567
|
+
}, watchdogMs);
|
|
568
|
+
if (typeof timer === "object" && "unref" in timer)
|
|
569
|
+
timer.unref();
|
|
570
|
+
try {
|
|
571
|
+
return await Promise.race([
|
|
572
|
+
invoke({ ...opts, signal: controller.signal }),
|
|
573
|
+
abort.promise,
|
|
574
|
+
timeout.promise
|
|
575
|
+
]);
|
|
576
|
+
} finally {
|
|
577
|
+
clearTimeout(timer);
|
|
578
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function completeChatGptCodex(model, body, opts) {
|
|
582
|
+
const controller = new AbortController;
|
|
583
|
+
const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
|
|
584
|
+
let watchdogReason = null;
|
|
585
|
+
let visibleChars = 0;
|
|
586
|
+
const abortFromCaller = () => controller.abort(opts.signal?.reason);
|
|
587
|
+
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
588
|
+
if (opts.signal?.aborted)
|
|
589
|
+
abortFromCaller();
|
|
590
|
+
const timer = setTimeout(() => {
|
|
591
|
+
watchdogReason = "time";
|
|
592
|
+
controller.abort("codex-watchdog");
|
|
593
|
+
}, watchdogMs);
|
|
594
|
+
timer.unref?.();
|
|
595
|
+
try {
|
|
596
|
+
const limited = { ...opts, signal: controller.signal };
|
|
597
|
+
const events = opts.reasoning === undefined ? (await resolveStream())(model, body, limited) : (await resolveStreamSimple())(model, body, limited);
|
|
598
|
+
let final;
|
|
599
|
+
for await (const event of events) {
|
|
600
|
+
visibleChars += streamedChars(event);
|
|
601
|
+
if (!watchdogReason && opts.maxTokens && visibleChars > opts.maxTokens * 3) {
|
|
602
|
+
watchdogReason = "visible-output";
|
|
603
|
+
controller.abort("codex-visible-output-cap");
|
|
604
|
+
}
|
|
605
|
+
if (event.type === "done")
|
|
606
|
+
final = event.message;
|
|
607
|
+
else if (event.type === "error")
|
|
608
|
+
final = event.error;
|
|
609
|
+
}
|
|
610
|
+
if (watchdogReason) {
|
|
611
|
+
throw new Error("Codex " + watchdogReason + " watchdog stopped generation after " + watchdogMs + "ms / " + visibleChars + " streamed chars");
|
|
612
|
+
}
|
|
613
|
+
if (!final)
|
|
614
|
+
throw new Error("Codex stream ended without a final message");
|
|
615
|
+
return assertSuccessful(final);
|
|
616
|
+
} finally {
|
|
617
|
+
clearTimeout(timer);
|
|
618
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
var rawLlmClient = {
|
|
622
|
+
complete: async (model, body, originalOpts) => {
|
|
623
|
+
const opts = withCodexWireLimit(model, originalOpts);
|
|
624
|
+
return withProviderDeadline(opts, async (bounded) => {
|
|
625
|
+
if (isChatGptCodex(model))
|
|
626
|
+
return completeChatGptCodex(model, body, bounded);
|
|
627
|
+
const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
|
|
628
|
+
return assertSuccessful(response);
|
|
629
|
+
}, model.id);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
var defaultLlmClient = rawLlmClient;
|
|
633
|
+
var _client = defaultLlmClient;
|
|
634
|
+
function getLlmClient() {
|
|
635
|
+
return _client;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/utils/cache.ts
|
|
639
|
+
import fs2 from "fs";
|
|
640
|
+
|
|
455
641
|
// src/utils/type-guards.ts
|
|
456
642
|
function isRecord(value) {
|
|
457
643
|
return typeof value === "object" && value !== null;
|
|
@@ -513,13 +699,53 @@ function buildUniquePathNeedles(filePath, allPaths) {
|
|
|
513
699
|
});
|
|
514
700
|
}
|
|
515
701
|
function isKnownPathReference(ref, knownPaths) {
|
|
516
|
-
const normalizedRef = normalizePath(ref);
|
|
702
|
+
const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
|
|
703
|
+
if (!normalizedRef)
|
|
704
|
+
return false;
|
|
705
|
+
const pathShaped = normalizedRef.includes("/");
|
|
517
706
|
return knownPaths.some((path) => {
|
|
518
|
-
const normalizedPath = normalizePath(path);
|
|
519
|
-
|
|
707
|
+
const normalizedPath = normalizePath(path).replace(/^\/+/, "");
|
|
708
|
+
if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
|
|
709
|
+
return true;
|
|
710
|
+
if (normalizedPath.endsWith(normalizedRef)) {
|
|
711
|
+
const boundary = normalizedPath[normalizedPath.length - normalizedRef.length - 1];
|
|
712
|
+
if (boundary && !/[\w./-]/.test(boundary))
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
if (!pathShaped)
|
|
716
|
+
return false;
|
|
717
|
+
return normalizedPath.split("/").some((_, index, parts) => parts.slice(index).join("/").startsWith(normalizedRef + "/"));
|
|
520
718
|
});
|
|
521
719
|
}
|
|
522
720
|
|
|
721
|
+
// src/utils/logger.ts
|
|
722
|
+
var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
|
|
723
|
+
function warn(msg, err) {
|
|
724
|
+
const detail = err instanceof Error ? err.message : err ?? "";
|
|
725
|
+
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/infra/paths.ts
|
|
729
|
+
import path from "path";
|
|
730
|
+
function home() {
|
|
731
|
+
return process.env.HOME ?? "/tmp";
|
|
732
|
+
}
|
|
733
|
+
function piAgentDir() {
|
|
734
|
+
return path.join(home(), ".pi", "agent");
|
|
735
|
+
}
|
|
736
|
+
function cacheDir() {
|
|
737
|
+
return path.join(piAgentDir(), ".cache");
|
|
738
|
+
}
|
|
739
|
+
function smartCompactCacheDir() {
|
|
740
|
+
return path.join(cacheDir(), "smart-compact");
|
|
741
|
+
}
|
|
742
|
+
function metricsLogFile() {
|
|
743
|
+
return path.join(cacheDir(), "compact-metrics.jsonl");
|
|
744
|
+
}
|
|
745
|
+
function damageReportsFile() {
|
|
746
|
+
return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
|
|
747
|
+
}
|
|
748
|
+
|
|
523
749
|
// src/domain/tool-semantics.ts
|
|
524
750
|
var PATH_KEYS = [
|
|
525
751
|
"path",
|
|
@@ -602,10 +828,20 @@ function isLikelyFileRef(candidate) {
|
|
|
602
828
|
return CODE_EXT_RE.test(candidate);
|
|
603
829
|
}
|
|
604
830
|
function extractFileRefs(summary) {
|
|
605
|
-
const
|
|
606
|
-
|
|
831
|
+
const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
|
|
832
|
+
const refs = [];
|
|
833
|
+
for (const match of summary.matchAll(matcher)) {
|
|
834
|
+
if (/[\\/]/.test(summary[(match.index ?? 0) + match[0].length] ?? ""))
|
|
835
|
+
continue;
|
|
836
|
+
if (isLikelyFileRef(match[0]))
|
|
837
|
+
refs.push(match[0]);
|
|
838
|
+
}
|
|
839
|
+
return refs;
|
|
607
840
|
}
|
|
608
841
|
|
|
842
|
+
// src/domain/summary-parse.ts
|
|
843
|
+
import { createHash } from "crypto";
|
|
844
|
+
|
|
609
845
|
// src/domain/summary-schema.ts
|
|
610
846
|
function classifyHeading(raw) {
|
|
611
847
|
const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
|
|
@@ -645,6 +881,58 @@ var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
|
|
|
645
881
|
function summaryEvidenceLine(value, maxLength) {
|
|
646
882
|
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
|
|
647
883
|
}
|
|
884
|
+
function summaryPathLine(value) {
|
|
885
|
+
return JSON.stringify(value);
|
|
886
|
+
}
|
|
887
|
+
function compactPathLine(value, maxLength, digest) {
|
|
888
|
+
const minimal = JSON.stringify("#" + digest);
|
|
889
|
+
if (minimal.length >= maxLength)
|
|
890
|
+
return minimal;
|
|
891
|
+
const chars = Array.from(value.replace(/\\/g, "/"));
|
|
892
|
+
let low = 0;
|
|
893
|
+
let high = chars.length;
|
|
894
|
+
let best = minimal;
|
|
895
|
+
while (low <= high) {
|
|
896
|
+
const length = Math.floor((low + high) / 2);
|
|
897
|
+
const candidate = JSON.stringify("\u2026/" + chars.slice(-length).join("") + "#" + digest);
|
|
898
|
+
if (candidate.length <= maxLength) {
|
|
899
|
+
best = candidate;
|
|
900
|
+
low = length + 1;
|
|
901
|
+
} else {
|
|
902
|
+
high = length - 1;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return best;
|
|
906
|
+
}
|
|
907
|
+
function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summaryBudgetTokens) {
|
|
908
|
+
const unique = Array.from(new Set(paths.filter(Boolean)));
|
|
909
|
+
if (!unique.length)
|
|
910
|
+
return new Map;
|
|
911
|
+
const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
|
|
912
|
+
const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
|
|
913
|
+
const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
|
|
914
|
+
if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
|
|
915
|
+
return new Map(full);
|
|
916
|
+
}
|
|
917
|
+
const digests = new Map;
|
|
918
|
+
const owners = new Map;
|
|
919
|
+
for (const path2 of unique) {
|
|
920
|
+
const fullDigest = createHash("sha256").update(path2).digest("base64url");
|
|
921
|
+
let digest = fullDigest.slice(0, 12);
|
|
922
|
+
const owner = owners.get(digest);
|
|
923
|
+
if (owner && owner !== path2) {
|
|
924
|
+
digest = fullDigest;
|
|
925
|
+
digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
|
|
926
|
+
}
|
|
927
|
+
owners.set(digest, path2);
|
|
928
|
+
digests.set(path2, digest);
|
|
929
|
+
}
|
|
930
|
+
const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
|
|
931
|
+
return new Map(unique.map((path2) => [
|
|
932
|
+
path2,
|
|
933
|
+
compactPathLine(path2, perPath, digests.get(path2) ?? "")
|
|
934
|
+
]));
|
|
935
|
+
}
|
|
648
936
|
function mergeBodies(first, second) {
|
|
649
937
|
const seen = new Set;
|
|
650
938
|
return [first, second].filter(Boolean).flatMap((body) => body.split(`
|
|
@@ -669,7 +957,11 @@ function parseSummary(markdown) {
|
|
|
669
957
|
if (existing)
|
|
670
958
|
existing.body = mergeBodies(existing.body, body);
|
|
671
959
|
else
|
|
672
|
-
sections.push({
|
|
960
|
+
sections.push({
|
|
961
|
+
kind: currentKind,
|
|
962
|
+
heading: currentHeading.trim(),
|
|
963
|
+
body
|
|
964
|
+
});
|
|
673
965
|
};
|
|
674
966
|
for (const line of lines) {
|
|
675
967
|
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
@@ -760,7 +1052,11 @@ function buildToolCallIndex(msgs) {
|
|
|
760
1052
|
for (let t = 0;t < nested.length; t++) {
|
|
761
1053
|
const tool = nested[t];
|
|
762
1054
|
const id = nestedToolCallId(b.id, i, t, tool.id);
|
|
763
|
-
idx.set(id, {
|
|
1055
|
+
idx.set(id, {
|
|
1056
|
+
name: tool.name,
|
|
1057
|
+
arguments: tool.arguments,
|
|
1058
|
+
msgIndex: i
|
|
1059
|
+
});
|
|
764
1060
|
}
|
|
765
1061
|
}
|
|
766
1062
|
}
|
|
@@ -768,46 +1064,42 @@ function buildToolCallIndex(msgs) {
|
|
|
768
1064
|
return idx;
|
|
769
1065
|
}
|
|
770
1066
|
var CONSTRAINT_PATTERNS = [
|
|
771
|
-
{
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
{
|
|
1067
|
+
{
|
|
1068
|
+
re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i,
|
|
1069
|
+
cat: "requirement",
|
|
1070
|
+
conf: TUNING.CONFIDENCE_HIGH
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i,
|
|
1074
|
+
cat: "prohibition",
|
|
1075
|
+
conf: TUNING.CONFIDENCE_MEDIUM
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i,
|
|
1079
|
+
cat: "preference",
|
|
1080
|
+
conf: TUNING.CONFIDENCE_LOW
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu,
|
|
1084
|
+
cat: "prohibition",
|
|
1085
|
+
conf: TUNING.CONFIDENCE_MEDIUM
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu,
|
|
1089
|
+
cat: "requirement",
|
|
1090
|
+
conf: TUNING.CONFIDENCE_MEDIUM
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu,
|
|
1094
|
+
cat: "preference",
|
|
1095
|
+
conf: TUNING.CONFIDENCE_LOW
|
|
1096
|
+
}
|
|
777
1097
|
];
|
|
778
1098
|
function isDiagnosticConstraintText(text) {
|
|
779
1099
|
const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
|
|
780
1100
|
return /^(?:\[[^\]]+\]\s*)?(?:npm\s+(?:error|warn|notice|audit|verbose|info)\b|(?:rg|grep):|command exited\b)/i.test(candidate);
|
|
781
1101
|
}
|
|
782
1102
|
|
|
783
|
-
// src/utils/logger.ts
|
|
784
|
-
var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
|
|
785
|
-
function warn(msg, err) {
|
|
786
|
-
const detail = err instanceof Error ? err.message : err ?? "";
|
|
787
|
-
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// src/infra/paths.ts
|
|
791
|
-
import path from "path";
|
|
792
|
-
function home() {
|
|
793
|
-
return process.env.HOME ?? "/tmp";
|
|
794
|
-
}
|
|
795
|
-
function piAgentDir() {
|
|
796
|
-
return path.join(home(), ".pi", "agent");
|
|
797
|
-
}
|
|
798
|
-
function cacheDir() {
|
|
799
|
-
return path.join(piAgentDir(), ".cache");
|
|
800
|
-
}
|
|
801
|
-
function smartCompactCacheDir() {
|
|
802
|
-
return path.join(cacheDir(), "smart-compact");
|
|
803
|
-
}
|
|
804
|
-
function metricsLogFile() {
|
|
805
|
-
return path.join(cacheDir(), "compact-metrics.jsonl");
|
|
806
|
-
}
|
|
807
|
-
function damageReportsFile() {
|
|
808
|
-
return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
|
|
809
|
-
}
|
|
810
|
-
|
|
811
1103
|
// src/infra/fs.ts
|
|
812
1104
|
import fs from "fs";
|
|
813
1105
|
function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
|
|
@@ -850,7 +1142,10 @@ import crypto from "crypto";
|
|
|
850
1142
|
|
|
851
1143
|
// src/domain/scrub.ts
|
|
852
1144
|
var SECRET_PATTERNS = [
|
|
853
|
-
{
|
|
1145
|
+
{
|
|
1146
|
+
kind: "private-key",
|
|
1147
|
+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
1148
|
+
},
|
|
854
1149
|
{ kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
855
1150
|
{ kind: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
|
|
856
1151
|
{ kind: "stripe-key", regex: /\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}\b/g },
|
|
@@ -859,8 +1154,15 @@ var SECRET_PATTERNS = [
|
|
|
859
1154
|
{ kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
|
|
860
1155
|
{ kind: "api-key", regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
861
1156
|
{ kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
862
|
-
{
|
|
863
|
-
|
|
1157
|
+
{
|
|
1158
|
+
kind: "jwt",
|
|
1159
|
+
regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
kind: "bearer-token",
|
|
1163
|
+
regex: /\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
|
|
1164
|
+
replacement: () => "Bearer [REDACTED:bearer-token]"
|
|
1165
|
+
},
|
|
864
1166
|
{
|
|
865
1167
|
kind: "connection-password",
|
|
866
1168
|
regex: /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+(@)/gi,
|
|
@@ -869,12 +1171,35 @@ var SECRET_PATTERNS = [
|
|
|
869
1171
|
{
|
|
870
1172
|
kind: "credential",
|
|
871
1173
|
regex: /\b((?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|auth[_-]?token|token|password|passwd|secret(?:[_-]?(?:access)?[_-]?key)?|client[_-]?secret)(?:[_-][A-Za-z0-9]+)*)\s*([:=])\s*["']?([^\s"']{16,})["']?/gi,
|
|
872
|
-
replacement: (name, separator) => name + separator + "[REDACTED:credential]"
|
|
1174
|
+
replacement: (name, separator, value, match) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) ? match : name + separator + "[REDACTED:credential]"
|
|
873
1175
|
}
|
|
874
1176
|
];
|
|
1177
|
+
function passesLuhn(candidate) {
|
|
1178
|
+
const digits = candidate.replace(/\D/g, "");
|
|
1179
|
+
if (digits.length < 13 || digits.length > 19)
|
|
1180
|
+
return false;
|
|
1181
|
+
if (/^(\d)\1+$/.test(digits))
|
|
1182
|
+
return false;
|
|
1183
|
+
let sum = 0, double = false;
|
|
1184
|
+
for (let i = digits.length - 1;i >= 0; i--) {
|
|
1185
|
+
let d = digits.charCodeAt(i) - 48;
|
|
1186
|
+
if (double) {
|
|
1187
|
+
d *= 2;
|
|
1188
|
+
if (d > 9)
|
|
1189
|
+
d -= 9;
|
|
1190
|
+
}
|
|
1191
|
+
sum += d;
|
|
1192
|
+
double = !double;
|
|
1193
|
+
}
|
|
1194
|
+
return sum % 10 === 0;
|
|
1195
|
+
}
|
|
875
1196
|
var PII_PATTERNS = [
|
|
876
1197
|
{ kind: "email", regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
877
|
-
{
|
|
1198
|
+
{
|
|
1199
|
+
kind: "payment-card",
|
|
1200
|
+
regex: /\b(?:\d[ -]*?){13,19}\b/g,
|
|
1201
|
+
replacement: (candidate) => passesLuhn(candidate) ? "[REDACTED:payment-card]" : candidate
|
|
1202
|
+
},
|
|
878
1203
|
{ kind: "phone", regex: /(?<![\w.])(?:\+?\d[\d ()-]{8,}\d)(?![\w.])/g }
|
|
879
1204
|
];
|
|
880
1205
|
function redact(text, patterns) {
|
|
@@ -882,15 +1207,22 @@ function redact(text, patterns) {
|
|
|
882
1207
|
let value = text;
|
|
883
1208
|
for (const pattern of patterns) {
|
|
884
1209
|
value = value.replace(pattern.regex, (...args) => {
|
|
885
|
-
|
|
1210
|
+
const match = String(args[0]);
|
|
1211
|
+
let replacement = "[REDACTED:" + pattern.kind + "]";
|
|
886
1212
|
if (pattern.replacement) {
|
|
887
1213
|
const groups = args.slice(1, -2).map(String);
|
|
888
|
-
|
|
1214
|
+
replacement = pattern.replacement(...groups, match);
|
|
889
1215
|
}
|
|
890
|
-
|
|
1216
|
+
if (replacement === match)
|
|
1217
|
+
return match;
|
|
1218
|
+
counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
|
|
1219
|
+
return replacement;
|
|
891
1220
|
});
|
|
892
1221
|
}
|
|
893
|
-
return {
|
|
1222
|
+
return {
|
|
1223
|
+
value,
|
|
1224
|
+
findings: [...counts].map(([kind, count]) => ({ kind, count }))
|
|
1225
|
+
};
|
|
894
1226
|
}
|
|
895
1227
|
function mergeFindings(target, findings) {
|
|
896
1228
|
for (const finding of findings)
|
|
@@ -920,7 +1252,6 @@ var SECRET_KEY_NAMES = {
|
|
|
920
1252
|
set_cookie: true,
|
|
921
1253
|
otp: true,
|
|
922
1254
|
one_time_password: true,
|
|
923
|
-
pin: true,
|
|
924
1255
|
passcode: true
|
|
925
1256
|
};
|
|
926
1257
|
function normalizeObjectKey(key) {
|
|
@@ -986,7 +1317,7 @@ class SecretScrubber {
|
|
|
986
1317
|
const output = {};
|
|
987
1318
|
seen.set(value2, output);
|
|
988
1319
|
for (const [key, item] of Object.entries(value2)) {
|
|
989
|
-
const carriesSecret = typeof item === "string"
|
|
1320
|
+
const carriesSecret = typeof item === "string" && item.length >= 8;
|
|
990
1321
|
if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
|
|
991
1322
|
output[key] = "[REDACTED:credential]";
|
|
992
1323
|
recordCredential();
|
|
@@ -997,7 +1328,10 @@ class SecretScrubber {
|
|
|
997
1328
|
return output;
|
|
998
1329
|
};
|
|
999
1330
|
const value = visit(input);
|
|
1000
|
-
return {
|
|
1331
|
+
return {
|
|
1332
|
+
value,
|
|
1333
|
+
findings: [...findings].map(([kind, count]) => ({ kind, count }))
|
|
1334
|
+
};
|
|
1001
1335
|
}
|
|
1002
1336
|
count() {
|
|
1003
1337
|
return this.total;
|
|
@@ -1283,6 +1617,76 @@ function readMetricsLog(limit = 100) {
|
|
|
1283
1617
|
// src/phases/verify.ts
|
|
1284
1618
|
var HIGH_RISK_OUTCOME_RE = /(?:\ball\s+tests?\s+(?:pass|passed|passing)\b|\btests?\s+(?:pass|passed|passing)\b|\b(?:build|deployment|migration)\s+(?:completed|succeeded|passed|successful)\b|\b(?:deployed|published|released)\b|\b(?:bug|issue|error)\s+(?:fixed|resolved)\b|\bno\s+(?:errors?|failures?)\b|\bcompleted successfully\b|\btestler?\s+(?:ge\u00E7ti|ba\u015Far\u0131l\u0131)\b|\bba\u015Far\u0131yla\s+(?:tamamland\u0131|da\u011F\u0131t\u0131ld\u0131|yay\u0131nland\u0131)\b|\b(?:deploy edildi|yay\u0131nland\u0131|hata yok)\b)/iu;
|
|
1285
1619
|
var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
|
|
1620
|
+
var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
1621
|
+
var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
1622
|
+
var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
|
|
1623
|
+
function noneBlockerLineIndexes(lines) {
|
|
1624
|
+
const indexes = new Set;
|
|
1625
|
+
const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
|
|
1626
|
+
for (const item of nonEmpty) {
|
|
1627
|
+
if (BULLET_NONE_BLOCKER_RE.test(item.text))
|
|
1628
|
+
indexes.add(item.index);
|
|
1629
|
+
}
|
|
1630
|
+
if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
|
|
1631
|
+
indexes.add(nonEmpty[0].index);
|
|
1632
|
+
}
|
|
1633
|
+
return indexes;
|
|
1634
|
+
}
|
|
1635
|
+
function collectListedPaths(body, expectedPaths) {
|
|
1636
|
+
const values = new Set;
|
|
1637
|
+
const encodedValues = new Set;
|
|
1638
|
+
for (const line of body.split(`
|
|
1639
|
+
`)) {
|
|
1640
|
+
const raw = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").trim();
|
|
1641
|
+
if (!raw)
|
|
1642
|
+
continue;
|
|
1643
|
+
if (raw.startsWith('"')) {
|
|
1644
|
+
try {
|
|
1645
|
+
const decoded = JSON.parse(raw);
|
|
1646
|
+
if (typeof decoded === "string") {
|
|
1647
|
+
values.add(decoded);
|
|
1648
|
+
encodedValues.add(decoded);
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
} catch {}
|
|
1652
|
+
}
|
|
1653
|
+
values.add(raw);
|
|
1654
|
+
if (expectedPaths.has(raw))
|
|
1655
|
+
continue;
|
|
1656
|
+
const unwrapped = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
|
|
1657
|
+
const unchecked = unwrapped.replace(/^\[[ x]\]\s+/i, "");
|
|
1658
|
+
if (expectedPaths.has(unchecked))
|
|
1659
|
+
values.add(unchecked);
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
values,
|
|
1663
|
+
encodedValues,
|
|
1664
|
+
normalizedValues: new Set(Array.from(values, normalizePath))
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function decodePathDisplay(display) {
|
|
1668
|
+
try {
|
|
1669
|
+
const decoded = JSON.parse(display);
|
|
1670
|
+
return typeof decoded === "string" ? decoded : display;
|
|
1671
|
+
} catch {
|
|
1672
|
+
return display;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
function hasListedPath(listed, file, display, normalizedOwners) {
|
|
1676
|
+
const decodedDisplay = decodePathDisplay(display);
|
|
1677
|
+
if (listed.encodedValues.has(decodedDisplay))
|
|
1678
|
+
return true;
|
|
1679
|
+
if (PATH_PLACEHOLDER_RE.test(file))
|
|
1680
|
+
return false;
|
|
1681
|
+
if (listed.values.has(file))
|
|
1682
|
+
return true;
|
|
1683
|
+
for (const candidate of [file, decodedDisplay]) {
|
|
1684
|
+
const normalized = normalizePath(candidate);
|
|
1685
|
+
if (normalizedOwners.get(normalized) === 1 && listed.normalizedValues.has(normalized))
|
|
1686
|
+
return true;
|
|
1687
|
+
}
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1286
1690
|
function outcomeClaims(summary) {
|
|
1287
1691
|
return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#")).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
|
|
1288
1692
|
}
|
|
@@ -1301,6 +1705,28 @@ function classifyOutcomeClaim(claim) {
|
|
|
1301
1705
|
return "generic";
|
|
1302
1706
|
}
|
|
1303
1707
|
var successfulToolEvidenceCache = new WeakMap;
|
|
1708
|
+
var sourceTextCache = new WeakMap;
|
|
1709
|
+
function sourceSupportsFileReference(ref, messages) {
|
|
1710
|
+
let texts = sourceTextCache.get(messages);
|
|
1711
|
+
if (!texts) {
|
|
1712
|
+
texts = messages.map((message) => extractText(message.content).replace(/\\/g, "/").toLowerCase());
|
|
1713
|
+
sourceTextCache.set(messages, texts);
|
|
1714
|
+
}
|
|
1715
|
+
const needle = ref.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
1716
|
+
if (!needle)
|
|
1717
|
+
return false;
|
|
1718
|
+
for (const text of texts) {
|
|
1719
|
+
let index = text.indexOf(needle);
|
|
1720
|
+
while (index >= 0) {
|
|
1721
|
+
const before = text[index - 1] ?? "";
|
|
1722
|
+
const after = text[index + needle.length] ?? "";
|
|
1723
|
+
if ((!before || !/[\w.-]/.test(before)) && (!after || !/[\w.-]/.test(after)))
|
|
1724
|
+
return true;
|
|
1725
|
+
index = text.indexOf(needle, index + 1);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return false;
|
|
1729
|
+
}
|
|
1304
1730
|
function successfulToolEvidence(messages) {
|
|
1305
1731
|
const cached = successfulToolEvidenceCache.get(messages);
|
|
1306
1732
|
if (cached)
|
|
@@ -1419,8 +1845,72 @@ var SEMANTIC_STOP = new Set([
|
|
|
1419
1845
|
"de\u011Fil",
|
|
1420
1846
|
"olmadan"
|
|
1421
1847
|
]);
|
|
1848
|
+
var TR_SUFFIXES = [
|
|
1849
|
+
"lar\u0131",
|
|
1850
|
+
"leri",
|
|
1851
|
+
"\u0131n\u0131n",
|
|
1852
|
+
"inin",
|
|
1853
|
+
"unun",
|
|
1854
|
+
"\xFCn\xFCn",
|
|
1855
|
+
"\u0131nda",
|
|
1856
|
+
"inde",
|
|
1857
|
+
"unda",
|
|
1858
|
+
"\xFCnde",
|
|
1859
|
+
"m\u0131\u015F",
|
|
1860
|
+
"mi\u015F",
|
|
1861
|
+
"mu\u015F",
|
|
1862
|
+
"m\xFC\u015F",
|
|
1863
|
+
"lar",
|
|
1864
|
+
"ler",
|
|
1865
|
+
"\u0131n\u0131",
|
|
1866
|
+
"ini",
|
|
1867
|
+
"unu",
|
|
1868
|
+
"\xFCn\xFC",
|
|
1869
|
+
"\u0131na",
|
|
1870
|
+
"ine",
|
|
1871
|
+
"una",
|
|
1872
|
+
"\xFCne",
|
|
1873
|
+
"dan",
|
|
1874
|
+
"den",
|
|
1875
|
+
"tan",
|
|
1876
|
+
"ten",
|
|
1877
|
+
"d\u0131r",
|
|
1878
|
+
"dir",
|
|
1879
|
+
"dur",
|
|
1880
|
+
"d\xFCr",
|
|
1881
|
+
"t\u0131r",
|
|
1882
|
+
"tir",
|
|
1883
|
+
"tur",
|
|
1884
|
+
"t\xFCr",
|
|
1885
|
+
"yor",
|
|
1886
|
+
"mak",
|
|
1887
|
+
"mek",
|
|
1888
|
+
"da",
|
|
1889
|
+
"de",
|
|
1890
|
+
"ta",
|
|
1891
|
+
"te",
|
|
1892
|
+
"d\u0131",
|
|
1893
|
+
"di",
|
|
1894
|
+
"du",
|
|
1895
|
+
"d\xFC",
|
|
1896
|
+
"t\u0131",
|
|
1897
|
+
"ti",
|
|
1898
|
+
"tu",
|
|
1899
|
+
"t\xFC",
|
|
1900
|
+
"\u0131n",
|
|
1901
|
+
"in",
|
|
1902
|
+
"un",
|
|
1903
|
+
"\xFCn",
|
|
1904
|
+
"sa",
|
|
1905
|
+
"se"
|
|
1906
|
+
];
|
|
1422
1907
|
function stemToken(token) {
|
|
1423
1908
|
const lower = token.toLocaleLowerCase();
|
|
1909
|
+
for (const suffix of TR_SUFFIXES) {
|
|
1910
|
+
if (lower.length >= 4 + suffix.length && lower.endsWith(suffix)) {
|
|
1911
|
+
return lower.slice(0, -suffix.length);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1424
1914
|
if (lower.length > 6 && lower.endsWith("ing"))
|
|
1425
1915
|
return lower.slice(0, -3);
|
|
1426
1916
|
if (lower.length > 5 && lower.endsWith("ed"))
|
|
@@ -1539,11 +2029,15 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1539
2029
|
};
|
|
1540
2030
|
const unresolvedEvidence = uniqueByText([
|
|
1541
2031
|
...extraction.errors.filter((error) => !error.resolved).map((error) => ({ message: error.message })),
|
|
1542
|
-
...(continuity?.unresolvedErrors ?? []).map((error) => ({
|
|
2032
|
+
...(continuity?.unresolvedErrors ?? []).map((error) => ({
|
|
2033
|
+
message: error.message
|
|
2034
|
+
}))
|
|
1543
2035
|
], (item) => item.message);
|
|
1544
2036
|
const resolvedEvidence = uniqueByText([
|
|
1545
2037
|
...extraction.errors.filter((error) => error.resolved).map((error) => ({ message: error.message })),
|
|
1546
|
-
...(continuity?.resolvedErrors ?? []).map((error) => ({
|
|
2038
|
+
...(continuity?.resolvedErrors ?? []).map((error) => ({
|
|
2039
|
+
message: error.message
|
|
2040
|
+
}))
|
|
1547
2041
|
], (item) => item.message).slice(-5);
|
|
1548
2042
|
const steeringConstraints = [
|
|
1549
2043
|
evidence.steering?.focus ? { text: "Preserve detail about: " + evidence.steering.focus } : null,
|
|
@@ -1570,40 +2064,64 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1570
2064
|
score -= req.penalty;
|
|
1571
2065
|
}
|
|
1572
2066
|
}
|
|
1573
|
-
const listedPaths = (kind) => new Set((findSection(parsed, kind)?.body ?? "").split(`
|
|
1574
|
-
`).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).map((line) => line.startsWith("`") && line.endsWith("`") ? line.slice(1, -1) : line).filter((line) => line.length > 0 && !/^none(?: recorded)?[.!]?$/i.test(line)).map(normalizePath));
|
|
1575
2067
|
const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
|
|
2068
|
+
const readPaths = extraction.readFiles;
|
|
2069
|
+
const deletedEvidence = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
|
|
2070
|
+
const requiredPaths = [...modifiedPaths, ...readPaths, ...deletedEvidence];
|
|
2071
|
+
const expectedPathSet = new Set(requiredPaths);
|
|
2072
|
+
const pathEvidence = buildSummaryPathEvidence(requiredPaths, evidence.summaryBudgetTokens);
|
|
2073
|
+
const normalizedOwnerSets = new Map;
|
|
2074
|
+
for (const file of requiredPaths) {
|
|
2075
|
+
const display = pathEvidence.get(file);
|
|
2076
|
+
for (const candidate of [
|
|
2077
|
+
file,
|
|
2078
|
+
...display ? [decodePathDisplay(display)] : []
|
|
2079
|
+
]) {
|
|
2080
|
+
const normalized = normalizePath(candidate);
|
|
2081
|
+
const owners = normalizedOwnerSets.get(normalized) ?? new Set;
|
|
2082
|
+
owners.add(file);
|
|
2083
|
+
normalizedOwnerSets.set(normalized, owners);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const normalizedOwners = new Map(Array.from(normalizedOwnerSets, ([path2, owners]) => [path2, owners.size]));
|
|
2087
|
+
const listedPaths = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expectedPathSet);
|
|
1576
2088
|
const modifiedListed = listedPaths("files-modified");
|
|
1577
2089
|
const readListed = listedPaths("files-read");
|
|
1578
2090
|
const deletedListed = listedPaths("files-deleted");
|
|
1579
2091
|
for (const file of modifiedPaths) {
|
|
1580
|
-
|
|
2092
|
+
const display = pathEvidence.get(file);
|
|
2093
|
+
if (display && !hasListedPath(modifiedListed, file, display, normalizedOwners)) {
|
|
1581
2094
|
gaps.push({ kind: "missing-file", path: file });
|
|
2095
|
+
}
|
|
1582
2096
|
}
|
|
1583
|
-
for (const file of
|
|
1584
|
-
|
|
2097
|
+
for (const file of readPaths) {
|
|
2098
|
+
const display = pathEvidence.get(file);
|
|
2099
|
+
if (display && !hasListedPath(readListed, file, display, normalizedOwners)) {
|
|
1585
2100
|
gaps.push({ kind: "missing-read-file", path: file });
|
|
2101
|
+
}
|
|
1586
2102
|
}
|
|
1587
|
-
const deletedEvidence = Array.from(new Set([
|
|
1588
|
-
...extraction.deletedFiles,
|
|
1589
|
-
...continuity?.deletedFiles ?? []
|
|
1590
|
-
]));
|
|
1591
2103
|
for (const file of deletedEvidence) {
|
|
1592
|
-
|
|
2104
|
+
const display = pathEvidence.get(file);
|
|
2105
|
+
if (display && !hasListedPath(deletedListed, file, display, normalizedOwners)) {
|
|
1593
2106
|
gaps.push({ kind: "missing-deleted-file", path: file });
|
|
2107
|
+
}
|
|
1594
2108
|
}
|
|
1595
2109
|
score -= gaps.filter((gap) => gap.kind === "missing-file" || gap.kind === "missing-read-file" || gap.kind === "missing-deleted-file").length * 5;
|
|
1596
2110
|
for (const error of unresolvedEvidence) {
|
|
1597
|
-
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
|
|
2111
|
+
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
|
|
1598
2112
|
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
1599
2113
|
gaps.push({ kind: "missing-error", message: error.message });
|
|
1600
2114
|
score -= 5;
|
|
1601
2115
|
}
|
|
1602
2116
|
}
|
|
1603
2117
|
for (const error of resolvedEvidence) {
|
|
1604
|
-
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
|
|
2118
|
+
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
|
|
1605
2119
|
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
1606
|
-
gaps.push({
|
|
2120
|
+
gaps.push({
|
|
2121
|
+
kind: "missing-error",
|
|
2122
|
+
message: error.message,
|
|
2123
|
+
resolved: true
|
|
2124
|
+
});
|
|
1607
2125
|
score -= 2;
|
|
1608
2126
|
}
|
|
1609
2127
|
}
|
|
@@ -1618,7 +2136,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1618
2136
|
score -= 8;
|
|
1619
2137
|
}
|
|
1620
2138
|
if (hasSemanticContradiction(constraint.text, constraintTarget)) {
|
|
1621
|
-
gaps.push({
|
|
2139
|
+
gaps.push({
|
|
2140
|
+
kind: "inconsistency",
|
|
2141
|
+
detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
|
|
2142
|
+
});
|
|
1622
2143
|
score -= 20;
|
|
1623
2144
|
}
|
|
1624
2145
|
}
|
|
@@ -1629,32 +2150,52 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1629
2150
|
score -= 12;
|
|
1630
2151
|
}
|
|
1631
2152
|
if (hasSemanticContradiction(goalEvidence, goalTarget)) {
|
|
1632
|
-
gaps.push({
|
|
2153
|
+
gaps.push({
|
|
2154
|
+
kind: "inconsistency",
|
|
2155
|
+
detail: "semantic-contradiction: goal polarity or condition changed"
|
|
2156
|
+
});
|
|
1633
2157
|
score -= 20;
|
|
1634
2158
|
}
|
|
1635
2159
|
}
|
|
1636
|
-
const
|
|
2160
|
+
const groundedEvidence = [
|
|
1637
2161
|
...unresolvedEvidence.map((item) => item.message),
|
|
2162
|
+
...resolvedEvidence.map((item) => item.message),
|
|
1638
2163
|
...constraintEvidence.map((item) => item.text),
|
|
1639
2164
|
...decisionEvidence.map((item) => item.summary),
|
|
1640
2165
|
...goalEvidence ? [goalEvidence] : [],
|
|
2166
|
+
...extraction.lastUserMessages,
|
|
2167
|
+
...extraction.timeline.map((item) => item.summary),
|
|
2168
|
+
...extraction.topics.map((item) => item.primaryFile ?? ""),
|
|
1641
2169
|
...continuity?.openLoops.map((item) => item.summary) ?? [],
|
|
1642
2170
|
...continuity?.criticalContext ?? []
|
|
1643
|
-
]
|
|
1644
|
-
const
|
|
2171
|
+
];
|
|
2172
|
+
const groundedEvidenceFiles = groundedEvidence.flatMap((value) => [
|
|
2173
|
+
value,
|
|
2174
|
+
summaryEvidenceLine(value, TRUNC.ERROR_SNIPPET),
|
|
2175
|
+
summaryEvidenceLine(value, TRUNC.TOPIC_LABEL),
|
|
2176
|
+
summaryEvidenceLine(value, TRUNC.PREVIEW),
|
|
2177
|
+
summaryEvidenceLine(value, TRUNC.MESSAGE)
|
|
2178
|
+
]).flatMap(extractFileRefs);
|
|
2179
|
+
const renderedPathEvidence = Array.from(pathEvidence.values()).flatMap((line) => [
|
|
2180
|
+
decodePathDisplay(line),
|
|
2181
|
+
line.startsWith('"') && line.endsWith('"') ? line.slice(1, -1) : line
|
|
2182
|
+
]);
|
|
2183
|
+
const rawKnownFiles = Array.from(new Set([
|
|
1645
2184
|
...modifiedPaths,
|
|
1646
|
-
...
|
|
1647
|
-
...
|
|
2185
|
+
...readPaths,
|
|
2186
|
+
...deletedEvidence,
|
|
1648
2187
|
...extraction.referencedFiles ?? [],
|
|
1649
2188
|
...groundedEvidenceFiles,
|
|
2189
|
+
...renderedPathEvidence,
|
|
2190
|
+
...renderedPathEvidence.flatMap(extractFileRefs),
|
|
1650
2191
|
...continuity?.modifiedFiles ?? [],
|
|
1651
2192
|
...continuity?.readFiles ?? [],
|
|
1652
|
-
...continuity?.deletedFiles ?? [],
|
|
1653
2193
|
...(continuity?.unresolvedErrors ?? []).flatMap((error) => error.files),
|
|
1654
2194
|
...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
|
|
1655
2195
|
]));
|
|
1656
2196
|
for (const ref of new Set(extractFileRefs(summary))) {
|
|
1657
|
-
|
|
2197
|
+
const grounded = isKnownPathReference(ref, rawKnownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
|
|
2198
|
+
if (!grounded) {
|
|
1658
2199
|
gaps.push({ kind: "fabricated-file", ref });
|
|
1659
2200
|
score -= 4;
|
|
1660
2201
|
}
|
|
@@ -1663,8 +2204,12 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1663
2204
|
if (progressSection) {
|
|
1664
2205
|
const doneSection = progressSection.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
|
|
1665
2206
|
const blockedSection = progressSection.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
|
|
1666
|
-
|
|
1667
|
-
|
|
2207
|
+
const blockedLines = blockedSection.split(/\r?\n/).slice(1);
|
|
2208
|
+
if (unresolvedEvidence.length > 0 && noneBlockerLineIndexes(blockedLines).size > 0) {
|
|
2209
|
+
gaps.push({
|
|
2210
|
+
kind: "inconsistency",
|
|
2211
|
+
detail: "blocked-none: Blocked says none despite unresolved errors"
|
|
2212
|
+
});
|
|
1668
2213
|
score -= 12;
|
|
1669
2214
|
}
|
|
1670
2215
|
const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
|
|
@@ -1678,7 +2223,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1678
2223
|
return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
|
|
1679
2224
|
});
|
|
1680
2225
|
if (unresolved) {
|
|
1681
|
-
gaps.push({
|
|
2226
|
+
gaps.push({
|
|
2227
|
+
kind: "inconsistency",
|
|
2228
|
+
detail: file.path + " marked Done but has unresolved error"
|
|
2229
|
+
});
|
|
1682
2230
|
score -= 5;
|
|
1683
2231
|
}
|
|
1684
2232
|
}
|
|
@@ -1690,7 +2238,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1690
2238
|
score -= 8;
|
|
1691
2239
|
}
|
|
1692
2240
|
if (hasSemanticContradiction(decision.summary, decisionBody)) {
|
|
1693
|
-
gaps.push({
|
|
2241
|
+
gaps.push({
|
|
2242
|
+
kind: "inconsistency",
|
|
2243
|
+
detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
2244
|
+
});
|
|
1694
2245
|
score -= 20;
|
|
1695
2246
|
}
|
|
1696
2247
|
}
|