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