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.
@@ -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.2.1";
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
- return normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef);
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 candidates = summary.match(FILE_REF_CANDIDATE_RE) ?? [];
606
- return candidates.filter(isLikelyFileRef);
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({ kind: currentKind, heading: currentHeading.trim(), body });
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, { name: tool.name, arguments: tool.arguments, msgIndex: i });
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
- { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
772
- { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
773
- { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
774
- { re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
775
- { 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, cat: "requirement", conf: TUNING.CONFIDENCE_MEDIUM },
776
- { re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
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
- { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
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
- { kind: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
863
- { kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}=*/gi, replacement: () => "Bearer [REDACTED:bearer-token]" },
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
- { kind: "payment-card", regex: /\b(?:\d[ -]*?){13,19}\b/g },
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
- counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
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
- return pattern.replacement(...groups);
1214
+ replacement = pattern.replacement(...groups, match);
889
1215
  }
890
- return "[REDACTED:" + pattern.kind + "]";
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 { value, findings: [...counts].map(([kind, count]) => ({ kind, count })) };
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" ? item.length > 0 : item != null;
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 { value, findings: [...findings].map(([kind, count]) => ({ kind, count })) };
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) => ({ message: error.message }))
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) => ({ message: error.message }))
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
- if (!modifiedListed.has(normalizePath(file)))
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 extraction.readFiles) {
1584
- if (!readListed.has(normalizePath(file)))
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
- if (!deletedListed.has(normalizePath(file)))
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({ kind: "missing-error", message: error.message, resolved: true });
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({ kind: "inconsistency", detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET) });
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({ kind: "inconsistency", detail: "semantic-contradiction: goal polarity or condition changed" });
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 groundedEvidenceFiles = [
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
- ].flatMap(extractFileRefs);
1644
- const knownFiles = Array.from(new Set([
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
- ...extraction.readFiles,
1647
- ...extraction.deletedFiles,
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
- if (!isKnownPathReference(ref, knownFiles)) {
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
- if (unresolvedEvidence.length > 0 && /(?:none|no blockers?|yok)\s*(?:recorded|known)?[.!]?\s*$/im.test(blockedSection)) {
1667
- gaps.push({ kind: "inconsistency", detail: "blocked-none: Blocked says none despite unresolved errors" });
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({ kind: "inconsistency", detail: file.path + " marked Done but has unresolved error" });
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({ kind: "inconsistency", detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET) });
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
  }