@agwab/pi-workflow 0.2.1 → 0.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.
Files changed (70) hide show
  1. package/dist/compiler.js +6 -8
  2. package/dist/dynamic-decision.d.ts +0 -1
  3. package/dist/dynamic-decision.js +0 -7
  4. package/dist/dynamic-profiles.d.ts +0 -1
  5. package/dist/dynamic-profiles.js +0 -3
  6. package/dist/engine-run-graph.d.ts +1 -0
  7. package/dist/engine-run-graph.js +142 -2
  8. package/dist/engine.d.ts +5 -0
  9. package/dist/engine.js +112 -27
  10. package/dist/extension.d.ts +2 -1
  11. package/dist/extension.js +27 -6
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +2 -1
  14. package/dist/store.js +55 -11
  15. package/dist/subagent-backend.js +155 -29
  16. package/dist/types.d.ts +6 -0
  17. package/dist/workflow-runtime.js +10 -1
  18. package/dist/workflow-view.js +3 -1
  19. package/dist/workflow-web-source-extension.js +167 -48
  20. package/dist/workflow-web-source.d.ts +2 -1
  21. package/dist/workflow-web-source.js +84 -19
  22. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  23. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  24. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  25. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  26. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  27. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  28. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  29. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  30. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  31. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  32. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  33. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  35. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  36. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  37. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  38. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  39. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  40. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  41. package/package.json +2 -2
  42. package/src/compiler.ts +14 -9
  43. package/src/dynamic-decision.ts +0 -11
  44. package/src/dynamic-profiles.ts +0 -4
  45. package/src/engine-run-graph.ts +185 -2
  46. package/src/engine.ts +145 -24
  47. package/src/extension.ts +33 -4
  48. package/src/index.ts +3 -1
  49. package/src/store.ts +74 -11
  50. package/src/subagent-backend.ts +201 -28
  51. package/src/types.ts +6 -0
  52. package/src/workflow-runtime.ts +18 -2
  53. package/src/workflow-view.ts +2 -1
  54. package/src/workflow-web-source-extension.ts +621 -228
  55. package/src/workflow-web-source.ts +118 -28
  56. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  57. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  58. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  59. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  60. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  61. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  62. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  63. package/workflows/impact-review/spec.json +3 -3
  64. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  65. package/dist/dynamic-loader.d.ts +0 -25
  66. package/dist/dynamic-loader.js +0 -13
  67. package/src/dynamic-loader.ts +0 -49
  68. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  69. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  70. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -98,7 +98,7 @@ export interface WorkflowWebSourceReadRequest {
98
98
  }
99
99
 
100
100
  export interface WorkflowWebSourceReadResult {
101
- status: "matched" | "not_found";
101
+ status: "matched" | "truncated" | "not_found";
102
102
  matchType?: "exact" | "normalized" | "terms";
103
103
  quote?: string;
104
104
  startOffset?: number;
@@ -108,6 +108,7 @@ export interface WorkflowWebSourceReadResult {
108
108
  missingTerms?: string[];
109
109
  coverageRatio?: number;
110
110
  candidateOnly?: boolean;
111
+ truncated?: boolean;
111
112
  }
112
113
 
113
114
  export interface WorkflowWebSourceCard {
@@ -791,6 +792,8 @@ function snippetForTerms(options: {
791
792
  const candidates: Array<{
792
793
  start: number;
793
794
  end: number;
795
+ anchorStart: number;
796
+ anchorEnd: number;
794
797
  matchedTerms: string[];
795
798
  missingTerms: string[];
796
799
  score: number;
@@ -822,23 +825,34 @@ function snippetForTerms(options: {
822
825
  if (right.score !== left.score) return right.score - left.score;
823
826
  return right.matchedTerms.length - left.matchedTerms.length;
824
827
  })[0]!;
825
- const raw = redactInlineSecrets(options.text.slice(best.start, best.end));
826
- const consumed = consumeWorkflowWebVisibleBudget(
827
- options.budget,
828
- raw,
829
- options.maxChars,
830
- );
828
+ const consumed = consumeAnchoredSnippet({
829
+ text: options.text,
830
+ anchorStart: best.anchorStart,
831
+ anchorEnd: best.anchorEnd,
832
+ maxChars: options.maxChars,
833
+ budget: options.budget,
834
+ });
835
+ const returnedWindowNorm = normalizeForSearch(
836
+ options.text.slice(consumed.sourceStart, consumed.sourceEnd),
837
+ ).normalized;
838
+ const matchedTerms = needles
839
+ .filter((term) => returnedWindowNorm.includes(term.normalized))
840
+ .map((term) => term.raw);
841
+ const missingTerms = needles
842
+ .filter((term) => !returnedWindowNorm.includes(term.normalized))
843
+ .map((term) => term.raw);
831
844
  return {
832
- status: "matched",
845
+ status: consumed.status,
833
846
  matchType: "terms",
834
- quote: consumed.text,
835
- startOffset: best.start,
836
- endOffset: best.end,
837
- visibleChars: consumed.text.length,
838
- matchedTerms: best.matchedTerms,
839
- missingTerms: best.missingTerms,
840
- coverageRatio: best.matchedTerms.length / Math.max(1, needles.length),
847
+ quote: consumed.quote || undefined,
848
+ startOffset: consumed.sourceStart,
849
+ endOffset: consumed.sourceEnd,
850
+ visibleChars: consumed.visibleChars,
851
+ matchedTerms,
852
+ missingTerms,
853
+ coverageRatio: matchedTerms.length / Math.max(1, needles.length),
841
854
  candidateOnly: true,
855
+ truncated: consumed.truncated || undefined,
842
856
  };
843
857
  }
844
858
 
@@ -854,6 +868,8 @@ function scoreTermWindow(
854
868
  matchedTerms: string[];
855
869
  missingTerms: string[];
856
870
  score: number;
871
+ anchorStart: number;
872
+ anchorEnd: number;
857
873
  } {
858
874
  const center = Math.floor((matchStart + matchEnd) / 2);
859
875
  const start = Math.max(0, center - Math.floor(maxChars / 2));
@@ -874,6 +890,8 @@ function scoreTermWindow(
874
890
  return {
875
891
  start,
876
892
  end,
893
+ anchorStart: matchStart,
894
+ anchorEnd: matchEnd,
877
895
  matchedTerms,
878
896
  missingTerms,
879
897
  score: matchedTerms.length * 1_000 + occurrenceScore,
@@ -963,27 +981,99 @@ function snippetForMatch(options: {
963
981
  maxChars: number;
964
982
  budget: WorkflowWebVisibleBudget;
965
983
  }): WorkflowWebSourceReadResult {
966
- const matchLength = Math.max(0, options.end - options.start);
967
- const slack = Math.max(0, options.maxChars - matchLength);
968
- const before = Math.floor(slack / 2);
969
- const snippetStart = Math.max(0, options.start - before);
970
- const snippetEnd = Math.min(
971
- options.text.length,
972
- snippetStart + options.maxChars,
984
+ const consumed = consumeAnchoredSnippet({
985
+ text: options.text,
986
+ anchorStart: options.start,
987
+ anchorEnd: options.end,
988
+ maxChars: options.maxChars,
989
+ budget: options.budget,
990
+ });
991
+ return {
992
+ status: consumed.status,
993
+ matchType: options.matchType,
994
+ quote: consumed.quote || undefined,
995
+ startOffset: options.start,
996
+ endOffset: options.end,
997
+ visibleChars: consumed.visibleChars,
998
+ truncated: consumed.truncated || undefined,
999
+ };
1000
+ }
1001
+
1002
+ type AnchoredSnippetResult = {
1003
+ status: "matched" | "truncated";
1004
+ quote: string;
1005
+ visibleChars: number;
1006
+ sourceStart: number;
1007
+ sourceEnd: number;
1008
+ truncated: boolean;
1009
+ };
1010
+
1011
+ function consumeAnchoredSnippet(options: {
1012
+ text: string;
1013
+ anchorStart: number;
1014
+ anchorEnd: number;
1015
+ maxChars: number;
1016
+ budget: WorkflowWebVisibleBudget;
1017
+ }): AnchoredSnippetResult {
1018
+ const maxChars = Math.max(0, Math.floor(options.maxChars));
1019
+ const remainingBefore = Math.max(
1020
+ 0,
1021
+ options.budget.limit - options.budget.used,
1022
+ );
1023
+ const visibleLimit = Math.max(0, Math.min(maxChars, remainingBefore));
1024
+ const anchorStart = Math.max(
1025
+ 0,
1026
+ Math.min(options.text.length, Math.floor(options.anchorStart)),
1027
+ );
1028
+ const anchorEnd = Math.max(
1029
+ anchorStart,
1030
+ Math.min(options.text.length, Math.floor(options.anchorEnd)),
973
1031
  );
974
- const raw = redactInlineSecrets(options.text.slice(snippetStart, snippetEnd));
1032
+ const anchorLength = Math.max(0, anchorEnd - anchorStart);
1033
+ if (visibleLimit <= 0) {
1034
+ return {
1035
+ status: "truncated",
1036
+ quote: "",
1037
+ visibleChars: 0,
1038
+ sourceStart: anchorStart,
1039
+ sourceEnd: anchorStart,
1040
+ truncated: true,
1041
+ };
1042
+ }
1043
+
1044
+ let sourceStart: number;
1045
+ let sourceEnd: number;
1046
+ let status: "matched" | "truncated" = "matched";
1047
+ if (anchorLength > visibleLimit) {
1048
+ sourceStart = anchorStart;
1049
+ sourceEnd = Math.min(options.text.length, sourceStart + visibleLimit);
1050
+ status = "truncated";
1051
+ } else {
1052
+ const slack = Math.max(0, visibleLimit - anchorLength);
1053
+ sourceStart = Math.max(0, anchorStart - Math.floor(slack / 2));
1054
+ sourceEnd = Math.min(options.text.length, sourceStart + visibleLimit);
1055
+ if (sourceEnd < anchorEnd) {
1056
+ sourceEnd = anchorEnd;
1057
+ sourceStart = Math.max(0, sourceEnd - visibleLimit);
1058
+ } else if (sourceEnd === options.text.length) {
1059
+ sourceStart = Math.max(0, sourceEnd - visibleLimit);
1060
+ }
1061
+ }
1062
+
1063
+ const raw = redactInlineSecrets(options.text.slice(sourceStart, sourceEnd));
975
1064
  const consumed = consumeWorkflowWebVisibleBudget(
976
1065
  options.budget,
977
1066
  raw,
978
- options.maxChars,
1067
+ visibleLimit,
979
1068
  );
1069
+ const truncated = status === "truncated" || consumed.truncated;
980
1070
  return {
981
- status: "matched",
982
- matchType: options.matchType,
1071
+ status,
983
1072
  quote: consumed.text,
984
- startOffset: options.start,
985
- endOffset: options.end,
986
1073
  visibleChars: consumed.text.length,
1074
+ sourceStart,
1075
+ sourceEnd,
1076
+ truncated,
987
1077
  };
988
1078
  }
989
1079
 
@@ -82,6 +82,38 @@ function collectEvidenceRefs(claim) {
82
82
  return refs;
83
83
  }
84
84
 
85
+ function addLocalEvidenceRef(refs, value) {
86
+ if (typeof value !== "string") return;
87
+ const text = value.trim();
88
+ if (!text || /^https?:\/\//i.test(text) || isWorkflowSourceRef(text)) return;
89
+ if (looksLikeLocalSourceRef(text)) refs.add(text);
90
+ }
91
+
92
+ function collectLocalEvidenceRefs(claim) {
93
+ const refs = new Set();
94
+ if (!claim || typeof claim !== "object") return refs;
95
+ for (const key of ["file", "path", "repoPath", "localPath", "sourceRef"]) {
96
+ addLocalEvidenceRef(refs, claim[key]);
97
+ }
98
+ for (const value of Array.isArray(claim.sourceRefs) ? claim.sourceRefs : []) {
99
+ addLocalEvidenceRef(refs, value);
100
+ }
101
+ for (const row of Array.isArray(claim.evidence) ? claim.evidence : []) {
102
+ if (!row || typeof row !== "object") continue;
103
+ for (const key of [
104
+ "file",
105
+ "path",
106
+ "repoPath",
107
+ "localPath",
108
+ "source",
109
+ "sourceRef",
110
+ ]) {
111
+ addLocalEvidenceRef(refs, row[key]);
112
+ }
113
+ }
114
+ return refs;
115
+ }
116
+
85
117
  function collectWorkflowSourceRefs(value, refs = new Set()) {
86
118
  if (typeof value === "string") {
87
119
  for (const match of value.matchAll(/\bwsrc_[a-f0-9]{32}\b/g))
@@ -127,9 +159,11 @@ function canonicalUrlKeys(value) {
127
159
  url.hash = "";
128
160
  const serialized = stripCitationUrlPunctuation(url.toString());
129
161
  keys.add(serialized);
162
+ addNpmDocsVersionAgnosticKey(keys, url);
130
163
  if (url.pathname !== "/" && url.pathname.endsWith("/")) {
131
164
  url.pathname = url.pathname.replace(/\/+$/u, "");
132
165
  keys.add(stripCitationUrlPunctuation(url.toString()));
166
+ addNpmDocsVersionAgnosticKey(keys, url);
133
167
  }
134
168
  } catch {
135
169
  // Keep the trimmed raw URL key only; malformed strings should not throw from
@@ -138,6 +172,17 @@ function canonicalUrlKeys(value) {
138
172
  return [...keys].filter(Boolean);
139
173
  }
140
174
 
175
+ function addNpmDocsVersionAgnosticKey(keys, url) {
176
+ if (url.hostname !== "docs.npmjs.com") return;
177
+ if (!/^\/cli\/(?:v\d+\/)?using-npm\//u.test(url.pathname)) return;
178
+ const versionless = new URL(url.toString());
179
+ versionless.pathname = versionless.pathname.replace(
180
+ /^\/cli\/v\d+\//u,
181
+ "/cli/",
182
+ );
183
+ keys.add(stripCitationUrlPunctuation(versionless.toString()));
184
+ }
185
+
141
186
  function addUrlSourceRef(urlToSourceRef, url, sourceRef) {
142
187
  if (!isWorkflowSourceRef(sourceRef)) return;
143
188
  for (const key of canonicalUrlKeys(url)) {
@@ -152,13 +197,6 @@ function buildUrlSourceRefLookup(normalizeInputPacket) {
152
197
  if (!source || typeof source !== "object") continue;
153
198
  addUrlSourceRef(urlToSourceRef, source.url, source.sourceRef);
154
199
  }
155
- const sourceRefIndex = asArray(
156
- normalizeInputPacket?.packet?.research?.sourceRefIndex,
157
- );
158
- for (const source of sourceRefIndex) {
159
- if (!source || typeof source !== "object") continue;
160
- addUrlSourceRef(urlToSourceRef, source.url, source.sourceRef);
161
- }
162
200
  return urlToSourceRef;
163
201
  }
164
202
 
@@ -241,19 +279,19 @@ function strongEvidenceIssue(claim) {
241
279
 
242
280
  function hasExactQuantitativeClaim(value) {
243
281
  const text = JSON.stringify(value ?? "");
244
- return /\b\d+(?:\.\d+)?\s*(?:%|percent|ms|s|sec|seconds|minutes|hours|x|×|usd|\$|k|m|b|tokens?|users?|samples?|n\s*=)\b/i.test(
282
+ return /\b\d+(?:\.\d+)?\s*(?:(?:%|×|\$|n\s*=)|(?:percent|ms|s|sec|seconds|minutes|hours|x|usd|k|m|b|tokens?|users?|samples?)\b)/i.test(
245
283
  text,
246
284
  );
247
285
  }
248
286
 
249
287
  function verdictOf(claim) {
250
- return (
288
+ const status =
251
289
  claim?.status ??
252
290
  claim?.verdict ??
253
291
  claim?.verdictDigest?.status ??
254
292
  claim?.verdictDigest?.verdict ??
255
- "unverified"
256
- );
293
+ "unverified";
294
+ return canonicalVerifierStatus(status);
257
295
  }
258
296
 
259
297
  function withVerdict(claim, verdict, reason, details = {}) {
@@ -416,9 +454,7 @@ function findSource(sources, stageId) {
416
454
  export default async function claimEvidenceGate({ sources, options = {} }) {
417
455
  const plan = findSource(sources, "plan");
418
456
  const normalizeClaims = findSource(sources, "normalize-claims");
419
- const sanitizedCandidates =
420
- findSource(sources, "sanitize-claims") ??
421
- findSource(sources, "sanitize-verification-candidates");
457
+ const sanitizedCandidates = findSource(sources, "sanitize-claims");
422
458
  const normalized = sanitizedCandidates ?? normalizeClaims;
423
459
  const normalizeInputPacket = findSource(sources, "normalize-input-packet");
424
460
  const urlToSourceRef = buildUrlSourceRefLookup(normalizeInputPacket);
@@ -474,8 +510,7 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
474
510
  !specId.startsWith("plan") &&
475
511
  !specId.startsWith("normalize-claims") &&
476
512
  !specId.startsWith("normalize-input-packet") &&
477
- !specId.startsWith("sanitize-claims") &&
478
- !specId.startsWith("sanitize-verification-candidates"),
513
+ !specId.startsWith("sanitize-claims"),
479
514
  )
480
515
  .flatMap(([sourceId, source]) =>
481
516
  asArray(source).map((claim, index) => ({ sourceId, claim, index })),
@@ -558,6 +593,10 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
558
593
  if (!claim || typeof claim !== "object") return;
559
594
  gateSummary.total += 1;
560
595
  const evidenceRefs = [...collectEvidenceRefs(claim)];
596
+ const localEvidenceRefs = new Set([
597
+ ...collectLocalEvidenceRefs(claim),
598
+ ...collectLocalEvidenceRefs(candidate),
599
+ ]);
561
600
  const workflowSourceRefs = new Set([...collectWorkflowSourceRefs(claim)]);
562
601
  const exactQuantitative = hasExactQuantitativeClaim(claim);
563
602
  const fetched = hasFetchedEvidence(claim);
@@ -627,6 +666,7 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
627
666
  claimId &&
628
667
  candidate &&
629
668
  workflowSourceRefs.size === 0 &&
669
+ localEvidenceRefs.size === 0 &&
630
670
  httpSourceUrls.length > 0
631
671
  ) {
632
672
  const failure = {
@@ -252,10 +252,7 @@ function buildSynthesisInput({
252
252
  export default async function finalAuditPacket({ sources }) {
253
253
  const plan = asObject(findSource(sources, "plan"));
254
254
  const normalizeClaims = asObject(findSource(sources, "normalize-claims"));
255
- const sanitizedCandidates = asObject(
256
- findSource(sources, "sanitize-claims") ??
257
- findSource(sources, "sanitize-verification-candidates"),
258
- );
255
+ const sanitizedCandidates = asObject(findSource(sources, "sanitize-claims"));
259
256
  const normalized =
260
257
  Object.keys(sanitizedCandidates).length > 0
261
258
  ? sanitizedCandidates
@@ -221,7 +221,7 @@ function isRequiredOrCriticalSlot(slot) {
221
221
  }
222
222
 
223
223
  function looksQuantitative(text) {
224
- return /\b\d+(?:\.\d+)?\s*(?:%|percent|ms|s|sec|seconds|minutes|hours|x|×|usd|\$|k|m|b|tokens?|users?|samples?|n\s*=|gb|mb|tb|requests?|qps|rps|per\s+month|\/month)\b/i.test(
224
+ return /\b\d+(?:\.\d+)?\s*(?:(?:%|×|\$|n\s*=)|(?:percent|ms|s|sec|seconds|minutes|hours|x|usd|k|m|b|tokens?|users?|samples?|gb|mb|tb|requests?|qps|rps|per\s+month|\/month)\b)/i.test(
225
225
  String(text ?? ""),
226
226
  );
227
227
  }
@@ -468,7 +468,14 @@ function packetGapRows(packet) {
468
468
  kind: "Coverage gap",
469
469
  ...gap,
470
470
  }));
471
- return [...remaining, ...coverage];
471
+ const sourceRefJoinFailures = asArray(packet?.sourceRefJoinFailures).map(
472
+ (gap, index) => ({
473
+ id: gapIdOf(gap) || numberedId("gap-source-ref", index),
474
+ kind: "Source reference gap",
475
+ ...gap,
476
+ }),
477
+ );
478
+ return [...remaining, ...coverage, ...sourceRefJoinFailures];
472
479
  }
473
480
 
474
481
  function rowsForIds(ids, rowById, warnings, label) {
@@ -987,11 +994,7 @@ function renderAuditSummary(report, claimSummary, slots) {
987
994
 
988
995
  function renderWarnings(sectionCounts) {
989
996
  const checks = [
990
- ["findings", "renderedFindings", "findings"],
991
- ["recommendations", "renderedRecommendations", "recommendations"],
992
- ["actionItems", "renderedActionItems", "action items"],
993
997
  ["caveatsAndGaps", "renderedCaveatsAndGaps", "caveats/gaps"],
994
- ["factSlots", "renderedFactSlots", "fact slots"],
995
998
  ["sourceUrls", "renderedSourceUrls", "source URLs"],
996
999
  ];
997
1000
  return checks
@@ -1261,15 +1264,6 @@ export default async function renderExecutive({
1261
1264
  maxUrls: Number.isFinite(Number(options.maxUrls))
1262
1265
  ? Math.max(0, Number(options.maxUrls))
1263
1266
  : Infinity,
1264
- maxFindings: Number.isFinite(Number(options.maxFindings))
1265
- ? Math.max(0, Number(options.maxFindings))
1266
- : undefined,
1267
- maxRecommendations: Number.isFinite(Number(options.maxRecommendations))
1268
- ? Math.max(0, Number(options.maxRecommendations))
1269
- : undefined,
1270
- maxGaps: Number.isFinite(Number(options.maxGaps))
1271
- ? Math.max(0, Number(options.maxGaps))
1272
- : undefined,
1273
1267
  };
1274
1268
  const rendered = renderResearchMarkdown(control, auditPacket, opts);
1275
1269
  let markdown = rendered.markdown;
@@ -1296,7 +1290,6 @@ export default async function renderExecutive({
1296
1290
  !serializationArtifact;
1297
1291
 
1298
1292
  let executiveSidecarPath;
1299
- let reportSidecarPath;
1300
1293
  let auditSidecarPath;
1301
1294
  try {
1302
1295
  if (context.cwd && context.runId && context.taskId) {
@@ -1310,10 +1303,8 @@ export default async function renderExecutive({
1310
1303
  );
1311
1304
  await mkdir(taskDir, { recursive: true });
1312
1305
  executiveSidecarPath = join(taskDir, "executive.md");
1313
- reportSidecarPath = join(taskDir, "report.md");
1314
1306
  auditSidecarPath = join(taskDir, "audit.md");
1315
1307
  await writeFile(executiveSidecarPath, `${markdown}\n`, "utf8");
1316
- await writeFile(reportSidecarPath, `${markdown}\n`, "utf8");
1317
1308
  await writeFile(auditSidecarPath, `${auditMarkdown}\n`, "utf8");
1318
1309
  }
1319
1310
  } catch {
@@ -1344,9 +1335,6 @@ export default async function renderExecutive({
1344
1335
  renderedAllStructuredItems,
1345
1336
  maxWords: Number.isFinite(opts.maxWords) ? opts.maxWords : null,
1346
1337
  maxUrls: Number.isFinite(opts.maxUrls) ? opts.maxUrls : null,
1347
- maxFindings: opts.maxFindings,
1348
- maxRecommendations: opts.maxRecommendations,
1349
- maxGaps: opts.maxGaps,
1350
1338
  truncated,
1351
1339
  truncatedWithOpenGaps,
1352
1340
  serializationArtifact,
@@ -1354,7 +1342,6 @@ export default async function renderExecutive({
1354
1342
  },
1355
1343
  auditArtifact: auditSidecarPath ? "audit.md" : "final-audit.control.json",
1356
1344
  ...(executiveSidecarPath ? { sidecarPath: "executive.md" } : {}),
1357
- ...(reportSidecarPath ? { reportSidecarPath: "report.md" } : {}),
1358
1345
  ...(auditSidecarPath ? { auditSidecarPath: "audit.md" } : {}),
1359
1346
  };
1360
1347
  }
@@ -89,12 +89,55 @@ function matchesAny(text, patterns) {
89
89
  return patterns.some((pattern) => pattern.test(text));
90
90
  }
91
91
 
92
+ const WORD_TOKEN_RE =
93
+ /[\p{Letter}\p{Number}][\p{Letter}\p{Number}\p{Mark}_-]*/gu;
94
+ const ASCII_TOKEN_RE = /^[a-z0-9][a-z0-9_-]{2,}$/iu;
95
+ const ASCII_RUN_RE = /[a-z0-9][a-z0-9_-]{2,}/giu;
96
+ const CJK_RUN_RE =
97
+ /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
98
+ const ASCII_CHAR_RE = /[a-z0-9]/iu;
99
+
100
+ function addCjkRunTokens(tokens, run) {
101
+ const chars = [...run];
102
+ if (chars.length === 0) return;
103
+ if (chars.length === 1) {
104
+ tokens.add(chars[0]);
105
+ return;
106
+ }
107
+ tokens.add(run);
108
+ for (const size of [2, 3]) {
109
+ if (chars.length < size) continue;
110
+ for (let index = 0; index <= chars.length - size; index += 1) {
111
+ tokens.add(chars.slice(index, index + size).join(""));
112
+ }
113
+ }
114
+ }
115
+
92
116
  function tokenSet(value) {
93
- return new Set(
94
- String(value ?? "")
95
- .toLowerCase()
96
- .match(/[a-z0-9][a-z0-9_-]{2,}/g) ?? [],
97
- );
117
+ const tokens = new Set();
118
+ const normalized = String(value ?? "")
119
+ .normalize("NFKC")
120
+ .toLocaleLowerCase();
121
+ for (const match of normalized.matchAll(WORD_TOKEN_RE)) {
122
+ const token = match[0].replace(/^[_-]+|[_-]+$/gu, "");
123
+ if (!token) continue;
124
+ for (const asciiMatch of token.matchAll(ASCII_RUN_RE)) {
125
+ tokens.add(asciiMatch[0]);
126
+ }
127
+ if (ASCII_TOKEN_RE.test(token)) continue;
128
+ let cjkMatched = false;
129
+ for (const cjkMatch of token.matchAll(CJK_RUN_RE)) {
130
+ cjkMatched = true;
131
+ addCjkRunTokens(tokens, cjkMatch[0]);
132
+ }
133
+ if (!cjkMatched && [...token].length >= 3) tokens.add(token);
134
+ }
135
+ return tokens;
136
+ }
137
+
138
+ function hasAsciiToken(tokens) {
139
+ for (const token of tokens) if (ASCII_CHAR_RE.test(token)) return true;
140
+ return false;
98
141
  }
99
142
 
100
143
  function setIntersectionCount(left, right) {
@@ -235,10 +278,15 @@ function evidenceHintsForCandidate(candidate, hintRows) {
235
278
  candidateSlots,
236
279
  );
237
280
  const tokenHits = setIntersectionCount(row._tokens, candidateTokens);
281
+ const unicodeSlotOnlyFallback =
282
+ slotHits > 0 &&
283
+ tokenHits < 2 &&
284
+ !hasAsciiToken(candidateTokens) &&
285
+ !hasAsciiToken(row._tokens);
238
286
  if (slotHits === 0 && tokenHits < 2) continue;
239
287
  const score =
240
288
  refHits * 6 + urlHits * 5 + slotHits * 2 + Math.min(tokenHits, 5);
241
- if (score < 7) continue;
289
+ if (score < 7 && !unicodeSlotOnlyFallback) continue;
242
290
  scored.push({ score, row });
243
291
  }
244
292
  scored.sort((left, right) => right.score - left.score);
@@ -376,11 +424,14 @@ function rewrittenCandidate(candidate, reasons, hints, urlToSourceRef) {
376
424
  if (!hint) return null;
377
425
  const replacement = stringOf(hint.value) || stringOf(hint.quote);
378
426
  if (!replacement || replacement === claimText(candidate)) return null;
427
+ const refs = backfillSourceRefs(candidate, [hint], urlToSourceRef);
428
+ if (refs.length === 0 && localEvidenceRefs(candidate).length === 0)
429
+ return null;
379
430
  return {
380
431
  ...candidate,
381
432
  originalClaim: claimText(candidate),
382
433
  claim: replacement,
383
- sourceRefs: backfillSourceRefs(candidate, [hint], urlToSourceRef),
434
+ sourceRefs: refs,
384
435
  sourceUrls: hint.url ? [hint.url] : sourceUrls(candidate),
385
436
  sanitizerRewriteReasons: rewriteReasons,
386
437
  reasonToVerify: `Deterministically rewritten to a source-backed atom from ${hint.sourceTitleOrPublisher ?? hint.url ?? hint.sourceRef ?? "source evidence"}.`,
@@ -399,6 +450,16 @@ function sanitizedCandidate(candidate, hints, urlToSourceRef) {
399
450
  };
400
451
  }
401
452
 
453
+ function withoutSanitizerGapReason(value) {
454
+ return stringOf(value)
455
+ .split(";")
456
+ .map((part) => part.trim())
457
+ .filter(
458
+ (part) => part && !part.startsWith("sanitized verifier candidates:"),
459
+ )
460
+ .join("; ");
461
+ }
462
+
402
463
  function adjustFactSlotCoverage(rows, demotedBySlot, keptIds) {
403
464
  return asArray(rows).map((row) => {
404
465
  const slot = { ...asObject(row) };
@@ -484,6 +545,7 @@ export default async function sanitizeVerificationCandidates({ sources }) {
484
545
  const webUrlOnlyDemotedIds = [];
485
546
  const promotedCandidateIds = [];
486
547
  const promotedBySlot = new Map();
548
+ const promotedPreservedClaims = new Set();
487
549
  const retainedCandidates = [];
488
550
  for (const [index, candidate] of keptCandidates.entries()) {
489
551
  const hasRefs = sourceRefs(candidate).length > 0;
@@ -550,6 +612,7 @@ export default async function sanitizeVerificationCandidates({ sources }) {
550
612
  for (const entry of promotable.slice(0, webUrlOnlyDemotedIds.length)) {
551
613
  takenIds.add(entry.id);
552
614
  promotedCandidateIds.push(entry.id);
615
+ promotedPreservedClaims.add(entry.preserved);
553
616
  for (const slotId of entry.slots) {
554
617
  const list = promotedBySlot.get(slotId) ?? [];
555
618
  list.push(entry.id);
@@ -584,19 +647,30 @@ export default async function sanitizeVerificationCandidates({ sources }) {
584
647
  const slotId = stringOf(row.slotId ?? row.id);
585
648
  const promoted = promotedBySlot.get(slotId) ?? [];
586
649
  if (promoted.length === 0) return row;
587
- return {
588
- ...row,
589
- verificationCandidateIds: compactStrings(
590
- [...asArray(row.verificationCandidateIds), ...promoted],
591
- 24,
592
- ),
593
- };
650
+ const verificationCandidateIds = compactStrings(
651
+ [...asArray(row.verificationCandidateIds), ...promoted],
652
+ 24,
653
+ );
654
+ const next = { ...row, verificationCandidateIds };
655
+ if (verificationCandidateIds.length > 0) {
656
+ if (next.status === "partial" || next.status === "missing") {
657
+ next.status = "filled";
658
+ }
659
+ const gapReason = withoutSanitizerGapReason(next.gapReason);
660
+ if (gapReason) next.gapReason = gapReason;
661
+ else delete next.gapReason;
662
+ }
663
+ return next;
594
664
  });
665
+ const outputPreservedClaims =
666
+ promotedPreservedClaims.size === 0
667
+ ? preservedClaims
668
+ : preservedClaims.filter((claim) => !promotedPreservedClaims.has(claim));
595
669
  return {
596
670
  schema: SCHEMA,
597
671
  claimInventory: {
598
672
  verificationCandidates: keptCandidates,
599
- preservedClaims,
673
+ preservedClaims: outputPreservedClaims,
600
674
  duplicates: asArray(claimInventory.duplicates),
601
675
  },
602
676
  factSlotCoverage: factSlotCoverageRows,
@@ -56,7 +56,6 @@
56
56
  },
57
57
  "auditArtifact": { "type": "string" },
58
58
  "sidecarPath": { "type": "string" },
59
- "reportSidecarPath": { "type": "string" },
60
59
  "auditSidecarPath": { "type": "string" },
61
60
  "reportMarkdown": { "type": "string" },
62
61
  "auditMarkdown": { "type": "string" },
@@ -20,7 +20,10 @@
20
20
  "id": {},
21
21
  "claim": {},
22
22
  "factSlotIds": {},
23
- "status": {},
23
+ "status": {
24
+ "type": "string",
25
+ "enum": ["verified", "partially_supported", "unsupported", "conflicting"]
26
+ },
24
27
  "verdictDigest": {},
25
28
  "evidence": {
26
29
  "type": "array",
@@ -92,7 +92,7 @@
92
92
  ],
93
93
  "sourcePolicy": "require-success",
94
94
  "output": {
95
- "controlSchema": "./schemas/state-data-impact-control.schema.json",
95
+ "controlSchema": "./schemas/api-contract-impact-control.schema.json",
96
96
  "analysis": {
97
97
  "required": true
98
98
  },
@@ -131,7 +131,7 @@
131
131
  ],
132
132
  "sourcePolicy": "require-success",
133
133
  "output": {
134
- "controlSchema": "./schemas/docs-release-impact-control.schema.json",
134
+ "controlSchema": "./schemas/api-contract-impact-control.schema.json",
135
135
  "analysis": {
136
136
  "required": true
137
137
  },
@@ -150,7 +150,7 @@
150
150
  ],
151
151
  "sourcePolicy": "require-success",
152
152
  "output": {
153
- "controlSchema": "./schemas/security-performance-impact-control.schema.json",
153
+ "controlSchema": "./schemas/api-contract-impact-control.schema.json",
154
154
  "analysis": {
155
155
  "required": true
156
156
  },
@@ -169,14 +169,7 @@ export default async function specReviewPipeline({ sources, options }) {
169
169
 
170
170
  function findAnalysisOutput(sources) {
171
171
  return (
172
- sources["analysis.candidate-findings"] ??
173
- sources["analysis.candidate-findings.main"] ??
174
- sources["candidate-findings"] ??
175
- sources["candidate-findings.main"] ??
176
- Object.entries(sources).find(([key]) =>
177
- key.includes("candidate-findings"),
178
- )?.[1] ??
179
- {}
172
+ sources["candidate-findings"] ?? sources["candidate-findings.main"] ?? {}
180
173
  );
181
174
  }
182
175