@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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Deterministic
|
|
1
|
+
// Deterministic evidence-backed renderer for deep-research.
|
|
2
2
|
//
|
|
3
3
|
// Input: final-audit.control.json from the full deep-research final stage.
|
|
4
|
-
// Output:
|
|
4
|
+
// Output: a parent-facing research report in executiveMarkdown plus sidecars.
|
|
5
5
|
//
|
|
6
6
|
// This intentionally treats final-audit.control.json as the source of truth and
|
|
7
7
|
// renders a bounded view. It does not re-verify or invent evidence.
|
|
@@ -10,16 +10,59 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
12
|
function findSource(sources, stageId) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const entries = Object.entries(sources ?? {});
|
|
14
|
+
const exact = entries.find(([specId]) => specId === stageId);
|
|
15
|
+
if (exact) return exact[1];
|
|
16
|
+
const dotted = entries.find(([specId]) => specId.startsWith(`${stageId}.`));
|
|
17
|
+
return dotted?.[1] ?? null;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function asArray(value) {
|
|
20
21
|
return Array.isArray(value) ? value : [];
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function flattenItems(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.flatMap((item) => flattenItems(item));
|
|
30
|
+
if (typeof value === "string") return value.trim() ? [value] : [];
|
|
31
|
+
if (!isRecord(value)) return [];
|
|
32
|
+
const renderFields = [
|
|
33
|
+
"gap",
|
|
34
|
+
"finding",
|
|
35
|
+
"claim",
|
|
36
|
+
"note",
|
|
37
|
+
"reason",
|
|
38
|
+
"nextStep",
|
|
39
|
+
"evidenceState",
|
|
40
|
+
"whyItMatters",
|
|
41
|
+
"parentImpact",
|
|
42
|
+
"recommendation",
|
|
43
|
+
"action",
|
|
44
|
+
"step",
|
|
45
|
+
];
|
|
46
|
+
if (
|
|
47
|
+
renderFields.some(
|
|
48
|
+
(field) => typeof value[field] === "string" && value[field].trim(),
|
|
49
|
+
)
|
|
50
|
+
) {
|
|
51
|
+
return [value];
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
value.id ||
|
|
55
|
+
value.gapId ||
|
|
56
|
+
value.slotId ||
|
|
57
|
+
Array.isArray(value.relatedFactSlotIds) ||
|
|
58
|
+
Array.isArray(value.sourceUrls) ||
|
|
59
|
+
Array.isArray(value.sourceRefs)
|
|
60
|
+
) {
|
|
61
|
+
return [value];
|
|
62
|
+
}
|
|
63
|
+
return Object.values(value).flatMap((item) => flattenItems(item));
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
function words(text) {
|
|
24
67
|
return (
|
|
25
68
|
String(text ?? "")
|
|
@@ -39,6 +82,52 @@ function cleanText(value) {
|
|
|
39
82
|
.trim();
|
|
40
83
|
}
|
|
41
84
|
|
|
85
|
+
function escapeTableCell(value) {
|
|
86
|
+
return cleanText(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stringifyItem(item) {
|
|
90
|
+
if (typeof item === "string") return cleanText(item) || "(empty string)";
|
|
91
|
+
try {
|
|
92
|
+
const json = JSON.stringify(item);
|
|
93
|
+
if (json) return cleanText(json);
|
|
94
|
+
} catch {
|
|
95
|
+
// Fall through to String below.
|
|
96
|
+
}
|
|
97
|
+
return cleanText(String(item)) || "(empty item)";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function summaryText(report, fallback) {
|
|
101
|
+
const summary = report?.summary;
|
|
102
|
+
if (typeof summary === "string" && summary.trim()) return cleanText(summary);
|
|
103
|
+
if (isRecord(summary)) {
|
|
104
|
+
const parts = [
|
|
105
|
+
summary.directAnswer,
|
|
106
|
+
summary.answer,
|
|
107
|
+
summary.summary,
|
|
108
|
+
summary.finding,
|
|
109
|
+
]
|
|
110
|
+
.filter((value) => typeof value === "string" && value.trim())
|
|
111
|
+
.map(cleanText);
|
|
112
|
+
const confidence = cleanText(summary.confidence ?? "");
|
|
113
|
+
const caveat = cleanText(summary.keyCaveat ?? summary.caveat ?? "");
|
|
114
|
+
return (
|
|
115
|
+
[
|
|
116
|
+
parts[0],
|
|
117
|
+
confidence ? `Confidence: ${confidence}.` : undefined,
|
|
118
|
+
caveat ? `Key caveat: ${caveat}.` : undefined,
|
|
119
|
+
]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join(" ") || stringifyItem(summary)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return cleanText(fallback ?? "Research completed with audited evidence.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasObjectSerializationArtifact(text) {
|
|
128
|
+
return /\[object Object\]/.test(String(text ?? ""));
|
|
129
|
+
}
|
|
130
|
+
|
|
42
131
|
function truncateWords(text, maxWords) {
|
|
43
132
|
const items = words(text);
|
|
44
133
|
if (items.length <= maxWords) return cleanText(text);
|
|
@@ -56,88 +145,204 @@ function hostOf(url) {
|
|
|
56
145
|
}
|
|
57
146
|
}
|
|
58
147
|
|
|
59
|
-
function
|
|
60
|
-
if (typeof
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
148
|
+
function normalizeUrl(url) {
|
|
149
|
+
if (typeof url !== "string") return null;
|
|
150
|
+
const trimmed = url.trim().replace(/[.,;:]+$/, "");
|
|
151
|
+
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = new URL(trimmed);
|
|
154
|
+
parsed.hash = "";
|
|
155
|
+
return parsed.toString();
|
|
156
|
+
} catch {
|
|
157
|
+
return trimmed;
|
|
64
158
|
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectStructuredUrls(value, urls = []) {
|
|
162
|
+
if (!value || typeof value !== "object") return urls;
|
|
65
163
|
if (Array.isArray(value)) {
|
|
66
|
-
for (const item of value)
|
|
164
|
+
for (const item of value) collectStructuredUrls(item, urls);
|
|
67
165
|
return urls;
|
|
68
166
|
}
|
|
69
|
-
|
|
70
|
-
|
|
167
|
+
for (const [key, item] of Object.entries(value)) {
|
|
168
|
+
if (
|
|
169
|
+
/^(sourceUrls?|evidenceUrls?|urls?|url|uri|href|links?|references?|refs?|basis|sources)$/i.test(
|
|
170
|
+
key,
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
for (const candidate of asArray(item).length ? item : [item]) {
|
|
174
|
+
const normalized = normalizeUrl(candidate);
|
|
175
|
+
if (normalized) urls.push(normalized);
|
|
176
|
+
else if (candidate && typeof candidate === "object") {
|
|
177
|
+
collectStructuredUrls(candidate, urls);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (item && typeof item === "object") collectStructuredUrls(item, urls);
|
|
71
183
|
}
|
|
72
184
|
return urls;
|
|
73
185
|
}
|
|
74
186
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (trimmed) locators.push(trimmed);
|
|
187
|
+
function uniqueStructuredUrls(...values) {
|
|
188
|
+
const out = [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
for (const value of values) {
|
|
191
|
+
for (const url of collectStructuredUrls(value, [])) {
|
|
192
|
+
if (seen.has(url)) continue;
|
|
193
|
+
seen.add(url);
|
|
194
|
+
out.push(url);
|
|
84
195
|
}
|
|
85
|
-
return locators;
|
|
86
196
|
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function urlsOf(item, limit = 3) {
|
|
201
|
+
return uniqueStructuredUrls(item).slice(0, limit);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeLocalRef(value) {
|
|
205
|
+
if (typeof value !== "string") return null;
|
|
206
|
+
const text = value.trim();
|
|
207
|
+
if (!text || /^https?:\/\//i.test(text) || isWorkflowSourceRefText(text))
|
|
208
|
+
return null;
|
|
209
|
+
const stripped = text.replace(/^(?:file|repo):/i, "");
|
|
210
|
+
if (!/[\w./-]+\.[\w]+(?:#L\d+(?:-L?\d+)?)?$/i.test(stripped)) return null;
|
|
211
|
+
return stripped;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isWorkflowSourceRefText(value) {
|
|
215
|
+
return /^wsrc_[a-z0-9]{16,}$/i.test(String(value ?? "").trim());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function collectLocalRefs(value, refs = []) {
|
|
219
|
+
if (!value || typeof value !== "object") return refs;
|
|
87
220
|
if (Array.isArray(value)) {
|
|
88
|
-
for (const item of value)
|
|
89
|
-
return
|
|
221
|
+
for (const item of value) collectLocalRefs(item, refs);
|
|
222
|
+
return refs;
|
|
90
223
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
224
|
+
for (const [key, item] of Object.entries(value)) {
|
|
225
|
+
if (/^(files?|paths?|sourceRefs?|sourceUrls?|sources?)$/i.test(key)) {
|
|
226
|
+
for (const candidate of asArray(item).length ? item : [item]) {
|
|
227
|
+
const ref = normalizeLocalRef(candidate);
|
|
228
|
+
if (ref) refs.push(ref);
|
|
229
|
+
else if (candidate && typeof candidate === "object")
|
|
230
|
+
collectLocalRefs(candidate, refs);
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
94
233
|
}
|
|
234
|
+
if (item && typeof item === "object") collectLocalRefs(item, refs);
|
|
95
235
|
}
|
|
96
|
-
return
|
|
236
|
+
return refs;
|
|
97
237
|
}
|
|
98
238
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
239
|
+
function localRefsOf(item, limit = 3) {
|
|
240
|
+
const out = [];
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
for (const ref of collectLocalRefs(item, [])) {
|
|
243
|
+
if (seen.has(ref)) continue;
|
|
244
|
+
seen.add(ref);
|
|
245
|
+
out.push(ref);
|
|
246
|
+
if (out.length >= limit) break;
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function referenceList(item, limit = 3) {
|
|
252
|
+
const urls = markdownLinkList(urlsOf(item, limit), limit);
|
|
253
|
+
const localRefs = localRefsOf(item, limit)
|
|
254
|
+
.map((ref) => `\`${ref}\``)
|
|
255
|
+
.join(", ");
|
|
256
|
+
return [urls, localRefs].filter(Boolean).join("; ");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function markdownLinkList(urls, maxItems = 3) {
|
|
260
|
+
return urls
|
|
261
|
+
.slice(0, maxItems)
|
|
262
|
+
.map((url) => `[${hostOf(url)}](${url})`)
|
|
263
|
+
.join(", ");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function itemText(item, fields, fallback = "") {
|
|
267
|
+
if (typeof item === "string") return cleanText(item) || fallback;
|
|
268
|
+
if (!item || typeof item !== "object") return fallback;
|
|
108
269
|
for (const field of fields) {
|
|
109
|
-
if (typeof item[field] === "string" && item[field].trim())
|
|
270
|
+
if (typeof item[field] === "string" && item[field].trim()) {
|
|
110
271
|
return cleanText(item[field]);
|
|
272
|
+
}
|
|
111
273
|
}
|
|
112
|
-
return
|
|
274
|
+
return fallback;
|
|
113
275
|
}
|
|
114
276
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (selected.length === 0) return "";
|
|
126
|
-
return ` (${selected.map((url) => hostOf(url)).join(", ")}: ${selected.join(" ")})`;
|
|
277
|
+
function evidenceStatusOf(item) {
|
|
278
|
+
if (!item || typeof item !== "object") return "not specified";
|
|
279
|
+
return cleanText(
|
|
280
|
+
item.evidenceStatus ??
|
|
281
|
+
item.status ??
|
|
282
|
+
item.confidence ??
|
|
283
|
+
item.sourceQuality ??
|
|
284
|
+
"not specified",
|
|
285
|
+
);
|
|
127
286
|
}
|
|
128
287
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
288
|
+
function confidenceOf(item) {
|
|
289
|
+
if (!item || typeof item !== "object") return "";
|
|
290
|
+
return cleanText(item.confidence ?? item.evidenceStatus ?? "");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function finiteNumber(value) {
|
|
294
|
+
const parsed = Number(value);
|
|
295
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeClaimStatus(status) {
|
|
299
|
+
const text = cleanText(status).toLowerCase();
|
|
300
|
+
if (!text) return "";
|
|
301
|
+
if (text.includes("conflict")) return "conflicting";
|
|
302
|
+
if (text.includes("unsupported")) return "unsupported";
|
|
303
|
+
if (text.includes("partial")) return "partially_supported";
|
|
304
|
+
if (text.includes("verified")) return "verified";
|
|
305
|
+
return text;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function coverageCounts(coverage, fallback) {
|
|
309
|
+
if (!coverage || typeof coverage !== "object") return null;
|
|
310
|
+
const counts = {
|
|
311
|
+
total: finiteNumber(coverage.verificationCandidates) ?? fallback.total,
|
|
312
|
+
verified: finiteNumber(coverage.verified) ?? fallback.verified,
|
|
313
|
+
partially_supported:
|
|
314
|
+
finiteNumber(coverage.partiallySupported) ??
|
|
315
|
+
finiteNumber(coverage.partially_supported) ??
|
|
316
|
+
fallback.partially_supported,
|
|
317
|
+
unsupported: finiteNumber(coverage.unsupported) ?? fallback.unsupported,
|
|
318
|
+
conflicting: finiteNumber(coverage.conflicting) ?? fallback.conflicting,
|
|
319
|
+
};
|
|
320
|
+
if (counts.total == null) {
|
|
321
|
+
counts.total =
|
|
322
|
+
counts.verified +
|
|
323
|
+
counts.partially_supported +
|
|
324
|
+
counts.unsupported +
|
|
325
|
+
counts.conflicting;
|
|
136
326
|
}
|
|
137
|
-
return
|
|
327
|
+
return counts;
|
|
138
328
|
}
|
|
139
329
|
|
|
140
|
-
function
|
|
330
|
+
function packetVerdictCounts(packet, fallback) {
|
|
331
|
+
const verdicts = packet?.verdictCounts;
|
|
332
|
+
if (!isRecord(verdicts)) return null;
|
|
333
|
+
const counts = coverageCounts(verdicts, fallback);
|
|
334
|
+
if (!counts) return null;
|
|
335
|
+
counts.total =
|
|
336
|
+
finiteNumber(packet?.invariantChecks?.candidateCount) ??
|
|
337
|
+
finiteNumber(verdicts.total) ??
|
|
338
|
+
counts.verified +
|
|
339
|
+
counts.partially_supported +
|
|
340
|
+
counts.unsupported +
|
|
341
|
+
counts.conflicting;
|
|
342
|
+
return counts;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function claimCounts(control, packet) {
|
|
141
346
|
const claims = asArray(control?.claimVerdictIndex?.claims);
|
|
142
347
|
const counts = {
|
|
143
348
|
total: claims.length,
|
|
@@ -147,130 +352,861 @@ function claimCounts(control) {
|
|
|
147
352
|
conflicting: 0,
|
|
148
353
|
};
|
|
149
354
|
for (const claim of claims) {
|
|
150
|
-
const status = claim?.status;
|
|
355
|
+
const status = normalizeClaimStatus(claim?.status);
|
|
151
356
|
if (status && Object.hasOwn(counts, status)) counts[status] += 1;
|
|
152
357
|
}
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
358
|
+
const packetCounts = packetVerdictCounts(packet, counts);
|
|
359
|
+
if (packetCounts) return packetCounts;
|
|
360
|
+
|
|
361
|
+
const coverage = coverageCounts(
|
|
362
|
+
control?.finalReport?.coverageSummary,
|
|
363
|
+
counts,
|
|
364
|
+
);
|
|
365
|
+
if (claims.length === 0 && coverage) return coverage;
|
|
366
|
+
if (!coverage) return counts;
|
|
367
|
+
|
|
368
|
+
const mismatches = [];
|
|
369
|
+
for (const key of [
|
|
370
|
+
"total",
|
|
371
|
+
"verified",
|
|
372
|
+
"partially_supported",
|
|
373
|
+
"unsupported",
|
|
374
|
+
"conflicting",
|
|
375
|
+
]) {
|
|
376
|
+
if (coverage[key] !== counts[key]) {
|
|
377
|
+
mismatches.push({
|
|
378
|
+
field: key,
|
|
379
|
+
claimVerdictIndex: counts[key],
|
|
380
|
+
coverageSummary: coverage[key],
|
|
381
|
+
});
|
|
382
|
+
}
|
|
169
383
|
}
|
|
170
|
-
return
|
|
384
|
+
return mismatches.length > 0
|
|
385
|
+
? { ...counts, coverageSummaryMismatch: mismatches }
|
|
386
|
+
: counts;
|
|
171
387
|
}
|
|
172
388
|
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
389
|
+
function factSlotSummary(factSlots) {
|
|
390
|
+
return {
|
|
391
|
+
total: factSlots.length,
|
|
392
|
+
filled: factSlots.filter((slot) => slot?.status === "filled").length,
|
|
393
|
+
partial: factSlots.filter((slot) => slot?.status === "partial").length,
|
|
394
|
+
missingOrConflicting: factSlots.filter((slot) =>
|
|
395
|
+
["missing", "gap", "conflicting"].includes(slot?.status),
|
|
396
|
+
).length,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function stringArray(value, limit = Infinity) {
|
|
401
|
+
const out = [];
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
for (const item of asArray(value)) {
|
|
404
|
+
if (typeof item !== "string") continue;
|
|
405
|
+
const text = item.trim();
|
|
406
|
+
if (!text || seen.has(text)) continue;
|
|
407
|
+
seen.add(text);
|
|
408
|
+
out.push(text);
|
|
409
|
+
if (out.length >= limit) break;
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function uniqueStrings(values, limit = Infinity) {
|
|
415
|
+
const out = [];
|
|
416
|
+
const seen = new Set();
|
|
417
|
+
for (const value of values) {
|
|
418
|
+
if (typeof value !== "string") continue;
|
|
419
|
+
const text = value.trim();
|
|
420
|
+
if (!text || seen.has(text)) continue;
|
|
421
|
+
seen.add(text);
|
|
422
|
+
out.push(text);
|
|
423
|
+
if (out.length >= limit) break;
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function packetOf(packetSource) {
|
|
429
|
+
return isRecord(packetSource?.packet) ? packetSource.packet : {};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function claimIdOf(row) {
|
|
433
|
+
return cleanText(row?.id ?? row?.claimId ?? "");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function gapIdOf(row) {
|
|
437
|
+
return cleanText(row?.id ?? row?.gapId ?? "");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function claimLedger(packet, control) {
|
|
441
|
+
const packetLedger = asArray(packet?.claimVerdictLedger);
|
|
442
|
+
return packetLedger.length
|
|
443
|
+
? packetLedger
|
|
444
|
+
: asArray(control?.claimVerdictIndex?.claims);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function mapById(rows, idFn) {
|
|
448
|
+
const out = new Map();
|
|
449
|
+
for (const row of rows) {
|
|
450
|
+
const id = idFn(row);
|
|
451
|
+
if (id && !out.has(id)) out.set(id, row);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function numberedId(prefix, index) {
|
|
457
|
+
return `${prefix}-${String(index + 1).padStart(3, "0")}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function packetGapRows(packet) {
|
|
461
|
+
const remaining = asArray(packet?.remainingGaps).map((gap, index) => ({
|
|
462
|
+
id: gapIdOf(gap) || numberedId("gap-remaining", index),
|
|
463
|
+
kind: "Gap",
|
|
464
|
+
...gap,
|
|
465
|
+
}));
|
|
466
|
+
const coverage = asArray(packet?.coverageGaps).map((gap, index) => ({
|
|
467
|
+
id: gapIdOf(gap) || numberedId("gap-coverage", index),
|
|
468
|
+
kind: "Coverage gap",
|
|
469
|
+
...gap,
|
|
470
|
+
}));
|
|
471
|
+
return [...remaining, ...coverage];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function rowsForIds(ids, rowById, warnings, label) {
|
|
475
|
+
const rows = [];
|
|
476
|
+
for (const id of ids) {
|
|
477
|
+
const row = rowById.get(id);
|
|
478
|
+
if (row) {
|
|
479
|
+
rows.push(row);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
warnings.push({
|
|
483
|
+
section: "references",
|
|
484
|
+
label,
|
|
485
|
+
total: 1,
|
|
486
|
+
rendered: 0,
|
|
487
|
+
missingId: id,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return rows;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function claimSourceUrls(rows, limit = 8) {
|
|
494
|
+
return uniqueStrings(
|
|
495
|
+
rows.flatMap((row) => [...asArray(row?.sourceUrls), ...urlsOf(row, limit)]),
|
|
496
|
+
limit,
|
|
194
497
|
);
|
|
195
|
-
|
|
498
|
+
}
|
|
196
499
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
state,
|
|
202
|
-
options,
|
|
500
|
+
function claimSourceRefs(rows, limit = 8) {
|
|
501
|
+
return uniqueStrings(
|
|
502
|
+
rows.flatMap((row) => asArray(row?.sourceRefs)),
|
|
503
|
+
limit,
|
|
203
504
|
);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function evidenceStrength(status) {
|
|
508
|
+
switch (normalizeClaimStatus(status)) {
|
|
509
|
+
case "verified":
|
|
510
|
+
return 3;
|
|
511
|
+
case "partially_supported":
|
|
512
|
+
return 2;
|
|
513
|
+
case "unsupported":
|
|
514
|
+
return 1;
|
|
515
|
+
case "conflicting":
|
|
516
|
+
return 0;
|
|
517
|
+
default:
|
|
518
|
+
return -1;
|
|
208
519
|
}
|
|
520
|
+
}
|
|
209
521
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
522
|
+
function evidenceStatusFromRows(rows, fallback) {
|
|
523
|
+
if (rows.length === 0) return cleanText(fallback) || "not specified";
|
|
524
|
+
let weakest = "verified";
|
|
525
|
+
let weakestScore = Infinity;
|
|
526
|
+
for (const row of rows) {
|
|
527
|
+
const status = normalizeClaimStatus(row?.status ?? row?.verdict);
|
|
528
|
+
const score = evidenceStrength(status);
|
|
529
|
+
if (score >= 0 && score < weakestScore) {
|
|
530
|
+
weakest = status;
|
|
531
|
+
weakestScore = score;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return weakestScore === Infinity
|
|
535
|
+
? cleanText(fallback) || "not specified"
|
|
536
|
+
: weakest;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function claimToFinding(row) {
|
|
540
|
+
return {
|
|
541
|
+
id: claimIdOf(row),
|
|
542
|
+
finding: cleanText(row?.claim ?? row?.support ?? stringifyItem(row)),
|
|
543
|
+
evidenceStatus: normalizeClaimStatus(row?.status) || row?.status,
|
|
544
|
+
confidence: row?.confidence,
|
|
545
|
+
sourceUrls: asArray(row?.sourceUrls),
|
|
546
|
+
sourceRefs: asArray(row?.sourceRefs),
|
|
547
|
+
rationale: row?.support,
|
|
548
|
+
caveat: row?.caveat,
|
|
549
|
+
correctionOrCounterclaim: row?.correctionOrCounterclaim,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function supportingClaimIds(item) {
|
|
554
|
+
return uniqueStrings([
|
|
555
|
+
...asArray(item?.supportingClaimIds),
|
|
556
|
+
...asArray(item?.claimIds),
|
|
557
|
+
...asArray(item?.relatedClaimIds),
|
|
558
|
+
]);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function withSupportingEvidence(item, claimRows) {
|
|
562
|
+
return {
|
|
563
|
+
...item,
|
|
564
|
+
evidenceStatus: evidenceStatusFromRows(claimRows, item?.evidenceStatus),
|
|
565
|
+
sourceUrls: uniqueStrings(
|
|
566
|
+
[...asArray(item?.sourceUrls), ...claimSourceUrls(claimRows)],
|
|
567
|
+
8,
|
|
568
|
+
),
|
|
569
|
+
sourceRefs: uniqueStrings(
|
|
570
|
+
[...asArray(item?.sourceRefs), ...claimSourceRefs(claimRows)],
|
|
571
|
+
8,
|
|
572
|
+
),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function coverageSummaryFromPacket(packet, fallback = {}) {
|
|
577
|
+
const counts = packetVerdictCounts(packet, {
|
|
578
|
+
total: 0,
|
|
579
|
+
verified: 0,
|
|
580
|
+
partially_supported: 0,
|
|
581
|
+
unsupported: 0,
|
|
582
|
+
conflicting: 0,
|
|
583
|
+
});
|
|
584
|
+
if (!counts) return fallback;
|
|
585
|
+
return {
|
|
586
|
+
...fallback,
|
|
587
|
+
verified: counts.verified,
|
|
588
|
+
partiallySupported: counts.partially_supported,
|
|
589
|
+
unsupported: counts.unsupported,
|
|
590
|
+
conflicting: counts.conflicting,
|
|
591
|
+
verificationCandidates: counts.total,
|
|
592
|
+
depth: packet?.researchMetadataSeed?.depth ?? fallback.depth,
|
|
593
|
+
researchQuestions:
|
|
594
|
+
packet?.researchMetadataSeed?.researchQuestions ??
|
|
595
|
+
fallback.researchQuestions,
|
|
596
|
+
preserved:
|
|
597
|
+
packet?.overflowLedger?.preservedClaimCount ?? fallback.preserved,
|
|
598
|
+
coverageGaps:
|
|
599
|
+
packet?.overflowLedger?.coverageGapCount ?? fallback.coverageGaps,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function composeResearchReport(control, packetSource) {
|
|
604
|
+
const packet = packetOf(packetSource);
|
|
605
|
+
const legacyReport = control?.finalReport ?? {};
|
|
606
|
+
const synthesis = isRecord(control?.synthesis) ? control.synthesis : null;
|
|
607
|
+
const ledger = claimLedger(packet, control);
|
|
608
|
+
const claimById = mapById(ledger, claimIdOf);
|
|
609
|
+
const gapRows = packetGapRows(packet);
|
|
610
|
+
const gapById = mapById(gapRows, gapIdOf);
|
|
611
|
+
const warnings = [];
|
|
612
|
+
|
|
613
|
+
if (!synthesis) {
|
|
614
|
+
const report = { ...legacyReport };
|
|
615
|
+
if (asArray(packet.factSlotCoverage).length > 0)
|
|
616
|
+
report.factSlotCoverage = packet.factSlotCoverage;
|
|
617
|
+
if (isRecord(packet.researchMetadataSeed))
|
|
618
|
+
report.researchMetadata = packet.researchMetadataSeed;
|
|
619
|
+
if (isRecord(packet.verdictCounts))
|
|
620
|
+
report.coverageSummary = coverageSummaryFromPacket(
|
|
621
|
+
packet,
|
|
622
|
+
report.coverageSummary,
|
|
623
|
+
);
|
|
624
|
+
if (asArray(report.remainingGaps).length === 0 && gapRows.length > 0)
|
|
625
|
+
report.remainingGaps = gapRows;
|
|
626
|
+
if (
|
|
627
|
+
asArray(report.researchScopeCoverage).length === 0 &&
|
|
628
|
+
asArray(packet.researchScopeCoverage).length > 0
|
|
629
|
+
) {
|
|
630
|
+
report.researchScopeCoverage = packet.researchScopeCoverage;
|
|
631
|
+
}
|
|
632
|
+
return { report, packet, ledger, warnings };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const keyFindingIds = stringArray(synthesis.keyFindingIds, 12);
|
|
636
|
+
const keyFindingRows = keyFindingIds.length
|
|
637
|
+
? rowsForIds(keyFindingIds, claimById, warnings, "key findings")
|
|
638
|
+
: ledger
|
|
639
|
+
.filter((row) => normalizeClaimStatus(row?.status) === "verified")
|
|
640
|
+
.slice(0, 8);
|
|
641
|
+
const mapOverlayItems = (items, textField) =>
|
|
642
|
+
asArray(items).map((item) => {
|
|
643
|
+
const ids = supportingClaimIds(item);
|
|
644
|
+
const rows = rowsForIds(ids, claimById, warnings, textField);
|
|
645
|
+
return withSupportingEvidence(item, rows);
|
|
646
|
+
});
|
|
647
|
+
const caveatNotes = asArray(synthesis.caveatNotes).map((item) => {
|
|
648
|
+
const rows = rowsForIds(
|
|
649
|
+
supportingClaimIds(item),
|
|
650
|
+
claimById,
|
|
651
|
+
warnings,
|
|
652
|
+
"caveat notes",
|
|
653
|
+
);
|
|
654
|
+
const gaps = rowsForIds(
|
|
655
|
+
stringArray(item?.gapIds, 12),
|
|
656
|
+
gapById,
|
|
657
|
+
warnings,
|
|
658
|
+
"gap notes",
|
|
659
|
+
);
|
|
660
|
+
return withSupportingEvidence(
|
|
661
|
+
{
|
|
662
|
+
...item,
|
|
663
|
+
relatedGaps: gaps,
|
|
664
|
+
},
|
|
665
|
+
rows,
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
const optionalUnsupported = rowsForIds(
|
|
669
|
+
stringArray(synthesis.notableUnsupportedClaimIds, 12),
|
|
670
|
+
claimById,
|
|
671
|
+
warnings,
|
|
672
|
+
"unsupported claims",
|
|
673
|
+
).map(claimToFinding);
|
|
674
|
+
const optionalContested = rowsForIds(
|
|
675
|
+
stringArray(synthesis.contestedClaimIds, 12),
|
|
676
|
+
claimById,
|
|
677
|
+
warnings,
|
|
678
|
+
"contested claims",
|
|
679
|
+
).map(claimToFinding);
|
|
680
|
+
const derivedUnsupported = ledger
|
|
681
|
+
.filter((row) => normalizeClaimStatus(row?.status) === "unsupported")
|
|
682
|
+
.map(claimToFinding);
|
|
683
|
+
const derivedContested = ledger
|
|
684
|
+
.filter((row) => normalizeClaimStatus(row?.status) === "conflicting")
|
|
685
|
+
.map(claimToFinding);
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
report: {
|
|
689
|
+
summary: synthesis.bottomLine ?? control?.digest,
|
|
690
|
+
researchMetadata: packet.researchMetadataSeed ?? {},
|
|
691
|
+
coverageSummary: coverageSummaryFromPacket(packet, {}),
|
|
692
|
+
factSlotCoverage: asArray(packet.factSlotCoverage),
|
|
693
|
+
mainFindings: keyFindingRows.map(claimToFinding),
|
|
694
|
+
recommendations: mapOverlayItems(
|
|
695
|
+
synthesis.recommendations,
|
|
696
|
+
"recommendations",
|
|
697
|
+
),
|
|
698
|
+
actionPlan: mapOverlayItems(synthesis.actionPlan, "action plan"),
|
|
699
|
+
caveatedFindings: caveatNotes,
|
|
700
|
+
contestedAreas: optionalContested.length
|
|
701
|
+
? optionalContested
|
|
702
|
+
: derivedContested,
|
|
703
|
+
notableUnsupportedClaims: optionalUnsupported.length
|
|
704
|
+
? optionalUnsupported
|
|
705
|
+
: derivedUnsupported,
|
|
706
|
+
unverifiedButRelevant: asArray(packet.preservedClaims),
|
|
707
|
+
parentDecisionNotes: mapOverlayItems(
|
|
708
|
+
synthesis.parentDecisionNotes,
|
|
709
|
+
"decision notes",
|
|
710
|
+
),
|
|
711
|
+
researchScopeCoverage: asArray(packet.researchScopeCoverage),
|
|
712
|
+
remainingGaps: gapRows,
|
|
713
|
+
},
|
|
714
|
+
packet,
|
|
715
|
+
ledger,
|
|
716
|
+
warnings,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function statusRank(item) {
|
|
721
|
+
const status =
|
|
722
|
+
`${item?.evidenceStatus ?? item?.status ?? item?.confidence ?? ""}`.toLowerCase();
|
|
723
|
+
if (
|
|
724
|
+
status.includes("missing") ||
|
|
725
|
+
status.includes("gap") ||
|
|
726
|
+
status.includes("conflict")
|
|
727
|
+
) {
|
|
728
|
+
return 0;
|
|
222
729
|
}
|
|
730
|
+
if (status.includes("unsupported")) return 1;
|
|
731
|
+
if (status.includes("partial")) return 2;
|
|
732
|
+
if (status.includes("verified") && !status.includes("partial")) return 3;
|
|
733
|
+
if (status.includes("filled") || status.includes("high")) return 4;
|
|
734
|
+
return 5;
|
|
735
|
+
}
|
|
223
736
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
737
|
+
function sortedFactSlots(report) {
|
|
738
|
+
return asArray(report.factSlotCoverage)
|
|
739
|
+
.slice()
|
|
740
|
+
.sort(
|
|
741
|
+
(a, b) =>
|
|
742
|
+
statusRank(a) - statusRank(b) ||
|
|
743
|
+
cleanText(a?.slotId ?? a?.label).localeCompare(
|
|
744
|
+
cleanText(b?.slotId ?? b?.label),
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderEvidenceStrength(report) {
|
|
750
|
+
const slots = sortedFactSlots(report);
|
|
751
|
+
const rows = slots.map((slot) => {
|
|
752
|
+
const area = escapeTableCell(
|
|
753
|
+
slot.label ?? slot.slotId ?? slot.bestValue ?? "Evidence area",
|
|
754
|
+
);
|
|
755
|
+
const status = escapeTableCell(evidenceStatusOf(slot));
|
|
756
|
+
const evidence = escapeTableCell(referenceList(slot, 2) || "—");
|
|
757
|
+
const impact = escapeTableCell(
|
|
758
|
+
slot.parentImpact ?? slot.whyItMatters ?? slot.notes ?? "",
|
|
759
|
+
);
|
|
760
|
+
return `| ${area || "Evidence area"} | ${status || "—"} | ${evidence} | ${impact || "—"} |`;
|
|
761
|
+
});
|
|
762
|
+
if (rows.length === 0) return [];
|
|
763
|
+
return [
|
|
764
|
+
"## Evidence strength",
|
|
765
|
+
"",
|
|
766
|
+
"| Area | Status | Evidence | Why it matters |",
|
|
767
|
+
"|---|---|---|---|",
|
|
768
|
+
...rows,
|
|
769
|
+
"",
|
|
228
770
|
];
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function mainFindingEntries(report) {
|
|
774
|
+
return asArray(report.mainFindings).map((item) => ({
|
|
775
|
+
item,
|
|
776
|
+
text: itemText(
|
|
777
|
+
item,
|
|
778
|
+
["finding", "summary", "bestValue", "claim"],
|
|
779
|
+
stringifyItem(item),
|
|
780
|
+
),
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function recommendationEntries(report) {
|
|
785
|
+
return asArray(report.recommendations).map((item) => ({
|
|
786
|
+
item,
|
|
787
|
+
text: itemText(
|
|
788
|
+
item,
|
|
789
|
+
["recommendation", "action", "step", "note"],
|
|
790
|
+
stringifyItem(item),
|
|
791
|
+
),
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function actionEntries(report) {
|
|
796
|
+
return asArray(report.actionPlan).map((item) => ({
|
|
797
|
+
item,
|
|
798
|
+
text:
|
|
799
|
+
itemText(item, ["action", "recommendation", "note"]) ||
|
|
800
|
+
(typeof item?.step === "string" && cleanText(item.step)) ||
|
|
801
|
+
stringifyItem(item),
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function renderMainFindings(report) {
|
|
806
|
+
const findings = mainFindingEntries(report);
|
|
807
|
+
if (findings.length === 0) return [];
|
|
808
|
+
const out = ["## Main findings", ""];
|
|
809
|
+
findings.forEach(({ item: finding, text }, index) => {
|
|
810
|
+
const status = evidenceStatusOf(finding);
|
|
811
|
+
const confidence = confidenceOf(finding);
|
|
812
|
+
const urls = referenceList(finding, 4);
|
|
813
|
+
out.push(`### ${index + 1}. ${text}`);
|
|
814
|
+
out.push("");
|
|
815
|
+
out.push(
|
|
816
|
+
`Evidence status: **${status || "not specified"}**${confidence && confidence !== status ? ` \nConfidence: **${confidence}**` : ""}`,
|
|
817
|
+
);
|
|
818
|
+
if (urls) out.push(`Sources: ${urls}`);
|
|
819
|
+
const explanation = itemText(finding, [
|
|
820
|
+
"rationale",
|
|
821
|
+
"explanation",
|
|
822
|
+
"details",
|
|
823
|
+
"notes",
|
|
824
|
+
]);
|
|
825
|
+
if (explanation && explanation !== text) out.push("", explanation);
|
|
826
|
+
out.push("");
|
|
827
|
+
});
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function renderRecommendations(report) {
|
|
832
|
+
const recommendations = recommendationEntries(report);
|
|
833
|
+
if (recommendations.length === 0) return [];
|
|
834
|
+
const out = ["## Recommendations", ""];
|
|
835
|
+
recommendations.forEach(({ item, text }, index) => {
|
|
836
|
+
const status = evidenceStatusOf(item);
|
|
837
|
+
const urls = referenceList(item, 4);
|
|
838
|
+
out.push(`${index + 1}. **${text}**`);
|
|
839
|
+
out.push(` - Evidence status: ${status || "not specified"}`);
|
|
840
|
+
if (urls) out.push(` - Sources: ${urls}`);
|
|
841
|
+
out.push("");
|
|
842
|
+
});
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function renderActionPlan(report) {
|
|
847
|
+
const actions = actionEntries(report);
|
|
848
|
+
if (actions.length === 0) return [];
|
|
849
|
+
const out = ["## Action plan", ""];
|
|
850
|
+
actions.forEach(({ item, text }, index) => {
|
|
851
|
+
const numericStep = Number(item?.step);
|
|
852
|
+
const step = Number.isFinite(numericStep) ? numericStep : index + 1;
|
|
853
|
+
const urls = referenceList(item, 3);
|
|
854
|
+
const evidence = evidenceStatusOf(item);
|
|
855
|
+
out.push(`${step}. ${text}`);
|
|
856
|
+
if (evidence && evidence !== "not specified")
|
|
857
|
+
out.push(` - Evidence: ${evidence}`);
|
|
858
|
+
if (urls) out.push(` - Sources: ${urls}`);
|
|
859
|
+
out.push("");
|
|
860
|
+
});
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function fallbackCaveatText(item) {
|
|
865
|
+
if (!isRecord(item)) return stringifyItem(item);
|
|
866
|
+
const id = cleanText(item.id ?? item.gapId ?? "");
|
|
867
|
+
const slotIds = uniqueStrings([
|
|
868
|
+
item.slotId,
|
|
869
|
+
...asArray(item.relatedFactSlotIds),
|
|
870
|
+
]).join(", ");
|
|
871
|
+
const kind = cleanText(item.kind ?? "gap");
|
|
872
|
+
return (
|
|
873
|
+
[kind, id, slotIds ? `related slots: ${slotIds}` : undefined]
|
|
874
|
+
.filter(Boolean)
|
|
875
|
+
.join(" — ") || stringifyItem(item)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function caveatText(item) {
|
|
880
|
+
return itemText(
|
|
881
|
+
item,
|
|
882
|
+
[
|
|
883
|
+
"gap",
|
|
884
|
+
"finding",
|
|
885
|
+
"claim",
|
|
886
|
+
"note",
|
|
887
|
+
"reason",
|
|
888
|
+
"nextStep",
|
|
889
|
+
"evidenceState",
|
|
890
|
+
"whyItMatters",
|
|
891
|
+
"parentImpact",
|
|
892
|
+
],
|
|
893
|
+
fallbackCaveatText(item),
|
|
236
894
|
);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function caveatCategories(report) {
|
|
898
|
+
return [
|
|
899
|
+
{ kind: "Gap", items: flattenItems(report.remainingGaps) },
|
|
900
|
+
{
|
|
901
|
+
kind: "Unsupported",
|
|
902
|
+
items: flattenItems(report.notableUnsupportedClaims),
|
|
903
|
+
},
|
|
904
|
+
{ kind: "Contested", items: flattenItems(report.contestedAreas) },
|
|
905
|
+
{ kind: "Caveat", items: flattenItems(report.caveatedFindings) },
|
|
906
|
+
{
|
|
907
|
+
kind: "Unverified lead",
|
|
908
|
+
items: flattenItems(report.unverifiedButRelevant),
|
|
909
|
+
},
|
|
910
|
+
{ kind: "Decision note", items: flattenItems(report.parentDecisionNotes) },
|
|
911
|
+
]
|
|
912
|
+
.map((category) => ({
|
|
913
|
+
kind: category.kind,
|
|
914
|
+
entries: category.items
|
|
915
|
+
.map((item) => ({ item, text: caveatText(item) }))
|
|
916
|
+
.filter((entry) => entry.text),
|
|
917
|
+
}))
|
|
918
|
+
.filter((category) => category.entries.length > 0);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function selectCaveats(report) {
|
|
922
|
+
const categories = caveatCategories(report);
|
|
923
|
+
const selected = [];
|
|
924
|
+
for (const category of categories) {
|
|
925
|
+
for (const entry of category.entries) {
|
|
926
|
+
selected.push({ kind: category.kind, ...entry });
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
selected,
|
|
931
|
+
total: selected.length,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function renderCaveats(report) {
|
|
936
|
+
const selection = selectCaveats(report);
|
|
937
|
+
if (selection.total === 0) return [];
|
|
938
|
+
const out = ["## Caveats and remaining gaps", ""];
|
|
939
|
+
for (const { kind, item, text } of selection.selected) {
|
|
940
|
+
const urls = referenceList(item, 3);
|
|
941
|
+
out.push(`- **${kind}:** ${text}${urls ? ` (${urls})` : ""}`);
|
|
942
|
+
}
|
|
943
|
+
out.push("");
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function renderSourceIndex(sourceIndex) {
|
|
948
|
+
if (sourceIndex.length === 0) return [];
|
|
949
|
+
const grouped = new Map();
|
|
950
|
+
for (const url of sourceIndex) {
|
|
951
|
+
const host = hostOf(url);
|
|
952
|
+
if (!grouped.has(host)) grouped.set(host, []);
|
|
953
|
+
grouped.get(host).push(url);
|
|
954
|
+
}
|
|
955
|
+
const out = ["## Source index", ""];
|
|
956
|
+
for (const [host, urls] of grouped) {
|
|
957
|
+
out.push(
|
|
958
|
+
`- **${host}**: ${urls.map((url) => `[${url}](${url})`).join(", ")}`,
|
|
959
|
+
);
|
|
241
960
|
}
|
|
961
|
+
out.push("");
|
|
962
|
+
return out;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function renderAuditSummary(report, claimSummary, slots) {
|
|
966
|
+
const coverage = report?.coverageSummary ?? {};
|
|
967
|
+
const mismatches = asArray(claimSummary.coverageSummaryMismatch);
|
|
968
|
+
return [
|
|
969
|
+
"## Audit summary",
|
|
970
|
+
"",
|
|
971
|
+
`- Claims: ${claimSummary.verified} verified, ${claimSummary.partially_supported} partially supported, ${claimSummary.unsupported} unsupported, ${claimSummary.conflicting} conflicting.`,
|
|
972
|
+
`- Fact slots: ${slots.filled} filled, ${slots.partial} partial, ${slots.missingOrConflicting} missing/conflicting, ${slots.total} total.`,
|
|
973
|
+
...(mismatches.length > 0
|
|
974
|
+
? [
|
|
975
|
+
`- Coverage summary mismatch: displayed claim counts come from \`claimVerdictIndex\`; model coverageSummary disagreed on ${mismatches
|
|
976
|
+
.map((mismatch) => mismatch.field)
|
|
977
|
+
.join(", ")}.`,
|
|
978
|
+
]
|
|
979
|
+
: []),
|
|
980
|
+
...(coverage.researchQuestions != null
|
|
981
|
+
? [`- Research questions: ${coverage.researchQuestions}.`]
|
|
982
|
+
: []),
|
|
983
|
+
"- Audit artifact: `audit.md`.",
|
|
984
|
+
"",
|
|
985
|
+
];
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function renderWarnings(sectionCounts) {
|
|
989
|
+
const checks = [
|
|
990
|
+
["findings", "renderedFindings", "findings"],
|
|
991
|
+
["recommendations", "renderedRecommendations", "recommendations"],
|
|
992
|
+
["actionItems", "renderedActionItems", "action items"],
|
|
993
|
+
["caveatsAndGaps", "renderedCaveatsAndGaps", "caveats/gaps"],
|
|
994
|
+
["factSlots", "renderedFactSlots", "fact slots"],
|
|
995
|
+
["sourceUrls", "renderedSourceUrls", "source URLs"],
|
|
996
|
+
];
|
|
997
|
+
return checks
|
|
998
|
+
.filter(([totalKey, renderedKey]) => {
|
|
999
|
+
const total = Number(sectionCounts[totalKey] ?? 0);
|
|
1000
|
+
const rendered = Number(sectionCounts[renderedKey] ?? 0);
|
|
1001
|
+
return total !== rendered;
|
|
1002
|
+
})
|
|
1003
|
+
.map(([totalKey, renderedKey, label]) => ({
|
|
1004
|
+
section: totalKey,
|
|
1005
|
+
label,
|
|
1006
|
+
total: sectionCounts[totalKey],
|
|
1007
|
+
rendered: sectionCounts[renderedKey],
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
242
1010
|
|
|
243
|
-
|
|
244
|
-
|
|
1011
|
+
function renderResearchMarkdown(control, packetSource, options = {}) {
|
|
1012
|
+
const composed = composeResearchReport(control, packetSource);
|
|
1013
|
+
const report = composed.report;
|
|
1014
|
+
const claimSummary = claimCounts(control, composed.packet);
|
|
1015
|
+
const factSlots = sortedFactSlots(report);
|
|
1016
|
+
const slots = factSlotSummary(asArray(report.factSlotCoverage));
|
|
1017
|
+
const findings = mainFindingEntries(report);
|
|
1018
|
+
const recommendations = recommendationEntries(report);
|
|
1019
|
+
const actions = actionEntries(report);
|
|
1020
|
+
const caveats = selectCaveats(report);
|
|
1021
|
+
const allSourceIndex = uniqueStructuredUrls(
|
|
1022
|
+
report.factSlotCoverage,
|
|
1023
|
+
report.mainFindings,
|
|
1024
|
+
report.recommendations,
|
|
1025
|
+
report.actionPlan,
|
|
1026
|
+
report.caveatedFindings,
|
|
1027
|
+
report.contestedAreas,
|
|
1028
|
+
report.notableUnsupportedClaims,
|
|
1029
|
+
report.remainingGaps,
|
|
1030
|
+
report.parentDecisionNotes,
|
|
1031
|
+
report.unverifiedButRelevant,
|
|
1032
|
+
composed.ledger,
|
|
245
1033
|
);
|
|
1034
|
+
const maxUrls = Number.isFinite(Number(options.maxUrls))
|
|
1035
|
+
? Math.max(0, Number(options.maxUrls))
|
|
1036
|
+
: Infinity;
|
|
1037
|
+
const sourceIndex = Number.isFinite(maxUrls)
|
|
1038
|
+
? allSourceIndex.slice(0, maxUrls)
|
|
1039
|
+
: allSourceIndex;
|
|
1040
|
+
const sectionCounts = {
|
|
1041
|
+
findings: asArray(report.mainFindings).length,
|
|
1042
|
+
renderedFindings: findings.length,
|
|
1043
|
+
recommendations: asArray(report.recommendations).length,
|
|
1044
|
+
renderedRecommendations: recommendations.length,
|
|
1045
|
+
actionItems: asArray(report.actionPlan).length,
|
|
1046
|
+
renderedActionItems: actions.length,
|
|
1047
|
+
caveatsAndGaps:
|
|
1048
|
+
flattenItems(report.remainingGaps).length +
|
|
1049
|
+
flattenItems(report.notableUnsupportedClaims).length +
|
|
1050
|
+
flattenItems(report.contestedAreas).length +
|
|
1051
|
+
flattenItems(report.caveatedFindings).length +
|
|
1052
|
+
flattenItems(report.unverifiedButRelevant).length +
|
|
1053
|
+
flattenItems(report.parentDecisionNotes).length,
|
|
1054
|
+
renderedCaveatsAndGaps: caveats.selected.length,
|
|
1055
|
+
factSlots: asArray(report.factSlotCoverage).length,
|
|
1056
|
+
renderedFactSlots: factSlots.length,
|
|
1057
|
+
sourceUrls: allSourceIndex.length,
|
|
1058
|
+
renderedSourceUrls: sourceIndex.length,
|
|
1059
|
+
};
|
|
1060
|
+
const warnings = [...renderWarnings(sectionCounts), ...composed.warnings];
|
|
1061
|
+
|
|
1062
|
+
const sections = [
|
|
1063
|
+
"# Research report",
|
|
1064
|
+
"",
|
|
1065
|
+
"## Bottom line",
|
|
1066
|
+
"",
|
|
1067
|
+
summaryText(report, control.digest),
|
|
1068
|
+
"",
|
|
1069
|
+
...renderEvidenceStrength(report),
|
|
1070
|
+
...renderMainFindings(report),
|
|
1071
|
+
...renderRecommendations(report),
|
|
1072
|
+
...renderActionPlan(report),
|
|
1073
|
+
...renderCaveats(report),
|
|
1074
|
+
...renderSourceIndex(sourceIndex),
|
|
1075
|
+
...renderAuditSummary(report, claimSummary, slots),
|
|
1076
|
+
];
|
|
246
1077
|
|
|
247
|
-
|
|
1078
|
+
const markdown = sections
|
|
248
1079
|
.join("\n")
|
|
249
1080
|
.replace(/\n{3,}/g, "\n\n")
|
|
250
1081
|
.trim();
|
|
251
|
-
let truncated = false;
|
|
252
|
-
if (countWords(markdown) > maxWords) {
|
|
253
|
-
truncated = true;
|
|
254
|
-
markdown = truncateWords(markdown, maxWords);
|
|
255
|
-
}
|
|
256
|
-
for (const locator of [...new Set(collectSourceLocators(control))]) {
|
|
257
|
-
if (state.urls.size >= options.maxUrls) break;
|
|
258
|
-
state.urls.add(locator);
|
|
259
|
-
}
|
|
260
1082
|
return {
|
|
261
1083
|
markdown,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
partial: partialSlots,
|
|
269
|
-
missingOrConflicting: missingSlots,
|
|
270
|
-
},
|
|
1084
|
+
sourceIndex,
|
|
1085
|
+
allSourceIndex,
|
|
1086
|
+
claimSummary,
|
|
1087
|
+
factSlotSummary: slots,
|
|
1088
|
+
sectionCounts,
|
|
1089
|
+
renderWarnings: warnings,
|
|
271
1090
|
};
|
|
272
1091
|
}
|
|
273
1092
|
|
|
1093
|
+
function stripLeadingHeading(markdown) {
|
|
1094
|
+
return String(markdown ?? "").replace(/^#\s+[^\n]+\n*/i, "");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function synthesisClaimRows(control, rows) {
|
|
1098
|
+
const synthesis = isRecord(control?.synthesis) ? control.synthesis : null;
|
|
1099
|
+
if (!synthesis) return asArray(control?.claimVerdictIndex?.claims);
|
|
1100
|
+
const rowById = mapById(rows, claimIdOf);
|
|
1101
|
+
const ids = uniqueStrings([
|
|
1102
|
+
...asArray(synthesis.keyFindingIds),
|
|
1103
|
+
...asArray(synthesis.notableUnsupportedClaimIds),
|
|
1104
|
+
...asArray(synthesis.contestedClaimIds),
|
|
1105
|
+
...asArray(synthesis.recommendations).flatMap(supportingClaimIds),
|
|
1106
|
+
...asArray(synthesis.actionPlan).flatMap(supportingClaimIds),
|
|
1107
|
+
...asArray(synthesis.caveatNotes).flatMap(supportingClaimIds),
|
|
1108
|
+
...asArray(synthesis.parentDecisionNotes).flatMap(supportingClaimIds),
|
|
1109
|
+
]);
|
|
1110
|
+
return ids.map((id) => rowById.get(id)).filter(Boolean);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function renderAuditMarkdown(control, packetSource, rendered) {
|
|
1114
|
+
const packet = packetSource?.packet ?? {};
|
|
1115
|
+
const report = control?.finalReport ?? {};
|
|
1116
|
+
const ledger = asArray(packet.claimVerdictLedger);
|
|
1117
|
+
const claims = synthesisClaimRows(control, ledger);
|
|
1118
|
+
const gaps = asArray(packet.remainingGaps).length
|
|
1119
|
+
? asArray(packet.remainingGaps)
|
|
1120
|
+
: asArray(report.remainingGaps);
|
|
1121
|
+
const sourceRefJoinFailures = asArray(packet.sourceRefJoinFailures).filter(
|
|
1122
|
+
(failure) => uniqueStructuredUrls(failure).length > 0,
|
|
1123
|
+
);
|
|
1124
|
+
const factSlots = asArray(packet.factSlotCoverage).length
|
|
1125
|
+
? asArray(packet.factSlotCoverage)
|
|
1126
|
+
: asArray(report.factSlotCoverage);
|
|
1127
|
+
const rows = ledger.length ? ledger : claims;
|
|
1128
|
+
const out = [
|
|
1129
|
+
"# Research audit",
|
|
1130
|
+
"",
|
|
1131
|
+
"This artifact preserves the detailed claim/gap/source ledger behind `executive.md`.",
|
|
1132
|
+
"",
|
|
1133
|
+
"## Claim verdict ledger",
|
|
1134
|
+
"",
|
|
1135
|
+
];
|
|
1136
|
+
if (rows.length > 0) {
|
|
1137
|
+
out.push(
|
|
1138
|
+
"| ID | Status | Claim/support | Caveat/source |",
|
|
1139
|
+
"|---|---|---|---|",
|
|
1140
|
+
);
|
|
1141
|
+
for (const row of rows) {
|
|
1142
|
+
const id = escapeTableCell(row.id ?? row.claimId ?? "—");
|
|
1143
|
+
const status = escapeTableCell(row.status ?? row.confidence ?? "—");
|
|
1144
|
+
const support = escapeTableCell(
|
|
1145
|
+
row.claim ??
|
|
1146
|
+
row.support ??
|
|
1147
|
+
row.verdictDigest?.support ??
|
|
1148
|
+
stringifyItem(row),
|
|
1149
|
+
);
|
|
1150
|
+
const caveat = escapeTableCell(
|
|
1151
|
+
row.caveat ??
|
|
1152
|
+
row.correctionOrCounterclaim ??
|
|
1153
|
+
markdownLinkList(urlsOf(row, 3), 3) ??
|
|
1154
|
+
"—",
|
|
1155
|
+
);
|
|
1156
|
+
out.push(`| ${id} | ${status} | ${support} | ${caveat || "—"} |`);
|
|
1157
|
+
}
|
|
1158
|
+
} else {
|
|
1159
|
+
out.push("No compact claim ledger was provided.");
|
|
1160
|
+
}
|
|
1161
|
+
out.push("", "## Fact slot coverage", "");
|
|
1162
|
+
if (factSlots.length > 0) {
|
|
1163
|
+
out.push(
|
|
1164
|
+
"| Slot | Status | Best value | Gap/impact |",
|
|
1165
|
+
"|---|---|---|---|",
|
|
1166
|
+
);
|
|
1167
|
+
for (const slot of factSlots) {
|
|
1168
|
+
out.push(
|
|
1169
|
+
`| ${escapeTableCell(slot.slotId ?? slot.label ?? "—")} | ${escapeTableCell(slot.status ?? "—")} | ${escapeTableCell(isRecord(slot.bestValue) ? stringifyItem(slot.bestValue) : (slot.bestValue ?? "—"))} | ${escapeTableCell(slot.gapReason || slot.parentImpact || "—")} |`,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
out.push("No fact-slot ledger was provided.");
|
|
1174
|
+
}
|
|
1175
|
+
out.push("", "## Remaining gaps", "");
|
|
1176
|
+
if (gaps.length > 0) {
|
|
1177
|
+
for (const gap of gaps)
|
|
1178
|
+
out.push(`- ${caveatText(gap) || stringifyItem(gap)}`);
|
|
1179
|
+
} else {
|
|
1180
|
+
out.push("No remaining gaps were reported.");
|
|
1181
|
+
}
|
|
1182
|
+
if (claims.length > 0 && ledger.length > 0) {
|
|
1183
|
+
out.push("", "## Claims used in executive synthesis", "");
|
|
1184
|
+
for (const claim of claims) {
|
|
1185
|
+
out.push(
|
|
1186
|
+
`- **${cleanText(claim.id ?? "claim")}** (${cleanText(claim.status ?? "unknown")}): ${cleanText(claim.claim ?? claim.support ?? stringifyItem(claim))}`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (sourceRefJoinFailures.length > 0) {
|
|
1191
|
+
out.push("", "## Source reference join failures", "");
|
|
1192
|
+
for (const failure of sourceRefJoinFailures)
|
|
1193
|
+
out.push(`- ${caveatText(failure) || stringifyItem(failure)}`);
|
|
1194
|
+
}
|
|
1195
|
+
out.push(
|
|
1196
|
+
"",
|
|
1197
|
+
"## Renderer diagnostics",
|
|
1198
|
+
"",
|
|
1199
|
+
`- Executive word count: ${countWords(rendered.markdown)}.`,
|
|
1200
|
+
`- Rendered source URLs: ${rendered.sourceIndex.length}/${rendered.allSourceIndex.length}.`,
|
|
1201
|
+
`- Render warnings: ${rendered.renderWarnings.length}.`,
|
|
1202
|
+
"",
|
|
1203
|
+
);
|
|
1204
|
+
return out
|
|
1205
|
+
.join("\n")
|
|
1206
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1207
|
+
.trim();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
274
1210
|
export default async function renderExecutive({
|
|
275
1211
|
sources,
|
|
276
1212
|
options = {},
|
|
@@ -279,34 +1215,89 @@ export default async function renderExecutive({
|
|
|
279
1215
|
const control =
|
|
280
1216
|
findSource(sources, "final-audit") ??
|
|
281
1217
|
sources?.[Object.keys(sources ?? {})[0]];
|
|
282
|
-
const
|
|
283
|
-
maxWords: Number(options.maxWords ?? 600),
|
|
284
|
-
maxUrls: Number(options.maxUrls ?? 5),
|
|
285
|
-
maxFindings: Number(options.maxFindings ?? 3),
|
|
286
|
-
maxRecommendations: Number(options.maxRecommendations ?? 3),
|
|
287
|
-
maxGaps: Number(options.maxGaps ?? 2),
|
|
288
|
-
};
|
|
1218
|
+
const auditPacket = findSource(sources, "final-audit-packet");
|
|
289
1219
|
if (!control || typeof control !== "object") {
|
|
290
1220
|
return {
|
|
291
1221
|
schema: "deep-research-executive-render-v1",
|
|
292
|
-
digest:
|
|
1222
|
+
digest:
|
|
1223
|
+
"Research report rendering failed: missing final-audit control source.",
|
|
293
1224
|
status: "blocked",
|
|
294
1225
|
blockers: ["missing final-audit control source"],
|
|
295
1226
|
executiveMarkdown: "",
|
|
1227
|
+
reportMarkdown: "",
|
|
1228
|
+
auditMarkdown: "",
|
|
296
1229
|
wordCount: 0,
|
|
297
1230
|
sourceUrlCount: 0,
|
|
298
|
-
|
|
1231
|
+
totalSourceUrlCount: 0,
|
|
1232
|
+
sourceUrls: [],
|
|
1233
|
+
sourceIndex: [],
|
|
1234
|
+
claimSummary: {
|
|
1235
|
+
total: 0,
|
|
1236
|
+
verified: 0,
|
|
1237
|
+
partially_supported: 0,
|
|
1238
|
+
unsupported: 0,
|
|
1239
|
+
conflicting: 0,
|
|
1240
|
+
},
|
|
1241
|
+
factSlotSummary: {
|
|
1242
|
+
total: 0,
|
|
1243
|
+
filled: 0,
|
|
1244
|
+
partial: 0,
|
|
1245
|
+
missingOrConflicting: 0,
|
|
1246
|
+
},
|
|
1247
|
+
sectionCounts: {},
|
|
1248
|
+
renderWarnings: [],
|
|
1249
|
+
gates: {
|
|
1250
|
+
renderedAllStructuredItems: false,
|
|
1251
|
+
passed: false,
|
|
1252
|
+
},
|
|
1253
|
+
auditArtifact: "final-audit.control.json",
|
|
299
1254
|
};
|
|
300
1255
|
}
|
|
301
1256
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1257
|
+
const opts = {
|
|
1258
|
+
maxWords: Number.isFinite(Number(options.maxWords))
|
|
1259
|
+
? Math.max(0, Number(options.maxWords))
|
|
1260
|
+
: Infinity,
|
|
1261
|
+
maxUrls: Number.isFinite(Number(options.maxUrls))
|
|
1262
|
+
? Math.max(0, Number(options.maxUrls))
|
|
1263
|
+
: 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
|
+
};
|
|
1274
|
+
const rendered = renderResearchMarkdown(control, auditPacket, opts);
|
|
1275
|
+
let markdown = rendered.markdown;
|
|
1276
|
+
let truncated = false;
|
|
1277
|
+
if (Number.isFinite(opts.maxWords) && countWords(markdown) > opts.maxWords) {
|
|
1278
|
+
truncated = true;
|
|
1279
|
+
markdown = truncateWords(markdown, opts.maxWords);
|
|
1280
|
+
}
|
|
1281
|
+
const auditMarkdown = renderAuditMarkdown(control, auditPacket, rendered);
|
|
1282
|
+
const serializationArtifact =
|
|
1283
|
+
hasObjectSerializationArtifact(markdown) ||
|
|
1284
|
+
hasObjectSerializationArtifact(auditMarkdown);
|
|
1285
|
+
const wordCount = countWords(markdown);
|
|
1286
|
+
const sourceUrlCount = rendered.sourceIndex.length;
|
|
1287
|
+
const substantiveRenderWarnings = rendered.renderWarnings.filter(
|
|
1288
|
+
(warning) => warning.section !== "sourceUrls",
|
|
1289
|
+
);
|
|
1290
|
+
const renderedAllStructuredItems = substantiveRenderWarnings.length === 0;
|
|
1291
|
+
const truncatedWithOpenGaps =
|
|
1292
|
+
truncated && Number(rendered.sectionCounts.caveatsAndGaps ?? 0) > 0;
|
|
1293
|
+
const passed =
|
|
1294
|
+
renderedAllStructuredItems &&
|
|
1295
|
+
!truncatedWithOpenGaps &&
|
|
1296
|
+
!serializationArtifact;
|
|
306
1297
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
let
|
|
1298
|
+
let executiveSidecarPath;
|
|
1299
|
+
let reportSidecarPath;
|
|
1300
|
+
let auditSidecarPath;
|
|
310
1301
|
try {
|
|
311
1302
|
if (context.cwd && context.runId && context.taskId) {
|
|
312
1303
|
const taskDir = join(
|
|
@@ -318,36 +1309,52 @@ export default async function renderExecutive({
|
|
|
318
1309
|
context.taskId,
|
|
319
1310
|
);
|
|
320
1311
|
await mkdir(taskDir, { recursive: true });
|
|
321
|
-
|
|
322
|
-
|
|
1312
|
+
executiveSidecarPath = join(taskDir, "executive.md");
|
|
1313
|
+
reportSidecarPath = join(taskDir, "report.md");
|
|
1314
|
+
auditSidecarPath = join(taskDir, "audit.md");
|
|
1315
|
+
await writeFile(executiveSidecarPath, `${markdown}\n`, "utf8");
|
|
1316
|
+
await writeFile(reportSidecarPath, `${markdown}\n`, "utf8");
|
|
1317
|
+
await writeFile(auditSidecarPath, `${auditMarkdown}\n`, "utf8");
|
|
323
1318
|
}
|
|
324
1319
|
} catch {
|
|
325
|
-
//
|
|
1320
|
+
// Sidecars are non-authoritative; keep control output deterministic.
|
|
326
1321
|
}
|
|
327
1322
|
|
|
328
1323
|
return {
|
|
329
1324
|
schema: "deep-research-executive-render-v1",
|
|
330
|
-
digest: truncateWords(
|
|
331
|
-
rendered.markdown.replace(/^# Executive summary\s*/i, ""),
|
|
332
|
-
45,
|
|
333
|
-
),
|
|
1325
|
+
digest: truncateWords(stripLeadingHeading(markdown), 45),
|
|
334
1326
|
status: passed ? "passed" : "failed",
|
|
335
|
-
|
|
1327
|
+
renderMode: "evidence-backed-report",
|
|
1328
|
+
executiveMarkdown: markdown,
|
|
1329
|
+
reportMarkdown: markdown,
|
|
1330
|
+
auditMarkdown,
|
|
336
1331
|
wordCount,
|
|
337
1332
|
sourceUrlCount,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
1333
|
+
totalSourceUrlCount: rendered.allSourceIndex.length,
|
|
1334
|
+
sourceUrls: rendered.sourceIndex,
|
|
1335
|
+
sourceIndex: rendered.sourceIndex.map((url) => ({
|
|
1336
|
+
url,
|
|
1337
|
+
host: hostOf(url),
|
|
1338
|
+
})),
|
|
1339
|
+
claimSummary: rendered.claimSummary,
|
|
1340
|
+
factSlotSummary: rendered.factSlotSummary,
|
|
1341
|
+
sectionCounts: rendered.sectionCounts,
|
|
1342
|
+
renderWarnings: rendered.renderWarnings,
|
|
341
1343
|
gates: {
|
|
342
|
-
|
|
343
|
-
|
|
1344
|
+
renderedAllStructuredItems,
|
|
1345
|
+
maxWords: Number.isFinite(opts.maxWords) ? opts.maxWords : null,
|
|
1346
|
+
maxUrls: Number.isFinite(opts.maxUrls) ? opts.maxUrls : null,
|
|
344
1347
|
maxFindings: opts.maxFindings,
|
|
345
1348
|
maxRecommendations: opts.maxRecommendations,
|
|
346
1349
|
maxGaps: opts.maxGaps,
|
|
347
|
-
truncated
|
|
1350
|
+
truncated,
|
|
1351
|
+
truncatedWithOpenGaps,
|
|
1352
|
+
serializationArtifact,
|
|
348
1353
|
passed,
|
|
349
1354
|
},
|
|
350
|
-
auditArtifact: "final-audit.control.json",
|
|
351
|
-
...(
|
|
1355
|
+
auditArtifact: auditSidecarPath ? "audit.md" : "final-audit.control.json",
|
|
1356
|
+
...(executiveSidecarPath ? { sidecarPath: "executive.md" } : {}),
|
|
1357
|
+
...(reportSidecarPath ? { reportSidecarPath: "report.md" } : {}),
|
|
1358
|
+
...(auditSidecarPath ? { auditSidecarPath: "audit.md" } : {}),
|
|
352
1359
|
};
|
|
353
1360
|
}
|