pi-smart-compact 8.0.7 → 9.0.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.
Files changed (81) hide show
  1. package/ARCHITECTURE.md +47 -37
  2. package/CHANGELOG.md +34 -2
  3. package/README.md +52 -30
  4. package/SECURITY.md +1 -1
  5. package/dist/app/native-continuity-bridge.d.ts.map +1 -1
  6. package/dist/app/preflight.d.ts +2 -1
  7. package/dist/app/preflight.d.ts.map +1 -1
  8. package/dist/app/run-context.d.ts +10 -10
  9. package/dist/app/run-context.d.ts.map +1 -1
  10. package/dist/app/run-smart-compact.d.ts +18 -2
  11. package/dist/app/run-smart-compact.d.ts.map +1 -1
  12. package/dist/app/smart-compact-input.d.ts +23 -0
  13. package/dist/app/smart-compact-input.d.ts.map +1 -0
  14. package/dist/app/steps/extract.d.ts.map +1 -1
  15. package/dist/app/steps/metrics.d.ts.map +1 -1
  16. package/dist/app/steps/persist.d.ts +2 -3
  17. package/dist/app/steps/persist.d.ts.map +1 -1
  18. package/dist/app/steps/prepare.d.ts +4 -6
  19. package/dist/app/steps/prepare.d.ts.map +1 -1
  20. package/dist/app/steps/recover.d.ts +1 -1
  21. package/dist/app/steps/recover.d.ts.map +1 -1
  22. package/dist/app/steps/state.d.ts.map +1 -1
  23. package/dist/app/steps/synthesize.d.ts.map +1 -1
  24. package/dist/app/steps/verify.d.ts.map +1 -1
  25. package/dist/app/steps/window.d.ts +5 -1
  26. package/dist/app/steps/window.d.ts.map +1 -1
  27. package/dist/constants.d.ts +21 -4
  28. package/dist/constants.d.ts.map +1 -1
  29. package/dist/domain/scrub.d.ts.map +1 -1
  30. package/dist/domain/summary-parse.d.ts +1 -1
  31. package/dist/domain/summary-parse.d.ts.map +1 -1
  32. package/dist/domain/summary-schema.d.ts +1 -1
  33. package/dist/domain/summary-schema.d.ts.map +1 -1
  34. package/dist/domain/tool-semantics.d.ts +9 -0
  35. package/dist/domain/tool-semantics.d.ts.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2082 -1126
  38. package/dist/infra/context-graph.d.ts.map +1 -1
  39. package/dist/infra/fs.d.ts +11 -15
  40. package/dist/infra/fs.d.ts.map +1 -1
  41. package/dist/infra/git.d.ts +5 -11
  42. package/dist/infra/git.d.ts.map +1 -1
  43. package/dist/infra/llm-client.d.ts +2 -0
  44. package/dist/infra/llm-client.d.ts.map +1 -1
  45. package/dist/infra/paths.d.ts +4 -2
  46. package/dist/infra/paths.d.ts.map +1 -1
  47. package/dist/infra/session-identity.d.ts +11 -0
  48. package/dist/infra/session-identity.d.ts.map +1 -1
  49. package/dist/infra/synthesis-cache.d.ts.map +1 -1
  50. package/dist/phases/explore.d.ts.map +1 -1
  51. package/dist/phases/synthesize.d.ts +4 -1
  52. package/dist/phases/synthesize.d.ts.map +1 -1
  53. package/dist/phases/verify.d.ts +10 -3
  54. package/dist/phases/verify.d.ts.map +1 -1
  55. package/dist/provider-eval.js +113 -43
  56. package/dist/provider-scenario-eval.js +206 -51
  57. package/dist/telemetry-report.js +113 -43
  58. package/dist/types.d.ts +45 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/ui/overlays.d.ts +2 -1
  61. package/dist/ui/overlays.d.ts.map +1 -1
  62. package/dist/utils/backups.d.ts +31 -0
  63. package/dist/utils/backups.d.ts.map +1 -0
  64. package/dist/utils/cache.d.ts +1 -0
  65. package/dist/utils/cache.d.ts.map +1 -1
  66. package/dist/utils/damage.d.ts.map +1 -1
  67. package/dist/utils/extraction.d.ts +6 -0
  68. package/dist/utils/extraction.d.ts.map +1 -1
  69. package/dist/utils/file-needles.d.ts +7 -0
  70. package/dist/utils/file-needles.d.ts.map +1 -1
  71. package/dist/utils/fingerprint.d.ts +5 -1
  72. package/dist/utils/fingerprint.d.ts.map +1 -1
  73. package/dist/utils/helpers.d.ts +1 -35
  74. package/dist/utils/helpers.d.ts.map +1 -1
  75. package/dist/utils/pruning.d.ts.map +1 -1
  76. package/dist/utils/session-log.d.ts +10 -18
  77. package/dist/utils/session-log.d.ts.map +1 -1
  78. package/dist/utils/state.d.ts +2 -2
  79. package/dist/utils/state.d.ts.map +1 -1
  80. package/docs/MIGRATING_TO_V8.md +2 -2
  81. package/package.json +2 -2
@@ -82,6 +82,35 @@ function assertSuccessful(message) {
82
82
  }
83
83
  return message;
84
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
+ }
85
114
  async function completeChatGptCodex(model, body, opts) {
86
115
  const controller = new AbortController;
87
116
  const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
@@ -125,10 +154,12 @@ async function completeChatGptCodex(model, body, opts) {
125
154
  var rawLlmClient = {
126
155
  complete: async (model, body, originalOpts) => {
127
156
  const opts = withCodexWireLimit(model, originalOpts);
128
- if (isChatGptCodex(model))
129
- return completeChatGptCodex(model, body, opts);
130
- const response = opts.reasoning === undefined ? await (await resolveComplete())(model, body, opts) : await (await resolveCompleteSimple())(model, body, opts);
131
- return assertSuccessful(response);
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
+ });
132
163
  }
133
164
  };
134
165
  var defaultLlmClient = rawLlmClient;
@@ -138,7 +169,7 @@ function getLlmClient() {
138
169
  }
139
170
 
140
171
  // src/constants.ts
141
- var VERSION = "8.0.7";
172
+ var VERSION = "9.0.0";
142
173
  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.";
143
174
  var PROFILES = {
144
175
  light: {
@@ -180,7 +211,7 @@ var DEFAULT_CONFIG = {
180
211
  backupEnabled: true,
181
212
  backupDir: "",
182
213
  minContextPercent: 60,
183
- requireApproval: false,
214
+ requireApproval: true,
184
215
  scrubSecrets: true,
185
216
  scrubPii: false,
186
217
  maxLlmCalls: 8,
@@ -220,6 +251,8 @@ Output EXACTLY this format:
220
251
  - **[Decision]**: [Rationale]
221
252
  ` + `## Files Modified
222
253
  - [Verified list from deterministic extraction]
254
+ ` + `## Files Deleted
255
+ - [Verified deleted paths from deterministic extraction]
223
256
  ` + `## Files Read
224
257
  - [Verified list from deterministic extraction]
225
258
  ` + `## Next Steps
@@ -243,6 +276,7 @@ Rules for Accuracy:
243
276
  ` + `**Summary**: [2-4 sentences: what happened, errors, code changes with paths]
244
277
  ` + `**Decisions**: [comma-separated, or "None"]
245
278
  ` + `**Modified**: [comma-separated paths, or "None"]
279
+ ` + `**Deleted**: [comma-separated paths, or "None"]
246
280
  ` + `**Read**: [comma-separated paths, or "None"]
247
281
  `;
248
282
  var ASSEMBLY_PROMPT_PREFIX = `Merge these topic summaries into ONE coherent summary.
@@ -274,6 +308,8 @@ var ASSEMBLY_PROMPT_PREFIX = `Merge these topic summaries into ONE coherent summ
274
308
  - **[Decision]**: [Rationale]
275
309
  ` + `## Files Modified
276
310
  - [Verified deterministic list]
311
+ ` + `## Files Deleted
312
+ - [Verified deterministic deleted paths]
277
313
  ` + `## Files Read
278
314
  - [Verified deterministic list]
279
315
  ` + `## Next Steps
@@ -328,6 +364,7 @@ var TRUNC = {
328
364
  BACKUP_PREVIEW_LINES: 5,
329
365
  FINGERPRINT_SEG: 2
330
366
  };
367
+ var LIKELY_ERROR_RE = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed|^FAIL\b|ERROR:)/i;
331
368
  var METRICS_BUFFER_MAX = 200;
332
369
  var RUNTIME_LOG_MAX_BYTES = 5 * 1024 * 1024;
333
370
  var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
@@ -406,6 +443,9 @@ function calibrationKey(provider, model) {
406
443
  }
407
444
 
408
445
  // src/utils/type-guards.ts
446
+ function isTextBlock(c) {
447
+ return typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string";
448
+ }
409
449
  var KNOWN_METHODS = new Set(["eesv", "single-pass", "heuristic"]);
410
450
  var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
411
451
  var KNOWN_MODES = new Set(["balanced", "aggressive", "fast", "thorough"]);
@@ -425,37 +465,9 @@ var GENERIC_BASENAMES = new Set([
425
465
  "lib.rs",
426
466
  "__init__.py"
427
467
  ]);
428
- var MIN_BARE_BASENAME_LEN = 5;
429
468
  function normalizePath(filePath) {
430
469
  return filePath.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
431
470
  }
432
- function buildPathNeedles(filePath) {
433
- const parts = normalizePath(filePath).split("/").filter(Boolean);
434
- if (parts.length === 0)
435
- return [];
436
- const needles = [];
437
- const basename = parts[parts.length - 1];
438
- if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
439
- needles.push(basename);
440
- }
441
- for (let j = parts.length - 2;j >= 0; j--) {
442
- needles.push(parts.slice(j).join("/"));
443
- }
444
- return needles;
445
- }
446
- function buildUniquePathNeedles(filePath, allPaths) {
447
- const normalized = allPaths.map(normalizePath);
448
- return buildPathNeedles(filePath).filter((needle) => {
449
- let owners = 0;
450
- for (const candidate of normalized) {
451
- if (candidate === needle || candidate.endsWith("/" + needle))
452
- owners++;
453
- if (owners > 1)
454
- return false;
455
- }
456
- return owners === 1;
457
- });
458
- }
459
471
  function isKnownPathReference(ref, knownPaths) {
460
472
  const normalizedRef = normalizePath(ref);
461
473
  return knownPaths.some((path) => {
@@ -499,6 +511,8 @@ function classifyHeading(raw) {
499
511
  return "files-modified";
500
512
  if (text.includes("file") && (text.includes("read") || text.includes("viewed")))
501
513
  return "files-read";
514
+ if (text.includes("file") && (text.includes("delet") || text.includes("remov")))
515
+ return "files-deleted";
502
516
  if (text.includes("next step") || text === "next actions")
503
517
  return "next-steps";
504
518
  if (text.includes("critical context") || text === "important context")
@@ -569,12 +583,25 @@ function findSection(summary, kind) {
569
583
  }
570
584
 
571
585
  // src/utils/extraction.ts
586
+ function extractText(content) {
587
+ if (typeof content === "string")
588
+ return content;
589
+ if (!Array.isArray(content))
590
+ return "";
591
+ return content.map((b) => {
592
+ if (typeof b === "string")
593
+ return b;
594
+ if (isTextBlock(b))
595
+ return b.text;
596
+ return "";
597
+ }).join("");
598
+ }
572
599
  var CONSTRAINT_PATTERNS = [
573
600
  { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
574
601
  { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
575
602
  { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
576
- { re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|asla|sak\u0131n|sak\u0131nha|bunu yapma|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 },
577
- { re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
603
+ { 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 },
604
+ { 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 },
578
605
  { re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
579
606
  ];
580
607
  function isDiagnosticConstraintText(text) {
@@ -641,7 +668,6 @@ function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
641
668
  }
642
669
  return values.slice(-limit);
643
670
  }
644
- var scheduledTailTrims = new Set;
645
671
 
646
672
  // src/infra/clock.ts
647
673
  var systemClock = {
@@ -655,14 +681,23 @@ import crypto from "crypto";
655
681
  var SECRET_PATTERNS = [
656
682
  { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
657
683
  { kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
658
- { kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g },
684
+ { kind: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
685
+ { kind: "stripe-key", regex: /\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}\b/g },
686
+ { kind: "gitlab-token", regex: /\bglpat-[0-9A-Za-z_-]{20,}\b/g },
687
+ { kind: "npm-token", regex: /\bnpm_[0-9A-Za-z]{30,}\b/g },
688
+ { kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
659
689
  { kind: "api-key", regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g },
660
- { kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
690
+ { kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
661
691
  { kind: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
662
- { kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]{24,}/gi, replacement: () => "Bearer [REDACTED:bearer-token]" },
692
+ { kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}=*/gi, replacement: () => "Bearer [REDACTED:bearer-token]" },
693
+ {
694
+ kind: "connection-password",
695
+ regex: /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+(@)/gi,
696
+ replacement: (prefix, suffix) => prefix + "[REDACTED:password]" + suffix
697
+ },
663
698
  {
664
699
  kind: "credential",
665
- regex: /\b(api[_-]?key|access[_-]?token|auth[_-]?token|token|password|passwd|secret)\s*([:=])\s*["']?([A-Za-z0-9._~+\/-]{16,})["']?/gi,
700
+ 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,
666
701
  replacement: (name, separator) => name + separator + "[REDACTED:credential]"
667
702
  }
668
703
  ];
@@ -690,6 +725,31 @@ function mergeFindings(target, findings) {
690
725
  for (const finding of findings)
691
726
  target.set(finding.kind, (target.get(finding.kind) ?? 0) + finding.count);
692
727
  }
728
+ var SECRET_KEY_NAMES = new Set([
729
+ "api_key",
730
+ "apikey",
731
+ "access_token",
732
+ "auth_token",
733
+ "authorization",
734
+ "password",
735
+ "passwd",
736
+ "secret",
737
+ "secret_key",
738
+ "secret_access_key",
739
+ "client_secret",
740
+ "private_key",
741
+ "database_url",
742
+ "connection_string"
743
+ ]);
744
+ function normalizeObjectKey(key) {
745
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
746
+ }
747
+ function isSecretBearingKey(key) {
748
+ const normalized = normalizeObjectKey(key);
749
+ if (SECRET_KEY_NAMES.has(normalized))
750
+ return true;
751
+ return /(?:^|_)(?:api_key|access_token|auth_token|password|passwd|secret_access_key|client_secret|private_key)(?:_|$)/.test(normalized);
752
+ }
693
753
 
694
754
  class SecretScrubber {
695
755
  secretsEnabled;
@@ -719,6 +779,10 @@ class SecretScrubber {
719
779
  scrubValue(input) {
720
780
  const findings = new Map;
721
781
  const seen = new WeakMap;
782
+ const recordCredential = () => {
783
+ findings.set("credential", (findings.get("credential") ?? 0) + 1);
784
+ this.total++;
785
+ };
722
786
  const visit = (value2) => {
723
787
  if (typeof value2 === "string") {
724
788
  const result = this.scrubText(value2);
@@ -739,8 +803,14 @@ class SecretScrubber {
739
803
  }
740
804
  const output = {};
741
805
  seen.set(value2, output);
742
- for (const [key, item] of Object.entries(value2))
743
- output[key] = visit(item);
806
+ for (const [key, item] of Object.entries(value2)) {
807
+ if (this.secretsEnabled && isSecretBearingKey(key) && typeof item === "string" && item.length > 0) {
808
+ output[key] = "[REDACTED:credential]";
809
+ recordCredential();
810
+ } else {
811
+ output[key] = visit(item);
812
+ }
813
+ }
744
814
  return output;
745
815
  };
746
816
  const value = visit(input);
@@ -1028,6 +1098,52 @@ function readMetricsLog(limit = 100) {
1028
1098
  }
1029
1099
 
1030
1100
  // src/phases/verify.ts
1101
+ 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;
1102
+ var NEGATED_OUTCOME_RE = /\b(?:not|never|pending|failed|failing|unresolved|hen\u00FCz|de\u011Fil|ba\u015Far\u0131s\u0131z)\b/iu;
1103
+ function outcomeClaims(summary) {
1104
+ 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);
1105
+ }
1106
+ function classifyOutcomeClaim(claim) {
1107
+ const lower = claim.toLowerCase();
1108
+ if (/\btests?\b|\btestler?\b/.test(lower))
1109
+ return "test";
1110
+ if (/\bbuild\b|\bcompil(?:e|ed|ation)\b|\btypecheck\b/.test(lower))
1111
+ return "build";
1112
+ if (/\bdeploy(?:ed|ment)?\b|\bpublish(?:ed)?\b|\breleas(?:e|ed)\b/.test(lower))
1113
+ return "release";
1114
+ if (/\bbug\b|\bissue\b|\berror\b|\bfail(?:ed|ure)?\b|\bhata\b/.test(lower))
1115
+ return "error";
1116
+ if (/\bfile\b|\bdosya\b/.test(lower))
1117
+ return "file";
1118
+ return "generic";
1119
+ }
1120
+ function successfulToolSupportsClaim(claim, messages, extraction) {
1121
+ const shape = semanticShape(claim);
1122
+ const category = classifyOutcomeClaim(claim);
1123
+ if (category === "error" && extraction.errors.some((error) => error.resolved && hasSemanticEvidence(claim, error.message)))
1124
+ return true;
1125
+ if (category === "file" && extraction.modifiedFiles.some((file) => claim.toLowerCase().includes(file.path.toLowerCase())))
1126
+ return true;
1127
+ for (const message of messages) {
1128
+ if (message.role !== "toolResult" || message.isError)
1129
+ continue;
1130
+ const bounded = extractText(message.content).slice(0, 8000);
1131
+ if (!bounded.trim() || LIKELY_ERROR_RE.test(bounded))
1132
+ continue;
1133
+ if (hasSemanticEvidence(claim, bounded))
1134
+ return true;
1135
+ const lower = bounded.toLowerCase();
1136
+ if (category === "test" && /\b\d+\s+(?:tests?\s+)?pass(?:ed)?\b/.test(lower) && !/\b(?:fail(?:ed|ures?)?|errors?)\s*[:=]?\s*[1-9]\d*\b/.test(lower))
1137
+ return true;
1138
+ if (category === "build" && /\b(?:build|compile|typecheck)\b/.test(lower) && /\b(?:succeeded|successful|passed|exit(?:ed)?\s+(?:code\s+)?0)\b/.test(lower))
1139
+ return true;
1140
+ if (category === "release" && /\b(?:publish|release|deploy)\b/.test(lower) && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
1141
+ return true;
1142
+ if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, bounded))
1143
+ return true;
1144
+ }
1145
+ return false;
1146
+ }
1031
1147
  var NEGATION_MARKERS = new Set([
1032
1148
  "no",
1033
1149
  "not",
@@ -1186,7 +1302,7 @@ function hasSemanticContradiction(source, target) {
1186
1302
  return conditional && !negative && !tokens.some((token) => CONDITION_MARKERS.has(token));
1187
1303
  });
1188
1304
  }
1189
- function verifySummary(summary, extraction, continuity = null) {
1305
+ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1190
1306
  const parsed = parseSummary(summary);
1191
1307
  const gaps = [];
1192
1308
  const lower = summary.toLowerCase().replace(/\\/g, "/");
@@ -1206,9 +1322,18 @@ function verifySummary(summary, extraction, continuity = null) {
1206
1322
  ...extraction.errors.filter((error) => !error.resolved).map((error) => ({ message: error.message })),
1207
1323
  ...(continuity?.unresolvedErrors ?? []).map((error) => ({ message: error.message }))
1208
1324
  ], (item) => item.message);
1325
+ const resolvedEvidence = uniqueByText([
1326
+ ...extraction.errors.filter((error) => error.resolved).map((error) => ({ message: error.message })),
1327
+ ...(continuity?.resolvedErrors ?? []).map((error) => ({ message: error.message }))
1328
+ ], (item) => item.message).slice(-5);
1329
+ const steeringConstraints = [
1330
+ evidence.steering?.focus ? { text: "Preserve detail about: " + evidence.steering.focus } : null,
1331
+ evidence.steering?.note ? { text: evidence.steering.note } : null
1332
+ ].filter((item) => Boolean(item?.text.trim()));
1209
1333
  const constraintEvidence = uniqueByText([
1210
1334
  ...extraction.constraints.filter((item) => item.confidence >= 0.8).map((item) => ({ text: item.text })),
1211
- ...(continuity?.constraints ?? []).filter((item) => item.confidence >= 0.8).map((item) => ({ text: item.text }))
1335
+ ...(continuity?.constraints ?? []).filter((item) => item.confidence >= 0.8).map((item) => ({ text: item.text })),
1336
+ ...steeringConstraints
1212
1337
  ], (item) => item.text).filter((item) => !isDiagnosticConstraintText(item.text));
1213
1338
  const decisionEvidence = uniqueByText([
1214
1339
  ...extraction.decisions.filter((item) => item.type === "explicit").map((item) => ({ summary: item.summary })),
@@ -1226,14 +1351,29 @@ function verifySummary(summary, extraction, continuity = null) {
1226
1351
  score -= req.penalty;
1227
1352
  }
1228
1353
  }
1354
+ const listedPaths = (kind) => new Set((findSection(parsed, kind)?.body ?? "").split(`
1355
+ `).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));
1229
1356
  const modifiedPaths = extraction.modifiedFiles.map((file) => file.path);
1230
- for (const file of extraction.modifiedFiles) {
1231
- const needles = buildUniquePathNeedles(file.path, modifiedPaths);
1232
- if (!needles.some((needle) => lower.includes(needle))) {
1233
- gaps.push({ kind: "missing-file", path: file.path });
1234
- score -= 5;
1235
- }
1357
+ const modifiedListed = listedPaths("files-modified");
1358
+ const readListed = listedPaths("files-read");
1359
+ const deletedListed = listedPaths("files-deleted");
1360
+ for (const file of modifiedPaths) {
1361
+ if (!modifiedListed.has(normalizePath(file)))
1362
+ gaps.push({ kind: "missing-file", path: file });
1363
+ }
1364
+ for (const file of extraction.readFiles) {
1365
+ if (!readListed.has(normalizePath(file)))
1366
+ gaps.push({ kind: "missing-read-file", path: file });
1367
+ }
1368
+ const deletedEvidence = Array.from(new Set([
1369
+ ...extraction.deletedFiles,
1370
+ ...continuity?.deletedFiles ?? []
1371
+ ]));
1372
+ for (const file of deletedEvidence) {
1373
+ if (!deletedListed.has(normalizePath(file)))
1374
+ gaps.push({ kind: "missing-deleted-file", path: file });
1236
1375
  }
1376
+ score -= gaps.filter((gap) => gap.kind === "missing-file" || gap.kind === "missing-read-file" || gap.kind === "missing-deleted-file").length * 5;
1237
1377
  for (const error of unresolvedEvidence) {
1238
1378
  const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
1239
1379
  if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
@@ -1241,6 +1381,13 @@ function verifySummary(summary, extraction, continuity = null) {
1241
1381
  score -= 5;
1242
1382
  }
1243
1383
  }
1384
+ for (const error of resolvedEvidence) {
1385
+ const snippet = summaryEvidenceLine(error.message, TRUNC.ERROR_SNIPPET).toLowerCase();
1386
+ if (snippet.length > 5 && !normalizedSummary.includes(snippet)) {
1387
+ gaps.push({ kind: "missing-error", message: error.message, resolved: true });
1388
+ score -= 2;
1389
+ }
1390
+ }
1244
1391
  const constraintTarget = [
1245
1392
  findSection(parsed, "constraints")?.body ?? "",
1246
1393
  findSection(parsed, "critical-context")?.body ?? ""
@@ -1331,6 +1478,14 @@ function verifySummary(summary, extraction, continuity = null) {
1331
1478
  gaps.push({ kind: "missing-open-loops", unresolvedCount });
1332
1479
  score -= 5;
1333
1480
  }
1481
+ if (evidence.sourceMessages) {
1482
+ for (const claim of outcomeClaims(summary)) {
1483
+ if (!successfulToolSupportsClaim(claim, evidence.sourceMessages, extraction)) {
1484
+ gaps.push({ kind: "unsupported-claim", claim });
1485
+ score -= 20;
1486
+ }
1487
+ }
1488
+ }
1334
1489
  const finalScore = Math.max(0, score);
1335
1490
  return { ok: gaps.length === 0 && finalScore >= 85, gaps, score: finalScore };
1336
1491
  }