@agwab/pi-workflow 0.1.2 → 0.2.1

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 (41) hide show
  1. package/README.md +9 -13
  2. package/dist/compiler.d.ts +5 -5
  3. package/dist/compiler.js +82 -24
  4. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  5. package/dist/dynamic-generated-task-runtime.js +21 -8
  6. package/dist/engine.d.ts +6 -5
  7. package/dist/engine.js +39 -54
  8. package/dist/extension.js +211 -24
  9. package/dist/store.d.ts +3 -1
  10. package/dist/store.js +135 -38
  11. package/dist/subagent-backend.d.ts +4 -0
  12. package/dist/subagent-backend.js +128 -4
  13. package/dist/types.d.ts +5 -0
  14. package/dist/workflow-progress-health.d.ts +37 -0
  15. package/dist/workflow-progress-health.js +296 -0
  16. package/dist/workflow-runtime.d.ts +8 -0
  17. package/dist/workflow-runtime.js +63 -10
  18. package/dist/workflow-view.d.ts +2 -0
  19. package/dist/workflow-view.js +97 -18
  20. package/dist/workflow-web-source.js +32 -14
  21. package/docs/usage.md +12 -1
  22. package/package.json +6 -6
  23. package/src/compiler.ts +136 -41
  24. package/src/dynamic-generated-task-runtime.ts +47 -12
  25. package/src/engine.ts +55 -100
  26. package/src/extension.ts +270 -34
  27. package/src/store.ts +180 -44
  28. package/src/subagent-backend.ts +170 -6
  29. package/src/types.ts +10 -0
  30. package/src/workflow-progress-health.ts +461 -0
  31. package/src/workflow-runtime.ts +85 -13
  32. package/src/workflow-view.ts +186 -41
  33. package/src/workflow-web-source.ts +192 -69
  34. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
  35. package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
  36. package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
  37. package/workflows/deep-research/helpers/render-executive.mjs +671 -37
  38. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  39. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
  40. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  41. package/workflows/deep-research/spec.json +41 -11
@@ -10,10 +10,11 @@ import { mkdir, writeFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
 
12
12
  function findSource(sources, stageId) {
13
- for (const [specId, source] of Object.entries(sources ?? {})) {
14
- if (specId === stageId || specId.startsWith(`${stageId}.`)) return source;
15
- }
16
- return null;
13
+ const entries = Object.entries(sources ?? {});
14
+ const exact = entries.find(([specId]) => specId === stageId);
15
+ if (exact) return exact[1];
16
+ const dotted = entries.find(([specId]) => specId.startsWith(`${stageId}.`));
17
+ return dotted?.[1] ?? null;
17
18
  }
18
19
 
19
20
  function asArray(value) {
@@ -33,6 +34,9 @@ function flattenItems(value) {
33
34
  "finding",
34
35
  "claim",
35
36
  "note",
37
+ "reason",
38
+ "nextStep",
39
+ "evidenceState",
36
40
  "whyItMatters",
37
41
  "parentImpact",
38
42
  "recommendation",
@@ -46,6 +50,16 @@ function flattenItems(value) {
46
50
  ) {
47
51
  return [value];
48
52
  }
53
+ if (
54
+ value.id ||
55
+ value.gapId ||
56
+ value.slotId ||
57
+ Array.isArray(value.relatedFactSlotIds) ||
58
+ Array.isArray(value.sourceUrls) ||
59
+ Array.isArray(value.sourceRefs)
60
+ ) {
61
+ return [value];
62
+ }
49
63
  return Object.values(value).flatMap((item) => flattenItems(item));
50
64
  }
51
65
 
@@ -83,6 +97,37 @@ function stringifyItem(item) {
83
97
  return cleanText(String(item)) || "(empty item)";
84
98
  }
85
99
 
100
+ function summaryText(report, fallback) {
101
+ const summary = report?.summary;
102
+ if (typeof summary === "string" && summary.trim()) return cleanText(summary);
103
+ if (isRecord(summary)) {
104
+ const parts = [
105
+ summary.directAnswer,
106
+ summary.answer,
107
+ summary.summary,
108
+ summary.finding,
109
+ ]
110
+ .filter((value) => typeof value === "string" && value.trim())
111
+ .map(cleanText);
112
+ const confidence = cleanText(summary.confidence ?? "");
113
+ const caveat = cleanText(summary.keyCaveat ?? summary.caveat ?? "");
114
+ return (
115
+ [
116
+ parts[0],
117
+ confidence ? `Confidence: ${confidence}.` : undefined,
118
+ caveat ? `Key caveat: ${caveat}.` : undefined,
119
+ ]
120
+ .filter(Boolean)
121
+ .join(" ") || stringifyItem(summary)
122
+ );
123
+ }
124
+ return cleanText(fallback ?? "Research completed with audited evidence.");
125
+ }
126
+
127
+ function hasObjectSerializationArtifact(text) {
128
+ return /\[object Object\]/.test(String(text ?? ""));
129
+ }
130
+
86
131
  function truncateWords(text, maxWords) {
87
132
  const items = words(text);
88
133
  if (items.length <= maxWords) return cleanText(text);
@@ -156,6 +201,61 @@ function urlsOf(item, limit = 3) {
156
201
  return uniqueStructuredUrls(item).slice(0, limit);
157
202
  }
158
203
 
204
+ function normalizeLocalRef(value) {
205
+ if (typeof value !== "string") return null;
206
+ const text = value.trim();
207
+ if (!text || /^https?:\/\//i.test(text) || isWorkflowSourceRefText(text))
208
+ return null;
209
+ const stripped = text.replace(/^(?:file|repo):/i, "");
210
+ if (!/[\w./-]+\.[\w]+(?:#L\d+(?:-L?\d+)?)?$/i.test(stripped)) return null;
211
+ return stripped;
212
+ }
213
+
214
+ function isWorkflowSourceRefText(value) {
215
+ return /^wsrc_[a-z0-9]{16,}$/i.test(String(value ?? "").trim());
216
+ }
217
+
218
+ function collectLocalRefs(value, refs = []) {
219
+ if (!value || typeof value !== "object") return refs;
220
+ if (Array.isArray(value)) {
221
+ for (const item of value) collectLocalRefs(item, refs);
222
+ return refs;
223
+ }
224
+ for (const [key, item] of Object.entries(value)) {
225
+ if (/^(files?|paths?|sourceRefs?|sourceUrls?|sources?)$/i.test(key)) {
226
+ for (const candidate of asArray(item).length ? item : [item]) {
227
+ const ref = normalizeLocalRef(candidate);
228
+ if (ref) refs.push(ref);
229
+ else if (candidate && typeof candidate === "object")
230
+ collectLocalRefs(candidate, refs);
231
+ }
232
+ continue;
233
+ }
234
+ if (item && typeof item === "object") collectLocalRefs(item, refs);
235
+ }
236
+ return refs;
237
+ }
238
+
239
+ function localRefsOf(item, limit = 3) {
240
+ const out = [];
241
+ const seen = new Set();
242
+ for (const ref of collectLocalRefs(item, [])) {
243
+ if (seen.has(ref)) continue;
244
+ seen.add(ref);
245
+ out.push(ref);
246
+ if (out.length >= limit) break;
247
+ }
248
+ return out;
249
+ }
250
+
251
+ function referenceList(item, limit = 3) {
252
+ const urls = markdownLinkList(urlsOf(item, limit), limit);
253
+ const localRefs = localRefsOf(item, limit)
254
+ .map((ref) => `\`${ref}\``)
255
+ .join(", ");
256
+ return [urls, localRefs].filter(Boolean).join("; ");
257
+ }
258
+
159
259
  function markdownLinkList(urls, maxItems = 3) {
160
260
  return urls
161
261
  .slice(0, maxItems)
@@ -195,19 +295,54 @@ function finiteNumber(value) {
195
295
  return Number.isFinite(parsed) ? parsed : undefined;
196
296
  }
197
297
 
298
+ function normalizeClaimStatus(status) {
299
+ const text = cleanText(status).toLowerCase();
300
+ if (!text) return "";
301
+ if (text.includes("conflict")) return "conflicting";
302
+ if (text.includes("unsupported")) return "unsupported";
303
+ if (text.includes("partial")) return "partially_supported";
304
+ if (text.includes("verified")) return "verified";
305
+ return text;
306
+ }
307
+
198
308
  function coverageCounts(coverage, fallback) {
199
309
  if (!coverage || typeof coverage !== "object") return null;
200
- return {
310
+ const counts = {
201
311
  total: finiteNumber(coverage.verificationCandidates) ?? fallback.total,
202
312
  verified: finiteNumber(coverage.verified) ?? fallback.verified,
203
313
  partially_supported:
204
- finiteNumber(coverage.partiallySupported) ?? fallback.partially_supported,
314
+ finiteNumber(coverage.partiallySupported) ??
315
+ finiteNumber(coverage.partially_supported) ??
316
+ fallback.partially_supported,
205
317
  unsupported: finiteNumber(coverage.unsupported) ?? fallback.unsupported,
206
318
  conflicting: finiteNumber(coverage.conflicting) ?? fallback.conflicting,
207
319
  };
320
+ if (counts.total == null) {
321
+ counts.total =
322
+ counts.verified +
323
+ counts.partially_supported +
324
+ counts.unsupported +
325
+ counts.conflicting;
326
+ }
327
+ return counts;
208
328
  }
209
329
 
210
- function claimCounts(control) {
330
+ function packetVerdictCounts(packet, fallback) {
331
+ const verdicts = packet?.verdictCounts;
332
+ if (!isRecord(verdicts)) return null;
333
+ const counts = coverageCounts(verdicts, fallback);
334
+ if (!counts) return null;
335
+ counts.total =
336
+ finiteNumber(packet?.invariantChecks?.candidateCount) ??
337
+ finiteNumber(verdicts.total) ??
338
+ counts.verified +
339
+ counts.partially_supported +
340
+ counts.unsupported +
341
+ counts.conflicting;
342
+ return counts;
343
+ }
344
+
345
+ function claimCounts(control, packet) {
211
346
  const claims = asArray(control?.claimVerdictIndex?.claims);
212
347
  const counts = {
213
348
  total: claims.length,
@@ -217,9 +352,12 @@ function claimCounts(control) {
217
352
  conflicting: 0,
218
353
  };
219
354
  for (const claim of claims) {
220
- const status = claim?.status;
355
+ const status = normalizeClaimStatus(claim?.status);
221
356
  if (status && Object.hasOwn(counts, status)) counts[status] += 1;
222
357
  }
358
+ const packetCounts = packetVerdictCounts(packet, counts);
359
+ if (packetCounts) return packetCounts;
360
+
223
361
  const coverage = coverageCounts(
224
362
  control?.finalReport?.coverageSummary,
225
363
  counts,
@@ -259,6 +397,326 @@ function factSlotSummary(factSlots) {
259
397
  };
260
398
  }
261
399
 
400
+ function stringArray(value, limit = Infinity) {
401
+ const out = [];
402
+ const seen = new Set();
403
+ for (const item of asArray(value)) {
404
+ if (typeof item !== "string") continue;
405
+ const text = item.trim();
406
+ if (!text || seen.has(text)) continue;
407
+ seen.add(text);
408
+ out.push(text);
409
+ if (out.length >= limit) break;
410
+ }
411
+ return out;
412
+ }
413
+
414
+ function uniqueStrings(values, limit = Infinity) {
415
+ const out = [];
416
+ const seen = new Set();
417
+ for (const value of values) {
418
+ if (typeof value !== "string") continue;
419
+ const text = value.trim();
420
+ if (!text || seen.has(text)) continue;
421
+ seen.add(text);
422
+ out.push(text);
423
+ if (out.length >= limit) break;
424
+ }
425
+ return out;
426
+ }
427
+
428
+ function packetOf(packetSource) {
429
+ return isRecord(packetSource?.packet) ? packetSource.packet : {};
430
+ }
431
+
432
+ function claimIdOf(row) {
433
+ return cleanText(row?.id ?? row?.claimId ?? "");
434
+ }
435
+
436
+ function gapIdOf(row) {
437
+ return cleanText(row?.id ?? row?.gapId ?? "");
438
+ }
439
+
440
+ function claimLedger(packet, control) {
441
+ const packetLedger = asArray(packet?.claimVerdictLedger);
442
+ return packetLedger.length
443
+ ? packetLedger
444
+ : asArray(control?.claimVerdictIndex?.claims);
445
+ }
446
+
447
+ function mapById(rows, idFn) {
448
+ const out = new Map();
449
+ for (const row of rows) {
450
+ const id = idFn(row);
451
+ if (id && !out.has(id)) out.set(id, row);
452
+ }
453
+ return out;
454
+ }
455
+
456
+ function numberedId(prefix, index) {
457
+ return `${prefix}-${String(index + 1).padStart(3, "0")}`;
458
+ }
459
+
460
+ function packetGapRows(packet) {
461
+ const remaining = asArray(packet?.remainingGaps).map((gap, index) => ({
462
+ id: gapIdOf(gap) || numberedId("gap-remaining", index),
463
+ kind: "Gap",
464
+ ...gap,
465
+ }));
466
+ const coverage = asArray(packet?.coverageGaps).map((gap, index) => ({
467
+ id: gapIdOf(gap) || numberedId("gap-coverage", index),
468
+ kind: "Coverage gap",
469
+ ...gap,
470
+ }));
471
+ return [...remaining, ...coverage];
472
+ }
473
+
474
+ function rowsForIds(ids, rowById, warnings, label) {
475
+ const rows = [];
476
+ for (const id of ids) {
477
+ const row = rowById.get(id);
478
+ if (row) {
479
+ rows.push(row);
480
+ continue;
481
+ }
482
+ warnings.push({
483
+ section: "references",
484
+ label,
485
+ total: 1,
486
+ rendered: 0,
487
+ missingId: id,
488
+ });
489
+ }
490
+ return rows;
491
+ }
492
+
493
+ function claimSourceUrls(rows, limit = 8) {
494
+ return uniqueStrings(
495
+ rows.flatMap((row) => [...asArray(row?.sourceUrls), ...urlsOf(row, limit)]),
496
+ limit,
497
+ );
498
+ }
499
+
500
+ function claimSourceRefs(rows, limit = 8) {
501
+ return uniqueStrings(
502
+ rows.flatMap((row) => asArray(row?.sourceRefs)),
503
+ limit,
504
+ );
505
+ }
506
+
507
+ function evidenceStrength(status) {
508
+ switch (normalizeClaimStatus(status)) {
509
+ case "verified":
510
+ return 3;
511
+ case "partially_supported":
512
+ return 2;
513
+ case "unsupported":
514
+ return 1;
515
+ case "conflicting":
516
+ return 0;
517
+ default:
518
+ return -1;
519
+ }
520
+ }
521
+
522
+ function evidenceStatusFromRows(rows, fallback) {
523
+ if (rows.length === 0) return cleanText(fallback) || "not specified";
524
+ let weakest = "verified";
525
+ let weakestScore = Infinity;
526
+ for (const row of rows) {
527
+ const status = normalizeClaimStatus(row?.status ?? row?.verdict);
528
+ const score = evidenceStrength(status);
529
+ if (score >= 0 && score < weakestScore) {
530
+ weakest = status;
531
+ weakestScore = score;
532
+ }
533
+ }
534
+ return weakestScore === Infinity
535
+ ? cleanText(fallback) || "not specified"
536
+ : weakest;
537
+ }
538
+
539
+ function claimToFinding(row) {
540
+ return {
541
+ id: claimIdOf(row),
542
+ finding: cleanText(row?.claim ?? row?.support ?? stringifyItem(row)),
543
+ evidenceStatus: normalizeClaimStatus(row?.status) || row?.status,
544
+ confidence: row?.confidence,
545
+ sourceUrls: asArray(row?.sourceUrls),
546
+ sourceRefs: asArray(row?.sourceRefs),
547
+ rationale: row?.support,
548
+ caveat: row?.caveat,
549
+ correctionOrCounterclaim: row?.correctionOrCounterclaim,
550
+ };
551
+ }
552
+
553
+ function supportingClaimIds(item) {
554
+ return uniqueStrings([
555
+ ...asArray(item?.supportingClaimIds),
556
+ ...asArray(item?.claimIds),
557
+ ...asArray(item?.relatedClaimIds),
558
+ ]);
559
+ }
560
+
561
+ function withSupportingEvidence(item, claimRows) {
562
+ return {
563
+ ...item,
564
+ evidenceStatus: evidenceStatusFromRows(claimRows, item?.evidenceStatus),
565
+ sourceUrls: uniqueStrings(
566
+ [...asArray(item?.sourceUrls), ...claimSourceUrls(claimRows)],
567
+ 8,
568
+ ),
569
+ sourceRefs: uniqueStrings(
570
+ [...asArray(item?.sourceRefs), ...claimSourceRefs(claimRows)],
571
+ 8,
572
+ ),
573
+ };
574
+ }
575
+
576
+ function coverageSummaryFromPacket(packet, fallback = {}) {
577
+ const counts = packetVerdictCounts(packet, {
578
+ total: 0,
579
+ verified: 0,
580
+ partially_supported: 0,
581
+ unsupported: 0,
582
+ conflicting: 0,
583
+ });
584
+ if (!counts) return fallback;
585
+ return {
586
+ ...fallback,
587
+ verified: counts.verified,
588
+ partiallySupported: counts.partially_supported,
589
+ unsupported: counts.unsupported,
590
+ conflicting: counts.conflicting,
591
+ verificationCandidates: counts.total,
592
+ depth: packet?.researchMetadataSeed?.depth ?? fallback.depth,
593
+ researchQuestions:
594
+ packet?.researchMetadataSeed?.researchQuestions ??
595
+ fallback.researchQuestions,
596
+ preserved:
597
+ packet?.overflowLedger?.preservedClaimCount ?? fallback.preserved,
598
+ coverageGaps:
599
+ packet?.overflowLedger?.coverageGapCount ?? fallback.coverageGaps,
600
+ };
601
+ }
602
+
603
+ function composeResearchReport(control, packetSource) {
604
+ const packet = packetOf(packetSource);
605
+ const legacyReport = control?.finalReport ?? {};
606
+ const synthesis = isRecord(control?.synthesis) ? control.synthesis : null;
607
+ const ledger = claimLedger(packet, control);
608
+ const claimById = mapById(ledger, claimIdOf);
609
+ const gapRows = packetGapRows(packet);
610
+ const gapById = mapById(gapRows, gapIdOf);
611
+ const warnings = [];
612
+
613
+ if (!synthesis) {
614
+ const report = { ...legacyReport };
615
+ if (asArray(packet.factSlotCoverage).length > 0)
616
+ report.factSlotCoverage = packet.factSlotCoverage;
617
+ if (isRecord(packet.researchMetadataSeed))
618
+ report.researchMetadata = packet.researchMetadataSeed;
619
+ if (isRecord(packet.verdictCounts))
620
+ report.coverageSummary = coverageSummaryFromPacket(
621
+ packet,
622
+ report.coverageSummary,
623
+ );
624
+ if (asArray(report.remainingGaps).length === 0 && gapRows.length > 0)
625
+ report.remainingGaps = gapRows;
626
+ if (
627
+ asArray(report.researchScopeCoverage).length === 0 &&
628
+ asArray(packet.researchScopeCoverage).length > 0
629
+ ) {
630
+ report.researchScopeCoverage = packet.researchScopeCoverage;
631
+ }
632
+ return { report, packet, ledger, warnings };
633
+ }
634
+
635
+ const keyFindingIds = stringArray(synthesis.keyFindingIds, 12);
636
+ const keyFindingRows = keyFindingIds.length
637
+ ? rowsForIds(keyFindingIds, claimById, warnings, "key findings")
638
+ : ledger
639
+ .filter((row) => normalizeClaimStatus(row?.status) === "verified")
640
+ .slice(0, 8);
641
+ const mapOverlayItems = (items, textField) =>
642
+ asArray(items).map((item) => {
643
+ const ids = supportingClaimIds(item);
644
+ const rows = rowsForIds(ids, claimById, warnings, textField);
645
+ return withSupportingEvidence(item, rows);
646
+ });
647
+ const caveatNotes = asArray(synthesis.caveatNotes).map((item) => {
648
+ const rows = rowsForIds(
649
+ supportingClaimIds(item),
650
+ claimById,
651
+ warnings,
652
+ "caveat notes",
653
+ );
654
+ const gaps = rowsForIds(
655
+ stringArray(item?.gapIds, 12),
656
+ gapById,
657
+ warnings,
658
+ "gap notes",
659
+ );
660
+ return withSupportingEvidence(
661
+ {
662
+ ...item,
663
+ relatedGaps: gaps,
664
+ },
665
+ rows,
666
+ );
667
+ });
668
+ const optionalUnsupported = rowsForIds(
669
+ stringArray(synthesis.notableUnsupportedClaimIds, 12),
670
+ claimById,
671
+ warnings,
672
+ "unsupported claims",
673
+ ).map(claimToFinding);
674
+ const optionalContested = rowsForIds(
675
+ stringArray(synthesis.contestedClaimIds, 12),
676
+ claimById,
677
+ warnings,
678
+ "contested claims",
679
+ ).map(claimToFinding);
680
+ const derivedUnsupported = ledger
681
+ .filter((row) => normalizeClaimStatus(row?.status) === "unsupported")
682
+ .map(claimToFinding);
683
+ const derivedContested = ledger
684
+ .filter((row) => normalizeClaimStatus(row?.status) === "conflicting")
685
+ .map(claimToFinding);
686
+
687
+ return {
688
+ report: {
689
+ summary: synthesis.bottomLine ?? control?.digest,
690
+ researchMetadata: packet.researchMetadataSeed ?? {},
691
+ coverageSummary: coverageSummaryFromPacket(packet, {}),
692
+ factSlotCoverage: asArray(packet.factSlotCoverage),
693
+ mainFindings: keyFindingRows.map(claimToFinding),
694
+ recommendations: mapOverlayItems(
695
+ synthesis.recommendations,
696
+ "recommendations",
697
+ ),
698
+ actionPlan: mapOverlayItems(synthesis.actionPlan, "action plan"),
699
+ caveatedFindings: caveatNotes,
700
+ contestedAreas: optionalContested.length
701
+ ? optionalContested
702
+ : derivedContested,
703
+ notableUnsupportedClaims: optionalUnsupported.length
704
+ ? optionalUnsupported
705
+ : derivedUnsupported,
706
+ unverifiedButRelevant: asArray(packet.preservedClaims),
707
+ parentDecisionNotes: mapOverlayItems(
708
+ synthesis.parentDecisionNotes,
709
+ "decision notes",
710
+ ),
711
+ researchScopeCoverage: asArray(packet.researchScopeCoverage),
712
+ remainingGaps: gapRows,
713
+ },
714
+ packet,
715
+ ledger,
716
+ warnings,
717
+ };
718
+ }
719
+
262
720
  function statusRank(item) {
263
721
  const status =
264
722
  `${item?.evidenceStatus ?? item?.status ?? item?.confidence ?? ""}`.toLowerCase();
@@ -295,9 +753,7 @@ function renderEvidenceStrength(report) {
295
753
  slot.label ?? slot.slotId ?? slot.bestValue ?? "Evidence area",
296
754
  );
297
755
  const status = escapeTableCell(evidenceStatusOf(slot));
298
- const evidence = escapeTableCell(
299
- markdownLinkList(urlsOf(slot, 2), 2) || "—",
300
- );
756
+ const evidence = escapeTableCell(referenceList(slot, 2) || "—");
301
757
  const impact = escapeTableCell(
302
758
  slot.parentImpact ?? slot.whyItMatters ?? slot.notes ?? "",
303
759
  );
@@ -353,7 +809,7 @@ function renderMainFindings(report) {
353
809
  findings.forEach(({ item: finding, text }, index) => {
354
810
  const status = evidenceStatusOf(finding);
355
811
  const confidence = confidenceOf(finding);
356
- const urls = markdownLinkList(urlsOf(finding, 4), 4);
812
+ const urls = referenceList(finding, 4);
357
813
  out.push(`### ${index + 1}. ${text}`);
358
814
  out.push("");
359
815
  out.push(
@@ -378,7 +834,7 @@ function renderRecommendations(report) {
378
834
  const out = ["## Recommendations", ""];
379
835
  recommendations.forEach(({ item, text }, index) => {
380
836
  const status = evidenceStatusOf(item);
381
- const urls = markdownLinkList(urlsOf(item, 4), 4);
837
+ const urls = referenceList(item, 4);
382
838
  out.push(`${index + 1}. **${text}**`);
383
839
  out.push(` - Evidence status: ${status || "not specified"}`);
384
840
  if (urls) out.push(` - Sources: ${urls}`);
@@ -394,7 +850,7 @@ function renderActionPlan(report) {
394
850
  actions.forEach(({ item, text }, index) => {
395
851
  const numericStep = Number(item?.step);
396
852
  const step = Number.isFinite(numericStep) ? numericStep : index + 1;
397
- const urls = markdownLinkList(urlsOf(item, 3), 3);
853
+ const urls = referenceList(item, 3);
398
854
  const evidence = evidenceStatusOf(item);
399
855
  out.push(`${step}. ${text}`);
400
856
  if (evidence && evidence !== "not specified")
@@ -405,21 +861,52 @@ function renderActionPlan(report) {
405
861
  return out;
406
862
  }
407
863
 
864
+ function fallbackCaveatText(item) {
865
+ if (!isRecord(item)) return stringifyItem(item);
866
+ const id = cleanText(item.id ?? item.gapId ?? "");
867
+ const slotIds = uniqueStrings([
868
+ item.slotId,
869
+ ...asArray(item.relatedFactSlotIds),
870
+ ]).join(", ");
871
+ const kind = cleanText(item.kind ?? "gap");
872
+ return (
873
+ [kind, id, slotIds ? `related slots: ${slotIds}` : undefined]
874
+ .filter(Boolean)
875
+ .join(" — ") || stringifyItem(item)
876
+ );
877
+ }
878
+
408
879
  function caveatText(item) {
409
880
  return itemText(
410
881
  item,
411
- ["gap", "finding", "claim", "note", "whyItMatters", "parentImpact"],
412
- stringifyItem(item),
882
+ [
883
+ "gap",
884
+ "finding",
885
+ "claim",
886
+ "note",
887
+ "reason",
888
+ "nextStep",
889
+ "evidenceState",
890
+ "whyItMatters",
891
+ "parentImpact",
892
+ ],
893
+ fallbackCaveatText(item),
413
894
  );
414
895
  }
415
896
 
416
897
  function caveatCategories(report) {
417
898
  return [
418
899
  { kind: "Gap", items: flattenItems(report.remainingGaps) },
419
- { kind: "Unsupported", items: flattenItems(report.notableUnsupportedClaims) },
900
+ {
901
+ kind: "Unsupported",
902
+ items: flattenItems(report.notableUnsupportedClaims),
903
+ },
420
904
  { kind: "Contested", items: flattenItems(report.contestedAreas) },
421
905
  { kind: "Caveat", items: flattenItems(report.caveatedFindings) },
422
- { kind: "Unverified lead", items: flattenItems(report.unverifiedButRelevant) },
906
+ {
907
+ kind: "Unverified lead",
908
+ items: flattenItems(report.unverifiedButRelevant),
909
+ },
423
910
  { kind: "Decision note", items: flattenItems(report.parentDecisionNotes) },
424
911
  ]
425
912
  .map((category) => ({
@@ -450,7 +937,7 @@ function renderCaveats(report) {
450
937
  if (selection.total === 0) return [];
451
938
  const out = ["## Caveats and remaining gaps", ""];
452
939
  for (const { kind, item, text } of selection.selected) {
453
- const urls = markdownLinkList(urlsOf(item, 3), 3);
940
+ const urls = referenceList(item, 3);
454
941
  out.push(`- **${kind}:** ${text}${urls ? ` (${urls})` : ""}`);
455
942
  }
456
943
  out.push("");
@@ -475,8 +962,8 @@ function renderSourceIndex(sourceIndex) {
475
962
  return out;
476
963
  }
477
964
 
478
- function renderAuditSummary(control, claimSummary, slots) {
479
- const coverage = control?.finalReport?.coverageSummary ?? {};
965
+ function renderAuditSummary(report, claimSummary, slots) {
966
+ const coverage = report?.coverageSummary ?? {};
480
967
  const mismatches = asArray(claimSummary.coverageSummaryMismatch);
481
968
  return [
482
969
  "## Audit summary",
@@ -493,7 +980,7 @@ function renderAuditSummary(control, claimSummary, slots) {
493
980
  ...(coverage.researchQuestions != null
494
981
  ? [`- Research questions: ${coverage.researchQuestions}.`]
495
982
  : []),
496
- "- Audit artifact: `final-audit.control.json`.",
983
+ "- Audit artifact: `audit.md`.",
497
984
  "",
498
985
  ];
499
986
  }
@@ -521,9 +1008,10 @@ function renderWarnings(sectionCounts) {
521
1008
  }));
522
1009
  }
523
1010
 
524
- function renderResearchMarkdown(control, options = {}) {
525
- const report = control?.finalReport ?? {};
526
- const claimSummary = claimCounts(control);
1011
+ function renderResearchMarkdown(control, packetSource, options = {}) {
1012
+ const composed = composeResearchReport(control, packetSource);
1013
+ const report = composed.report;
1014
+ const claimSummary = claimCounts(control, composed.packet);
527
1015
  const factSlots = sortedFactSlots(report);
528
1016
  const slots = factSlotSummary(asArray(report.factSlotCoverage));
529
1017
  const findings = mainFindingEntries(report);
@@ -541,7 +1029,7 @@ function renderResearchMarkdown(control, options = {}) {
541
1029
  report.remainingGaps,
542
1030
  report.parentDecisionNotes,
543
1031
  report.unverifiedButRelevant,
544
- control?.claimVerdictIndex?.claims,
1032
+ composed.ledger,
545
1033
  );
546
1034
  const maxUrls = Number.isFinite(Number(options.maxUrls))
547
1035
  ? Math.max(0, Number(options.maxUrls))
@@ -569,18 +1057,14 @@ function renderResearchMarkdown(control, options = {}) {
569
1057
  sourceUrls: allSourceIndex.length,
570
1058
  renderedSourceUrls: sourceIndex.length,
571
1059
  };
572
- const warnings = renderWarnings(sectionCounts);
1060
+ const warnings = [...renderWarnings(sectionCounts), ...composed.warnings];
573
1061
 
574
1062
  const sections = [
575
1063
  "# Research report",
576
1064
  "",
577
1065
  "## Bottom line",
578
1066
  "",
579
- cleanText(
580
- report.summary ??
581
- control.digest ??
582
- "Research completed with audited evidence.",
583
- ),
1067
+ summaryText(report, control.digest),
584
1068
  "",
585
1069
  ...renderEvidenceStrength(report),
586
1070
  ...renderMainFindings(report),
@@ -588,7 +1072,7 @@ function renderResearchMarkdown(control, options = {}) {
588
1072
  ...renderActionPlan(report),
589
1073
  ...renderCaveats(report),
590
1074
  ...renderSourceIndex(sourceIndex),
591
- ...renderAuditSummary(control, claimSummary, slots),
1075
+ ...renderAuditSummary(report, claimSummary, slots),
592
1076
  ];
593
1077
 
594
1078
  const markdown = sections
@@ -610,10 +1094,128 @@ function stripLeadingHeading(markdown) {
610
1094
  return String(markdown ?? "").replace(/^#\s+[^\n]+\n*/i, "");
611
1095
  }
612
1096
 
613
- export default async function renderExecutive({ sources, options = {}, context = {} }) {
1097
+ function synthesisClaimRows(control, rows) {
1098
+ const synthesis = isRecord(control?.synthesis) ? control.synthesis : null;
1099
+ if (!synthesis) return asArray(control?.claimVerdictIndex?.claims);
1100
+ const rowById = mapById(rows, claimIdOf);
1101
+ const ids = uniqueStrings([
1102
+ ...asArray(synthesis.keyFindingIds),
1103
+ ...asArray(synthesis.notableUnsupportedClaimIds),
1104
+ ...asArray(synthesis.contestedClaimIds),
1105
+ ...asArray(synthesis.recommendations).flatMap(supportingClaimIds),
1106
+ ...asArray(synthesis.actionPlan).flatMap(supportingClaimIds),
1107
+ ...asArray(synthesis.caveatNotes).flatMap(supportingClaimIds),
1108
+ ...asArray(synthesis.parentDecisionNotes).flatMap(supportingClaimIds),
1109
+ ]);
1110
+ return ids.map((id) => rowById.get(id)).filter(Boolean);
1111
+ }
1112
+
1113
+ function renderAuditMarkdown(control, packetSource, rendered) {
1114
+ const packet = packetSource?.packet ?? {};
1115
+ const report = control?.finalReport ?? {};
1116
+ const ledger = asArray(packet.claimVerdictLedger);
1117
+ const claims = synthesisClaimRows(control, ledger);
1118
+ const gaps = asArray(packet.remainingGaps).length
1119
+ ? asArray(packet.remainingGaps)
1120
+ : asArray(report.remainingGaps);
1121
+ const sourceRefJoinFailures = asArray(packet.sourceRefJoinFailures).filter(
1122
+ (failure) => uniqueStructuredUrls(failure).length > 0,
1123
+ );
1124
+ const factSlots = asArray(packet.factSlotCoverage).length
1125
+ ? asArray(packet.factSlotCoverage)
1126
+ : asArray(report.factSlotCoverage);
1127
+ const rows = ledger.length ? ledger : claims;
1128
+ const out = [
1129
+ "# Research audit",
1130
+ "",
1131
+ "This artifact preserves the detailed claim/gap/source ledger behind `executive.md`.",
1132
+ "",
1133
+ "## Claim verdict ledger",
1134
+ "",
1135
+ ];
1136
+ if (rows.length > 0) {
1137
+ out.push(
1138
+ "| ID | Status | Claim/support | Caveat/source |",
1139
+ "|---|---|---|---|",
1140
+ );
1141
+ for (const row of rows) {
1142
+ const id = escapeTableCell(row.id ?? row.claimId ?? "—");
1143
+ const status = escapeTableCell(row.status ?? row.confidence ?? "—");
1144
+ const support = escapeTableCell(
1145
+ row.claim ??
1146
+ row.support ??
1147
+ row.verdictDigest?.support ??
1148
+ stringifyItem(row),
1149
+ );
1150
+ const caveat = escapeTableCell(
1151
+ row.caveat ??
1152
+ row.correctionOrCounterclaim ??
1153
+ markdownLinkList(urlsOf(row, 3), 3) ??
1154
+ "—",
1155
+ );
1156
+ out.push(`| ${id} | ${status} | ${support} | ${caveat || "—"} |`);
1157
+ }
1158
+ } else {
1159
+ out.push("No compact claim ledger was provided.");
1160
+ }
1161
+ out.push("", "## Fact slot coverage", "");
1162
+ if (factSlots.length > 0) {
1163
+ out.push(
1164
+ "| Slot | Status | Best value | Gap/impact |",
1165
+ "|---|---|---|---|",
1166
+ );
1167
+ for (const slot of factSlots) {
1168
+ out.push(
1169
+ `| ${escapeTableCell(slot.slotId ?? slot.label ?? "—")} | ${escapeTableCell(slot.status ?? "—")} | ${escapeTableCell(isRecord(slot.bestValue) ? stringifyItem(slot.bestValue) : (slot.bestValue ?? "—"))} | ${escapeTableCell(slot.gapReason || slot.parentImpact || "—")} |`,
1170
+ );
1171
+ }
1172
+ } else {
1173
+ out.push("No fact-slot ledger was provided.");
1174
+ }
1175
+ out.push("", "## Remaining gaps", "");
1176
+ if (gaps.length > 0) {
1177
+ for (const gap of gaps)
1178
+ out.push(`- ${caveatText(gap) || stringifyItem(gap)}`);
1179
+ } else {
1180
+ out.push("No remaining gaps were reported.");
1181
+ }
1182
+ if (claims.length > 0 && ledger.length > 0) {
1183
+ out.push("", "## Claims used in executive synthesis", "");
1184
+ for (const claim of claims) {
1185
+ out.push(
1186
+ `- **${cleanText(claim.id ?? "claim")}** (${cleanText(claim.status ?? "unknown")}): ${cleanText(claim.claim ?? claim.support ?? stringifyItem(claim))}`,
1187
+ );
1188
+ }
1189
+ }
1190
+ if (sourceRefJoinFailures.length > 0) {
1191
+ out.push("", "## Source reference join failures", "");
1192
+ for (const failure of sourceRefJoinFailures)
1193
+ out.push(`- ${caveatText(failure) || stringifyItem(failure)}`);
1194
+ }
1195
+ out.push(
1196
+ "",
1197
+ "## Renderer diagnostics",
1198
+ "",
1199
+ `- Executive word count: ${countWords(rendered.markdown)}.`,
1200
+ `- Rendered source URLs: ${rendered.sourceIndex.length}/${rendered.allSourceIndex.length}.`,
1201
+ `- Render warnings: ${rendered.renderWarnings.length}.`,
1202
+ "",
1203
+ );
1204
+ return out
1205
+ .join("\n")
1206
+ .replace(/\n{3,}/g, "\n\n")
1207
+ .trim();
1208
+ }
1209
+
1210
+ export default async function renderExecutive({
1211
+ sources,
1212
+ options = {},
1213
+ context = {},
1214
+ }) {
614
1215
  const control =
615
1216
  findSource(sources, "final-audit") ??
616
1217
  sources?.[Object.keys(sources ?? {})[0]];
1218
+ const auditPacket = findSource(sources, "final-audit-packet");
617
1219
  if (!control || typeof control !== "object") {
618
1220
  return {
619
1221
  schema: "deep-research-executive-render-v1",
@@ -623,13 +1225,32 @@ export default async function renderExecutive({ sources, options = {}, context =
623
1225
  blockers: ["missing final-audit control source"],
624
1226
  executiveMarkdown: "",
625
1227
  reportMarkdown: "",
1228
+ auditMarkdown: "",
626
1229
  wordCount: 0,
627
1230
  sourceUrlCount: 0,
1231
+ totalSourceUrlCount: 0,
1232
+ sourceUrls: [],
1233
+ sourceIndex: [],
1234
+ claimSummary: {
1235
+ total: 0,
1236
+ verified: 0,
1237
+ partially_supported: 0,
1238
+ unsupported: 0,
1239
+ conflicting: 0,
1240
+ },
1241
+ factSlotSummary: {
1242
+ total: 0,
1243
+ filled: 0,
1244
+ partial: 0,
1245
+ missingOrConflicting: 0,
1246
+ },
1247
+ sectionCounts: {},
628
1248
  renderWarnings: [],
629
1249
  gates: {
630
1250
  renderedAllStructuredItems: false,
631
1251
  passed: false,
632
1252
  },
1253
+ auditArtifact: "final-audit.control.json",
633
1254
  };
634
1255
  }
635
1256
 
@@ -650,13 +1271,17 @@ export default async function renderExecutive({ sources, options = {}, context =
650
1271
  ? Math.max(0, Number(options.maxGaps))
651
1272
  : undefined,
652
1273
  };
653
- const rendered = renderResearchMarkdown(control, opts);
1274
+ const rendered = renderResearchMarkdown(control, auditPacket, opts);
654
1275
  let markdown = rendered.markdown;
655
1276
  let truncated = false;
656
1277
  if (Number.isFinite(opts.maxWords) && countWords(markdown) > opts.maxWords) {
657
1278
  truncated = true;
658
1279
  markdown = truncateWords(markdown, opts.maxWords);
659
1280
  }
1281
+ const auditMarkdown = renderAuditMarkdown(control, auditPacket, rendered);
1282
+ const serializationArtifact =
1283
+ hasObjectSerializationArtifact(markdown) ||
1284
+ hasObjectSerializationArtifact(auditMarkdown);
660
1285
  const wordCount = countWords(markdown);
661
1286
  const sourceUrlCount = rendered.sourceIndex.length;
662
1287
  const substantiveRenderWarnings = rendered.renderWarnings.filter(
@@ -665,10 +1290,14 @@ export default async function renderExecutive({ sources, options = {}, context =
665
1290
  const renderedAllStructuredItems = substantiveRenderWarnings.length === 0;
666
1291
  const truncatedWithOpenGaps =
667
1292
  truncated && Number(rendered.sectionCounts.caveatsAndGaps ?? 0) > 0;
668
- const passed = renderedAllStructuredItems && !truncatedWithOpenGaps;
1293
+ const passed =
1294
+ renderedAllStructuredItems &&
1295
+ !truncatedWithOpenGaps &&
1296
+ !serializationArtifact;
669
1297
 
670
1298
  let executiveSidecarPath;
671
1299
  let reportSidecarPath;
1300
+ let auditSidecarPath;
672
1301
  try {
673
1302
  if (context.cwd && context.runId && context.taskId) {
674
1303
  const taskDir = join(
@@ -682,8 +1311,10 @@ export default async function renderExecutive({ sources, options = {}, context =
682
1311
  await mkdir(taskDir, { recursive: true });
683
1312
  executiveSidecarPath = join(taskDir, "executive.md");
684
1313
  reportSidecarPath = join(taskDir, "report.md");
1314
+ auditSidecarPath = join(taskDir, "audit.md");
685
1315
  await writeFile(executiveSidecarPath, `${markdown}\n`, "utf8");
686
1316
  await writeFile(reportSidecarPath, `${markdown}\n`, "utf8");
1317
+ await writeFile(auditSidecarPath, `${auditMarkdown}\n`, "utf8");
687
1318
  }
688
1319
  } catch {
689
1320
  // Sidecars are non-authoritative; keep control output deterministic.
@@ -696,6 +1327,7 @@ export default async function renderExecutive({ sources, options = {}, context =
696
1327
  renderMode: "evidence-backed-report",
697
1328
  executiveMarkdown: markdown,
698
1329
  reportMarkdown: markdown,
1330
+ auditMarkdown,
699
1331
  wordCount,
700
1332
  sourceUrlCount,
701
1333
  totalSourceUrlCount: rendered.allSourceIndex.length,
@@ -717,10 +1349,12 @@ export default async function renderExecutive({ sources, options = {}, context =
717
1349
  maxGaps: opts.maxGaps,
718
1350
  truncated,
719
1351
  truncatedWithOpenGaps,
1352
+ serializationArtifact,
720
1353
  passed,
721
1354
  },
722
- auditArtifact: "final-audit.control.json",
1355
+ auditArtifact: auditSidecarPath ? "audit.md" : "final-audit.control.json",
723
1356
  ...(executiveSidecarPath ? { sidecarPath: "executive.md" } : {}),
724
1357
  ...(reportSidecarPath ? { reportSidecarPath: "report.md" } : {}),
1358
+ ...(auditSidecarPath ? { auditSidecarPath: "audit.md" } : {}),
725
1359
  };
726
1360
  }