pi-smart-compact 9.0.0 → 9.2.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 (56) hide show
  1. package/ARCHITECTURE.md +61 -36
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +30 -5
  4. package/dist/app/native-continuity-bridge.d.ts.map +1 -1
  5. package/dist/app/pending-slot.d.ts.map +1 -1
  6. package/dist/app/preflight.d.ts +3 -3
  7. package/dist/app/preflight.d.ts.map +1 -1
  8. package/dist/app/run-smart-compact.d.ts.map +1 -1
  9. package/dist/app/session-run-lock.d.ts.map +1 -1
  10. package/dist/app/settled-auto-trigger.d.ts +20 -0
  11. package/dist/app/settled-auto-trigger.d.ts.map +1 -0
  12. package/dist/app/steps/extract.d.ts.map +1 -1
  13. package/dist/app/steps/metrics.d.ts +4 -4
  14. package/dist/app/steps/metrics.d.ts.map +1 -1
  15. package/dist/app/steps/persist.d.ts.map +1 -1
  16. package/dist/app/steps/synthesize.d.ts.map +1 -1
  17. package/dist/app/steps/window.d.ts +1 -1
  18. package/dist/app/steps/window.d.ts.map +1 -1
  19. package/dist/constants.d.ts +8 -1
  20. package/dist/constants.d.ts.map +1 -1
  21. package/dist/domain/scrub.d.ts.map +1 -1
  22. package/dist/domain/summary-parse.d.ts.map +1 -1
  23. package/dist/domain/tool-semantics.d.ts +6 -9
  24. package/dist/domain/tool-semantics.d.ts.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1216 -364
  27. package/dist/infra/ai-messages.d.ts +6 -0
  28. package/dist/infra/ai-messages.d.ts.map +1 -1
  29. package/dist/infra/context-graph.d.ts.map +1 -1
  30. package/dist/infra/fs.d.ts +2 -0
  31. package/dist/infra/fs.d.ts.map +1 -1
  32. package/dist/phases/explore.d.ts +2 -1
  33. package/dist/phases/explore.d.ts.map +1 -1
  34. package/dist/phases/synthesize.d.ts.map +1 -1
  35. package/dist/phases/verify.d.ts +3 -2
  36. package/dist/phases/verify.d.ts.map +1 -1
  37. package/dist/provider-eval.js +216 -33
  38. package/dist/provider-scenario-eval.js +279 -57
  39. package/dist/telemetry-report.js +216 -33
  40. package/dist/types.d.ts +12 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/ui/dashboard-format.d.ts.map +1 -1
  43. package/dist/utils/cache.d.ts +2 -2
  44. package/dist/utils/cache.d.ts.map +1 -1
  45. package/dist/utils/extraction.d.ts +2 -1
  46. package/dist/utils/extraction.d.ts.map +1 -1
  47. package/dist/utils/helpers.d.ts +14 -2
  48. package/dist/utils/helpers.d.ts.map +1 -1
  49. package/dist/utils/session-log.d.ts +1 -1
  50. package/dist/utils/session-log.d.ts.map +1 -1
  51. package/dist/utils/state.d.ts.map +1 -1
  52. package/dist/utils/tokens.d.ts +2 -0
  53. package/dist/utils/tokens.d.ts.map +1 -1
  54. package/dist/utils/type-guards.d.ts +2 -0
  55. package/dist/utils/type-guards.d.ts.map +1 -1
  56. package/package.json +1 -1
@@ -169,7 +169,8 @@ function getLlmClient() {
169
169
  }
170
170
 
171
171
  // src/constants.ts
172
- var VERSION = "9.0.0";
172
+ var VERSION = "9.2.0";
173
+ var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
173
174
  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.";
174
175
  var PROFILES = {
175
176
  light: {
@@ -207,6 +208,7 @@ var DEFAULT_CONFIG = {
207
208
  summaryThinkingLevel: "minimal",
208
209
  segmentationThinkingLevel: "minimal",
209
210
  autoTrigger: true,
211
+ autoTriggerStrategy: "native-hook",
210
212
  autoTriggerTimeoutMs: 120000,
211
213
  backupEnabled: true,
212
214
  backupDir: "",
@@ -325,6 +327,14 @@ var FIVE_MINUTES_MS = 5 * 60 * 1000;
325
327
  var ONE_HOUR_MS = 60 * 60 * 1000;
326
328
  var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
327
329
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
330
+ var ID_PREFIX = {
331
+ PROJECT: "proj-",
332
+ COMPACT_SESSION: "sc-",
333
+ MULTI_TOOL_USE_SYNTHETIC: "mtu_",
334
+ OPEN_LOOP: "loop-",
335
+ DECISION: "decision-",
336
+ ERROR: "error-"
337
+ };
328
338
  var TUNING = {
329
339
  EMA_PREV: 0.7,
330
340
  EMA_SAMPLE: 0.3,
@@ -443,8 +453,14 @@ function calibrationKey(provider, model) {
443
453
  }
444
454
 
445
455
  // src/utils/type-guards.ts
456
+ function isRecord(value) {
457
+ return typeof value === "object" && value !== null;
458
+ }
446
459
  function isTextBlock(c) {
447
- return typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string";
460
+ return isRecord(c) && c.type === "text" && typeof c.text === "string";
461
+ }
462
+ function isToolCallBlock(c) {
463
+ return isRecord(c) && c.type === "toolCall" && typeof c.name === "string" && isRecord(c.arguments);
448
464
  }
449
465
  var KNOWN_METHODS = new Set(["eesv", "single-pass", "heuristic"]);
450
466
  var KNOWN_PROFILES = new Set(["light", "balanced", "aggressive"]);
@@ -465,9 +481,37 @@ var GENERIC_BASENAMES = new Set([
465
481
  "lib.rs",
466
482
  "__init__.py"
467
483
  ]);
484
+ var MIN_BARE_BASENAME_LEN = 5;
468
485
  function normalizePath(filePath) {
469
486
  return filePath.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
470
487
  }
488
+ function buildPathNeedles(filePath) {
489
+ const parts = normalizePath(filePath).split("/").filter(Boolean);
490
+ if (parts.length === 0)
491
+ return [];
492
+ const needles = [];
493
+ const basename = parts[parts.length - 1];
494
+ if (!GENERIC_BASENAMES.has(basename) && basename.length >= MIN_BARE_BASENAME_LEN) {
495
+ needles.push(basename);
496
+ }
497
+ for (let j = parts.length - 2;j >= 0; j--) {
498
+ needles.push(parts.slice(j).join("/"));
499
+ }
500
+ return needles;
501
+ }
502
+ function buildUniquePathNeedles(filePath, allPaths) {
503
+ const normalized = allPaths.map(normalizePath);
504
+ return buildPathNeedles(filePath).filter((needle) => {
505
+ let owners = 0;
506
+ for (const candidate of normalized) {
507
+ if (candidate === needle || candidate.endsWith("/" + needle))
508
+ owners++;
509
+ if (owners > 1)
510
+ return false;
511
+ }
512
+ return owners === 1;
513
+ });
514
+ }
471
515
  function isKnownPathReference(ref, knownPaths) {
472
516
  const normalizedRef = normalizePath(ref);
473
517
  return knownPaths.some((path) => {
@@ -476,6 +520,74 @@ function isKnownPathReference(ref, knownPaths) {
476
520
  });
477
521
  }
478
522
 
523
+ // src/domain/tool-semantics.ts
524
+ var PATH_KEYS = [
525
+ "path",
526
+ "file_path",
527
+ "filePath",
528
+ "filename",
529
+ "file",
530
+ "target_file",
531
+ "file_uri",
532
+ "absolute_path"
533
+ ];
534
+ var PAYLOAD_KEYS = [
535
+ "content",
536
+ "newText",
537
+ "oldText",
538
+ "new_str",
539
+ "old_str",
540
+ "new_string",
541
+ "old_string",
542
+ "edits",
543
+ "patch",
544
+ "replacement"
545
+ ];
546
+ var COMMAND_KEYS = ["command", "cmd", "script"];
547
+ function hasPresent(args, keys) {
548
+ return keys.some((k) => args[k] != null);
549
+ }
550
+ function extractToolPath(args) {
551
+ if (!args || typeof args !== "object")
552
+ return;
553
+ const a = args;
554
+ for (const k of PATH_KEYS) {
555
+ const v = a[k];
556
+ if (typeof v === "string" && v.length > 0)
557
+ return v;
558
+ }
559
+ return;
560
+ }
561
+ function normalizeToolName(name) {
562
+ if (typeof name !== "string")
563
+ return "";
564
+ return name.replace(/^functions[.:/_-]+/i, "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
565
+ }
566
+ function nameHas(name, hints) {
567
+ const words = name.split("_");
568
+ return hints.some((hint) => words.includes(hint));
569
+ }
570
+ function classifyToolOperation(args, toolName) {
571
+ const a = args && typeof args === "object" ? args : {};
572
+ const name = normalizeToolName(toolName);
573
+ const hasPath = extractToolPath(a) !== undefined;
574
+ if (hasPath && hasPresent(a, PAYLOAD_KEYS))
575
+ return "mutate";
576
+ if (hasPresent(a, COMMAND_KEYS))
577
+ return "execute";
578
+ if (hasPath && hasPresent(a, ["text"]) && nameHas(name, ["write", "edit", "patch", "replace", "append", "create", "update", "insert"]))
579
+ return "mutate";
580
+ if (hasPath && nameHas(name, ["delete", "remove", "unlink"]))
581
+ return "delete";
582
+ if (hasPresent(a, ["pattern", "query", "glob"]) || nameHas(name, ["grep", "search", "find", "glob", "rg"]))
583
+ return "search";
584
+ if (nameHas(name, ["list", "ls", "tree"]))
585
+ return "list";
586
+ if (hasPath || nameHas(name, ["read"]))
587
+ return "read";
588
+ return "unknown";
589
+ }
590
+
479
591
  // src/utils/file-ref-detect.ts
480
592
  var CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|rb|cs|cpp|c|h|hpp|swift|kt|scala|php|css|scss|html|json|yaml|yml|toml|md|mdx|sh|sql|tf|ini|env|lock|gradle|xml)$/i;
481
593
  var VERSION_RE = /^v?\d+(?:\.\d+)+(?:[-+][\w.-]+)?$/i;
@@ -546,6 +658,7 @@ function parseSummary(markdown) {
546
658
  let currentHeading = "";
547
659
  let currentKind = "unknown";
548
660
  let bodyLines = [];
661
+ let fence = null;
549
662
  let started = false;
550
663
  const flush = () => {
551
664
  if (!started)
@@ -559,16 +672,31 @@ function parseSummary(markdown) {
559
672
  sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
560
673
  };
561
674
  for (const line of lines) {
562
- const m = line.match(HEADING_RE);
563
- if (m) {
564
- const kind = classifyHeading(m[2]);
565
- if (m[1].length <= 2 || kind !== "unknown") {
566
- flush();
567
- currentHeading = "## " + m[2].trim();
568
- currentKind = kind;
569
- bodyLines = [];
570
- started = true;
571
- continue;
675
+ const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
676
+ if (fenceMatch) {
677
+ const marker = fenceMatch[1][0];
678
+ const markerLength = fenceMatch[1].length;
679
+ if (!fence) {
680
+ fence = { marker, length: markerLength };
681
+ } else if (marker === fence.marker && markerLength >= fence.length && !fenceMatch[2].trim()) {
682
+ fence = null;
683
+ }
684
+ if (started)
685
+ bodyLines.push(line);
686
+ continue;
687
+ }
688
+ if (!fence) {
689
+ const heading = line.match(HEADING_RE);
690
+ if (heading) {
691
+ const kind = classifyHeading(heading[2]);
692
+ if (heading[1].length <= 2 || kind !== "unknown") {
693
+ flush();
694
+ currentHeading = "## " + heading[2].trim();
695
+ currentKind = kind;
696
+ bodyLines = [];
697
+ started = true;
698
+ continue;
699
+ }
572
700
  }
573
701
  }
574
702
  if (started)
@@ -583,6 +711,24 @@ function findSection(summary, kind) {
583
711
  }
584
712
 
585
713
  // src/utils/extraction.ts
714
+ function nestedToolCallId(wrapperId, messageIndex, toolIndex, nestedId) {
715
+ return typeof nestedId === "string" ? nestedId : wrapperId ? wrapperId + "_" + toolIndex : ID_PREFIX.MULTI_TOOL_USE_SYNTHETIC + messageIndex + "_" + toolIndex;
716
+ }
717
+ function flattenToolCallBlock(b) {
718
+ if (!isToolCallBlock(b))
719
+ return [];
720
+ if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
721
+ return b.arguments.tool_uses.map((u) => {
722
+ const recipient = u?.recipient_name ?? "";
723
+ return {
724
+ name: recipient.replace(/^functions\./, ""),
725
+ id: u?.id ?? undefined,
726
+ arguments: u?.parameters ?? {}
727
+ };
728
+ });
729
+ }
730
+ return [{ name: b.name, id: b.id, arguments: b.arguments }];
731
+ }
586
732
  function extractText(content) {
587
733
  if (typeof content === "string")
588
734
  return content;
@@ -596,6 +742,31 @@ function extractText(content) {
596
742
  return "";
597
743
  }).join("");
598
744
  }
745
+ function buildToolCallIndex(msgs) {
746
+ const idx = new Map;
747
+ for (let i = 0;i < msgs.length; i++) {
748
+ const m = msgs[i];
749
+ if (m.role !== "assistant")
750
+ continue;
751
+ const blocks = Array.isArray(m.content) ? m.content : [];
752
+ for (const b of blocks) {
753
+ if (!isToolCallBlock(b))
754
+ continue;
755
+ if (b.id) {
756
+ idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
757
+ }
758
+ if (b.name === "multi_tool_use.parallel" && Array.isArray(b.arguments?.tool_uses)) {
759
+ const nested = flattenToolCallBlock(b);
760
+ for (let t = 0;t < nested.length; t++) {
761
+ const tool = nested[t];
762
+ const id = nestedToolCallId(b.id, i, t, tool.id);
763
+ idx.set(id, { name: tool.name, arguments: tool.arguments, msgIndex: i });
764
+ }
765
+ }
766
+ }
767
+ }
768
+ return idx;
769
+ }
599
770
  var CONSTRAINT_PATTERNS = [
600
771
  { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
601
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 },
@@ -725,30 +896,41 @@ function mergeFindings(target, findings) {
725
896
  for (const finding of findings)
726
897
  target.set(finding.kind, (target.get(finding.kind) ?? 0) + finding.count);
727
898
  }
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
- ]);
899
+ var SECRET_KEY_NAMES = {
900
+ api_key: true,
901
+ apikey: true,
902
+ access_token: true,
903
+ auth_token: true,
904
+ authorization: true,
905
+ password: true,
906
+ passwd: true,
907
+ secret: true,
908
+ secret_key: true,
909
+ secret_access_key: true,
910
+ client_secret: true,
911
+ private_key: true,
912
+ database_url: true,
913
+ connection_string: true,
914
+ token: true,
915
+ refresh_token: true,
916
+ session_token: true,
917
+ credential: true,
918
+ credentials: true,
919
+ cookie: true,
920
+ set_cookie: true,
921
+ otp: true,
922
+ one_time_password: true,
923
+ pin: true,
924
+ passcode: true
925
+ };
744
926
  function normalizeObjectKey(key) {
745
927
  return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
746
928
  }
747
929
  function isSecretBearingKey(key) {
748
930
  const normalized = normalizeObjectKey(key);
749
- if (SECRET_KEY_NAMES.has(normalized))
931
+ if (SECRET_KEY_NAMES[normalized])
750
932
  return true;
751
- return /(?:^|_)(?:api_key|access_token|auth_token|password|passwd|secret_access_key|client_secret|private_key)(?:_|$)/.test(normalized);
933
+ return /(?:^|_)(?:api_key|access_token|auth_token|password|passwd|secret_access_key|client_secret|private_key|refresh_token|session_token|one_time_password|passcode)(?:_|$)/.test(normalized);
752
934
  }
753
935
 
754
936
  class SecretScrubber {
@@ -804,7 +986,8 @@ class SecretScrubber {
804
986
  const output = {};
805
987
  seen.set(value2, output);
806
988
  for (const [key, item] of Object.entries(value2)) {
807
- if (this.secretsEnabled && isSecretBearingKey(key) && typeof item === "string" && item.length > 0) {
989
+ const carriesSecret = typeof item === "string" ? item.length > 0 : item != null;
990
+ if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
808
991
  output[key] = "[REDACTED:credential]";
809
992
  recordCredential();
810
993
  } else {
@@ -1069,8 +1252,8 @@ function readMetricsLog(limit = 100) {
1069
1252
  const fd = fs2.openSync(logPath, "r");
1070
1253
  try {
1071
1254
  const buf = Buffer.alloc(wantBytes);
1072
- fs2.readSync(fd, buf, 0, wantBytes, startPos);
1073
- let text = buf.toString("utf8");
1255
+ const bytesRead = fs2.readSync(fd, buf, 0, wantBytes, startPos);
1256
+ let text = buf.subarray(0, bytesRead).toString("utf8");
1074
1257
  if (startPos > 0) {
1075
1258
  const nl = text.indexOf(`
1076
1259
  `);
@@ -1117,29 +1300,56 @@ function classifyOutcomeClaim(claim) {
1117
1300
  return "file";
1118
1301
  return "generic";
1119
1302
  }
1120
- function successfulToolSupportsClaim(claim, messages, extraction) {
1303
+ var successfulToolEvidenceCache = new WeakMap;
1304
+ function successfulToolEvidence(messages) {
1305
+ const cached = successfulToolEvidenceCache.get(messages);
1306
+ if (cached)
1307
+ return cached;
1308
+ const toolCalls = buildToolCallIndex(messages);
1309
+ const evidence = [];
1310
+ for (const message of messages) {
1311
+ if (message.role !== "toolResult" || message.isError)
1312
+ continue;
1313
+ const call = toolCalls.get(message.toolCallId ?? "");
1314
+ if (!call)
1315
+ continue;
1316
+ const result = extractText(message.content).slice(0, 8000);
1317
+ if (!result.trim() || LIKELY_ERROR_RE.test(result))
1318
+ continue;
1319
+ const command = [call.arguments.command, call.arguments.cmd, call.arguments.script].find((value) => typeof value === "string") ?? "";
1320
+ evidence.push({
1321
+ name: normalizeToolName(call.name),
1322
+ operation: classifyToolOperation(call.arguments, call.name),
1323
+ command,
1324
+ path: extractToolPath(call.arguments),
1325
+ result
1326
+ });
1327
+ }
1328
+ successfulToolEvidenceCache.set(messages, evidence);
1329
+ return evidence;
1330
+ }
1331
+ function successfulToolSupportsClaim(claim, tools, extraction) {
1121
1332
  const shape = semanticShape(claim);
1122
1333
  const category = classifyOutcomeClaim(claim);
1123
1334
  if (category === "error" && extraction.errors.some((error) => error.resolved && hasSemanticEvidence(claim, error.message)))
1124
1335
  return true;
1125
1336
  if (category === "file" && extraction.modifiedFiles.some((file) => claim.toLowerCase().includes(file.path.toLowerCase())))
1126
1337
  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))
1338
+ for (const tool of tools) {
1339
+ const operationText = tool.name + " " + tool.command;
1340
+ const operationSupports = category === "test" ? /\b(?:test|tests|pytest|jest|vitest|mocha|rspec)\b/i.test(operationText) : category === "build" ? /\b(?:build|compile|typecheck|tsc|check)\b/i.test(operationText) : category === "release" ? /\b(?:deploy|publish|release)\b/i.test(operationText) : category === "file" ? tool.operation === "mutate" || tool.operation === "delete" : category === "error" ? tool.operation === "execute" || tool.operation === "mutate" || tool.operation === "delete" : tool.operation !== "read" && tool.operation !== "search" && tool.operation !== "list";
1341
+ if (!operationSupports)
1132
1342
  continue;
1133
- if (hasSemanticEvidence(claim, bounded))
1343
+ if (hasSemanticEvidence(claim, tool.result))
1134
1344
  return true;
1135
- const lower = bounded.toLowerCase();
1345
+ const lower = tool.result.toLowerCase();
1136
1346
  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
1347
  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))
1348
+ if (category === "build" && /\b(?:succeeded|successful|passed|exit(?:ed)?\s+(?:code\s+)?0)\b/.test(lower))
1139
1349
  return true;
1140
- if (category === "release" && /\b(?:publish|release|deploy)\b/.test(lower) && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
1350
+ if (category === "release" && /\b(?:succeeded|successful|completed|published|deployed|released)\b/.test(lower))
1141
1351
  return true;
1142
- if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, bounded))
1352
+ if (category === "error" && shape.concepts.length > 0 && /\b(?:fixed|resolved|passed|succeeded|successful)\b/.test(lower) && hasSemanticEvidence(claim, tool.result))
1143
1353
  return true;
1144
1354
  }
1145
1355
  return false;
@@ -1224,9 +1434,15 @@ function stemToken(token) {
1224
1434
  function semanticTokens(text) {
1225
1435
  return (text.normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2);
1226
1436
  }
1227
- function evidenceFragments(text) {
1228
- const fragments = text.split(/\r?\n/).flatMap((line) => [line, ...line.split(/[.;]/)]).map((part) => part.replace(/^\s*[-*\d.)]+\s*/, "").trim()).filter(Boolean);
1229
- return Array.from(new Set(fragments));
1437
+ var semanticShapeCache = new Map;
1438
+ var semanticFragmentCache = new Map;
1439
+ function semanticFragments(text) {
1440
+ const cached = lruGet(semanticFragmentCache, text);
1441
+ if (cached)
1442
+ return cached;
1443
+ const fragments = Array.from(new Set(text.split(/\r?\n/).flatMap((line) => [line, ...line.split(/[.;]/)]).map((part) => part.replace(/^\s*[-*\d.)]+\s*/, "").trim()).filter(Boolean))).map(semanticTokens);
1444
+ lruSet(semanticFragmentCache, text, fragments, 256);
1445
+ return fragments;
1230
1446
  }
1231
1447
  function hasNearbyMarker(tokens, anchor, markers) {
1232
1448
  return tokens.some((token, index) => token === anchor && tokens.slice(Math.max(0, index - 2), index + 3).some((near) => markers.has(near)));
@@ -1250,20 +1466,24 @@ function hasEffectiveTargetNegation(tokens, anchor) {
1250
1466
  });
1251
1467
  }
1252
1468
  function semanticShape(source) {
1469
+ const cached = lruGet(semanticShapeCache, source);
1470
+ if (cached)
1471
+ return cached;
1253
1472
  const sourceTokens = semanticTokens(source);
1254
1473
  const concepts = Array.from(new Set(sourceTokens.filter((token) => !/^\d+$/.test(token) && !SEMANTIC_STOP.has(token) && !NEGATION_MARKERS.has(token) && !CONDITION_MARKERS.has(token))));
1255
1474
  const negative = sourceTokens.some((token) => NEGATION_MARKERS.has(token));
1256
1475
  const conditional = sourceTokens.some((token) => CONDITION_MARKERS.has(token));
1257
1476
  const anchor = concepts.find((concept) => hasNearbyMarker(sourceTokens, concept, NEGATION_MARKERS)) ?? concepts[0] ?? "";
1258
- return { sourceTokens, concepts, anchor, negative, conditional };
1477
+ const shape = { sourceTokens, concepts, anchor, negative, conditional };
1478
+ lruSet(semanticShapeCache, source, shape, 512);
1479
+ return shape;
1259
1480
  }
1260
1481
  function hasSemanticEvidence(source, target) {
1261
1482
  const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
1262
1483
  if (!concepts.length)
1263
1484
  return true;
1264
1485
  const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
1265
- return evidenceFragments(target).some((fragment) => {
1266
- const tokens = semanticTokens(fragment);
1486
+ return semanticFragments(target).some((tokens) => {
1267
1487
  const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
1268
1488
  if (overlap < required)
1269
1489
  return false;
@@ -1285,8 +1505,7 @@ function hasSemanticContradiction(source, target) {
1285
1505
  if (!anchor)
1286
1506
  return false;
1287
1507
  const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
1288
- return evidenceFragments(target).some((fragment) => {
1289
- const tokens = semanticTokens(fragment);
1508
+ return semanticFragments(target).some((tokens) => {
1290
1509
  if (!tokens.includes(anchor))
1291
1510
  return false;
1292
1511
  const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
@@ -1448,16 +1667,18 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1448
1667
  gaps.push({ kind: "inconsistency", detail: "blocked-none: Blocked says none despite unresolved errors" });
1449
1668
  score -= 12;
1450
1669
  }
1670
+ const doneRefs = new Set(extractFileRefs(doneSection).map(normalizePath));
1451
1671
  for (const file of extraction.modifiedFiles) {
1452
- const basename = file.path.split("/").pop() ?? "";
1453
- if (!doneSection.toLowerCase().includes(basename.toLowerCase()))
1672
+ const uniqueNeedles = buildUniquePathNeedles(file.path, modifiedPaths);
1673
+ if (!uniqueNeedles.some((needle) => doneRefs.has(normalizePath(needle))))
1454
1674
  continue;
1455
1675
  const unresolved = unresolvedEvidence.find((error) => {
1456
1676
  const firstLine = error.message.split(/\r?\n/, 1)[0] ?? "";
1457
- return extractFileRefs(firstLine).some((ref) => isKnownPathReference(ref, [file.path]));
1677
+ const errorRefs = extractFileRefs(firstLine).map(normalizePath);
1678
+ return uniqueNeedles.some((needle) => errorRefs.includes(normalizePath(needle)));
1458
1679
  });
1459
1680
  if (unresolved) {
1460
- gaps.push({ kind: "inconsistency", detail: basename + " marked Done but has unresolved error" });
1681
+ gaps.push({ kind: "inconsistency", detail: file.path + " marked Done but has unresolved error" });
1461
1682
  score -= 5;
1462
1683
  }
1463
1684
  }
@@ -1479,8 +1700,9 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
1479
1700
  score -= 5;
1480
1701
  }
1481
1702
  if (evidence.sourceMessages) {
1703
+ const tools = successfulToolEvidence(evidence.sourceMessages);
1482
1704
  for (const claim of outcomeClaims(summary)) {
1483
- if (!successfulToolSupportsClaim(claim, evidence.sourceMessages, extraction)) {
1705
+ if (!successfulToolSupportsClaim(claim, tools, extraction)) {
1484
1706
  gaps.push({ kind: "unsupported-claim", claim });
1485
1707
  score -= 20;
1486
1708
  }