@agwab/pi-workflow 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -15
- package/agents/researcher.md +17 -7
- package/dist/artifact-graph-runtime.js +1 -0
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +29 -4
- package/dist/dynamic-generated-task-runtime.js +4 -3
- package/dist/dynamic-runtime-bundle.js +3 -2
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +3 -2
- package/dist/extension.js +240 -16
- package/dist/store.js +1 -0
- package/dist/subagent-backend.js +82 -27
- package/dist/tool-metadata.d.ts +1 -0
- package/dist/tool-metadata.js +13 -1
- package/dist/types.d.ts +3 -0
- package/dist/workflow-artifact-extension.js +3 -2
- package/dist/workflow-artifact-tool.js +84 -4
- package/dist/workflow-progress-health.d.ts +37 -0
- package/dist/workflow-progress-health.js +296 -0
- package/dist/workflow-runtime.d.ts +6 -0
- package/dist/workflow-runtime.js +33 -10
- package/dist/workflow-view.d.ts +2 -0
- package/dist/workflow-view.js +97 -18
- package/dist/workflow-web-source-extension.d.ts +43 -0
- package/dist/workflow-web-source-extension.js +1194 -0
- package/dist/workflow-web-source.d.ts +171 -0
- package/dist/workflow-web-source.js +915 -0
- package/docs/usage.md +32 -18
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
- package/package.json +7 -7
- package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
- package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
- package/src/artifact-graph-runtime.ts +1 -0
- package/src/compiler.ts +43 -3
- package/src/dynamic-generated-task-runtime.ts +4 -2
- package/src/dynamic-runtime-bundle.ts +3 -2
- package/src/engine.ts +7 -16
- package/src/extension.ts +299 -22
- package/src/store.ts +1 -0
- package/src/subagent-backend.ts +121 -37
- package/src/tool-metadata.ts +22 -1
- package/src/types.ts +4 -0
- package/src/workflow-artifact-extension.ts +3 -2
- package/src/workflow-artifact-tool.ts +96 -4
- package/src/workflow-progress-health.ts +461 -0
- package/src/workflow-runtime.ts +50 -13
- package/src/workflow-view.ts +186 -41
- package/src/workflow-web-source-extension.ts +1411 -0
- package/src/workflow-web-source.ts +1294 -0
- package/workflows/README.md +1 -1
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +552 -44
- package/workflows/deep-research/helpers/final-audit-packet.mjs +396 -0
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +545 -0
- package/workflows/deep-research/helpers/render-executive.mjs +1199 -192
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +37 -8
- package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
- package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
- package/workflows/deep-research/spec.json +71 -26
- package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
- package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
- package/workflows/deep-review/spec.json +22 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// Deterministic sanitizer between normalize-claims and verify-claims.
|
|
2
|
+
//
|
|
3
|
+
// This helper does not decide truth. It only keeps verifier fanout focused on
|
|
4
|
+
// source-stated, source-locatable factual claims and preserves demoted material
|
|
5
|
+
// as explicit coverage gaps/backlog rows for final synthesis. The goal is to
|
|
6
|
+
// avoid spending verifier budget on workflow-context metadata, evidence-gap
|
|
7
|
+
// statements, and synthesized recommendations that are better represented as
|
|
8
|
+
// gaps or caveated guidance.
|
|
9
|
+
|
|
10
|
+
const SCHEMA = "deep-research-verification-candidate-sanitizer-v1";
|
|
11
|
+
const VERIFIER_INPUT_POLICY =
|
|
12
|
+
"use_sourceRefs_or_sourceUrls_only_do_not_call_workflow_artifact";
|
|
13
|
+
|
|
14
|
+
function asArray(value) {
|
|
15
|
+
return Array.isArray(value) ? value : [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asObject(value) {
|
|
19
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value
|
|
21
|
+
: {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stringOf(value) {
|
|
25
|
+
return typeof value === "string" ? value.trim() : "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compactStrings(values, limit = 12) {
|
|
29
|
+
if (!Array.isArray(values)) return [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
const text = stringOf(value);
|
|
34
|
+
if (!text || seen.has(text)) continue;
|
|
35
|
+
seen.add(text);
|
|
36
|
+
out.push(text);
|
|
37
|
+
if (out.length >= limit) break;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findSource(sources, stageId) {
|
|
43
|
+
for (const [specId, source] of Object.entries(sources ?? {})) {
|
|
44
|
+
if (specId === stageId || specId.startsWith(`${stageId}.`)) return source;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function claimText(candidate) {
|
|
50
|
+
return stringOf(candidate?.claim ?? candidate?.text ?? candidate?.statement);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function candidateId(candidate) {
|
|
54
|
+
return stringOf(candidate?.id ?? candidate?.claimId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sourceRefs(candidate) {
|
|
58
|
+
return compactStrings(candidate?.sourceRefs, 16);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sourceUrls(candidate) {
|
|
62
|
+
return compactStrings(candidate?.sourceUrls, 16);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function localEvidenceRefs(candidate) {
|
|
66
|
+
const refs = [];
|
|
67
|
+
for (const key of ["file", "path", "repoPath", "localPath"]) {
|
|
68
|
+
const value = stringOf(candidate?.[key]);
|
|
69
|
+
if (value) refs.push(value);
|
|
70
|
+
}
|
|
71
|
+
for (const row of asArray(candidate?.evidence)) {
|
|
72
|
+
for (const key of ["file", "path", "source", "sourceRef"]) {
|
|
73
|
+
const value = stringOf(row?.[key]);
|
|
74
|
+
if (value && !/^https?:\/\//i.test(value)) refs.push(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return compactStrings(refs, 8);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasSourceLocator(candidate) {
|
|
81
|
+
return (
|
|
82
|
+
sourceRefs(candidate).length > 0 ||
|
|
83
|
+
sourceUrls(candidate).length > 0 ||
|
|
84
|
+
localEvidenceRefs(candidate).length > 0
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function matchesAny(text, patterns) {
|
|
89
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tokenSet(value) {
|
|
93
|
+
return new Set(
|
|
94
|
+
String(value ?? "")
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.match(/[a-z0-9][a-z0-9_-]{2,}/g) ?? [],
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function setIntersectionCount(left, right) {
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const value of left) if (right.has(value)) count += 1;
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isSyntheticEvidenceText(value) {
|
|
107
|
+
return /(?:^|[^a-z0-9])(?:synthesis|synthesized|derived|inference)(?:$|[^a-z0-9])/i.test(
|
|
108
|
+
String(value ?? ""),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function weakEvidenceHintText(value) {
|
|
113
|
+
return matchesAny(String(value ?? ""), [
|
|
114
|
+
/\bexact\b[^.]{0,80}\b(?:quote|wording|text)\b[^.]{0,80}\b(?:not|unavailable|missing|limited|could not|was not)\b/i,
|
|
115
|
+
/\b(?:quote|wording|text)\b[^.]{0,80}\b(?:not|unavailable|missing|limited|could not|was not)\b/i,
|
|
116
|
+
/\b(?:cite cautiously|requires? assumptions?|implementation[- ]specific|not direct evidence|not direct support)\b/i,
|
|
117
|
+
/\b(?:did not|does not|could not|not found|not confirm|not indicate|budget exhausted)\b/i,
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sufficientlyQuoteBackedValue(valueTokens, quoteTokens) {
|
|
122
|
+
if (valueTokens.size === 0) return false;
|
|
123
|
+
const hits = setIntersectionCount(valueTokens, quoteTokens);
|
|
124
|
+
return hits >= 4 && hits / valueTokens.size >= 0.55;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildEvidenceHintRows(normalizeInputPacket) {
|
|
128
|
+
const rows = [];
|
|
129
|
+
const facts = asArray(normalizeInputPacket?.packet?.research?.extractedFacts);
|
|
130
|
+
for (const fact of facts) {
|
|
131
|
+
const quote = stringOf(fact?.quote);
|
|
132
|
+
if (!quote) continue;
|
|
133
|
+
const refs = sourceRefs(fact);
|
|
134
|
+
const urls = sourceUrls(fact);
|
|
135
|
+
if (refs.length === 0 && urls.length === 0) continue;
|
|
136
|
+
const sourceTitleOrPublisher = stringOf(fact?.sourceTitleOrPublisher);
|
|
137
|
+
const sourceQuality = stringOf(fact?.sourceQuality);
|
|
138
|
+
const notes = stringOf(fact?.notes);
|
|
139
|
+
if (
|
|
140
|
+
isSyntheticEvidenceText(
|
|
141
|
+
`${sourceTitleOrPublisher} ${sourceQuality} ${notes}`,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
continue;
|
|
145
|
+
const value = stringOf(fact?.value);
|
|
146
|
+
const quoteTokens = tokenSet(quote);
|
|
147
|
+
const valueTokens = tokenSet(value);
|
|
148
|
+
const supportedValue =
|
|
149
|
+
value &&
|
|
150
|
+
!weakEvidenceHintText(`${value} ${notes}`) &&
|
|
151
|
+
sufficientlyQuoteBackedValue(valueTokens, quoteTokens)
|
|
152
|
+
? value
|
|
153
|
+
: "";
|
|
154
|
+
rows.push({
|
|
155
|
+
sourceRef: refs[0],
|
|
156
|
+
sourceRefs: refs,
|
|
157
|
+
url: urls[0],
|
|
158
|
+
sourceUrls: urls,
|
|
159
|
+
sourceTitleOrPublisher: sourceTitleOrPublisher || undefined,
|
|
160
|
+
dateOrYear: stringOf(fact?.dateOrYear) || undefined,
|
|
161
|
+
quote,
|
|
162
|
+
value: supportedValue || undefined,
|
|
163
|
+
factSlotIds: compactStrings(
|
|
164
|
+
[fact?.slotId, ...asArray(fact?.factSlotIds)],
|
|
165
|
+
8,
|
|
166
|
+
),
|
|
167
|
+
sourceQuality: sourceQuality || undefined,
|
|
168
|
+
relevance: notes || supportedValue || undefined,
|
|
169
|
+
_tokens: tokenSet(`${supportedValue} ${quote}`),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function canonicalUrl(value) {
|
|
176
|
+
const raw = stringOf(value).replace(/[.,;:]+$/u, "");
|
|
177
|
+
if (!/^https?:\/\//i.test(raw)) return "";
|
|
178
|
+
try {
|
|
179
|
+
const url = new URL(raw);
|
|
180
|
+
url.protocol = url.protocol.toLowerCase();
|
|
181
|
+
url.hostname = url.hostname.toLowerCase();
|
|
182
|
+
url.hash = "";
|
|
183
|
+
return url.toString().replace(/\/$/u, "");
|
|
184
|
+
} catch {
|
|
185
|
+
return raw;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildUrlSourceRefLookup(normalizeInputPacket) {
|
|
190
|
+
const lookup = new Map();
|
|
191
|
+
const sources = asArray(normalizeInputPacket?.packet?.research?.sources);
|
|
192
|
+
for (const source of sources) {
|
|
193
|
+
const ref = sourceRefs(source)[0] || stringOf(source?.sourceRef);
|
|
194
|
+
if (!ref) continue;
|
|
195
|
+
for (const url of sourceUrls(source).length > 0
|
|
196
|
+
? sourceUrls(source)
|
|
197
|
+
: [source?.url]) {
|
|
198
|
+
const key = canonicalUrl(url);
|
|
199
|
+
if (key && !lookup.has(key)) lookup.set(key, ref);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return lookup;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function backfillSourceRefs(candidate, hints, urlToSourceRef) {
|
|
206
|
+
const refs = sourceRefs(candidate);
|
|
207
|
+
for (const hint of hints) {
|
|
208
|
+
if (hint.sourceRef && !refs.includes(hint.sourceRef))
|
|
209
|
+
refs.push(hint.sourceRef);
|
|
210
|
+
}
|
|
211
|
+
for (const url of sourceUrls(candidate)) {
|
|
212
|
+
const ref = urlToSourceRef.get(canonicalUrl(url));
|
|
213
|
+
if (ref && !refs.includes(ref)) refs.push(ref);
|
|
214
|
+
}
|
|
215
|
+
return refs.slice(0, 16);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function evidenceHintsForCandidate(candidate, hintRows) {
|
|
219
|
+
const candidateRefs = new Set(sourceRefs(candidate));
|
|
220
|
+
const candidateUrls = new Set(sourceUrls(candidate));
|
|
221
|
+
const candidateSlots = new Set(compactStrings(candidate?.factSlotIds, 12));
|
|
222
|
+
const candidateTokens = tokenSet(claimText(candidate));
|
|
223
|
+
const scored = [];
|
|
224
|
+
for (const row of hintRows) {
|
|
225
|
+
const refHits = setIntersectionCount(
|
|
226
|
+
new Set(row.sourceRefs),
|
|
227
|
+
candidateRefs,
|
|
228
|
+
);
|
|
229
|
+
const urlHits = setIntersectionCount(
|
|
230
|
+
new Set(row.sourceUrls),
|
|
231
|
+
candidateUrls,
|
|
232
|
+
);
|
|
233
|
+
const slotHits = setIntersectionCount(
|
|
234
|
+
new Set(row.factSlotIds),
|
|
235
|
+
candidateSlots,
|
|
236
|
+
);
|
|
237
|
+
const tokenHits = setIntersectionCount(row._tokens, candidateTokens);
|
|
238
|
+
if (slotHits === 0 && tokenHits < 2) continue;
|
|
239
|
+
const score =
|
|
240
|
+
refHits * 6 + urlHits * 5 + slotHits * 2 + Math.min(tokenHits, 5);
|
|
241
|
+
if (score < 7) continue;
|
|
242
|
+
scored.push({ score, row });
|
|
243
|
+
}
|
|
244
|
+
scored.sort((left, right) => right.score - left.score);
|
|
245
|
+
return scored.slice(0, 3).map(({ row }) => ({
|
|
246
|
+
sourceRef: row.sourceRef || undefined,
|
|
247
|
+
url: row.url || undefined,
|
|
248
|
+
sourceTitleOrPublisher: row.sourceTitleOrPublisher,
|
|
249
|
+
dateOrYear: row.dateOrYear,
|
|
250
|
+
quote: row.quote,
|
|
251
|
+
value: row.value,
|
|
252
|
+
factSlotIds: row.factSlotIds,
|
|
253
|
+
sourceQuality: row.sourceQuality,
|
|
254
|
+
relevance: row.relevance,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function classifyCandidate(candidate, seenIds) {
|
|
259
|
+
const id = candidateId(candidate);
|
|
260
|
+
const claim = claimText(candidate);
|
|
261
|
+
const reasons = [];
|
|
262
|
+
|
|
263
|
+
if (!id) reasons.push("missing_candidate_id");
|
|
264
|
+
else if (seenIds.has(id)) reasons.push("duplicate_candidate_id");
|
|
265
|
+
if (!claim) reasons.push("missing_candidate_text");
|
|
266
|
+
if (!hasSourceLocator(candidate)) reasons.push("missing_candidate_source");
|
|
267
|
+
|
|
268
|
+
if (claim) {
|
|
269
|
+
if (
|
|
270
|
+
matchesAny(claim, [
|
|
271
|
+
/\b(?:fetched|retrieved|accessed|inspected|collected|reviewed|cached)\b[^.]{0,80}\b20\d{2}-\d{2}-\d{2}\b/i,
|
|
272
|
+
/\b20\d{2}-\d{2}-\d{2}\b[^.]{0,80}\b(?:fetched|retrieved|accessed|inspected|collected|reviewed|cached)\b/i,
|
|
273
|
+
])
|
|
274
|
+
) {
|
|
275
|
+
reasons.push("workflow_context_date_claim");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
matchesAny(claim, [
|
|
280
|
+
/\b(?:sourceRef|workflow[_ -]?artifact|cached source|artifact read|tool call)\b/i,
|
|
281
|
+
/\b(?:evidence|source|page|doc|documentation)\s+(?:was|were)\s+(?:fetched|retrieved|cached|inspected|reviewed)\b/i,
|
|
282
|
+
])
|
|
283
|
+
) {
|
|
284
|
+
reasons.push("meta_evidence_freshness_claim");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
matchesAny(claim, [
|
|
289
|
+
/\b(?:no|not|never)\s+(?:direct|exact|primary|source-backed)?\s*(?:evidence|quote|rule|wording|support)\b/i,
|
|
290
|
+
/\bno\s+(?:retrieved|available|visible|primary|cited|supporting)?\s*sources?\s+(?:found|available|visible|retrieved|confirmed|cited|support(?:s|ing)?)\b/i,
|
|
291
|
+
/\b(?:evidence|quote|source|rule|wording|support)\s+(?:was|were|is|are)\s+not\s+(?:found|available|visible|present|exposed|retrieved|confirmed|reliably extracted)\b/i,
|
|
292
|
+
/\b(?:did not|does not|failed to|could not|cannot)\s+(?:find|show|establish|confirm|retrieve|extract|expose|verify|support)\b/i,
|
|
293
|
+
/\bnot\s+reliably\s+(?:extracted|confirmed|established|verified)\b/i,
|
|
294
|
+
/\b(?:gap|missing|unavailable|inconclusive)\s+(?:in|for|from)\s+(?:evidence|source|documentation|retrieval)\b/i,
|
|
295
|
+
])
|
|
296
|
+
) {
|
|
297
|
+
reasons.push("evidence_gap_claim");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (
|
|
301
|
+
matchesAny(claim, [
|
|
302
|
+
/\bcan\s+be\s+(?:synthesized|derived|combined)\b/i,
|
|
303
|
+
/\b(?:feasible|pragmatic|low-overhead|small[- ]team|small[- ]SaaS|baseline|tiering|action plan|implementation plan)\b[^.]{0,120}\b(?:use|combine|adopt|implement|separate|prioriti[sz]e|choose|form)\b/i,
|
|
304
|
+
/\b(?:practical|feasible)\s+(?:governance\s+)?baseline\b/i,
|
|
305
|
+
/\b(?:teams|organizations|implementers|small[- ]SaaS)\s+(?:should|can|could|may)\b/i,
|
|
306
|
+
/\b(?:minimum|defensible|lightweight|reporting architecture|control set|runbook|checklist)\b[^.]{0,160}\b(?:should|can|could|may|use|adopt|define|separate|include|treat|cite|retain|review)\b/i,
|
|
307
|
+
/\b(?:should|can|could|may)\s+(?:define|separate|include|treat|use|adopt|prefer|document|retain|review|label|choose)\b/i,
|
|
308
|
+
])
|
|
309
|
+
) {
|
|
310
|
+
reasons.push("synthesized_recommendation_claim");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
matchesAny(claim, [
|
|
315
|
+
/\b(?:all|every|always|never|none|no)\s+(?:major\s+)?(?:vendors?|providers?|tools?|frameworks?|products?|services?)\b/i,
|
|
316
|
+
/\b(?:vendors?|providers?|tools?|frameworks?|services?)\s+(?:all|always|never|uniformly)\b/i,
|
|
317
|
+
/\b(?:AI\s+coding\s+agents|coding\s+agents|agents)\s+should\b/i,
|
|
318
|
+
/\bapplicable\s+to\b[^.]{0,80}\b(?:agent|agents|small\s+team|small\s+teams)\b/i,
|
|
319
|
+
/\bused\s+for\b[^.]{0,120}\b(?:framing|basis|checklist|guidance|implementation|reporting)\b/i,
|
|
320
|
+
])
|
|
321
|
+
) {
|
|
322
|
+
reasons.push("source_broader_than_evidence_claim");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [...new Set(reasons)];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function demotionGap(candidate, reasons) {
|
|
330
|
+
const id = candidateId(candidate);
|
|
331
|
+
const claim = claimText(candidate);
|
|
332
|
+
return {
|
|
333
|
+
claimId: id || undefined,
|
|
334
|
+
slotId: compactStrings(candidate?.factSlotIds, 1)[0],
|
|
335
|
+
relatedFactSlotIds: compactStrings(candidate?.factSlotIds, 8),
|
|
336
|
+
evidenceState: "not_sent_to_verifier",
|
|
337
|
+
reason: `sanitized from verifier candidates: ${reasons.join(", ")}`,
|
|
338
|
+
nextStep:
|
|
339
|
+
"Replace with a narrow source-stated factual atom, or keep as an explicit final-report gap/recommendation caveat.",
|
|
340
|
+
sourceUrls: sourceUrls(candidate).slice(0, 6),
|
|
341
|
+
claim: claim || undefined,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function preservedClaim(candidate, reasons, fallbackIndex) {
|
|
346
|
+
const id =
|
|
347
|
+
candidateId(candidate) ||
|
|
348
|
+
`candidate-${String(fallbackIndex + 1).padStart(3, "0")}`;
|
|
349
|
+
return {
|
|
350
|
+
...candidate,
|
|
351
|
+
id: `preserved-${id}`,
|
|
352
|
+
originalCandidateId: candidateId(candidate) || undefined,
|
|
353
|
+
claim: claimText(candidate) || undefined,
|
|
354
|
+
status: "preserved_not_sent_to_verifier",
|
|
355
|
+
sanitizerDemotionReasons: reasons,
|
|
356
|
+
whyItMatters:
|
|
357
|
+
stringOf(candidate?.whyItMatters) ||
|
|
358
|
+
stringOf(candidate?.reasonToVerify) ||
|
|
359
|
+
"Demoted by deterministic pre-verifier sanitizer and preserved for final caveats/gaps.",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const REWRITEABLE_REASONS = new Set([
|
|
364
|
+
"synthesized_recommendation_claim",
|
|
365
|
+
"source_broader_than_evidence_claim",
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
function rewrittenCandidate(candidate, reasons, hints, urlToSourceRef) {
|
|
369
|
+
const rewriteReasons = reasons.filter((reason) =>
|
|
370
|
+
REWRITEABLE_REASONS.has(reason),
|
|
371
|
+
);
|
|
372
|
+
if (rewriteReasons.length === 0 || rewriteReasons.length !== reasons.length)
|
|
373
|
+
return null;
|
|
374
|
+
const hint =
|
|
375
|
+
hints.find((item) => item.value) ?? hints.find((item) => item.quote);
|
|
376
|
+
if (!hint) return null;
|
|
377
|
+
const replacement = stringOf(hint.value) || stringOf(hint.quote);
|
|
378
|
+
if (!replacement || replacement === claimText(candidate)) return null;
|
|
379
|
+
return {
|
|
380
|
+
...candidate,
|
|
381
|
+
originalClaim: claimText(candidate),
|
|
382
|
+
claim: replacement,
|
|
383
|
+
sourceRefs: backfillSourceRefs(candidate, [hint], urlToSourceRef),
|
|
384
|
+
sourceUrls: hint.url ? [hint.url] : sourceUrls(candidate),
|
|
385
|
+
sanitizerRewriteReasons: rewriteReasons,
|
|
386
|
+
reasonToVerify: `Deterministically rewritten to a source-backed atom from ${hint.sourceTitleOrPublisher ?? hint.url ?? hint.sourceRef ?? "source evidence"}.`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function sanitizedCandidate(candidate, hints, urlToSourceRef) {
|
|
391
|
+
return {
|
|
392
|
+
...candidate,
|
|
393
|
+
id: candidateId(candidate),
|
|
394
|
+
claim: claimText(candidate),
|
|
395
|
+
sourceRefs: backfillSourceRefs(candidate, hints, urlToSourceRef),
|
|
396
|
+
sourceUrls: sourceUrls(candidate),
|
|
397
|
+
...(hints.length > 0 ? { sourceEvidenceHints: hints } : {}),
|
|
398
|
+
verifierInputPolicy: VERIFIER_INPUT_POLICY,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function adjustFactSlotCoverage(rows, demotedBySlot, keptIds) {
|
|
403
|
+
return asArray(rows).map((row) => {
|
|
404
|
+
const slot = { ...asObject(row) };
|
|
405
|
+
const originalIds = compactStrings(slot.verificationCandidateIds, 24);
|
|
406
|
+
const filteredIds = originalIds.filter((id) => keptIds.has(id));
|
|
407
|
+
const demotedIds =
|
|
408
|
+
demotedBySlot.get(stringOf(slot.slotId ?? slot.id)) ?? [];
|
|
409
|
+
if (originalIds.length > 0 || demotedIds.length > 0) {
|
|
410
|
+
slot.verificationCandidateIds = filteredIds;
|
|
411
|
+
}
|
|
412
|
+
if (demotedIds.length > 0 && filteredIds.length === 0) {
|
|
413
|
+
if (slot.status === "filled") slot.status = "partial";
|
|
414
|
+
const prefix = stringOf(slot.gapReason);
|
|
415
|
+
const note = `sanitized verifier candidates: ${demotedIds.join(", ")}`;
|
|
416
|
+
slot.gapReason = prefix ? `${prefix}; ${note}` : note;
|
|
417
|
+
}
|
|
418
|
+
return slot;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default async function sanitizeVerificationCandidates({ sources }) {
|
|
423
|
+
const normalized = asObject(findSource(sources, "normalize-claims"));
|
|
424
|
+
const normalizeInputPacket = asObject(
|
|
425
|
+
findSource(sources, "normalize-input-packet"),
|
|
426
|
+
);
|
|
427
|
+
const evidenceHintRows = buildEvidenceHintRows(normalizeInputPacket);
|
|
428
|
+
const urlToSourceRef = buildUrlSourceRefLookup(normalizeInputPacket);
|
|
429
|
+
const claimInventory = asObject(normalized.claimInventory);
|
|
430
|
+
const originalCandidates = asArray(claimInventory.verificationCandidates);
|
|
431
|
+
const keptCandidates = [];
|
|
432
|
+
const preservedClaims = [...asArray(claimInventory.preservedClaims)];
|
|
433
|
+
const coverageGaps = [...asArray(normalized.coverageGaps)];
|
|
434
|
+
const demotedBySlot = new Map();
|
|
435
|
+
const demotionReasonCounts = {};
|
|
436
|
+
const rewriteReasonCounts = {};
|
|
437
|
+
const demotedCandidateIds = [];
|
|
438
|
+
const rewrittenCandidateIds = [];
|
|
439
|
+
const seenIds = new Set();
|
|
440
|
+
|
|
441
|
+
for (const [index, candidate] of originalCandidates.entries()) {
|
|
442
|
+
const id = candidateId(candidate);
|
|
443
|
+
const hints = evidenceHintsForCandidate(candidate, evidenceHintRows);
|
|
444
|
+
const reasons = classifyCandidate(candidate, seenIds);
|
|
445
|
+
if (id) seenIds.add(id);
|
|
446
|
+
if (reasons.length === 0) {
|
|
447
|
+
keptCandidates.push(sanitizedCandidate(candidate, hints, urlToSourceRef));
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const rewrite = rewrittenCandidate(
|
|
451
|
+
candidate,
|
|
452
|
+
reasons,
|
|
453
|
+
hints,
|
|
454
|
+
urlToSourceRef,
|
|
455
|
+
);
|
|
456
|
+
if (rewrite) {
|
|
457
|
+
for (const reason of rewrite.sanitizerRewriteReasons) {
|
|
458
|
+
rewriteReasonCounts[reason] = (rewriteReasonCounts[reason] ?? 0) + 1;
|
|
459
|
+
}
|
|
460
|
+
rewrittenCandidateIds.push(id || `index-${index}`);
|
|
461
|
+
preservedClaims.push({
|
|
462
|
+
...preservedClaim(candidate, reasons, index),
|
|
463
|
+
status: "preserved_rewritten_before_verification",
|
|
464
|
+
});
|
|
465
|
+
keptCandidates.push(sanitizedCandidate(rewrite, hints, urlToSourceRef));
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
for (const reason of reasons) {
|
|
469
|
+
demotionReasonCounts[reason] = (demotionReasonCounts[reason] ?? 0) + 1;
|
|
470
|
+
}
|
|
471
|
+
demotedCandidateIds.push(id || `index-${index}`);
|
|
472
|
+
preservedClaims.push(preservedClaim(candidate, reasons, index));
|
|
473
|
+
coverageGaps.push(demotionGap(candidate, reasons));
|
|
474
|
+
for (const slotId of compactStrings(candidate?.factSlotIds, 12)) {
|
|
475
|
+
const list = demotedBySlot.get(slotId) ?? [];
|
|
476
|
+
list.push(id || `index-${index}`);
|
|
477
|
+
demotedBySlot.set(slotId, list);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Web URL-only candidates cannot rejoin the wsrc ledger at audit time
|
|
482
|
+
// (observed as sourceRefJoinFailures on never-fetched URLs), so route them
|
|
483
|
+
// to backlog and refill the pool from source-backed preserved claims.
|
|
484
|
+
const webUrlOnlyDemotedIds = [];
|
|
485
|
+
const promotedCandidateIds = [];
|
|
486
|
+
const promotedBySlot = new Map();
|
|
487
|
+
const retainedCandidates = [];
|
|
488
|
+
for (const [index, candidate] of keptCandidates.entries()) {
|
|
489
|
+
const hasRefs = sourceRefs(candidate).length > 0;
|
|
490
|
+
const hasLocal = localEvidenceRefs(candidate).length > 0;
|
|
491
|
+
if (hasRefs || hasLocal || sourceUrls(candidate).length === 0) {
|
|
492
|
+
retainedCandidates.push(candidate);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const id = candidateId(candidate) || `index-${index}`;
|
|
496
|
+
const reasons = ["web_url_without_source_ref_after_backfill"];
|
|
497
|
+
demotionReasonCounts[reasons[0]] =
|
|
498
|
+
(demotionReasonCounts[reasons[0]] ?? 0) + 1;
|
|
499
|
+
webUrlOnlyDemotedIds.push(id);
|
|
500
|
+
demotedCandidateIds.push(id);
|
|
501
|
+
preservedClaims.push({
|
|
502
|
+
...preservedClaim(candidate, reasons, index),
|
|
503
|
+
status: "preserved_missing_source_ref",
|
|
504
|
+
});
|
|
505
|
+
coverageGaps.push({
|
|
506
|
+
...demotionGap(candidate, reasons),
|
|
507
|
+
nextStep:
|
|
508
|
+
"Reacquire this claim's source with workflow_web_fetch_source so a wsrc_* sourceRef exists, or keep it as an explicit final-report gap.",
|
|
509
|
+
});
|
|
510
|
+
for (const slotId of compactStrings(candidate?.factSlotIds, 12)) {
|
|
511
|
+
const list = demotedBySlot.get(slotId) ?? [];
|
|
512
|
+
list.push(id);
|
|
513
|
+
demotedBySlot.set(slotId, list);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
keptCandidates.length = 0;
|
|
517
|
+
keptCandidates.push(...retainedCandidates);
|
|
518
|
+
|
|
519
|
+
if (webUrlOnlyDemotedIds.length > 0) {
|
|
520
|
+
const takenIds = new Set(
|
|
521
|
+
keptCandidates.map((candidate) => candidateId(candidate)),
|
|
522
|
+
);
|
|
523
|
+
const promotable = [];
|
|
524
|
+
for (const [index, preserved] of asArray(
|
|
525
|
+
claimInventory.preservedClaims,
|
|
526
|
+
).entries()) {
|
|
527
|
+
const claim = claimText(preserved);
|
|
528
|
+
if (!claim) continue;
|
|
529
|
+
const id =
|
|
530
|
+
candidateId(preserved) ||
|
|
531
|
+
`promoted-${String(index + 1).padStart(3, "0")}`;
|
|
532
|
+
if (takenIds.has(id)) continue;
|
|
533
|
+
const hints = evidenceHintsForCandidate(preserved, evidenceHintRows);
|
|
534
|
+
const refs = backfillSourceRefs(preserved, hints, urlToSourceRef);
|
|
535
|
+
if (refs.length === 0) continue;
|
|
536
|
+
if (!hints.some((hint) => stringOf(hint.quote))) continue;
|
|
537
|
+
if (classifyCandidate({ ...preserved, id }, new Set()).length > 0) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const slots = compactStrings(preserved?.factSlotIds, 12);
|
|
541
|
+
if (slots.length === 0) continue;
|
|
542
|
+
const rescuesSlot = slots.some(
|
|
543
|
+
(slotId) => (demotedBySlot.get(slotId) ?? []).length > 0,
|
|
544
|
+
);
|
|
545
|
+
promotable.push({ preserved, id, hints, refs, slots, rescuesSlot });
|
|
546
|
+
}
|
|
547
|
+
promotable.sort(
|
|
548
|
+
(left, right) => Number(right.rescuesSlot) - Number(left.rescuesSlot),
|
|
549
|
+
);
|
|
550
|
+
for (const entry of promotable.slice(0, webUrlOnlyDemotedIds.length)) {
|
|
551
|
+
takenIds.add(entry.id);
|
|
552
|
+
promotedCandidateIds.push(entry.id);
|
|
553
|
+
for (const slotId of entry.slots) {
|
|
554
|
+
const list = promotedBySlot.get(slotId) ?? [];
|
|
555
|
+
list.push(entry.id);
|
|
556
|
+
promotedBySlot.set(slotId, list);
|
|
557
|
+
}
|
|
558
|
+
keptCandidates.push(
|
|
559
|
+
sanitizedCandidate(
|
|
560
|
+
{
|
|
561
|
+
...entry.preserved,
|
|
562
|
+
id: entry.id,
|
|
563
|
+
sourceRefs: entry.refs,
|
|
564
|
+
verificationNeed:
|
|
565
|
+
stringOf(entry.preserved?.verificationNeed) || "useful",
|
|
566
|
+
reasonToVerify:
|
|
567
|
+
stringOf(entry.preserved?.reasonToVerify) ||
|
|
568
|
+
stringOf(entry.preserved?.whyItMatters) ||
|
|
569
|
+
"Promoted source-backed preserved claim to replace a URL-only candidate.",
|
|
570
|
+
},
|
|
571
|
+
entry.hints,
|
|
572
|
+
urlToSourceRef,
|
|
573
|
+
),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const keptIds = new Set(keptCandidates.map((candidate) => candidate.id));
|
|
579
|
+
const factSlotCoverageRows = adjustFactSlotCoverage(
|
|
580
|
+
normalized.factSlotCoverage,
|
|
581
|
+
demotedBySlot,
|
|
582
|
+
keptIds,
|
|
583
|
+
).map((row) => {
|
|
584
|
+
const slotId = stringOf(row.slotId ?? row.id);
|
|
585
|
+
const promoted = promotedBySlot.get(slotId) ?? [];
|
|
586
|
+
if (promoted.length === 0) return row;
|
|
587
|
+
return {
|
|
588
|
+
...row,
|
|
589
|
+
verificationCandidateIds: compactStrings(
|
|
590
|
+
[...asArray(row.verificationCandidateIds), ...promoted],
|
|
591
|
+
24,
|
|
592
|
+
),
|
|
593
|
+
};
|
|
594
|
+
});
|
|
595
|
+
return {
|
|
596
|
+
schema: SCHEMA,
|
|
597
|
+
claimInventory: {
|
|
598
|
+
verificationCandidates: keptCandidates,
|
|
599
|
+
preservedClaims,
|
|
600
|
+
duplicates: asArray(claimInventory.duplicates),
|
|
601
|
+
},
|
|
602
|
+
factSlotCoverage: factSlotCoverageRows,
|
|
603
|
+
coverageGaps,
|
|
604
|
+
researchScopeCoverage: asArray(normalized.researchScopeCoverage),
|
|
605
|
+
normalizationNotes: normalized.normalizationNotes,
|
|
606
|
+
sanitizerDiagnostics: {
|
|
607
|
+
inputCandidateCount: originalCandidates.length,
|
|
608
|
+
keptCandidateCount: keptCandidates.length,
|
|
609
|
+
demotedCandidateCount: demotedCandidateIds.length,
|
|
610
|
+
rewrittenCandidateCount: rewrittenCandidateIds.length,
|
|
611
|
+
webUrlOnlyDemotedCount: webUrlOnlyDemotedIds.length,
|
|
612
|
+
promotedCandidateCount: promotedCandidateIds.length,
|
|
613
|
+
demotionReasonCounts,
|
|
614
|
+
rewriteReasonCounts,
|
|
615
|
+
keptCandidateIds: keptCandidates.map((candidate) => candidate.id),
|
|
616
|
+
demotedCandidateIds,
|
|
617
|
+
webUrlOnlyDemotedIds,
|
|
618
|
+
promotedCandidateIds,
|
|
619
|
+
rewrittenCandidateIds,
|
|
620
|
+
verifierInputPolicy: VERIFIER_INPUT_POLICY,
|
|
621
|
+
sourceEvidenceHintRows: evidenceHintRows.length,
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"executiveMarkdown": { "type": "string" },
|
|
21
21
|
"wordCount": { "type": "number" },
|
|
22
22
|
"sourceUrlCount": { "type": "number" },
|
|
23
|
+
"totalSourceUrlCount": { "type": "number" },
|
|
23
24
|
"sourceUrls": { "type": "array", "items": { "type": "string" } },
|
|
24
25
|
"claimSummary": {
|
|
25
26
|
"type": "object",
|
|
@@ -46,20 +47,48 @@
|
|
|
46
47
|
},
|
|
47
48
|
"gates": {
|
|
48
49
|
"type": "object",
|
|
49
|
-
"required": ["
|
|
50
|
+
"required": ["renderedAllStructuredItems", "passed"],
|
|
50
51
|
"properties": {
|
|
51
|
-
"
|
|
52
|
-
"maxUrls": { "type": "number" },
|
|
53
|
-
"maxFindings": { "type": "number" },
|
|
54
|
-
"maxRecommendations": { "type": "number" },
|
|
55
|
-
"maxGaps": { "type": "number" },
|
|
56
|
-
"truncated": { "type": "boolean" },
|
|
52
|
+
"renderedAllStructuredItems": { "type": "boolean" },
|
|
57
53
|
"passed": { "type": "boolean" }
|
|
58
54
|
},
|
|
59
55
|
"additionalProperties": true
|
|
60
56
|
},
|
|
61
57
|
"auditArtifact": { "type": "string" },
|
|
62
|
-
"sidecarPath": { "type": "string" }
|
|
58
|
+
"sidecarPath": { "type": "string" },
|
|
59
|
+
"reportSidecarPath": { "type": "string" },
|
|
60
|
+
"auditSidecarPath": { "type": "string" },
|
|
61
|
+
"reportMarkdown": { "type": "string" },
|
|
62
|
+
"auditMarkdown": { "type": "string" },
|
|
63
|
+
"renderMode": { "type": "string" },
|
|
64
|
+
"sourceIndex": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"url": { "type": "string" },
|
|
70
|
+
"host": { "type": "string" }
|
|
71
|
+
},
|
|
72
|
+
"additionalProperties": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"sectionCounts": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"additionalProperties": true
|
|
78
|
+
},
|
|
79
|
+
"renderWarnings": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"items": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"section": { "type": "string" },
|
|
85
|
+
"label": { "type": "string" },
|
|
86
|
+
"total": { "type": "number" },
|
|
87
|
+
"rendered": { "type": "number" }
|
|
88
|
+
},
|
|
89
|
+
"additionalProperties": true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
63
92
|
},
|
|
64
93
|
"additionalProperties": true
|
|
65
94
|
}
|