@agwab/pi-workflow 0.1.2 → 0.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 (34) hide show
  1. package/README.md +7 -13
  2. package/dist/compiler.d.ts +2 -0
  3. package/dist/compiler.js +27 -2
  4. package/dist/engine.d.ts +2 -0
  5. package/dist/engine.js +3 -2
  6. package/dist/extension.js +201 -16
  7. package/dist/store.js +1 -0
  8. package/dist/types.d.ts +3 -0
  9. package/dist/workflow-progress-health.d.ts +37 -0
  10. package/dist/workflow-progress-health.js +296 -0
  11. package/dist/workflow-runtime.d.ts +6 -0
  12. package/dist/workflow-runtime.js +33 -10
  13. package/dist/workflow-view.d.ts +2 -0
  14. package/dist/workflow-view.js +97 -18
  15. package/dist/workflow-web-source.js +32 -14
  16. package/docs/usage.md +1 -1
  17. package/package.json +6 -6
  18. package/src/compiler.ts +41 -2
  19. package/src/engine.ts +7 -16
  20. package/src/extension.ts +254 -22
  21. package/src/store.ts +1 -0
  22. package/src/types.ts +4 -0
  23. package/src/workflow-progress-health.ts +461 -0
  24. package/src/workflow-runtime.ts +50 -13
  25. package/src/workflow-view.ts +186 -41
  26. package/src/workflow-web-source.ts +192 -69
  27. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
  28. package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
  29. package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
  30. package/workflows/deep-research/helpers/render-executive.mjs +671 -37
  31. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  32. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
  33. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  34. package/workflows/deep-research/spec.json +41 -11
@@ -50,8 +50,11 @@ function collectUrls(value, urls = new Set()) {
50
50
  }
51
51
 
52
52
  function looksLikeLocalSourceRef(value) {
53
- const text = String(value ?? "").trim();
54
- return /^(?:\.?[\w.-]+\/)?[\w./-]+\.(?:md|json|ya?ml|ts|tsx|js|mjs|cjs|py|go|rs|zig|txt)$/i.test(
53
+ const text = String(value ?? "")
54
+ .trim()
55
+ .replace(/^(?:file|repo):/i, "")
56
+ .replace(/#L\d+(?:-L?\d+)?$/i, "");
57
+ return /^(?:\.?[\w.-]+\/)?[\w./-]+\.(?:md|json|ya?ml|ts|tsx|js|mjs|cjs|py|go|rs|zig|txt|sol|java|kt|swift|rb|php|c|cc|cpp|h|hpp)$/i.test(
55
58
  text,
56
59
  );
57
60
  }
@@ -60,9 +63,19 @@ function collectEvidenceRefs(claim) {
60
63
  const refs = new Set([...collectUrls(claim)]);
61
64
  for (const row of Array.isArray(claim?.evidence) ? claim.evidence : []) {
62
65
  if (!row || typeof row !== "object") continue;
63
- for (const value of [row.url, row.source, row.file, row.path, row.sourceRef]) {
66
+ for (const value of [
67
+ row.url,
68
+ row.source,
69
+ row.file,
70
+ row.path,
71
+ row.sourceRef,
72
+ ]) {
64
73
  if (typeof value !== "string") continue;
65
- if (/^https?:\/\//i.test(value) || isWorkflowSourceRef(value) || looksLikeLocalSourceRef(value))
74
+ if (
75
+ /^https?:\/\//i.test(value) ||
76
+ isWorkflowSourceRef(value) ||
77
+ looksLikeLocalSourceRef(value)
78
+ )
66
79
  refs.add(value.trim());
67
80
  }
68
81
  }
@@ -71,7 +84,8 @@ function collectEvidenceRefs(claim) {
71
84
 
72
85
  function collectWorkflowSourceRefs(value, refs = new Set()) {
73
86
  if (typeof value === "string") {
74
- for (const match of value.matchAll(/\bwsrc_[a-f0-9]{32}\b/g)) refs.add(match[0]);
87
+ for (const match of value.matchAll(/\bwsrc_[a-f0-9]{32}\b/g))
88
+ refs.add(match[0]);
75
89
  return refs;
76
90
  }
77
91
  if (Array.isArray(value)) {
@@ -79,7 +93,8 @@ function collectWorkflowSourceRefs(value, refs = new Set()) {
79
93
  return refs;
80
94
  }
81
95
  if (value && typeof value === "object") {
82
- for (const item of Object.values(value)) collectWorkflowSourceRefs(item, refs);
96
+ for (const item of Object.values(value))
97
+ collectWorkflowSourceRefs(item, refs);
83
98
  }
84
99
  return refs;
85
100
  }
@@ -90,7 +105,9 @@ function isWorkflowSourceRef(value) {
90
105
 
91
106
  function sourceUrlArray(value) {
92
107
  if (!Array.isArray(value)) return [];
93
- return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
108
+ return value
109
+ .filter((item) => typeof item === "string" && item.trim())
110
+ .map((item) => item.trim());
94
111
  }
95
112
 
96
113
  function stripCitationUrlPunctuation(value) {
@@ -135,7 +152,9 @@ function buildUrlSourceRefLookup(normalizeInputPacket) {
135
152
  if (!source || typeof source !== "object") continue;
136
153
  addUrlSourceRef(urlToSourceRef, source.url, source.sourceRef);
137
154
  }
138
- const sourceRefIndex = asArray(normalizeInputPacket?.packet?.research?.sourceRefIndex);
155
+ const sourceRefIndex = asArray(
156
+ normalizeInputPacket?.packet?.research?.sourceRefIndex,
157
+ );
139
158
  for (const source of sourceRefIndex) {
140
159
  if (!source || typeof source !== "object") continue;
141
160
  addUrlSourceRef(urlToSourceRef, source.url, source.sourceRef);
@@ -162,7 +181,9 @@ function sourceRefsForUrls(urls, urlToSourceRef) {
162
181
  // a keyword scan over the serialized claim, this cannot be satisfied by merely
163
182
  // mentioning a URL/path in prose.
164
183
  function hasFetchedEvidence(claim) {
165
- return Array.isArray(claim?.evidence) && claim.evidence.some(hasStrongEvidenceRow);
184
+ return (
185
+ Array.isArray(claim?.evidence) && claim.evidence.some(hasStrongEvidenceRow)
186
+ );
166
187
  }
167
188
 
168
189
  function hasStrongEvidenceRow(row) {
@@ -170,26 +191,51 @@ function hasStrongEvidenceRow(row) {
170
191
  const refs = [row.url, row.source, row.file, row.path, row.sourceRef].filter(
171
192
  (value) => typeof value === "string",
172
193
  );
173
- const sourceRef = refs.some(
174
- (value) =>
175
- /^https?:\/\//i.test(value) ||
176
- isWorkflowSourceRef(value) ||
177
- looksLikeLocalSourceRef(value),
194
+ const hasExternalRef = refs.some(
195
+ (value) => /^https?:\/\//i.test(value) || isWorkflowSourceRef(value),
178
196
  );
197
+ const hasLocalRef = refs.some((value) => looksLikeLocalSourceRef(value));
198
+ const hasLocatedLocalRef =
199
+ hasLocalRef &&
200
+ (refs.some(hasLineFragment) || hasLocalEvidenceLocation(row));
201
+ const sourceRef = hasExternalRef || hasLocatedLocalRef;
179
202
  const quote = typeof row.quote === "string" && row.quote.trim().length > 0;
180
203
  if (!sourceRef || !quote) return false;
181
204
  if (isCandidateEvidenceRow(row)) return false;
182
205
  return true;
183
206
  }
184
207
 
208
+ function hasLineFragment(value) {
209
+ return /#L\d+(?:-L?\d+)?$/i.test(String(value ?? "").trim());
210
+ }
211
+
212
+ function hasLocalEvidenceLocation(row) {
213
+ return [
214
+ row.line,
215
+ row.lineStart,
216
+ row.lineEnd,
217
+ row.lines,
218
+ row.excerptLocation,
219
+ ].some(
220
+ (value) =>
221
+ typeof value === "number" ||
222
+ (typeof value === "string" && value.trim().length > 0),
223
+ );
224
+ }
225
+
185
226
  function isCandidateEvidenceRow(row) {
186
- return row?.candidateOnly === true || row?.matchType === "terms" || row?.sourceRead?.matchType === "terms";
227
+ return (
228
+ row?.candidateOnly === true ||
229
+ row?.matchType === "terms" ||
230
+ row?.sourceRead?.matchType === "terms"
231
+ );
187
232
  }
188
233
 
189
234
  function strongEvidenceIssue(claim) {
190
235
  const rows = Array.isArray(claim?.evidence) ? claim.evidence : [];
191
236
  if (rows.length === 0) return "missing_structured_evidence_rows";
192
- if (rows.some(isCandidateEvidenceRow)) return "candidate_only_evidence_not_strong";
237
+ if (rows.some(isCandidateEvidenceRow))
238
+ return "candidate_only_evidence_not_strong";
193
239
  return "evidence_rows_missing_source_or_quote";
194
240
  }
195
241
 
@@ -275,7 +321,10 @@ function conservativeVerifierStatus(statuses) {
275
321
  if (normalized.includes(status)) return status;
276
322
  }
277
323
  if (normalized.every((status) => status === "verified")) return "verified";
278
- return normalized.find((status) => typeof status === "string" && status) ?? "unverified";
324
+ return (
325
+ normalized.find((status) => typeof status === "string" && status) ??
326
+ "unverified"
327
+ );
279
328
  }
280
329
 
281
330
  function issueForVerifierRow({ sourceId, claim, reason, claimId, index }) {
@@ -303,20 +352,24 @@ function gapForVerifierIssue(issue) {
303
352
 
304
353
  function mergeVerifierRows(rows) {
305
354
  const first = rows[0];
306
- if (rows.length === 1) return { sourceId: first.sourceId, claim: first.claim, duplicate: null };
355
+ if (rows.length === 1)
356
+ return { sourceId: first.sourceId, claim: first.claim, duplicate: null };
307
357
  const sourceIds = rows.map((row) => row.sourceId);
308
358
  const statusInputs = rows.map((row) => verdictOf(row.claim));
309
359
  const selectedStatus = conservativeVerifierStatus(statusInputs);
310
360
  const selectedRow =
311
- rows.find((row) => canonicalVerifierStatus(verdictOf(row.claim)) === selectedStatus) ??
312
- first;
361
+ rows.find(
362
+ (row) => canonicalVerifierStatus(verdictOf(row.claim)) === selectedStatus,
363
+ ) ?? first;
313
364
  const merged = { ...selectedRow.claim };
314
365
  const evidence = rows.flatMap((row) =>
315
366
  Array.isArray(row.claim?.evidence) ? row.claim.evidence : [],
316
367
  );
317
368
  if (evidence.length > 0) merged.evidence = evidence;
318
369
  for (const field of ["sourceRefs", "sourceUrls", "factSlotIds"]) {
319
- const values = compactStrings(rows.flatMap((row) => row.claim?.[field] ?? []));
370
+ const values = compactStrings(
371
+ rows.flatMap((row) => row.claim?.[field] ?? []),
372
+ );
320
373
  if (values.length > 0) merged[field] = values;
321
374
  }
322
375
  merged.status = selectedStatus;
@@ -362,7 +415,11 @@ function findSource(sources, stageId) {
362
415
 
363
416
  export default async function claimEvidenceGate({ sources, options = {} }) {
364
417
  const plan = findSource(sources, "plan");
365
- const normalized = findSource(sources, "normalize-claims");
418
+ const normalizeClaims = findSource(sources, "normalize-claims");
419
+ const sanitizedCandidates =
420
+ findSource(sources, "sanitize-claims") ??
421
+ findSource(sources, "sanitize-verification-candidates");
422
+ const normalized = sanitizedCandidates ?? normalizeClaims;
366
423
  const normalizeInputPacket = findSource(sources, "normalize-input-packet");
367
424
  const urlToSourceRef = buildUrlSourceRefLookup(normalizeInputPacket);
368
425
  const candidateRecords = [];
@@ -406,7 +463,8 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
406
463
  );
407
464
  // Legacy layout: when no verify-claims.* source ids exist (for example a
408
465
  // single from: string dependency), fall back to every non-plan/non-normalize
409
- // source.
466
+ // source. Exclude sanitizer sources because they are canonicalizer inputs, not
467
+ // verifier verdict rows.
410
468
  const verifierClaims =
411
469
  claims.length > 0
412
470
  ? claims
@@ -415,7 +473,9 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
415
473
  ([specId]) =>
416
474
  !specId.startsWith("plan") &&
417
475
  !specId.startsWith("normalize-claims") &&
418
- !specId.startsWith("normalize-input-packet"),
476
+ !specId.startsWith("normalize-input-packet") &&
477
+ !specId.startsWith("sanitize-claims") &&
478
+ !specId.startsWith("sanitize-verification-candidates"),
419
479
  )
420
480
  .flatMap(([sourceId, source]) =>
421
481
  asArray(source).map((claim, index) => ({ sourceId, claim, index })),
@@ -488,7 +548,13 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
488
548
  }
489
549
  }
490
550
 
491
- function auditClaim({ sourceId, claim, candidate, claimId, missingVerifierResult = false }) {
551
+ function auditClaim({
552
+ sourceId,
553
+ claim,
554
+ candidate,
555
+ claimId,
556
+ missingVerifierResult = false,
557
+ }) {
492
558
  if (!claim || typeof claim !== "object") return;
493
559
  gateSummary.total += 1;
494
560
  const evidenceRefs = [...collectEvidenceRefs(claim)];
@@ -549,22 +615,24 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
549
615
  workflowSourceRefs.size - beforeUrlBackfillSourceRefCount;
550
616
  }
551
617
  if (workflowSourceRefs.size > 0) next.sourceRefs = [...workflowSourceRefs];
618
+ const httpSourceUrls = [
619
+ ...new Set([
620
+ ...sourceUrlArray(candidate?.sourceUrls).filter((ref) =>
621
+ /^https?:\/\//i.test(ref),
622
+ ),
623
+ ...evidenceRefs.filter((ref) => /^https?:\/\//i.test(ref)),
624
+ ]),
625
+ ];
552
626
  if (
553
627
  claimId &&
554
628
  candidate &&
555
629
  workflowSourceRefs.size === 0 &&
556
- (sourceUrlArray(candidate.sourceUrls).length > 0 ||
557
- evidenceRefs.some((ref) => /^https?:\/\//i.test(ref)))
630
+ httpSourceUrls.length > 0
558
631
  ) {
559
632
  const failure = {
560
633
  claimId,
561
634
  evidenceState: "source_ref_not_available",
562
- sourceUrls: [
563
- ...new Set([
564
- ...sourceUrlArray(candidate?.sourceUrls),
565
- ...evidenceRefs.filter((ref) => /^https?:\/\//i.test(ref)),
566
- ]),
567
- ],
635
+ sourceUrls: httpSourceUrls,
568
636
  nextStep:
569
637
  "Preserve sourceRefs from workflow_web_fetch_source through research and normalization when available.",
570
638
  };
@@ -573,7 +641,8 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
573
641
  }
574
642
 
575
643
  const verdict = verdictOf(next);
576
- const exactQuantitativeForGate = exactQuantitative || hasExactQuantitativeClaim(next);
644
+ const exactQuantitativeForGate =
645
+ exactQuantitative || hasExactQuantitativeClaim(next);
577
646
  if (
578
647
  verdict === "verified" &&
579
648
  options.requireFetchedEvidenceForVerified !== false &&
@@ -610,7 +679,8 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
610
679
  gateSummary.downgraded += 1;
611
680
  remainingGaps.push({
612
681
  claimId: next.id ?? next.claimId,
613
- evidenceState: next.evidenceGate?.reasonCode ?? "insufficient_for_verified",
682
+ evidenceState:
683
+ next.evidenceGate?.reasonCode ?? "insufficient_for_verified",
614
684
  reason: next.evidenceGate?.reason,
615
685
  sourceUrls: evidenceRefs,
616
686
  nextStep:
@@ -653,7 +723,10 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
653
723
  status === "partiallySupported" ? "partially_supported" : status,
654
724
  );
655
725
  const hasStatusConflict = new Set(statuses).size > 1;
656
- const duplicate = { ...merged.duplicate, statusConflict: hasStatusConflict };
726
+ const duplicate = {
727
+ ...merged.duplicate,
728
+ statusConflict: hasStatusConflict,
729
+ };
657
730
  duplicateVerifierRows.push(duplicate);
658
731
  gateSummary.duplicateVerifierClaims += 1;
659
732
  gateSummary.duplicateVerifierRows += rows.length - 1;
@@ -764,6 +837,7 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
764
837
  droppedSlotIds,
765
838
  },
766
839
  identityJoinNotes,
767
- precisionGuardDiagnostics: normalizeInputPacket?.packet?.precisionGuard?.summary,
840
+ precisionGuardDiagnostics:
841
+ normalizeInputPacket?.packet?.precisionGuard?.summary,
768
842
  };
769
843
  }
@@ -20,7 +20,9 @@ function asArray(value) {
20
20
  }
21
21
 
22
22
  function asObject(value) {
23
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
23
+ return value && typeof value === "object" && !Array.isArray(value)
24
+ ? value
25
+ : {};
24
26
  }
25
27
 
26
28
  function stringOf(value) {
@@ -46,6 +48,14 @@ function compactStrings(values, limit = 5) {
46
48
  return out;
47
49
  }
48
50
 
51
+ function truncateText(value, limit = 240) {
52
+ const text = stringOf(value);
53
+ if (!text) return undefined;
54
+ const normalized = text.replace(/\s+/g, " ").trim();
55
+ if (normalized.length <= limit) return normalized;
56
+ return `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
57
+ }
58
+
49
59
  function compactClaimDigest(claim) {
50
60
  const digest = asObject(claim);
51
61
  return {
@@ -56,7 +66,11 @@ function compactClaimDigest(claim) {
56
66
  factSlotIds: compactStrings(digest.factSlotIds, 12),
57
67
  sourceRefs: compactStrings(digest.sourceRefs, 8),
58
68
  sourceUrls: compactStrings(digest.sourceUrls, 8),
59
- support: stringOf(digest.verdictDigest?.support ?? digest.verdictDigest?.summary ?? digest.verdictDigest),
69
+ support: stringOf(
70
+ digest.verdictDigest?.support ??
71
+ digest.verdictDigest?.summary ??
72
+ digest.verdictDigest,
73
+ ),
60
74
  caveat: stringOf(digest.verdictDigest?.caveat ?? digest.caveat),
61
75
  correctionOrCounterclaim: stringOf(digest.correctionOrCounterclaim),
62
76
  ...(digest.evidenceGate ? { evidenceGate: digest.evidenceGate } : {}),
@@ -81,6 +95,7 @@ function compactSlot(slot) {
81
95
  function compactGap(gap) {
82
96
  const item = asObject(gap);
83
97
  return {
98
+ id: stringOf(item.id ?? item.gapId),
84
99
  claimId: stringOf(item.claimId),
85
100
  slotId: stringOf(item.slotId),
86
101
  evidenceState: stringOf(item.evidenceState),
@@ -108,7 +123,9 @@ function compactDuplicateVerifierRow(row) {
108
123
  const item = asObject(row);
109
124
  return {
110
125
  claimId: stringOf(item.claimId),
111
- rowCount: Number.isFinite(Number(item.rowCount)) ? Number(item.rowCount) : undefined,
126
+ rowCount: Number.isFinite(Number(item.rowCount))
127
+ ? Number(item.rowCount)
128
+ : undefined,
112
129
  sourceIds: compactStrings(item.sourceIds, 8),
113
130
  statusInputs: compactStrings(item.statusInputs, 8),
114
131
  selectedStatus: stringOf(item.selectedStatus),
@@ -126,9 +143,124 @@ function countByStatus(slots) {
126
143
  return counts;
127
144
  }
128
145
 
146
+ function withGeneratedIds(items, prefix) {
147
+ return items.map((item, index) => ({
148
+ ...item,
149
+ id: stringOf(item.id) ?? `${prefix}-${String(index + 1).padStart(3, "0")}`,
150
+ }));
151
+ }
152
+
153
+ function synthesisClaimDigest(claim) {
154
+ const item = compactClaimDigest(claim);
155
+ return {
156
+ id: item.id,
157
+ claim: truncateText(item.claim, 260),
158
+ status: item.status,
159
+ confidence: item.confidence,
160
+ factSlotIds: compactStrings(item.factSlotIds, 8),
161
+ support: truncateText(item.support, 240),
162
+ caveat: truncateText(item.caveat, 180),
163
+ correctionOrCounterclaim: truncateText(item.correctionOrCounterclaim, 180),
164
+ hasSourceUrls: compactStrings(item.sourceUrls, 1).length > 0,
165
+ hasSourceRefs: compactStrings(item.sourceRefs, 1).length > 0,
166
+ };
167
+ }
168
+
169
+ function synthesisFactSlot(slot) {
170
+ const item = asObject(slot);
171
+ return {
172
+ slotId: stringOf(item.slotId),
173
+ label: truncateText(item.label, 120),
174
+ status: stringOf(item.status),
175
+ gapReason: truncateText(item.gapReason, 120),
176
+ parentImpact: truncateText(item.parentImpact, 120),
177
+ };
178
+ }
179
+
180
+ function synthesisGap(gap) {
181
+ const item = asObject(gap);
182
+ return {
183
+ id: stringOf(item.id),
184
+ kind: stringOf(item.kind),
185
+ claimId: stringOf(item.claimId),
186
+ slotId: stringOf(item.slotId),
187
+ evidenceState: stringOf(item.evidenceState),
188
+ reason: truncateText(item.reason, 220),
189
+ nextStep: truncateText(item.nextStep, 180),
190
+ scopeItem: truncateText(item.scopeItem, 160),
191
+ whyItMatters: truncateText(item.whyItMatters, 180),
192
+ };
193
+ }
194
+
195
+ function synthesisScopeCoverage(row) {
196
+ const item = asObject(row);
197
+ return {
198
+ scopeItem: truncateText(item.scopeItem ?? item.item ?? item.topic, 160),
199
+ status: stringOf(item.status ?? item.coverageStatus),
200
+ evidenceState: stringOf(item.evidenceState),
201
+ summary: truncateText(item.summary ?? item.reason, 220),
202
+ whyItMatters: truncateText(item.whyItMatters, 180),
203
+ };
204
+ }
205
+
206
+ function buildSynthesisInput({
207
+ plan,
208
+ factSlotCoverage,
209
+ claimDigests,
210
+ preservedClaims,
211
+ coverageGaps,
212
+ remainingGaps,
213
+ sourceRefJoinFailures,
214
+ researchScopeCoverage,
215
+ integritySummary,
216
+ audit,
217
+ }) {
218
+ return {
219
+ researchMetadata: {
220
+ depth: stringOf(plan.depth),
221
+ taskType: stringOf(plan.taskType),
222
+ expectedFinalShape: stringOf(plan.expectedFinalShape),
223
+ researchQuestions: asArray(plan.researchQuestions).length,
224
+ plannedFactSlots: asArray(plan.factSlots).length,
225
+ },
226
+ verdictCounts: asObject(audit.verdictCounts),
227
+ factSlotStatusCounts: countByStatus(factSlotCoverage),
228
+ integritySummary,
229
+ researchScopeCoverage: asArray(researchScopeCoverage)
230
+ .slice(0, 24)
231
+ .map(synthesisScopeCoverage),
232
+ factSlots: factSlotCoverage.map(synthesisFactSlot),
233
+ claims: claimDigests.map(synthesisClaimDigest),
234
+ preservedClaims: preservedClaims.slice(0, 12).map((claim) => ({
235
+ id: idOf(claim),
236
+ claim: truncateText(claim.claim, 240),
237
+ factSlotIds: compactStrings(claim.factSlotIds, 8),
238
+ whyItMatters: truncateText(claim.whyItMatters ?? claim.reason, 180),
239
+ })),
240
+ gaps: [
241
+ ...remainingGaps.map((gap) =>
242
+ synthesisGap({ ...gap, kind: "remaining" }),
243
+ ),
244
+ ...coverageGaps.map((gap) => synthesisGap({ ...gap, kind: "coverage" })),
245
+ ...sourceRefJoinFailures.map((gap) =>
246
+ synthesisGap({ ...gap, kind: "sourceRefJoinFailure" }),
247
+ ),
248
+ ],
249
+ };
250
+ }
251
+
129
252
  export default async function finalAuditPacket({ sources }) {
130
253
  const plan = asObject(findSource(sources, "plan"));
131
- const normalized = asObject(findSource(sources, "normalize-claims"));
254
+ const normalizeClaims = asObject(findSource(sources, "normalize-claims"));
255
+ const sanitizedCandidates = asObject(
256
+ findSource(sources, "sanitize-claims") ??
257
+ findSource(sources, "sanitize-verification-candidates"),
258
+ );
259
+ const normalized =
260
+ Object.keys(sanitizedCandidates).length > 0
261
+ ? sanitizedCandidates
262
+ : normalizeClaims;
263
+ const sanitizerDiagnostics = asObject(normalized.sanitizerDiagnostics);
132
264
  const audit = asObject(findSource(sources, "audit-claims"));
133
265
  const claimInventory = asObject(normalized.claimInventory);
134
266
  const verificationCandidates = asArray(claimInventory.verificationCandidates);
@@ -137,12 +269,27 @@ export default async function finalAuditPacket({ sources }) {
137
269
  const auditedIds = new Set(claimDigests.map(idOf).filter(Boolean));
138
270
  const candidateIds = verificationCandidates.map(idOf).filter(Boolean);
139
271
  const omittedCandidateIds = candidateIds.filter((id) => !auditedIds.has(id));
140
- const factSlotCoverage = asArray(normalized.factSlotCoverage).map(compactSlot);
141
- const coverageGaps = asArray(normalized.coverageGaps).map(compactGap);
142
- const remainingGaps = asArray(audit.remainingGaps).map(compactGap);
143
- const sourceRefJoinFailures = asArray(audit.sourceRefJoinFailures).map(compactGap);
144
- const invalidVerifierRows = asArray(audit.invalidVerifierRows).map(compactVerifierIssue);
145
- const duplicateVerifierRows = asArray(audit.duplicateVerifierRows).map(compactDuplicateVerifierRow);
272
+ const factSlotCoverage = asArray(normalized.factSlotCoverage).map(
273
+ compactSlot,
274
+ );
275
+ const coverageGaps = withGeneratedIds(
276
+ asArray(normalized.coverageGaps).map(compactGap),
277
+ "gap-coverage",
278
+ );
279
+ const remainingGaps = withGeneratedIds(
280
+ asArray(audit.remainingGaps).map(compactGap),
281
+ "gap-remaining",
282
+ );
283
+ const sourceRefJoinFailures = withGeneratedIds(
284
+ asArray(audit.sourceRefJoinFailures).map(compactGap),
285
+ "gap-source-ref",
286
+ );
287
+ const invalidVerifierRows = asArray(audit.invalidVerifierRows).map(
288
+ compactVerifierIssue,
289
+ );
290
+ const duplicateVerifierRows = asArray(audit.duplicateVerifierRows).map(
291
+ compactDuplicateVerifierRow,
292
+ );
146
293
  const gateSummary = asObject(audit.gateSummary);
147
294
  const precisionGuardDiagnostics = asObject(audit.precisionGuardDiagnostics);
148
295
  const sourceRefCoverage = {
@@ -154,10 +301,31 @@ export default async function finalAuditPacket({ sources }) {
154
301
  ).length,
155
302
  sourceRefJoinFailures: sourceRefJoinFailures.length,
156
303
  };
304
+ const integritySummary = {
305
+ omittedVerificationCandidateCount: omittedCandidateIds.length,
306
+ sourceRefJoinFailures: sourceRefJoinFailures.length,
307
+ invalidVerifierRows: invalidVerifierRows.length,
308
+ duplicateVerifierRows: duplicateVerifierRows.length,
309
+ missingVerifierResults: Number(gateSummary.missingVerifierResults ?? 0),
310
+ sourceRefCoverage,
311
+ };
312
+ const synthesisInput = buildSynthesisInput({
313
+ plan,
314
+ factSlotCoverage,
315
+ claimDigests,
316
+ preservedClaims,
317
+ coverageGaps,
318
+ remainingGaps,
319
+ sourceRefJoinFailures,
320
+ researchScopeCoverage: normalized.researchScopeCoverage,
321
+ integritySummary,
322
+ audit,
323
+ });
157
324
 
158
325
  return {
159
326
  schema: SCHEMA,
160
327
  packet: {
328
+ synthesisInput,
161
329
  researchMetadataSeed: {
162
330
  depth: stringOf(plan.depth),
163
331
  taskType: stringOf(plan.taskType),
@@ -165,9 +333,15 @@ export default async function finalAuditPacket({ sources }) {
165
333
  researchQuestions: asArray(plan.researchQuestions).length,
166
334
  sourcePolicy: asObject(plan.sourcePolicy),
167
335
  plannedFactSlots: asArray(plan.factSlots).length,
168
- filledFactSlots: factSlotCoverage.filter((slot) => slot.status === "filled").length,
169
- partialFactSlots: factSlotCoverage.filter((slot) => slot.status === "partial").length,
170
- missingFactSlots: factSlotCoverage.filter((slot) => slot.status === "missing").length,
336
+ filledFactSlots: factSlotCoverage.filter(
337
+ (slot) => slot.status === "filled",
338
+ ).length,
339
+ partialFactSlots: factSlotCoverage.filter(
340
+ (slot) => slot.status === "partial",
341
+ ).length,
342
+ missingFactSlots: factSlotCoverage.filter(
343
+ (slot) => slot.status === "missing",
344
+ ).length,
171
345
  },
172
346
  verdictCounts: asObject(audit.verdictCounts),
173
347
  statusPartitions: asObject(audit.statusPartitions),
@@ -184,6 +358,7 @@ export default async function finalAuditPacket({ sources }) {
184
358
  },
185
359
  normalizerDiagnostics: {
186
360
  precisionGuard: precisionGuardDiagnostics,
361
+ sanitizer: sanitizerDiagnostics,
187
362
  },
188
363
  preservedClaims: preservedClaims.map((claim) => ({
189
364
  id: idOf(claim),
@@ -203,7 +378,9 @@ export default async function finalAuditPacket({ sources }) {
203
378
  verifierIntegrity: {
204
379
  invalidVerifierRows: invalidVerifierRows.length,
205
380
  duplicateVerifierRows: duplicateVerifierRows.length,
206
- missingVerifierResults: Number(gateSummary.missingVerifierResults ?? 0),
381
+ missingVerifierResults: Number(
382
+ gateSummary.missingVerifierResults ?? 0,
383
+ ),
207
384
  },
208
385
  },
209
386
  overflowLedger: {