@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
@@ -0,0 +1,136 @@
1
+ // Deterministic verification candidate batch planner for deep-research.
2
+ //
3
+ // This helper is intentionally planning-only: it groups sanitized verification
4
+ // candidates into stable batch records but does not change verifier semantics or
5
+ // skip single-claim verification. A later workflow can consume these batches only
6
+ // after per-claim result identity and fallback gates pass.
7
+
8
+ function asArray(value) {
9
+ if (Array.isArray(value)) return value;
10
+ if (value && typeof value === "object") {
11
+ if (Array.isArray(value.claimInventory?.verificationCandidates))
12
+ return value.claimInventory.verificationCandidates;
13
+ if (Array.isArray(value.verificationCandidates))
14
+ return value.verificationCandidates;
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 stableId(value, fallback) {
41
+ const id = typeof value?.id === "string" ? value.id.trim() : "";
42
+ return id || fallback;
43
+ }
44
+
45
+ function sourceKey(candidate) {
46
+ const refs = Array.isArray(candidate?.sourceRefs)
47
+ ? candidate.sourceRefs.filter(
48
+ (ref) => typeof ref === "string" && ref.trim(),
49
+ )
50
+ : [];
51
+ if (refs.length > 0) return `refs:${refs.slice().sort().join("|")}`;
52
+ const urls = Array.isArray(candidate?.sourceUrls)
53
+ ? candidate.sourceUrls.filter(
54
+ (url) => typeof url === "string" && url.trim(),
55
+ )
56
+ : [];
57
+ if (urls.length > 0) return `urls:${urls.slice().sort().join("|")}`;
58
+ return "refs:none";
59
+ }
60
+
61
+ function normalizeMaxBatchSize(value) {
62
+ const parsed = Number(value ?? 2);
63
+ if (!Number.isInteger(parsed) || parsed < 1) return 2;
64
+ return Math.min(parsed, 4);
65
+ }
66
+
67
+ function cloneCandidate(candidate, id) {
68
+ return {
69
+ ...candidate,
70
+ id,
71
+ ...(Array.isArray(candidate.sourceRefs)
72
+ ? { sourceRefs: [...candidate.sourceRefs] }
73
+ : {}),
74
+ ...(Array.isArray(candidate.sourceUrls)
75
+ ? { sourceUrls: [...candidate.sourceUrls] }
76
+ : {}),
77
+ ...(Array.isArray(candidate.sourceEvidenceHints)
78
+ ? {
79
+ sourceEvidenceHints: candidate.sourceEvidenceHints.map((hint) => ({
80
+ ...hint,
81
+ })),
82
+ }
83
+ : {}),
84
+ };
85
+ }
86
+
87
+ export default async function batchVerificationCandidates({
88
+ sources,
89
+ options = {},
90
+ }) {
91
+ const maxBatchSize = normalizeMaxBatchSize(options.maxBatchSize);
92
+ const rawCandidates = findCandidates(sources);
93
+ const candidates = rawCandidates
94
+ .map((candidate, index) => ({
95
+ candidate,
96
+ id: stableId(
97
+ candidate,
98
+ `candidate-${String(index + 1).padStart(3, "0")}`,
99
+ ),
100
+ index,
101
+ }))
102
+ .sort((left, right) => left.id.localeCompare(right.id));
103
+
104
+ const groups = new Map();
105
+ for (const item of candidates) {
106
+ const key = sourceKey(item.candidate);
107
+ const group = groups.get(key) ?? [];
108
+ group.push(item);
109
+ groups.set(key, group);
110
+ }
111
+
112
+ const batches = [];
113
+ for (const [key, items] of [...groups.entries()].sort(([a], [b]) =>
114
+ a.localeCompare(b),
115
+ )) {
116
+ for (let offset = 0; offset < items.length; offset += maxBatchSize) {
117
+ const slice = items.slice(offset, offset + maxBatchSize);
118
+ const claimIds = slice.map((item) => item.id);
119
+ batches.push({
120
+ id: `vbatch-${String(batches.length + 1).padStart(3, "0")}`,
121
+ sourceKey: key,
122
+ claimIds,
123
+ claims: slice.map((item) => cloneCandidate(item.candidate, item.id)),
124
+ });
125
+ }
126
+ }
127
+
128
+ return {
129
+ schema: "deep-research-verification-batches-v1",
130
+ digest: `${batches.length} verification batch(es), ${candidates.length} candidate(s), maxBatchSize=${maxBatchSize}`,
131
+ maxBatchSize,
132
+ candidateCount: candidates.length,
133
+ batchCount: batches.length,
134
+ batches,
135
+ };
136
+ }
@@ -1,3 +1,9 @@
1
+ import {
2
+ VERIFICATION_STATUS,
3
+ VERIFICATION_STATUS_BUCKETS,
4
+ canonicalVerificationStatus,
5
+ } from "./verification-ontology.mjs";
6
+
1
7
  // Deterministic claim audit for deep-research.
2
8
  //
3
9
  // Sources: plan (optional), normalize-claims (optional), verify-claims foreach
@@ -16,10 +22,6 @@ function asArray(value) {
16
22
  if (Array.isArray(value)) return value;
17
23
  if (value && typeof value === "object") {
18
24
  if (Array.isArray(value.auditedClaims)) return value.auditedClaims;
19
- if (Array.isArray(value.claims)) return value.claims;
20
- if (Array.isArray(value.claimVerdicts)) return value.claimVerdicts;
21
- if (Array.isArray(value.verdicts)) return value.verdicts;
22
- if (Array.isArray(value.items)) return value.items;
23
25
  if (
24
26
  "status" in value ||
25
27
  "verdict" in value ||
@@ -28,6 +30,11 @@ function asArray(value) {
28
30
  "id" in value
29
31
  )
30
32
  return [value];
33
+ if (Array.isArray(value.results)) return value.results;
34
+ if (Array.isArray(value.claims)) return value.claims;
35
+ if (Array.isArray(value.claimVerdicts)) return value.claimVerdicts;
36
+ if (Array.isArray(value.verdicts)) return value.verdicts;
37
+ if (Array.isArray(value.items)) return value.items;
31
38
  return Object.values(value).flatMap(asArray);
32
39
  }
33
40
  return [];
@@ -82,6 +89,38 @@ function collectEvidenceRefs(claim) {
82
89
  return refs;
83
90
  }
84
91
 
92
+ function addLocalEvidenceRef(refs, value) {
93
+ if (typeof value !== "string") return;
94
+ const text = value.trim();
95
+ if (!text || /^https?:\/\//i.test(text) || isWorkflowSourceRef(text)) return;
96
+ if (looksLikeLocalSourceRef(text)) refs.add(text);
97
+ }
98
+
99
+ function collectLocalEvidenceRefs(claim) {
100
+ const refs = new Set();
101
+ if (!claim || typeof claim !== "object") return refs;
102
+ for (const key of ["file", "path", "repoPath", "localPath", "sourceRef"]) {
103
+ addLocalEvidenceRef(refs, claim[key]);
104
+ }
105
+ for (const value of Array.isArray(claim.sourceRefs) ? claim.sourceRefs : []) {
106
+ addLocalEvidenceRef(refs, value);
107
+ }
108
+ for (const row of Array.isArray(claim.evidence) ? claim.evidence : []) {
109
+ if (!row || typeof row !== "object") continue;
110
+ for (const key of [
111
+ "file",
112
+ "path",
113
+ "repoPath",
114
+ "localPath",
115
+ "source",
116
+ "sourceRef",
117
+ ]) {
118
+ addLocalEvidenceRef(refs, row[key]);
119
+ }
120
+ }
121
+ return refs;
122
+ }
123
+
85
124
  function collectWorkflowSourceRefs(value, refs = new Set()) {
86
125
  if (typeof value === "string") {
87
126
  for (const match of value.matchAll(/\bwsrc_[a-f0-9]{32}\b/g))
@@ -127,9 +166,11 @@ function canonicalUrlKeys(value) {
127
166
  url.hash = "";
128
167
  const serialized = stripCitationUrlPunctuation(url.toString());
129
168
  keys.add(serialized);
169
+ addNpmDocsVersionAgnosticKey(keys, url);
130
170
  if (url.pathname !== "/" && url.pathname.endsWith("/")) {
131
171
  url.pathname = url.pathname.replace(/\/+$/u, "");
132
172
  keys.add(stripCitationUrlPunctuation(url.toString()));
173
+ addNpmDocsVersionAgnosticKey(keys, url);
133
174
  }
134
175
  } catch {
135
176
  // Keep the trimmed raw URL key only; malformed strings should not throw from
@@ -138,6 +179,17 @@ function canonicalUrlKeys(value) {
138
179
  return [...keys].filter(Boolean);
139
180
  }
140
181
 
182
+ function addNpmDocsVersionAgnosticKey(keys, url) {
183
+ if (url.hostname !== "docs.npmjs.com") return;
184
+ if (!/^\/cli\/(?:v\d+\/)?using-npm\//u.test(url.pathname)) return;
185
+ const versionless = new URL(url.toString());
186
+ versionless.pathname = versionless.pathname.replace(
187
+ /^\/cli\/v\d+\//u,
188
+ "/cli/",
189
+ );
190
+ keys.add(stripCitationUrlPunctuation(versionless.toString()));
191
+ }
192
+
141
193
  function addUrlSourceRef(urlToSourceRef, url, sourceRef) {
142
194
  if (!isWorkflowSourceRef(sourceRef)) return;
143
195
  for (const key of canonicalUrlKeys(url)) {
@@ -152,13 +204,6 @@ function buildUrlSourceRefLookup(normalizeInputPacket) {
152
204
  if (!source || typeof source !== "object") continue;
153
205
  addUrlSourceRef(urlToSourceRef, source.url, source.sourceRef);
154
206
  }
155
- const sourceRefIndex = asArray(
156
- normalizeInputPacket?.packet?.research?.sourceRefIndex,
157
- );
158
- for (const source of sourceRefIndex) {
159
- if (!source || typeof source !== "object") continue;
160
- addUrlSourceRef(urlToSourceRef, source.url, source.sourceRef);
161
- }
162
207
  return urlToSourceRef;
163
208
  }
164
209
 
@@ -241,19 +286,19 @@ function strongEvidenceIssue(claim) {
241
286
 
242
287
  function hasExactQuantitativeClaim(value) {
243
288
  const text = JSON.stringify(value ?? "");
244
- return /\b\d+(?:\.\d+)?\s*(?:%|percent|ms|s|sec|seconds|minutes|hours|x|×|usd|\$|k|m|b|tokens?|users?|samples?|n\s*=)\b/i.test(
289
+ return /\b\d+(?:\.\d+)?\s*(?:(?:%|×|\$|n\s*=)|(?:percent|ms|s|sec|seconds|minutes|hours|x|usd|k|m|b|tokens?|users?|samples?)\b)/i.test(
245
290
  text,
246
291
  );
247
292
  }
248
293
 
249
294
  function verdictOf(claim) {
250
- return (
295
+ const status =
251
296
  claim?.status ??
252
297
  claim?.verdict ??
253
298
  claim?.verdictDigest?.status ??
254
299
  claim?.verdictDigest?.verdict ??
255
- "unverified"
256
- );
300
+ "unverified";
301
+ return canonicalVerifierStatus(status);
257
302
  }
258
303
 
259
304
  function withVerdict(claim, verdict, reason, details = {}) {
@@ -307,40 +352,133 @@ function compactStrings(values) {
307
352
  }
308
353
 
309
354
  function canonicalVerifierStatus(status) {
310
- return status === "partiallySupported" ? "partially_supported" : status;
355
+ return canonicalVerificationStatus(status);
311
356
  }
312
357
 
313
358
  function conservativeVerifierStatus(statuses) {
314
359
  const normalized = statuses.map(canonicalVerifierStatus);
315
360
  for (const status of [
316
- "conflicting",
317
- "unsupported",
318
- "partially_supported",
319
- "unverified",
361
+ VERIFICATION_STATUS.CONFLICTING,
362
+ VERIFICATION_STATUS.UNSUPPORTED,
363
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED,
364
+ VERIFICATION_STATUS.PARTIALLY_SUPPORTED,
365
+ VERIFICATION_STATUS.UNVERIFIED,
320
366
  ]) {
321
367
  if (normalized.includes(status)) return status;
322
368
  }
323
- if (normalized.every((status) => status === "verified")) return "verified";
369
+ if (normalized.every((status) => status === VERIFICATION_STATUS.VERIFIED))
370
+ return VERIFICATION_STATUS.VERIFIED;
324
371
  return (
325
372
  normalized.find((status) => typeof status === "string" && status) ??
326
- "unverified"
373
+ VERIFICATION_STATUS.UNVERIFIED
327
374
  );
328
375
  }
329
376
 
330
- function issueForVerifierRow({ sourceId, claim, reason, claimId, index }) {
377
+ function issueForVerifierRow({
378
+ sourceId,
379
+ claim,
380
+ reason,
381
+ claimId,
382
+ index,
383
+ ...details
384
+ }) {
331
385
  return {
332
386
  sourceId,
333
387
  ...(Number.isInteger(index) ? { index } : {}),
334
388
  ...(claimId ? { claimId } : {}),
389
+ ...details,
335
390
  reason,
336
391
  status: verdictOf(claim),
337
392
  nextStep:
338
393
  reason === "unknown_claim_id"
339
394
  ? "Verify-claims output did not match any normalized verification candidate; quarantine it from claim counts."
340
- : "Verifier output is missing a usable string id/claimId; rerun or repair the verifier row before counting it.",
395
+ : reason === "batch_result_id_not_in_source_batch"
396
+ ? "Verifier batch output included a claim id outside the source batch; rerun or repair the batch before counting any row."
397
+ : reason === "unknown_verification_batch_id"
398
+ ? "Verifier batch output came from an unknown batch id; rerun or repair the batch before counting any row."
399
+ : "Verifier output is missing a usable string id/claimId; rerun or repair the verifier row before counting it.",
341
400
  };
342
401
  }
343
402
 
403
+ function asBatchArray(value) {
404
+ if (Array.isArray(value?.batches)) return value.batches;
405
+ if (Array.isArray(value)) return value;
406
+ return [];
407
+ }
408
+
409
+ function buildBatchMembershipById(verificationBatches) {
410
+ const batches = new Map();
411
+ for (const batch of asBatchArray(verificationBatches)) {
412
+ const id = typeof batch?.id === "string" ? batch.id.trim() : "";
413
+ if (!id) continue;
414
+ const claimIds = Array.isArray(batch.claimIds)
415
+ ? batch.claimIds
416
+ : Array.isArray(batch.claims)
417
+ ? batch.claims.map(
418
+ (claim, index) =>
419
+ claimIdOf(claim).id ??
420
+ `candidate-${String(index + 1).padStart(3, "0")}`,
421
+ )
422
+ : [];
423
+ batches.set(
424
+ id,
425
+ new Set(
426
+ claimIds
427
+ .filter((claimId) => typeof claimId === "string")
428
+ .map((claimId) => claimId.trim())
429
+ .filter(Boolean),
430
+ ),
431
+ );
432
+ }
433
+ return batches;
434
+ }
435
+
436
+ function verifierBatchId(sourceId) {
437
+ const prefix = "verify-claims.";
438
+ if (typeof sourceId !== "string" || !sourceId.startsWith(prefix)) return null;
439
+ const id = sourceId.slice(prefix.length).trim();
440
+ return id || null;
441
+ }
442
+
443
+ function buildBatchIdBySourceName(sourceStatuses) {
444
+ const bySource = new Map();
445
+ for (const status of Array.isArray(sourceStatuses) ? sourceStatuses : []) {
446
+ const source = typeof status?.source === "string" ? status.source : "";
447
+ const batchId = verifierBatchId(status?.specId);
448
+ if (source && batchId) bySource.set(source, batchId);
449
+ }
450
+ return bySource;
451
+ }
452
+
453
+ function batchMembershipIssue({
454
+ sourceId,
455
+ claimId,
456
+ batchMembershipById,
457
+ batchIdBySourceName,
458
+ }) {
459
+ if (!(batchMembershipById instanceof Map) || batchMembershipById.size === 0)
460
+ return null;
461
+ const batchId =
462
+ verifierBatchId(sourceId) ?? batchIdBySourceName?.get(sourceId);
463
+ if (!batchId) return null;
464
+ const expectedClaimIds = batchMembershipById.get(batchId);
465
+ if (!expectedClaimIds) {
466
+ return {
467
+ reason: "unknown_verification_batch_id",
468
+ batchId,
469
+ expectedBatchIds: [...batchMembershipById.keys()],
470
+ };
471
+ }
472
+ if (!expectedClaimIds.has(claimId)) {
473
+ return {
474
+ reason: "batch_result_id_not_in_source_batch",
475
+ batchId,
476
+ expectedClaimIds: [...expectedClaimIds],
477
+ };
478
+ }
479
+ return null;
480
+ }
481
+
344
482
  function gapForVerifierIssue(issue) {
345
483
  return {
346
484
  ...(issue.claimId ? { claimId: issue.claimId } : {}),
@@ -399,12 +537,33 @@ function mergeVerifierRows(rows) {
399
537
  };
400
538
  }
401
539
 
402
- const STATUS_BUCKETS = {
403
- verified: "verified",
404
- partially_supported: "partiallySupported",
405
- unsupported: "unsupported",
406
- conflicting: "conflicting",
407
- };
540
+ function buildBatchAdoptionReadiness({ gateSummary, candidateCount }) {
541
+ const checks = [
542
+ ["invalid_verifier_rows", gateSummary.invalidVerifierRows],
543
+ ["missing_verifier_results", gateSummary.missingVerifierResults],
544
+ ["duplicate_verifier_rows", gateSummary.duplicateVerifierRows],
545
+ ["duplicate_status_conflicts", gateSummary.duplicateStatusConflicts],
546
+ ["invalid_normalized_candidates", gateSummary.invalidNormalizedCandidates],
547
+ ["source_ref_join_failures", gateSummary.sourceRefJoinFailures],
548
+ ];
549
+ const blockers = checks
550
+ .filter(([, count]) => Number(count ?? 0) > 0)
551
+ .map(([reason, count]) => ({ reason, count }));
552
+ if (candidateCount === 0)
553
+ blockers.push({ reason: "no_verification_candidates", count: 0 });
554
+ return {
555
+ status: blockers.length === 0 ? "eligible_for_canary" : "blocked",
556
+ adopted: false,
557
+ canaryRequired: true,
558
+ reason:
559
+ blockers.length === 0
560
+ ? "Verifier identity/sourceRef integrity is clean; batch adoption still requires a non-holdout canary before use."
561
+ : "Batch adoption is blocked until verifier identity/sourceRef integrity issues are resolved.",
562
+ blockers,
563
+ };
564
+ }
565
+
566
+ const STATUS_BUCKETS = VERIFICATION_STATUS_BUCKETS;
408
567
 
409
568
  function findSource(sources, stageId) {
410
569
  for (const [specId, source] of Object.entries(sources ?? {})) {
@@ -413,13 +572,18 @@ function findSource(sources, stageId) {
413
572
  return null;
414
573
  }
415
574
 
416
- export default async function claimEvidenceGate({ sources, options = {} }) {
575
+ export default async function claimEvidenceGate({
576
+ sources,
577
+ options = {},
578
+ context = {},
579
+ }) {
417
580
  const plan = findSource(sources, "plan");
418
581
  const normalizeClaims = findSource(sources, "normalize-claims");
419
- const sanitizedCandidates =
420
- findSource(sources, "sanitize-claims") ??
421
- findSource(sources, "sanitize-verification-candidates");
582
+ const sanitizedCandidates = findSource(sources, "sanitize-claims");
422
583
  const normalized = sanitizedCandidates ?? normalizeClaims;
584
+ const verificationBatches = findSource(sources, "verification-batches");
585
+ const batchMembershipById = buildBatchMembershipById(verificationBatches);
586
+ const batchIdBySourceName = buildBatchIdBySourceName(context.sourceStatuses);
423
587
  const normalizeInputPacket = findSource(sources, "normalize-input-packet");
424
588
  const urlToSourceRef = buildUrlSourceRefLookup(normalizeInputPacket);
425
589
  const candidateRecords = [];
@@ -474,8 +638,7 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
474
638
  !specId.startsWith("plan") &&
475
639
  !specId.startsWith("normalize-claims") &&
476
640
  !specId.startsWith("normalize-input-packet") &&
477
- !specId.startsWith("sanitize-claims") &&
478
- !specId.startsWith("sanitize-verification-candidates"),
641
+ !specId.startsWith("sanitize-claims"),
479
642
  )
480
643
  .flatMap(([sourceId, source]) =>
481
644
  asArray(source).map((claim, index) => ({ sourceId, claim, index })),
@@ -533,6 +696,25 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
533
696
  gateSummary.invalidVerifierRows += 1;
534
697
  continue;
535
698
  }
699
+ const batchIssue = batchMembershipIssue({
700
+ sourceId,
701
+ claimId: idCheck.id,
702
+ batchMembershipById,
703
+ batchIdBySourceName,
704
+ });
705
+ if (batchIssue) {
706
+ const issue = issueForVerifierRow({
707
+ sourceId,
708
+ claim,
709
+ index,
710
+ claimId: idCheck.id,
711
+ ...batchIssue,
712
+ });
713
+ invalidVerifierRows.push(issue);
714
+ remainingGaps.push(gapForVerifierIssue(issue));
715
+ gateSummary.invalidVerifierRows += 1;
716
+ continue;
717
+ }
536
718
  const row = {
537
719
  sourceId,
538
720
  claimId: idCheck.id,
@@ -558,6 +740,10 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
558
740
  if (!claim || typeof claim !== "object") return;
559
741
  gateSummary.total += 1;
560
742
  const evidenceRefs = [...collectEvidenceRefs(claim)];
743
+ const localEvidenceRefs = new Set([
744
+ ...collectLocalEvidenceRefs(claim),
745
+ ...collectLocalEvidenceRefs(candidate),
746
+ ]);
561
747
  const workflowSourceRefs = new Set([...collectWorkflowSourceRefs(claim)]);
562
748
  const exactQuantitative = hasExactQuantitativeClaim(claim);
563
749
  const fetched = hasFetchedEvidence(claim);
@@ -627,6 +813,7 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
627
813
  claimId &&
628
814
  candidate &&
629
815
  workflowSourceRefs.size === 0 &&
816
+ localEvidenceRefs.size === 0 &&
630
817
  httpSourceUrls.length > 0
631
818
  ) {
632
819
  const failure = {
@@ -766,6 +953,7 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
766
953
  partiallySupported: [],
767
954
  unsupported: [],
768
955
  conflicting: [],
956
+ verificationBlocked: [],
769
957
  other: [],
770
958
  };
771
959
  for (const claim of auditedClaims) {
@@ -819,11 +1007,16 @@ export default async function claimEvidenceGate({ sources, options = {} }) {
819
1007
  verdictDigest: claim.verdictDigest,
820
1008
  correctionOrCounterclaim: claim.correctionOrCounterclaim,
821
1009
  }));
1010
+ const batchAdoptionReadiness = buildBatchAdoptionReadiness({
1011
+ gateSummary,
1012
+ candidateCount: candidateRecords.length,
1013
+ });
822
1014
 
823
1015
  return {
824
1016
  auditedClaims,
825
1017
  claimDigests,
826
1018
  gateSummary,
1019
+ batchAdoptionReadiness,
827
1020
  remainingGaps,
828
1021
  sourceRefJoinFailures,
829
1022
  invalidVerifierRows,
@@ -252,10 +252,7 @@ function buildSynthesisInput({
252
252
  export default async function finalAuditPacket({ sources }) {
253
253
  const plan = asObject(findSource(sources, "plan"));
254
254
  const normalizeClaims = asObject(findSource(sources, "normalize-claims"));
255
- const sanitizedCandidates = asObject(
256
- findSource(sources, "sanitize-claims") ??
257
- findSource(sources, "sanitize-verification-candidates"),
258
- );
255
+ const sanitizedCandidates = asObject(findSource(sources, "sanitize-claims"));
259
256
  const normalized =
260
257
  Object.keys(sanitizedCandidates).length > 0
261
258
  ? sanitizedCandidates
@@ -157,6 +157,60 @@ function compactGap(gap, sourceId, index) {
157
157
  };
158
158
  }
159
159
 
160
+ function compactBudgetLedger(source, sourceId) {
161
+ const ledger = asObject(source.budgetLedger);
162
+ if (Object.keys(ledger).length === 0) return undefined;
163
+ const searchBudget = Number(ledger.searchBudget);
164
+ const searchCallsUsed = Number(ledger.searchCallsUsed);
165
+ return {
166
+ sourceId,
167
+ question: stringOf(source.question)?.slice(0, 300),
168
+ ...(Number.isFinite(searchBudget) ? { searchBudget } : {}),
169
+ ...(Number.isFinite(searchCallsUsed) ? { searchCallsUsed } : {}),
170
+ searchQueriesAttempted: compactStrings(ledger.searchQueriesAttempted, 12),
171
+ omittedSearchQueries: compactStrings(ledger.omittedSearchQueries, 12),
172
+ budgetExhausted: ledger.budgetExhausted === true,
173
+ gapRecorded: ledger.gapRecorded === true,
174
+ };
175
+ }
176
+
177
+ function sourceStatusesOf(context) {
178
+ return asArray(context?.sourceStatuses).map(asObject);
179
+ }
180
+
181
+ function isResearchQuestionStatus(status) {
182
+ return [status.source, status.specId, status.displayName, status.stageId]
183
+ .map((value) => String(value ?? ""))
184
+ .some(
185
+ (value) =>
186
+ value === "research-questions" ||
187
+ value.startsWith("research-questions."),
188
+ );
189
+ }
190
+
191
+ function compactSourceStatusGap(status, index) {
192
+ const sourceId =
193
+ stringOf(status.specId) ??
194
+ stringOf(status.source) ??
195
+ stringOf(status.displayName) ??
196
+ `research-questions.status-${String(index + 1).padStart(3, "0")}`;
197
+ const detail = compactStrings(
198
+ [status.statusDetail, status.errorType, status.lastMessage],
199
+ 3,
200
+ ).join("; ");
201
+ return {
202
+ originLocator: `${sourceId}.status-gap`,
203
+ sourceId,
204
+ lead: `Research question source ${sourceId} ended with status ${String(status.status ?? "unknown")}${detail ? ` (${detail.slice(0, 300)})` : ""}.`,
205
+ sourceUrls: [],
206
+ sourceRefs: [],
207
+ factSlotIds: [],
208
+ reason: "research_question_non_completed",
209
+ status: String(status.status ?? "unknown"),
210
+ ...(stringOf(status.taskId) ? { taskId: stringOf(status.taskId) } : {}),
211
+ };
212
+ }
213
+
160
214
  function pushBounded(target, overflow, items, limit, overflowKind) {
161
215
  for (const item of items) {
162
216
  if (target.length < limit) target.push(item);
@@ -221,7 +275,7 @@ function isRequiredOrCriticalSlot(slot) {
221
275
  }
222
276
 
223
277
  function looksQuantitative(text) {
224
- return /\b\d+(?:\.\d+)?\s*(?:%|percent|ms|s|sec|seconds|minutes|hours|x|×|usd|\$|k|m|b|tokens?|users?|samples?|n\s*=|gb|mb|tb|requests?|qps|rps|per\s+month|\/month)\b/i.test(
278
+ return /\b\d+(?:\.\d+)?\s*(?:(?:%|×|\$|n\s*=)|(?:percent|ms|s|sec|seconds|minutes|hours|x|usd|k|m|b|tokens?|users?|samples?|gb|mb|tb|requests?|qps|rps|per\s+month|\/month)\b)/i.test(
225
279
  String(text ?? ""),
226
280
  );
227
281
  }
@@ -428,19 +482,21 @@ function buildPrecisionGuard({ claims, planSlots }) {
428
482
  };
429
483
  }
430
484
 
431
- export default async function normalizeInputPacket({ sources }) {
485
+ export default async function normalizeInputPacket({ sources, context } = {}) {
432
486
  const plan = asObject(findSource(sources, "plan"));
433
487
  const research = researchSources(sources);
434
488
  const extractedFacts = [];
435
489
  const claims = [];
436
490
  const sourceCards = [];
437
491
  const evidenceGaps = [];
492
+ const questionBudgetLedger = [];
438
493
  const overflow = {};
439
494
  const limits = {
440
495
  extractedFacts: 240,
441
496
  claims: 240,
442
497
  sources: 160,
443
498
  evidenceGaps: 120,
499
+ questionBudgetLedger: 80,
444
500
  };
445
501
 
446
502
  for (const { sourceId, source } of research) {
@@ -480,7 +536,29 @@ export default async function normalizeInputPacket({ sources }) {
480
536
  limits.evidenceGaps,
481
537
  "omittedEvidenceGaps",
482
538
  );
539
+ const budgetLedger = compactBudgetLedger(source, sourceId);
540
+ if (budgetLedger) {
541
+ pushBounded(
542
+ questionBudgetLedger,
543
+ overflow,
544
+ [budgetLedger],
545
+ limits.questionBudgetLedger,
546
+ "omittedQuestionBudgetLedgers",
547
+ );
548
+ }
483
549
  }
550
+ pushBounded(
551
+ evidenceGaps,
552
+ overflow,
553
+ sourceStatusesOf(context)
554
+ .filter(
555
+ (status) =>
556
+ isResearchQuestionStatus(status) && status.status !== "completed",
557
+ )
558
+ .map(compactSourceStatusGap),
559
+ limits.evidenceGaps,
560
+ "omittedEvidenceGaps",
561
+ );
484
562
 
485
563
  const planSlots = asArray(plan.factSlots).map(compactPlanSlot);
486
564
  const precisionGuard = buildPrecisionGuard({ claims, planSlots });
@@ -506,6 +584,7 @@ export default async function normalizeInputPacket({ sources }) {
506
584
  claims,
507
585
  sources: sourceCards,
508
586
  evidenceGaps,
587
+ questionBudgetLedger,
509
588
  },
510
589
  slotPreservation,
511
590
  precisionGuard,