@agwab/pi-workflow 0.2.1 → 0.4.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 (119) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { mkdir, writeFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
+ import { canonicalVerificationStatus } from "./verification-ontology.mjs";
11
12
 
12
13
  function findSource(sources, stageId) {
13
14
  const entries = Object.entries(sources ?? {});
@@ -298,11 +299,24 @@ function finiteNumber(value) {
298
299
  function normalizeClaimStatus(status) {
299
300
  const text = cleanText(status).toLowerCase();
300
301
  if (!text) return "";
302
+ const canonical = canonicalVerificationStatus(text);
303
+ if (canonical !== "unverified") return canonical;
304
+ if (
305
+ text === "unverified" ||
306
+ text.includes("not verified") ||
307
+ text.includes("not_verified")
308
+ )
309
+ return "unverified";
310
+ if (
311
+ text.includes("verification_blocked") ||
312
+ text.includes("verification blocked")
313
+ )
314
+ return "verification_blocked";
301
315
  if (text.includes("conflict")) return "conflicting";
302
316
  if (text.includes("unsupported")) return "unsupported";
303
317
  if (text.includes("partial")) return "partially_supported";
304
- if (text.includes("verified")) return "verified";
305
- return text;
318
+ if (/\bverified\b/.test(text)) return "verified";
319
+ return canonical;
306
320
  }
307
321
 
308
322
  function coverageCounts(coverage, fallback) {
@@ -316,13 +330,18 @@ function coverageCounts(coverage, fallback) {
316
330
  fallback.partially_supported,
317
331
  unsupported: finiteNumber(coverage.unsupported) ?? fallback.unsupported,
318
332
  conflicting: finiteNumber(coverage.conflicting) ?? fallback.conflicting,
333
+ verification_blocked:
334
+ finiteNumber(coverage.verificationBlocked) ??
335
+ finiteNumber(coverage.verification_blocked) ??
336
+ fallback.verification_blocked,
319
337
  };
320
338
  if (counts.total == null) {
321
339
  counts.total =
322
340
  counts.verified +
323
341
  counts.partially_supported +
324
342
  counts.unsupported +
325
- counts.conflicting;
343
+ counts.conflicting +
344
+ counts.verification_blocked;
326
345
  }
327
346
  return counts;
328
347
  }
@@ -338,7 +357,8 @@ function packetVerdictCounts(packet, fallback) {
338
357
  counts.verified +
339
358
  counts.partially_supported +
340
359
  counts.unsupported +
341
- counts.conflicting;
360
+ counts.conflicting +
361
+ counts.verification_blocked;
342
362
  return counts;
343
363
  }
344
364
 
@@ -350,6 +370,7 @@ function claimCounts(control, packet) {
350
370
  partially_supported: 0,
351
371
  unsupported: 0,
352
372
  conflicting: 0,
373
+ verification_blocked: 0,
353
374
  };
354
375
  for (const claim of claims) {
355
376
  const status = normalizeClaimStatus(claim?.status);
@@ -372,6 +393,7 @@ function claimCounts(control, packet) {
372
393
  "partially_supported",
373
394
  "unsupported",
374
395
  "conflicting",
396
+ "verification_blocked",
375
397
  ]) {
376
398
  if (coverage[key] !== counts[key]) {
377
399
  mismatches.push({
@@ -468,7 +490,14 @@ function packetGapRows(packet) {
468
490
  kind: "Coverage gap",
469
491
  ...gap,
470
492
  }));
471
- return [...remaining, ...coverage];
493
+ const sourceRefJoinFailures = asArray(packet?.sourceRefJoinFailures).map(
494
+ (gap, index) => ({
495
+ id: gapIdOf(gap) || numberedId("gap-source-ref", index),
496
+ kind: "Source reference gap",
497
+ ...gap,
498
+ }),
499
+ );
500
+ return [...remaining, ...coverage, ...sourceRefJoinFailures];
472
501
  }
473
502
 
474
503
  function rowsForIds(ids, rowById, warnings, label) {
@@ -513,6 +542,8 @@ function evidenceStrength(status) {
513
542
  case "unsupported":
514
543
  return 1;
515
544
  case "conflicting":
545
+ case "verification_blocked":
546
+ case "unverified":
516
547
  return 0;
517
548
  default:
518
549
  return -1;
@@ -580,6 +611,7 @@ function coverageSummaryFromPacket(packet, fallback = {}) {
580
611
  partially_supported: 0,
581
612
  unsupported: 0,
582
613
  conflicting: 0,
614
+ verification_blocked: 0,
583
615
  });
584
616
  if (!counts) return fallback;
585
617
  return {
@@ -588,6 +620,7 @@ function coverageSummaryFromPacket(packet, fallback = {}) {
588
620
  partiallySupported: counts.partially_supported,
589
621
  unsupported: counts.unsupported,
590
622
  conflicting: counts.conflicting,
623
+ verificationBlocked: counts.verification_blocked,
591
624
  verificationCandidates: counts.total,
592
625
  depth: packet?.researchMetadataSeed?.depth ?? fallback.depth,
593
626
  researchQuestions:
@@ -968,7 +1001,7 @@ function renderAuditSummary(report, claimSummary, slots) {
968
1001
  return [
969
1002
  "## Audit summary",
970
1003
  "",
971
- `- Claims: ${claimSummary.verified} verified, ${claimSummary.partially_supported} partially supported, ${claimSummary.unsupported} unsupported, ${claimSummary.conflicting} conflicting.`,
1004
+ `- Claims: ${claimSummary.verified} verified, ${claimSummary.partially_supported} partially supported, ${claimSummary.unsupported} unsupported, ${claimSummary.conflicting} conflicting, ${claimSummary.verification_blocked} verification blocked.`,
972
1005
  `- Fact slots: ${slots.filled} filled, ${slots.partial} partial, ${slots.missingOrConflicting} missing/conflicting, ${slots.total} total.`,
973
1006
  ...(mismatches.length > 0
974
1007
  ? [
@@ -987,11 +1020,7 @@ function renderAuditSummary(report, claimSummary, slots) {
987
1020
 
988
1021
  function renderWarnings(sectionCounts) {
989
1022
  const checks = [
990
- ["findings", "renderedFindings", "findings"],
991
- ["recommendations", "renderedRecommendations", "recommendations"],
992
- ["actionItems", "renderedActionItems", "action items"],
993
1023
  ["caveatsAndGaps", "renderedCaveatsAndGaps", "caveats/gaps"],
994
- ["factSlots", "renderedFactSlots", "fact slots"],
995
1024
  ["sourceUrls", "renderedSourceUrls", "source URLs"],
996
1025
  ];
997
1026
  return checks
@@ -1237,6 +1266,7 @@ export default async function renderExecutive({
1237
1266
  partially_supported: 0,
1238
1267
  unsupported: 0,
1239
1268
  conflicting: 0,
1269
+ verification_blocked: 0,
1240
1270
  },
1241
1271
  factSlotSummary: {
1242
1272
  total: 0,
@@ -1261,15 +1291,6 @@ export default async function renderExecutive({
1261
1291
  maxUrls: Number.isFinite(Number(options.maxUrls))
1262
1292
  ? Math.max(0, Number(options.maxUrls))
1263
1293
  : 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
1294
  };
1274
1295
  const rendered = renderResearchMarkdown(control, auditPacket, opts);
1275
1296
  let markdown = rendered.markdown;
@@ -1296,7 +1317,6 @@ export default async function renderExecutive({
1296
1317
  !serializationArtifact;
1297
1318
 
1298
1319
  let executiveSidecarPath;
1299
- let reportSidecarPath;
1300
1320
  let auditSidecarPath;
1301
1321
  try {
1302
1322
  if (context.cwd && context.runId && context.taskId) {
@@ -1310,10 +1330,8 @@ export default async function renderExecutive({
1310
1330
  );
1311
1331
  await mkdir(taskDir, { recursive: true });
1312
1332
  executiveSidecarPath = join(taskDir, "executive.md");
1313
- reportSidecarPath = join(taskDir, "report.md");
1314
1333
  auditSidecarPath = join(taskDir, "audit.md");
1315
1334
  await writeFile(executiveSidecarPath, `${markdown}\n`, "utf8");
1316
- await writeFile(reportSidecarPath, `${markdown}\n`, "utf8");
1317
1335
  await writeFile(auditSidecarPath, `${auditMarkdown}\n`, "utf8");
1318
1336
  }
1319
1337
  } catch {
@@ -1344,9 +1362,6 @@ export default async function renderExecutive({
1344
1362
  renderedAllStructuredItems,
1345
1363
  maxWords: Number.isFinite(opts.maxWords) ? opts.maxWords : null,
1346
1364
  maxUrls: Number.isFinite(opts.maxUrls) ? opts.maxUrls : null,
1347
- maxFindings: opts.maxFindings,
1348
- maxRecommendations: opts.maxRecommendations,
1349
- maxGaps: opts.maxGaps,
1350
1365
  truncated,
1351
1366
  truncatedWithOpenGaps,
1352
1367
  serializationArtifact,
@@ -1354,7 +1369,6 @@ export default async function renderExecutive({
1354
1369
  },
1355
1370
  auditArtifact: auditSidecarPath ? "audit.md" : "final-audit.control.json",
1356
1371
  ...(executiveSidecarPath ? { sidecarPath: "executive.md" } : {}),
1357
- ...(reportSidecarPath ? { reportSidecarPath: "report.md" } : {}),
1358
1372
  ...(auditSidecarPath ? { auditSidecarPath: "audit.md" } : {}),
1359
1373
  };
1360
1374
  }
@@ -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,
@@ -0,0 +1,229 @@
1
+ // Shadow-only selective verification reporter for deep-research.
2
+ //
3
+ // This helper never skips verification. It records which candidates a future
4
+ // selector might skip, then joins those shadow decisions to actual audit output
5
+ // so adoption can be judged with evidence before any verifier is removed.
6
+
7
+ function asArray(value) {
8
+ if (Array.isArray(value)) return value;
9
+ if (value && typeof value === "object") {
10
+ if (Array.isArray(value.claimInventory?.verificationCandidates))
11
+ return value.claimInventory.verificationCandidates;
12
+ if (Array.isArray(value.verificationCandidates))
13
+ return value.verificationCandidates;
14
+ if (Array.isArray(value.auditedClaims)) return value.auditedClaims;
15
+ if (Array.isArray(value.claims)) return value.claims;
16
+ if (Array.isArray(value.items)) return value.items;
17
+ }
18
+ return [];
19
+ }
20
+
21
+ function findCandidates(sources) {
22
+ for (const [specId, source] of Object.entries(sources ?? {})) {
23
+ if (specId === "sanitize-claims" || specId.startsWith("sanitize-claims.")) {
24
+ const candidates = asArray(source);
25
+ if (candidates.length > 0) return candidates;
26
+ }
27
+ }
28
+ for (const [specId, source] of Object.entries(sources ?? {})) {
29
+ if (
30
+ specId === "normalize-claims" ||
31
+ specId.startsWith("normalize-claims.")
32
+ ) {
33
+ const candidates = asArray(source);
34
+ if (candidates.length > 0) return candidates;
35
+ }
36
+ }
37
+ return [];
38
+ }
39
+
40
+ function findAuditedClaims(sources) {
41
+ for (const [specId, source] of Object.entries(sources ?? {})) {
42
+ if (specId === "audit-claims" || specId.startsWith("audit-claims.")) {
43
+ const claims = asArray(source);
44
+ if (claims.length > 0) return claims;
45
+ }
46
+ }
47
+ return [];
48
+ }
49
+
50
+ function findAuditSource(sources) {
51
+ for (const [specId, source] of Object.entries(sources ?? {})) {
52
+ if (specId === "audit-claims" || specId.startsWith("audit-claims."))
53
+ return source && typeof source === "object" ? source : {};
54
+ }
55
+ return {};
56
+ }
57
+
58
+ function issueCount(audit, field) {
59
+ const fromSummary = Number(audit?.gateSummary?.[field] ?? 0);
60
+ const fromArray = Array.isArray(audit?.[field]) ? audit[field].length : 0;
61
+ return Math.max(Number.isFinite(fromSummary) ? fromSummary : 0, fromArray);
62
+ }
63
+
64
+ function auditIntegrityBlockers(audit) {
65
+ return [
66
+ ["audit_invalid_verifier_rows", issueCount(audit, "invalidVerifierRows")],
67
+ [
68
+ "audit_missing_verifier_results",
69
+ issueCount(audit, "missingVerifierResults"),
70
+ ],
71
+ [
72
+ "audit_duplicate_verifier_rows",
73
+ issueCount(audit, "duplicateVerifierRows"),
74
+ ],
75
+ [
76
+ "audit_duplicate_status_conflicts",
77
+ issueCount(audit, "duplicateStatusConflicts"),
78
+ ],
79
+ [
80
+ "audit_invalid_normalized_candidates",
81
+ issueCount(audit, "invalidNormalizedCandidates"),
82
+ ],
83
+ [
84
+ "audit_source_ref_join_failures",
85
+ issueCount(audit, "sourceRefJoinFailures"),
86
+ ],
87
+ ]
88
+ .filter(([, count]) => count > 0)
89
+ .map(([reason, count]) => ({ reason, count }));
90
+ }
91
+
92
+ function candidateId(candidate, index) {
93
+ const id = typeof candidate?.id === "string" ? candidate.id.trim() : "";
94
+ return id || `candidate-${String(index + 1).padStart(3, "0")}`;
95
+ }
96
+
97
+ function hasExactQuantitativeText(candidate) {
98
+ return /\b\d+(?:\.\d+)?\b|\b(v\d+(?:\.\d+)*)\b|\b\d+\s*(?:ms|s|min|hour|day|%|percent|usd|dollars?|tokens?|kb|mb|gb)\b/iu.test(
99
+ String(candidate?.claim ?? ""),
100
+ );
101
+ }
102
+
103
+ function hasCriticalSlots(candidate) {
104
+ const ids = Array.isArray(candidate?.factSlotIds)
105
+ ? candidate.factSlotIds
106
+ : [];
107
+ return ids.some((id) =>
108
+ /(price|pricing|cost|ttl|limit|version|date|policy|security|numeric|slot-critical)/iu.test(
109
+ String(id),
110
+ ),
111
+ );
112
+ }
113
+
114
+ function hasExactSourceHint(candidate) {
115
+ return (
116
+ Array.isArray(candidate?.sourceEvidenceHints) &&
117
+ candidate.sourceEvidenceHints.some(
118
+ (hint) =>
119
+ typeof hint?.quote === "string" &&
120
+ hint.quote.trim() &&
121
+ (typeof hint.sourceRef === "string" ||
122
+ typeof hint.sourceUrl === "string"),
123
+ )
124
+ );
125
+ }
126
+
127
+ function decide(candidate) {
128
+ const reasonCodes = [];
129
+ if (hasExactQuantitativeText(candidate))
130
+ reasonCodes.push("exact_quantitative");
131
+ if (hasCriticalSlots(candidate)) reasonCodes.push("critical_fact_slot");
132
+ if (!hasExactSourceHint(candidate)) reasonCodes.push("no_exact_source_hint");
133
+ if (reasonCodes.length > 0) {
134
+ return { decision: "would_verify", reasonCodes };
135
+ }
136
+ return {
137
+ decision: "would_skip_shadow_only",
138
+ reasonCodes: ["exact_source_hint_noncritical"],
139
+ };
140
+ }
141
+
142
+ export default async function shadowSelectVerification({ sources }) {
143
+ const audit = findAuditSource(sources);
144
+ const candidates = findCandidates(sources).map((candidate, index) => ({
145
+ candidate,
146
+ id: candidateId(candidate, index),
147
+ }));
148
+ const auditedById = new Map(
149
+ findAuditedClaims(sources).map((claim, index) => [
150
+ candidateId(claim, index),
151
+ claim,
152
+ ]),
153
+ );
154
+ const decisions = candidates.map(({ candidate, id }) => {
155
+ const decision = decide(candidate);
156
+ const audited = auditedById.get(id);
157
+ return {
158
+ id,
159
+ decision: decision.decision,
160
+ reasonCodes: decision.reasonCodes,
161
+ actualStatus:
162
+ typeof audited?.status === "string" ? audited.status : undefined,
163
+ actualVerified: audited?.status === "verified",
164
+ factSlotIds: Array.isArray(candidate.factSlotIds)
165
+ ? [...candidate.factSlotIds]
166
+ : [],
167
+ };
168
+ });
169
+ const wouldSkip = decisions.filter(
170
+ (decision) => decision.decision === "would_skip_shadow_only",
171
+ );
172
+ const wouldVerify = decisions.filter(
173
+ (decision) => decision.decision === "would_verify",
174
+ );
175
+ const skippedButVerified = wouldSkip.filter(
176
+ (decision) => decision.actualVerified,
177
+ );
178
+ const wouldSkipWithoutAudit = wouldSkip.filter(
179
+ (decision) => typeof decision.actualStatus !== "string",
180
+ );
181
+ const blockers = [];
182
+ if (skippedButVerified.length > 0)
183
+ blockers.push({
184
+ reason: "would_skip_verified_claims",
185
+ count: skippedButVerified.length,
186
+ });
187
+ if (wouldSkipWithoutAudit.length > 0)
188
+ blockers.push({
189
+ reason: "would_skip_without_audit_result",
190
+ count: wouldSkipWithoutAudit.length,
191
+ });
192
+ blockers.push(...auditIntegrityBlockers(audit));
193
+ if (wouldSkip.length === 0)
194
+ blockers.push({ reason: "no_shadow_skip_candidates", count: 0 });
195
+ const realSkipReadiness = {
196
+ status: blockers.length === 0 ? "eligible_for_canary" : "blocked",
197
+ realSkippingEnabled: false,
198
+ adopted: false,
199
+ canaryRequired: true,
200
+ reason:
201
+ blockers.length === 0
202
+ ? "Shadow selector found skip candidates with no verified or missing-audit rows; real skipping still requires a non-holdout canary."
203
+ : "Real selective verification is blocked until shadow decisions prove no verified or unaudited claims would be skipped.",
204
+ blockers,
205
+ };
206
+ return {
207
+ schema: "deep-research-verification-shadow-selector-v1",
208
+ digest: `${wouldSkip.length} would-skip shadow candidate(s), ${wouldVerify.length} would-verify candidate(s); real skipping disabled`,
209
+ realSkippingEnabled: false,
210
+ candidateCount: decisions.length,
211
+ summary: {
212
+ wouldSkip: wouldSkip.length,
213
+ wouldVerify: wouldVerify.length,
214
+ skippedButVerified: skippedButVerified.length,
215
+ wouldSkipWithoutAudit: wouldSkipWithoutAudit.length,
216
+ skippedCritical: wouldSkip.filter((decision) =>
217
+ decision.reasonCodes.includes("critical_fact_slot"),
218
+ ).length,
219
+ shadowOnly: true,
220
+ },
221
+ realSkipReadiness,
222
+ decisions,
223
+ w8FastProfilePrerequisite: {
224
+ status: "not_met",
225
+ reason:
226
+ "selective verification is shadow-only; no verified cost/speed reduction is available to package as an opt-in fast profile",
227
+ },
228
+ };
229
+ }
@@ -0,0 +1,77 @@
1
+ // Bundle-local compatibility surface for the package verification ontology.
2
+ //
3
+ // Workflow support helpers are bundled from the workflow spec directory, so this
4
+ // dependency-free helper intentionally lives inside the deep-research bundle.
5
+ // Keep it in semantic parity with src/verification-ontology.ts.
6
+
7
+ export const VERIFICATION_STATUS = Object.freeze({
8
+ VERIFIED: "verified",
9
+ PARTIALLY_SUPPORTED: "partially_supported",
10
+ UNSUPPORTED: "unsupported",
11
+ CONFLICTING: "conflicting",
12
+ VERIFICATION_BLOCKED: "verification_blocked",
13
+ UNVERIFIED: "unverified",
14
+ });
15
+
16
+ export const VERIFICATION_STATUS_VALUES = Object.freeze([
17
+ VERIFICATION_STATUS.VERIFIED,
18
+ VERIFICATION_STATUS.PARTIALLY_SUPPORTED,
19
+ VERIFICATION_STATUS.UNSUPPORTED,
20
+ VERIFICATION_STATUS.CONFLICTING,
21
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED,
22
+ ]);
23
+
24
+ export const VERIFICATION_STATUS_BUCKETS = Object.freeze({
25
+ [VERIFICATION_STATUS.VERIFIED]: "verified",
26
+ [VERIFICATION_STATUS.PARTIALLY_SUPPORTED]: "partiallySupported",
27
+ [VERIFICATION_STATUS.UNSUPPORTED]: "unsupported",
28
+ [VERIFICATION_STATUS.CONFLICTING]: "conflicting",
29
+ [VERIFICATION_STATUS.VERIFICATION_BLOCKED]: "verificationBlocked",
30
+ });
31
+
32
+ export const VERIFICATION_STATUS_LABELS = Object.freeze({
33
+ [VERIFICATION_STATUS.VERIFIED]: "verified",
34
+ [VERIFICATION_STATUS.PARTIALLY_SUPPORTED]: "partially supported",
35
+ [VERIFICATION_STATUS.UNSUPPORTED]: "unsupported",
36
+ [VERIFICATION_STATUS.CONFLICTING]: "conflicting",
37
+ [VERIFICATION_STATUS.VERIFICATION_BLOCKED]: "verification blocked",
38
+ [VERIFICATION_STATUS.UNVERIFIED]: "unverified",
39
+ });
40
+
41
+ export function canonicalVerificationStatus(status) {
42
+ const text = String(status ?? "").trim();
43
+ if (!text) return VERIFICATION_STATUS.UNVERIFIED;
44
+ if (text === "partiallySupported")
45
+ return VERIFICATION_STATUS.PARTIALLY_SUPPORTED;
46
+ if (text === "verificationBlocked" || text === "blocked")
47
+ return VERIFICATION_STATUS.VERIFICATION_BLOCKED;
48
+ return Object.values(VERIFICATION_STATUS).includes(text)
49
+ ? text
50
+ : VERIFICATION_STATUS.UNVERIFIED;
51
+ }
52
+
53
+ export function verificationStatusBucket(status) {
54
+ return (
55
+ VERIFICATION_STATUS_BUCKETS[canonicalVerificationStatus(status)] ?? "other"
56
+ );
57
+ }
58
+
59
+ export function isVerifiedStatus(status) {
60
+ return canonicalVerificationStatus(status) === VERIFICATION_STATUS.VERIFIED;
61
+ }
62
+
63
+ export function isVerificationBlockedStatus(status) {
64
+ return (
65
+ canonicalVerificationStatus(status) ===
66
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED
67
+ );
68
+ }
69
+
70
+ export function isNonVerifiedTerminalStatus(status) {
71
+ return [
72
+ VERIFICATION_STATUS.PARTIALLY_SUPPORTED,
73
+ VERIFICATION_STATUS.UNSUPPORTED,
74
+ VERIFICATION_STATUS.CONFLICTING,
75
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED,
76
+ ].includes(canonicalVerificationStatus(status));
77
+ }
@@ -24,13 +24,14 @@
24
24
  "sourceUrls": { "type": "array", "items": { "type": "string" } },
25
25
  "claimSummary": {
26
26
  "type": "object",
27
- "required": ["total", "verified", "partially_supported", "unsupported", "conflicting"],
27
+ "required": ["total", "verified", "partially_supported", "unsupported", "conflicting", "verification_blocked"],
28
28
  "properties": {
29
29
  "total": { "type": "number" },
30
30
  "verified": { "type": "number" },
31
31
  "partially_supported": { "type": "number" },
32
32
  "unsupported": { "type": "number" },
33
- "conflicting": { "type": "number" }
33
+ "conflicting": { "type": "number" },
34
+ "verification_blocked": { "type": "number" }
34
35
  },
35
36
  "additionalProperties": true
36
37
  },
@@ -56,7 +57,6 @@
56
57
  },
57
58
  "auditArtifact": { "type": "string" },
58
59
  "sidecarPath": { "type": "string" },
59
- "reportSidecarPath": { "type": "string" },
60
60
  "auditSidecarPath": { "type": "string" },
61
61
  "reportMarkdown": { "type": "string" },
62
62
  "auditMarkdown": { "type": "string" },