pi-smart-compact 9.2.1 → 9.3.1
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 +101 -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 +721 -155
- package/dist/infra/llm-client.d.ts +1 -1
- package/dist/infra/llm-client.d.ts.map +1 -1
- package/dist/infra/paths.d.ts +9 -0
- package/dist/infra/paths.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 +393 -58
- package/dist/provider-scenario-eval.js +801 -249
- package/dist/telemetry-report.js +366 -31
- 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/session-log.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.1";
|
|
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,54 @@ 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
|
+
import os from "os";
|
|
731
|
+
function home() {
|
|
732
|
+
return process.env.HOME ?? os.homedir();
|
|
733
|
+
}
|
|
734
|
+
function piAgentDir() {
|
|
735
|
+
return path.join(home(), ".pi", "agent");
|
|
736
|
+
}
|
|
737
|
+
function cacheDir() {
|
|
738
|
+
return path.join(piAgentDir(), ".cache");
|
|
739
|
+
}
|
|
740
|
+
function smartCompactCacheDir() {
|
|
741
|
+
return path.join(cacheDir(), "smart-compact");
|
|
742
|
+
}
|
|
743
|
+
function metricsLogFile() {
|
|
744
|
+
return path.join(cacheDir(), "compact-metrics.jsonl");
|
|
745
|
+
}
|
|
746
|
+
function damageReportsFile() {
|
|
747
|
+
return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
|
|
748
|
+
}
|
|
749
|
+
|
|
523
750
|
// src/domain/tool-semantics.ts
|
|
524
751
|
var PATH_KEYS = [
|
|
525
752
|
"path",
|
|
@@ -602,10 +829,20 @@ function isLikelyFileRef(candidate) {
|
|
|
602
829
|
return CODE_EXT_RE.test(candidate);
|
|
603
830
|
}
|
|
604
831
|
function extractFileRefs(summary) {
|
|
605
|
-
const
|
|
606
|
-
|
|
832
|
+
const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
|
|
833
|
+
const refs = [];
|
|
834
|
+
for (const match of summary.matchAll(matcher)) {
|
|
835
|
+
if (/[\\/]/.test(summary[(match.index ?? 0) + match[0].length] ?? ""))
|
|
836
|
+
continue;
|
|
837
|
+
if (isLikelyFileRef(match[0]))
|
|
838
|
+
refs.push(match[0]);
|
|
839
|
+
}
|
|
840
|
+
return refs;
|
|
607
841
|
}
|
|
608
842
|
|
|
843
|
+
// src/domain/summary-parse.ts
|
|
844
|
+
import { createHash } from "crypto";
|
|
845
|
+
|
|
609
846
|
// src/domain/summary-schema.ts
|
|
610
847
|
function classifyHeading(raw) {
|
|
611
848
|
const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
|
|
@@ -645,6 +882,58 @@ var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
|
|
|
645
882
|
function summaryEvidenceLine(value, maxLength) {
|
|
646
883
|
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
|
|
647
884
|
}
|
|
885
|
+
function summaryPathLine(value) {
|
|
886
|
+
return JSON.stringify(value);
|
|
887
|
+
}
|
|
888
|
+
function compactPathLine(value, maxLength, digest) {
|
|
889
|
+
const minimal = JSON.stringify("#" + digest);
|
|
890
|
+
if (minimal.length >= maxLength)
|
|
891
|
+
return minimal;
|
|
892
|
+
const chars = Array.from(value.replace(/\\/g, "/"));
|
|
893
|
+
let low = 0;
|
|
894
|
+
let high = chars.length;
|
|
895
|
+
let best = minimal;
|
|
896
|
+
while (low <= high) {
|
|
897
|
+
const length = Math.floor((low + high) / 2);
|
|
898
|
+
const candidate = JSON.stringify("\u2026/" + chars.slice(-length).join("") + "#" + digest);
|
|
899
|
+
if (candidate.length <= maxLength) {
|
|
900
|
+
best = candidate;
|
|
901
|
+
low = length + 1;
|
|
902
|
+
} else {
|
|
903
|
+
high = length - 1;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return best;
|
|
907
|
+
}
|
|
908
|
+
function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summaryBudgetTokens) {
|
|
909
|
+
const unique = Array.from(new Set(paths.filter(Boolean)));
|
|
910
|
+
if (!unique.length)
|
|
911
|
+
return new Map;
|
|
912
|
+
const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
|
|
913
|
+
const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
|
|
914
|
+
const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
|
|
915
|
+
if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
|
|
916
|
+
return new Map(full);
|
|
917
|
+
}
|
|
918
|
+
const digests = new Map;
|
|
919
|
+
const owners = new Map;
|
|
920
|
+
for (const path2 of unique) {
|
|
921
|
+
const fullDigest = createHash("sha256").update(path2).digest("base64url");
|
|
922
|
+
let digest = fullDigest.slice(0, 12);
|
|
923
|
+
const owner = owners.get(digest);
|
|
924
|
+
if (owner && owner !== path2) {
|
|
925
|
+
digest = fullDigest;
|
|
926
|
+
digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
|
|
927
|
+
}
|
|
928
|
+
owners.set(digest, path2);
|
|
929
|
+
digests.set(path2, digest);
|
|
930
|
+
}
|
|
931
|
+
const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
|
|
932
|
+
return new Map(unique.map((path2) => [
|
|
933
|
+
path2,
|
|
934
|
+
compactPathLine(path2, perPath, digests.get(path2) ?? "")
|
|
935
|
+
]));
|
|
936
|
+
}
|
|
648
937
|
function mergeBodies(first, second) {
|
|
649
938
|
const seen = new Set;
|
|
650
939
|
return [first, second].filter(Boolean).flatMap((body) => body.split(`
|
|
@@ -669,7 +958,11 @@ function parseSummary(markdown) {
|
|
|
669
958
|
if (existing)
|
|
670
959
|
existing.body = mergeBodies(existing.body, body);
|
|
671
960
|
else
|
|
672
|
-
sections.push({
|
|
961
|
+
sections.push({
|
|
962
|
+
kind: currentKind,
|
|
963
|
+
heading: currentHeading.trim(),
|
|
964
|
+
body
|
|
965
|
+
});
|
|
673
966
|
};
|
|
674
967
|
for (const line of lines) {
|
|
675
968
|
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
@@ -760,7 +1053,11 @@ function buildToolCallIndex(msgs) {
|
|
|
760
1053
|
for (let t = 0;t < nested.length; t++) {
|
|
761
1054
|
const tool = nested[t];
|
|
762
1055
|
const id = nestedToolCallId(b.id, i, t, tool.id);
|
|
763
|
-
idx.set(id, {
|
|
1056
|
+
idx.set(id, {
|
|
1057
|
+
name: tool.name,
|
|
1058
|
+
arguments: tool.arguments,
|
|
1059
|
+
msgIndex: i
|
|
1060
|
+
});
|
|
764
1061
|
}
|
|
765
1062
|
}
|
|
766
1063
|
}
|
|
@@ -768,46 +1065,42 @@ function buildToolCallIndex(msgs) {
|
|
|
768
1065
|
return idx;
|
|
769
1066
|
}
|
|
770
1067
|
var CONSTRAINT_PATTERNS = [
|
|
771
|
-
{
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
{
|
|
1068
|
+
{
|
|
1069
|
+
re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i,
|
|
1070
|
+
cat: "requirement",
|
|
1071
|
+
conf: TUNING.CONFIDENCE_HIGH
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i,
|
|
1075
|
+
cat: "prohibition",
|
|
1076
|
+
conf: TUNING.CONFIDENCE_MEDIUM
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i,
|
|
1080
|
+
cat: "preference",
|
|
1081
|
+
conf: TUNING.CONFIDENCE_LOW
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu,
|
|
1085
|
+
cat: "prohibition",
|
|
1086
|
+
conf: TUNING.CONFIDENCE_MEDIUM
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
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,
|
|
1090
|
+
cat: "requirement",
|
|
1091
|
+
conf: TUNING.CONFIDENCE_MEDIUM
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu,
|
|
1095
|
+
cat: "preference",
|
|
1096
|
+
conf: TUNING.CONFIDENCE_LOW
|
|
1097
|
+
}
|
|
777
1098
|
];
|
|
778
1099
|
function isDiagnosticConstraintText(text) {
|
|
779
1100
|
const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
|
|
780
1101
|
return /^(?:\[[^\]]+\]\s*)?(?:npm\s+(?:error|warn|notice|audit|verbose|info)\b|(?:rg|grep):|command exited\b)/i.test(candidate);
|
|
781
1102
|
}
|
|
782
1103
|
|
|
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
1104
|
// src/infra/fs.ts
|
|
812
1105
|
import fs from "fs";
|
|
813
1106
|
function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
|
|
@@ -850,7 +1143,10 @@ import crypto from "crypto";
|
|
|
850
1143
|
|
|
851
1144
|
// src/domain/scrub.ts
|
|
852
1145
|
var SECRET_PATTERNS = [
|
|
853
|
-
{
|
|
1146
|
+
{
|
|
1147
|
+
kind: "private-key",
|
|
1148
|
+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
1149
|
+
},
|
|
854
1150
|
{ kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
855
1151
|
{ kind: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
|
|
856
1152
|
{ kind: "stripe-key", regex: /\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}\b/g },
|
|
@@ -859,8 +1155,15 @@ var SECRET_PATTERNS = [
|
|
|
859
1155
|
{ kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
|
|
860
1156
|
{ kind: "api-key", regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
861
1157
|
{ kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
862
|
-
{
|
|
863
|
-
|
|
1158
|
+
{
|
|
1159
|
+
kind: "jwt",
|
|
1160
|
+
regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
kind: "bearer-token",
|
|
1164
|
+
regex: /\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
|
|
1165
|
+
replacement: () => "Bearer [REDACTED:bearer-token]"
|
|
1166
|
+
},
|
|
864
1167
|
{
|
|
865
1168
|
kind: "connection-password",
|
|
866
1169
|
regex: /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+(@)/gi,
|
|
@@ -869,12 +1172,35 @@ var SECRET_PATTERNS = [
|
|
|
869
1172
|
{
|
|
870
1173
|
kind: "credential",
|
|
871
1174
|
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]"
|
|
1175
|
+
replacement: (name, separator, value, match) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) ? match : name + separator + "[REDACTED:credential]"
|
|
873
1176
|
}
|
|
874
1177
|
];
|
|
1178
|
+
function passesLuhn(candidate) {
|
|
1179
|
+
const digits = candidate.replace(/\D/g, "");
|
|
1180
|
+
if (digits.length < 13 || digits.length > 19)
|
|
1181
|
+
return false;
|
|
1182
|
+
if (/^(\d)\1+$/.test(digits))
|
|
1183
|
+
return false;
|
|
1184
|
+
let sum = 0, double = false;
|
|
1185
|
+
for (let i = digits.length - 1;i >= 0; i--) {
|
|
1186
|
+
let d = digits.charCodeAt(i) - 48;
|
|
1187
|
+
if (double) {
|
|
1188
|
+
d *= 2;
|
|
1189
|
+
if (d > 9)
|
|
1190
|
+
d -= 9;
|
|
1191
|
+
}
|
|
1192
|
+
sum += d;
|
|
1193
|
+
double = !double;
|
|
1194
|
+
}
|
|
1195
|
+
return sum % 10 === 0;
|
|
1196
|
+
}
|
|
875
1197
|
var PII_PATTERNS = [
|
|
876
1198
|
{ kind: "email", regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
877
|
-
{
|
|
1199
|
+
{
|
|
1200
|
+
kind: "payment-card",
|
|
1201
|
+
regex: /\b(?:\d[ -]*?){13,19}\b/g,
|
|
1202
|
+
replacement: (candidate) => passesLuhn(candidate) ? "[REDACTED:payment-card]" : candidate
|
|
1203
|
+
},
|
|
878
1204
|
{ kind: "phone", regex: /(?<![\w.])(?:\+?\d[\d ()-]{8,}\d)(?![\w.])/g }
|
|
879
1205
|
];
|
|
880
1206
|
function redact(text, patterns) {
|
|
@@ -882,15 +1208,22 @@ function redact(text, patterns) {
|
|
|
882
1208
|
let value = text;
|
|
883
1209
|
for (const pattern of patterns) {
|
|
884
1210
|
value = value.replace(pattern.regex, (...args) => {
|
|
885
|
-
|
|
1211
|
+
const match = String(args[0]);
|
|
1212
|
+
let replacement = "[REDACTED:" + pattern.kind + "]";
|
|
886
1213
|
if (pattern.replacement) {
|
|
887
1214
|
const groups = args.slice(1, -2).map(String);
|
|
888
|
-
|
|
1215
|
+
replacement = pattern.replacement(...groups, match);
|
|
889
1216
|
}
|
|
890
|
-
|
|
1217
|
+
if (replacement === match)
|
|
1218
|
+
return match;
|
|
1219
|
+
counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
|
|
1220
|
+
return replacement;
|
|
891
1221
|
});
|
|
892
1222
|
}
|
|
893
|
-
return {
|
|
1223
|
+
return {
|
|
1224
|
+
value,
|
|
1225
|
+
findings: [...counts].map(([kind, count]) => ({ kind, count }))
|
|
1226
|
+
};
|
|
894
1227
|
}
|
|
895
1228
|
function mergeFindings(target, findings) {
|
|
896
1229
|
for (const finding of findings)
|
|
@@ -920,7 +1253,6 @@ var SECRET_KEY_NAMES = {
|
|
|
920
1253
|
set_cookie: true,
|
|
921
1254
|
otp: true,
|
|
922
1255
|
one_time_password: true,
|
|
923
|
-
pin: true,
|
|
924
1256
|
passcode: true
|
|
925
1257
|
};
|
|
926
1258
|
function normalizeObjectKey(key) {
|
|
@@ -986,7 +1318,7 @@ class SecretScrubber {
|
|
|
986
1318
|
const output = {};
|
|
987
1319
|
seen.set(value2, output);
|
|
988
1320
|
for (const [key, item] of Object.entries(value2)) {
|
|
989
|
-
const carriesSecret = typeof item === "string"
|
|
1321
|
+
const carriesSecret = typeof item === "string" && item.length >= 8;
|
|
990
1322
|
if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
|
|
991
1323
|
output[key] = "[REDACTED:credential]";
|
|
992
1324
|
recordCredential();
|
|
@@ -997,7 +1329,10 @@ class SecretScrubber {
|
|
|
997
1329
|
return output;
|
|
998
1330
|
};
|
|
999
1331
|
const value = visit(input);
|
|
1000
|
-
return {
|
|
1332
|
+
return {
|
|
1333
|
+
value,
|
|
1334
|
+
findings: [...findings].map(([kind, count]) => ({ kind, count }))
|
|
1335
|
+
};
|
|
1001
1336
|
}
|
|
1002
1337
|
count() {
|
|
1003
1338
|
return this.total;
|
|
@@ -1283,6 +1618,76 @@ function readMetricsLog(limit = 100) {
|
|
|
1283
1618
|
// src/phases/verify.ts
|
|
1284
1619
|
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
1620
|
var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
|
|
1621
|
+
var NONE_BLOCKER_VALUE_RE = /^(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
1622
|
+
var BULLET_NONE_BLOCKER_RE = /^(?:[-*+]|\d+[.)])\s+(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?$/i;
|
|
1623
|
+
var PATH_PLACEHOLDER_RE = /^(?:none|none recorded|no blockers?|yok)[.!]?$/i;
|
|
1624
|
+
function noneBlockerLineIndexes(lines) {
|
|
1625
|
+
const indexes = new Set;
|
|
1626
|
+
const nonEmpty = lines.map((line, index) => ({ index, text: line.trim() })).filter((item) => item.text);
|
|
1627
|
+
for (const item of nonEmpty) {
|
|
1628
|
+
if (BULLET_NONE_BLOCKER_RE.test(item.text))
|
|
1629
|
+
indexes.add(item.index);
|
|
1630
|
+
}
|
|
1631
|
+
if (nonEmpty.length === 1 && NONE_BLOCKER_VALUE_RE.test(nonEmpty[0].text)) {
|
|
1632
|
+
indexes.add(nonEmpty[0].index);
|
|
1633
|
+
}
|
|
1634
|
+
return indexes;
|
|
1635
|
+
}
|
|
1636
|
+
function collectListedPaths(body, expectedPaths) {
|
|
1637
|
+
const values = new Set;
|
|
1638
|
+
const encodedValues = new Set;
|
|
1639
|
+
for (const line of body.split(`
|
|
1640
|
+
`)) {
|
|
1641
|
+
const raw = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").trim();
|
|
1642
|
+
if (!raw)
|
|
1643
|
+
continue;
|
|
1644
|
+
if (raw.startsWith('"')) {
|
|
1645
|
+
try {
|
|
1646
|
+
const decoded = JSON.parse(raw);
|
|
1647
|
+
if (typeof decoded === "string") {
|
|
1648
|
+
values.add(decoded);
|
|
1649
|
+
encodedValues.add(decoded);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
} catch {}
|
|
1653
|
+
}
|
|
1654
|
+
values.add(raw);
|
|
1655
|
+
if (expectedPaths.has(raw))
|
|
1656
|
+
continue;
|
|
1657
|
+
const unwrapped = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
|
|
1658
|
+
const unchecked = unwrapped.replace(/^\[[ x]\]\s+/i, "");
|
|
1659
|
+
if (expectedPaths.has(unchecked))
|
|
1660
|
+
values.add(unchecked);
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
values,
|
|
1664
|
+
encodedValues,
|
|
1665
|
+
normalizedValues: new Set(Array.from(values, normalizePath))
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function decodePathDisplay(display) {
|
|
1669
|
+
try {
|
|
1670
|
+
const decoded = JSON.parse(display);
|
|
1671
|
+
return typeof decoded === "string" ? decoded : display;
|
|
1672
|
+
} catch {
|
|
1673
|
+
return display;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function hasListedPath(listed, file, display, normalizedOwners) {
|
|
1677
|
+
const decodedDisplay = decodePathDisplay(display);
|
|
1678
|
+
if (listed.encodedValues.has(decodedDisplay))
|
|
1679
|
+
return true;
|
|
1680
|
+
if (PATH_PLACEHOLDER_RE.test(file))
|
|
1681
|
+
return false;
|
|
1682
|
+
if (listed.values.has(file))
|
|
1683
|
+
return true;
|
|
1684
|
+
for (const candidate of [file, decodedDisplay]) {
|
|
1685
|
+
const normalized = normalizePath(candidate);
|
|
1686
|
+
if (normalizedOwners.get(normalized) === 1 && listed.normalizedValues.has(normalized))
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1286
1691
|
function outcomeClaims(summary) {
|
|
1287
1692
|
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
1693
|
}
|
|
@@ -1301,6 +1706,28 @@ function classifyOutcomeClaim(claim) {
|
|
|
1301
1706
|
return "generic";
|
|
1302
1707
|
}
|
|
1303
1708
|
var successfulToolEvidenceCache = new WeakMap;
|
|
1709
|
+
var sourceTextCache = new WeakMap;
|
|
1710
|
+
function sourceSupportsFileReference(ref, messages) {
|
|
1711
|
+
let texts = sourceTextCache.get(messages);
|
|
1712
|
+
if (!texts) {
|
|
1713
|
+
texts = messages.map((message) => extractText(message.content).replace(/\\/g, "/").toLowerCase());
|
|
1714
|
+
sourceTextCache.set(messages, texts);
|
|
1715
|
+
}
|
|
1716
|
+
const needle = ref.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
1717
|
+
if (!needle)
|
|
1718
|
+
return false;
|
|
1719
|
+
for (const text of texts) {
|
|
1720
|
+
let index = text.indexOf(needle);
|
|
1721
|
+
while (index >= 0) {
|
|
1722
|
+
const before = text[index - 1] ?? "";
|
|
1723
|
+
const after = text[index + needle.length] ?? "";
|
|
1724
|
+
if ((!before || !/[\w.-]/.test(before)) && (!after || !/[\w.-]/.test(after)))
|
|
1725
|
+
return true;
|
|
1726
|
+
index = text.indexOf(needle, index + 1);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1304
1731
|
function successfulToolEvidence(messages) {
|
|
1305
1732
|
const cached = successfulToolEvidenceCache.get(messages);
|
|
1306
1733
|
if (cached)
|
|
@@ -1419,8 +1846,72 @@ var SEMANTIC_STOP = new Set([
|
|
|
1419
1846
|
"de\u011Fil",
|
|
1420
1847
|
"olmadan"
|
|
1421
1848
|
]);
|
|
1849
|
+
var TR_SUFFIXES = [
|
|
1850
|
+
"lar\u0131",
|
|
1851
|
+
"leri",
|
|
1852
|
+
"\u0131n\u0131n",
|
|
1853
|
+
"inin",
|
|
1854
|
+
"unun",
|
|
1855
|
+
"\xFCn\xFCn",
|
|
1856
|
+
"\u0131nda",
|
|
1857
|
+
"inde",
|
|
1858
|
+
"unda",
|
|
1859
|
+
"\xFCnde",
|
|
1860
|
+
"m\u0131\u015F",
|
|
1861
|
+
"mi\u015F",
|
|
1862
|
+
"mu\u015F",
|
|
1863
|
+
"m\xFC\u015F",
|
|
1864
|
+
"lar",
|
|
1865
|
+
"ler",
|
|
1866
|
+
"\u0131n\u0131",
|
|
1867
|
+
"ini",
|
|
1868
|
+
"unu",
|
|
1869
|
+
"\xFCn\xFC",
|
|
1870
|
+
"\u0131na",
|
|
1871
|
+
"ine",
|
|
1872
|
+
"una",
|
|
1873
|
+
"\xFCne",
|
|
1874
|
+
"dan",
|
|
1875
|
+
"den",
|
|
1876
|
+
"tan",
|
|
1877
|
+
"ten",
|
|
1878
|
+
"d\u0131r",
|
|
1879
|
+
"dir",
|
|
1880
|
+
"dur",
|
|
1881
|
+
"d\xFCr",
|
|
1882
|
+
"t\u0131r",
|
|
1883
|
+
"tir",
|
|
1884
|
+
"tur",
|
|
1885
|
+
"t\xFCr",
|
|
1886
|
+
"yor",
|
|
1887
|
+
"mak",
|
|
1888
|
+
"mek",
|
|
1889
|
+
"da",
|
|
1890
|
+
"de",
|
|
1891
|
+
"ta",
|
|
1892
|
+
"te",
|
|
1893
|
+
"d\u0131",
|
|
1894
|
+
"di",
|
|
1895
|
+
"du",
|
|
1896
|
+
"d\xFC",
|
|
1897
|
+
"t\u0131",
|
|
1898
|
+
"ti",
|
|
1899
|
+
"tu",
|
|
1900
|
+
"t\xFC",
|
|
1901
|
+
"\u0131n",
|
|
1902
|
+
"in",
|
|
1903
|
+
"un",
|
|
1904
|
+
"\xFCn",
|
|
1905
|
+
"sa",
|
|
1906
|
+
"se"
|
|
1907
|
+
];
|
|
1422
1908
|
function stemToken(token) {
|
|
1423
1909
|
const lower = token.toLocaleLowerCase();
|
|
1910
|
+
for (const suffix of TR_SUFFIXES) {
|
|
1911
|
+
if (lower.length >= 4 + suffix.length && lower.endsWith(suffix)) {
|
|
1912
|
+
return lower.slice(0, -suffix.length);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1424
1915
|
if (lower.length > 6 && lower.endsWith("ing"))
|
|
1425
1916
|
return lower.slice(0, -3);
|
|
1426
1917
|
if (lower.length > 5 && lower.endsWith("ed"))
|
|
@@ -1539,11 +2030,15 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1539
2030
|
};
|
|
1540
2031
|
const unresolvedEvidence = uniqueByText([
|
|
1541
2032
|
...extraction.errors.filter((error) => !error.resolved).map((error) => ({ message: error.message })),
|
|
1542
|
-
...(continuity?.unresolvedErrors ?? []).map((error) => ({
|
|
2033
|
+
...(continuity?.unresolvedErrors ?? []).map((error) => ({
|
|
2034
|
+
message: error.message
|
|
2035
|
+
}))
|
|
1543
2036
|
], (item) => item.message);
|
|
1544
2037
|
const resolvedEvidence = uniqueByText([
|
|
1545
2038
|
...extraction.errors.filter((error) => error.resolved).map((error) => ({ message: error.message })),
|
|
1546
|
-
...(continuity?.resolvedErrors ?? []).map((error) => ({
|
|
2039
|
+
...(continuity?.resolvedErrors ?? []).map((error) => ({
|
|
2040
|
+
message: error.message
|
|
2041
|
+
}))
|
|
1547
2042
|
], (item) => item.message).slice(-5);
|
|
1548
2043
|
const steeringConstraints = [
|
|
1549
2044
|
evidence.steering?.focus ? { text: "Preserve detail about: " + evidence.steering.focus } : null,
|
|
@@ -1570,40 +2065,64 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1570
2065
|
score -= req.penalty;
|
|
1571
2066
|
}
|
|
1572
2067
|
}
|
|
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
2068
|
const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
|
|
2069
|
+
const readPaths = extraction.readFiles;
|
|
2070
|
+
const deletedEvidence = Array.from(new Set([...extraction.deletedFiles, ...continuity?.deletedFiles ?? []]));
|
|
2071
|
+
const requiredPaths = [...modifiedPaths, ...readPaths, ...deletedEvidence];
|
|
2072
|
+
const expectedPathSet = new Set(requiredPaths);
|
|
2073
|
+
const pathEvidence = buildSummaryPathEvidence(requiredPaths, evidence.summaryBudgetTokens);
|
|
2074
|
+
const normalizedOwnerSets = new Map;
|
|
2075
|
+
for (const file of requiredPaths) {
|
|
2076
|
+
const display = pathEvidence.get(file);
|
|
2077
|
+
for (const candidate of [
|
|
2078
|
+
file,
|
|
2079
|
+
...display ? [decodePathDisplay(display)] : []
|
|
2080
|
+
]) {
|
|
2081
|
+
const normalized = normalizePath(candidate);
|
|
2082
|
+
const owners = normalizedOwnerSets.get(normalized) ?? new Set;
|
|
2083
|
+
owners.add(file);
|
|
2084
|
+
normalizedOwnerSets.set(normalized, owners);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
const normalizedOwners = new Map(Array.from(normalizedOwnerSets, ([path2, owners]) => [path2, owners.size]));
|
|
2088
|
+
const listedPaths = (kind) => collectListedPaths(findSection(parsed, kind)?.body ?? "", expectedPathSet);
|
|
1576
2089
|
const modifiedListed = listedPaths("files-modified");
|
|
1577
2090
|
const readListed = listedPaths("files-read");
|
|
1578
2091
|
const deletedListed = listedPaths("files-deleted");
|
|
1579
2092
|
for (const file of modifiedPaths) {
|
|
1580
|
-
|
|
2093
|
+
const display = pathEvidence.get(file);
|
|
2094
|
+
if (display && !hasListedPath(modifiedListed, file, display, normalizedOwners)) {
|
|
1581
2095
|
gaps.push({ kind: "missing-file", path: file });
|
|
2096
|
+
}
|
|
1582
2097
|
}
|
|
1583
|
-
for (const file of
|
|
1584
|
-
|
|
2098
|
+
for (const file of readPaths) {
|
|
2099
|
+
const display = pathEvidence.get(file);
|
|
2100
|
+
if (display && !hasListedPath(readListed, file, display, normalizedOwners)) {
|
|
1585
2101
|
gaps.push({ kind: "missing-read-file", path: file });
|
|
2102
|
+
}
|
|
1586
2103
|
}
|
|
1587
|
-
const deletedEvidence = Array.from(new Set([
|
|
1588
|
-
...extraction.deletedFiles,
|
|
1589
|
-
...continuity?.deletedFiles ?? []
|
|
1590
|
-
]));
|
|
1591
2104
|
for (const file of deletedEvidence) {
|
|
1592
|
-
|
|
2105
|
+
const display = pathEvidence.get(file);
|
|
2106
|
+
if (display && !hasListedPath(deletedListed, file, display, normalizedOwners)) {
|
|
1593
2107
|
gaps.push({ kind: "missing-deleted-file", path: file });
|
|
2108
|
+
}
|
|
1594
2109
|
}
|
|
1595
2110
|
score -= gaps.filter((gap) => gap.kind === "missing-file" || gap.kind === "missing-read-file" || gap.kind === "missing-deleted-file").length * 5;
|
|
1596
2111
|
for (const error of unresolvedEvidence) {
|
|
1597
|
-
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
|
|
2112
|
+
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
|
|
1598
2113
|
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
1599
2114
|
gaps.push({ kind: "missing-error", message: error.message });
|
|
1600
2115
|
score -= 5;
|
|
1601
2116
|
}
|
|
1602
2117
|
}
|
|
1603
2118
|
for (const error of resolvedEvidence) {
|
|
1604
|
-
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
|
|
2119
|
+
const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase().replace(/\\/g, "/");
|
|
1605
2120
|
if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
|
|
1606
|
-
gaps.push({
|
|
2121
|
+
gaps.push({
|
|
2122
|
+
kind: "missing-error",
|
|
2123
|
+
message: error.message,
|
|
2124
|
+
resolved: true
|
|
2125
|
+
});
|
|
1607
2126
|
score -= 2;
|
|
1608
2127
|
}
|
|
1609
2128
|
}
|
|
@@ -1618,7 +2137,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1618
2137
|
score -= 8;
|
|
1619
2138
|
}
|
|
1620
2139
|
if (hasSemanticContradiction(constraint.text, constraintTarget)) {
|
|
1621
|
-
gaps.push({
|
|
2140
|
+
gaps.push({
|
|
2141
|
+
kind: "inconsistency",
|
|
2142
|
+
detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
|
|
2143
|
+
});
|
|
1622
2144
|
score -= 20;
|
|
1623
2145
|
}
|
|
1624
2146
|
}
|
|
@@ -1629,32 +2151,52 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1629
2151
|
score -= 12;
|
|
1630
2152
|
}
|
|
1631
2153
|
if (hasSemanticContradiction(goalEvidence, goalTarget)) {
|
|
1632
|
-
gaps.push({
|
|
2154
|
+
gaps.push({
|
|
2155
|
+
kind: "inconsistency",
|
|
2156
|
+
detail: "semantic-contradiction: goal polarity or condition changed"
|
|
2157
|
+
});
|
|
1633
2158
|
score -= 20;
|
|
1634
2159
|
}
|
|
1635
2160
|
}
|
|
1636
|
-
const
|
|
2161
|
+
const groundedEvidence = [
|
|
1637
2162
|
...unresolvedEvidence.map((item) => item.message),
|
|
2163
|
+
...resolvedEvidence.map((item) => item.message),
|
|
1638
2164
|
...constraintEvidence.map((item) => item.text),
|
|
1639
2165
|
...decisionEvidence.map((item) => item.summary),
|
|
1640
2166
|
...goalEvidence ? [goalEvidence] : [],
|
|
2167
|
+
...extraction.lastUserMessages,
|
|
2168
|
+
...extraction.timeline.map((item) => item.summary),
|
|
2169
|
+
...extraction.topics.map((item) => item.primaryFile ?? ""),
|
|
1641
2170
|
...continuity?.openLoops.map((item) => item.summary) ?? [],
|
|
1642
2171
|
...continuity?.criticalContext ?? []
|
|
1643
|
-
]
|
|
1644
|
-
const
|
|
2172
|
+
];
|
|
2173
|
+
const groundedEvidenceFiles = groundedEvidence.flatMap((value) => [
|
|
2174
|
+
value,
|
|
2175
|
+
summaryEvidenceLine(value, TRUNC.ERROR_SNIPPET),
|
|
2176
|
+
summaryEvidenceLine(value, TRUNC.TOPIC_LABEL),
|
|
2177
|
+
summaryEvidenceLine(value, TRUNC.PREVIEW),
|
|
2178
|
+
summaryEvidenceLine(value, TRUNC.MESSAGE)
|
|
2179
|
+
]).flatMap(extractFileRefs);
|
|
2180
|
+
const renderedPathEvidence = Array.from(pathEvidence.values()).flatMap((line) => [
|
|
2181
|
+
decodePathDisplay(line),
|
|
2182
|
+
line.startsWith('"') && line.endsWith('"') ? line.slice(1, -1) : line
|
|
2183
|
+
]);
|
|
2184
|
+
const rawKnownFiles = Array.from(new Set([
|
|
1645
2185
|
...modifiedPaths,
|
|
1646
|
-
...
|
|
1647
|
-
...
|
|
2186
|
+
...readPaths,
|
|
2187
|
+
...deletedEvidence,
|
|
1648
2188
|
...extraction.referencedFiles ?? [],
|
|
1649
2189
|
...groundedEvidenceFiles,
|
|
2190
|
+
...renderedPathEvidence,
|
|
2191
|
+
...renderedPathEvidence.flatMap(extractFileRefs),
|
|
1650
2192
|
...continuity?.modifiedFiles ?? [],
|
|
1651
2193
|
...continuity?.readFiles ?? [],
|
|
1652
|
-
...continuity?.deletedFiles ?? [],
|
|
1653
2194
|
...(continuity?.unresolvedErrors ?? []).flatMap((error) => error.files),
|
|
1654
2195
|
...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
|
|
1655
2196
|
]));
|
|
1656
2197
|
for (const ref of new Set(extractFileRefs(summary))) {
|
|
1657
|
-
|
|
2198
|
+
const grounded = isKnownPathReference(ref, rawKnownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
|
|
2199
|
+
if (!grounded) {
|
|
1658
2200
|
gaps.push({ kind: "fabricated-file", ref });
|
|
1659
2201
|
score -= 4;
|
|
1660
2202
|
}
|
|
@@ -1663,8 +2205,12 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1663
2205
|
if (progressSection) {
|
|
1664
2206
|
const doneSection = progressSection.body.match(/###\s*Done[\s\S]*?(?=###|$)/i)?.[0] ?? "";
|
|
1665
2207
|
const blockedSection = progressSection.body.match(/###\s*Blocked[\s\S]*?(?=###|$)/i)?.[0] ?? "";
|
|
1666
|
-
|
|
1667
|
-
|
|
2208
|
+
const blockedLines = blockedSection.split(/\r?\n/).slice(1);
|
|
2209
|
+
if (unresolvedEvidence.length > 0 && noneBlockerLineIndexes(blockedLines).size > 0) {
|
|
2210
|
+
gaps.push({
|
|
2211
|
+
kind: "inconsistency",
|
|
2212
|
+
detail: "blocked-none: Blocked says none despite unresolved errors"
|
|
2213
|
+
});
|
|
1668
2214
|
score -= 12;
|
|
1669
2215
|
}
|
|
1670
2216
|
const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
|
|
@@ -1678,7 +2224,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1678
2224
|
return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
|
|
1679
2225
|
});
|
|
1680
2226
|
if (unresolved) {
|
|
1681
|
-
gaps.push({
|
|
2227
|
+
gaps.push({
|
|
2228
|
+
kind: "inconsistency",
|
|
2229
|
+
detail: file.path + " marked Done but has unresolved error"
|
|
2230
|
+
});
|
|
1682
2231
|
score -= 5;
|
|
1683
2232
|
}
|
|
1684
2233
|
}
|
|
@@ -1690,7 +2239,10 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
1690
2239
|
score -= 8;
|
|
1691
2240
|
}
|
|
1692
2241
|
if (hasSemanticContradiction(decision.summary, decisionBody)) {
|
|
1693
|
-
gaps.push({
|
|
2242
|
+
gaps.push({
|
|
2243
|
+
kind: "inconsistency",
|
|
2244
|
+
detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
2245
|
+
});
|
|
1694
2246
|
score -= 20;
|
|
1695
2247
|
}
|
|
1696
2248
|
}
|