@wildorder/nightshift 0.15.0 → 0.17.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 +22 -2
- package/dist/agent-runner.d.ts +56 -4
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +253 -28
- package/dist/agent-runner.js.map +1 -1
- package/dist/author.d.ts +12 -0
- package/dist/author.d.ts.map +1 -1
- package/dist/author.js +183 -88
- package/dist/author.js.map +1 -1
- package/dist/causal-analysis.d.ts +212 -0
- package/dist/causal-analysis.d.ts.map +1 -0
- package/dist/causal-analysis.js +733 -0
- package/dist/causal-analysis.js.map +1 -0
- package/dist/decider-review.d.ts +6 -1
- package/dist/decider-review.d.ts.map +1 -1
- package/dist/decider-review.js +19 -7
- package/dist/decider-review.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/preflight.d.ts +3 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +73 -57
- package/dist/preflight.js.map +1 -1
- package/dist/prompt-telemetry.d.ts +64 -0
- package/dist/prompt-telemetry.d.ts.map +1 -0
- package/dist/prompt-telemetry.js +112 -0
- package/dist/prompt-telemetry.js.map +1 -0
- package/dist/provider-telemetry.d.ts +106 -0
- package/dist/provider-telemetry.d.ts.map +1 -0
- package/dist/provider-telemetry.js +423 -0
- package/dist/provider-telemetry.js.map +1 -0
- package/dist/run-analytics-report.d.ts +173 -0
- package/dist/run-analytics-report.d.ts.map +1 -0
- package/dist/run-analytics-report.js +650 -0
- package/dist/run-analytics-report.js.map +1 -0
- package/dist/run-analytics.d.ts +738 -0
- package/dist/run-analytics.d.ts.map +1 -0
- package/dist/run-analytics.js +545 -0
- package/dist/run-analytics.js.map +1 -0
- package/dist/run-program.d.ts +69 -1
- package/dist/run-program.d.ts.map +1 -1
- package/dist/run-program.js +1263 -716
- package/dist/run-program.js.map +1 -1
- package/dist/whole-program-review.d.ts +3 -0
- package/dist/whole-program-review.d.ts.map +1 -1
- package/dist/whole-program-review.js +8 -1
- package/dist/whole-program-review.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { invokeAgent, resolveReviewerAgent, tail, } from "./agent-runner.js";
|
|
3
|
+
import { resolveSummary, summaryContract } from "./agent-summary.js";
|
|
4
|
+
import { NOOP_PERMITS_CONTEXT } from "./permits.js";
|
|
5
|
+
import { clippedInputPoint, createBriefBuilder, promptComponentSizePoints, } from "./prompt-telemetry.js";
|
|
6
|
+
import { NOOP_RUN_RECORDER } from "./run-analytics.js";
|
|
7
|
+
import { aggregateRunAnalytics } from "./run-analytics-report.js";
|
|
8
|
+
/**
|
|
9
|
+
* WS-05: an optional, independent, read-only causal-analysis pass, spawned
|
|
10
|
+
* through the existing `reviewerAgent`, that traces measured run costs
|
|
11
|
+
* (WS-04's `RunAnalyticsSummary`) toward planner-actionable causes — every
|
|
12
|
+
* step cited against retained evidence the run actually observed, and an
|
|
13
|
+
* unresolved result whenever the evidence runs out. Structured after
|
|
14
|
+
* `whole-program-review.ts`: a brief builder, a total non-throwing response
|
|
15
|
+
* parser, a deterministic evidence gate, a fail-open runner, and a pure
|
|
16
|
+
* renderer. Nothing here computes a duration, grades the run, or mutates
|
|
17
|
+
* anything — it consumes WS-04's numbers and cites them by handle.
|
|
18
|
+
*/
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Shared formatting (duplicated from run-analytics-report.ts — that module is
|
|
21
|
+
// WS-04's, not this workstream's, and importing its private, unexported
|
|
22
|
+
// helpers is not an option; the same duplication precedent already exists
|
|
23
|
+
// between whole-program-review.ts and run-program.ts for TEST_CRITIQUE_DIFF_LIMIT).
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function formatDuration(ms) {
|
|
26
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
27
|
+
ms = 0;
|
|
28
|
+
if (ms < 1000)
|
|
29
|
+
return `${Math.round(ms)}ms`;
|
|
30
|
+
const totalSeconds = ms / 1000;
|
|
31
|
+
if (totalSeconds < 60)
|
|
32
|
+
return `${totalSeconds.toFixed(1)}s`;
|
|
33
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
34
|
+
const seconds = Math.round(totalSeconds - minutes * 60);
|
|
35
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
36
|
+
}
|
|
37
|
+
function plural(count, noun) {
|
|
38
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
39
|
+
}
|
|
40
|
+
/** Combines every workstream's spec content into one labelled block, ready for the caller to clip like the diff and program narrative. Pure. */
|
|
41
|
+
export function combineWorkstreamSpecs(specs) {
|
|
42
|
+
if (specs.length === 0)
|
|
43
|
+
return "";
|
|
44
|
+
return specs
|
|
45
|
+
.map((entry) => `#### ${entry.id} ${entry.name} (\`${entry.taskFile}\`)\n\n` +
|
|
46
|
+
(entry.spec && entry.spec.trim() !== ""
|
|
47
|
+
? entry.spec.trim()
|
|
48
|
+
: "(this workstream's spec file could not be read)"))
|
|
49
|
+
.join("\n\n");
|
|
50
|
+
}
|
|
51
|
+
/** span, point, evidence, and ledger-event handles are concrete observations; bucket/workstream/verify-command are category labels — context, never support on their own (D2). */
|
|
52
|
+
export function isConcreteHandleKind(kind) {
|
|
53
|
+
return kind === "span" || kind === "point" || kind === "evidence" || kind === "ledger-event";
|
|
54
|
+
}
|
|
55
|
+
function spanDescription(span) {
|
|
56
|
+
const duration = span.endOffsetMs === undefined
|
|
57
|
+
? "open (no recorded end)"
|
|
58
|
+
: formatDuration(span.endOffsetMs - span.startOffsetMs);
|
|
59
|
+
const parts = [`stage=${span.stage}`, `bucket=${span.bucket}`, `duration=${duration}`];
|
|
60
|
+
const dims = span.dimensions;
|
|
61
|
+
if (dims.workstream !== undefined)
|
|
62
|
+
parts.push(`workstream=${dims.workstream}`);
|
|
63
|
+
if (dims.role !== undefined)
|
|
64
|
+
parts.push(`role=${dims.role}`);
|
|
65
|
+
if (dims.attemptSeat !== undefined)
|
|
66
|
+
parts.push(`attemptSeat=${dims.attemptSeat}`);
|
|
67
|
+
if (dims.attemptIndex !== undefined)
|
|
68
|
+
parts.push(`attemptIndex=${dims.attemptIndex}`);
|
|
69
|
+
if (dims.attemptReason !== undefined)
|
|
70
|
+
parts.push(`attemptReason=${dims.attemptReason}`);
|
|
71
|
+
if (dims.verifyCommand !== undefined)
|
|
72
|
+
parts.push(`verifyCommand=${dims.verifyCommand}`);
|
|
73
|
+
if (dims.outcome !== undefined)
|
|
74
|
+
parts.push(`outcome=${dims.outcome}`);
|
|
75
|
+
return parts.join(", ");
|
|
76
|
+
}
|
|
77
|
+
function pointDescription(point) {
|
|
78
|
+
const parts = [`kind=${point.kind}`];
|
|
79
|
+
if (point.label !== undefined)
|
|
80
|
+
parts.push(`label=${point.label}`);
|
|
81
|
+
if (point.value !== undefined)
|
|
82
|
+
parts.push(`value=${point.value}${point.unit ? point.unit : ""}`);
|
|
83
|
+
if (point.detail !== undefined)
|
|
84
|
+
parts.push(`detail=${point.detail}`);
|
|
85
|
+
parts.push(`coverage=${point.coverage}`);
|
|
86
|
+
return parts.join(", ");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Builds the citable-handle registry from what this run actually observed
|
|
90
|
+
* (D2): every span and point id in `snapshot`, every `EvidenceReference.ref`
|
|
91
|
+
* carried on them, every ledger event id, and — only when the run actually
|
|
92
|
+
* has a span under them — bucket, workstream, and verify-command category
|
|
93
|
+
* labels sourced from `summary`'s own already-observed groupings. Pure,
|
|
94
|
+
* deterministic; never a static vocabulary dump.
|
|
95
|
+
*/
|
|
96
|
+
export function buildHandleRegistry(snapshot, summary, ledger) {
|
|
97
|
+
const registry = new Map();
|
|
98
|
+
for (const span of snapshot.spans) {
|
|
99
|
+
registry.set(span.id, { kind: "span", value: spanDescription(span) });
|
|
100
|
+
for (const ref of span.dimensions.evidence ?? []) {
|
|
101
|
+
registry.set(ref.ref, {
|
|
102
|
+
kind: "evidence",
|
|
103
|
+
value: `${ref.kind} evidence (local)${ref.note ? ` — ${ref.note}` : ""}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const point of snapshot.points) {
|
|
108
|
+
registry.set(point.id, { kind: "point", value: pointDescription(point) });
|
|
109
|
+
for (const ref of point.evidence ?? []) {
|
|
110
|
+
registry.set(ref.ref, {
|
|
111
|
+
kind: "evidence",
|
|
112
|
+
value: `${ref.kind} evidence (local)${ref.note ? ` — ${ref.note}` : ""}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const decision of ledger.decisions) {
|
|
117
|
+
registry.set(decision.id, {
|
|
118
|
+
kind: "ledger-event",
|
|
119
|
+
value: `decision "${decision.decision.title}" (${decision.status})`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
for (const findingRecord of ledger.findings) {
|
|
123
|
+
registry.set(findingRecord.id, {
|
|
124
|
+
kind: "ledger-event",
|
|
125
|
+
value: `finding "${findingRecord.subject}" (${findingRecord.severity}, ${findingRecord.status})`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
for (const denial of ledger.denials) {
|
|
129
|
+
registry.set(denial.id, {
|
|
130
|
+
kind: "ledger-event",
|
|
131
|
+
value: `denied command \`${denial.command}\` (${denial.source}) in ${denial.workstream}/${denial.attempt}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
for (const bucket of summary.buckets) {
|
|
135
|
+
if (bucket.spanCount === 0)
|
|
136
|
+
continue;
|
|
137
|
+
registry.set(bucket.bucket, {
|
|
138
|
+
kind: "bucket",
|
|
139
|
+
value: `${formatDuration(bucket.durationMs)} across ${plural(bucket.spanCount, "span")}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
for (const workstream of summary.rankings.byWorkstream) {
|
|
143
|
+
registry.set(workstream.key, {
|
|
144
|
+
kind: "workstream",
|
|
145
|
+
value: `${formatDuration(workstream.durationMs)} across ${plural(workstream.count, "span")}`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
for (const command of summary.rankings.byVerifyCommand) {
|
|
149
|
+
registry.set(command.command, {
|
|
150
|
+
kind: "verify-command",
|
|
151
|
+
value: `${formatDuration(command.totalMs)} across ${plural(command.invocations, "invocation")}`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return registry;
|
|
155
|
+
}
|
|
156
|
+
function registryLines(registry, kind) {
|
|
157
|
+
return [...registry.entries()]
|
|
158
|
+
.filter(([, entry]) => entry.kind === kind)
|
|
159
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
160
|
+
.map(([handle, entry]) => `- \`${handle}\`: ${entry.value}`);
|
|
161
|
+
}
|
|
162
|
+
const causalStepSchema = z.object({
|
|
163
|
+
claim: z.string().min(1),
|
|
164
|
+
cites: z.array(z.string()).default([]),
|
|
165
|
+
from: z.string().optional(),
|
|
166
|
+
to: z.string().optional(),
|
|
167
|
+
});
|
|
168
|
+
const planningConstraintSchema = z.object({
|
|
169
|
+
constraint: z.string().min(1),
|
|
170
|
+
cites: z.array(z.string()).default([]),
|
|
171
|
+
});
|
|
172
|
+
const INTERPRETATION_PATTERN = /```interpretation[^\S\r\n]*\r?\n([\s\S]*?)```/gu;
|
|
173
|
+
const CONSTRAINTS_PATTERN = /```constraints[^\S\r\n]*\r?\n([\s\S]*?)```/gu;
|
|
174
|
+
const UNRESOLVED_PATTERN = /```unresolved[^\S\r\n]*\r?\n([\s\S]*?)```/gu;
|
|
175
|
+
/** The last non-empty fenced block's body, or undefined when none appeared — the same echo guard used throughout (extractSummary, parseSnapshotHalf). */
|
|
176
|
+
function lastNonEmptyBlock(output, pattern) {
|
|
177
|
+
const bodies = [...output.matchAll(pattern)]
|
|
178
|
+
.map((match) => match[1]?.trim() ?? "")
|
|
179
|
+
.filter((body) => body !== "");
|
|
180
|
+
return bodies.length > 0 ? bodies[bodies.length - 1] : undefined;
|
|
181
|
+
}
|
|
182
|
+
function parseJsonArray(body, schema, label, errors) {
|
|
183
|
+
if (body === undefined)
|
|
184
|
+
return [];
|
|
185
|
+
let json;
|
|
186
|
+
try {
|
|
187
|
+
json = JSON.parse(body);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
errors.push(`${label} block is not valid JSON: ${error.message}`);
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(json)) {
|
|
194
|
+
errors.push(`${label} block is not a JSON array`);
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const out = [];
|
|
198
|
+
json.forEach((element, index) => {
|
|
199
|
+
const result = schema.safeParse(element);
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
const issues = result.error.issues
|
|
202
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
203
|
+
.join("; ");
|
|
204
|
+
errors.push(`${label}[${index}] has the wrong shape: ${issues}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
out.push(result.data);
|
|
208
|
+
});
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function parseUnresolved(body) {
|
|
212
|
+
if (body === undefined)
|
|
213
|
+
return [];
|
|
214
|
+
try {
|
|
215
|
+
const json = JSON.parse(body);
|
|
216
|
+
if (Array.isArray(json)) {
|
|
217
|
+
return json
|
|
218
|
+
.filter((item) => typeof item === "string" && item.trim() !== "")
|
|
219
|
+
.map((item) => item.trim());
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Not JSON — treat the block as bulleted prose below.
|
|
224
|
+
}
|
|
225
|
+
return body
|
|
226
|
+
.split(/\r?\n/u)
|
|
227
|
+
.map((line) => line.replace(/^[-*]\s*/u, "").trim())
|
|
228
|
+
.filter((line) => line !== "");
|
|
229
|
+
}
|
|
230
|
+
/** Parses the analyzer's three-layer reply. Total: a missing block is empty, malformed JSON is a recorded error and an empty layer, and a reply with none of the three blocks is `blockless` — the analyzer-error path (§3.4). */
|
|
231
|
+
export function parseCausalAnalysis(output) {
|
|
232
|
+
const errors = [];
|
|
233
|
+
const interpretationBody = lastNonEmptyBlock(output, INTERPRETATION_PATTERN);
|
|
234
|
+
const constraintsBody = lastNonEmptyBlock(output, CONSTRAINTS_PATTERN);
|
|
235
|
+
const unresolvedBody = lastNonEmptyBlock(output, UNRESOLVED_PATTERN);
|
|
236
|
+
const interpretation = parseJsonArray(interpretationBody, causalStepSchema, "interpretation", errors).map((step) => ({
|
|
237
|
+
claim: step.claim,
|
|
238
|
+
cites: step.cites,
|
|
239
|
+
...(step.from === undefined ? {} : { from: step.from }),
|
|
240
|
+
...(step.to === undefined ? {} : { to: step.to }),
|
|
241
|
+
}));
|
|
242
|
+
const constraints = parseJsonArray(constraintsBody, planningConstraintSchema, "constraints", errors).map((constraint) => ({ constraint: constraint.constraint, cites: constraint.cites }));
|
|
243
|
+
const unresolved = parseUnresolved(unresolvedBody);
|
|
244
|
+
return {
|
|
245
|
+
interpretation,
|
|
246
|
+
constraints,
|
|
247
|
+
unresolved,
|
|
248
|
+
errors,
|
|
249
|
+
blockless: interpretationBody === undefined && constraintsBody === undefined && unresolvedBody === undefined,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function validateCites(cites, registry) {
|
|
253
|
+
const resolved = [];
|
|
254
|
+
const unresolvedCites = [];
|
|
255
|
+
for (const cite of cites) {
|
|
256
|
+
const entry = registry.get(cite);
|
|
257
|
+
if (entry === undefined) {
|
|
258
|
+
unresolvedCites.push(cite);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
resolved.push({ handle: cite, kind: entry.kind, value: entry.value });
|
|
262
|
+
}
|
|
263
|
+
const supported = resolved.some((citation) => isConcreteHandleKind(citation.kind));
|
|
264
|
+
return { resolved, unresolvedCites, supported };
|
|
265
|
+
}
|
|
266
|
+
/** The deterministic gate: proves every rendered causal step cites at least one concrete observation the run actually retained, and no cited handle is fabricated. Never judges whether a resolving citation logically supports its claim (D2) — that is left to the human. Pure. */
|
|
267
|
+
export function validateCausalEvidence(reply, registry) {
|
|
268
|
+
const supportedSteps = [];
|
|
269
|
+
const unsupportedSteps = [];
|
|
270
|
+
for (const step of reply.interpretation) {
|
|
271
|
+
const validation = validateCites(step.cites, registry);
|
|
272
|
+
const claim = { source: step, ...validation };
|
|
273
|
+
(validation.supported ? supportedSteps : unsupportedSteps).push(claim);
|
|
274
|
+
}
|
|
275
|
+
const supportedConstraints = [];
|
|
276
|
+
const unsupportedConstraints = [];
|
|
277
|
+
for (const constraint of reply.constraints) {
|
|
278
|
+
const validation = validateCites(constraint.cites, registry);
|
|
279
|
+
const claim = { source: constraint, ...validation };
|
|
280
|
+
(validation.supported ? supportedConstraints : unsupportedConstraints).push(claim);
|
|
281
|
+
}
|
|
282
|
+
return { supportedSteps, unsupportedSteps, supportedConstraints, unsupportedConstraints };
|
|
283
|
+
}
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Observed-measurements: rendered deterministically from `RunAnalyticsSummary`
|
|
286
|
+
// (D4) — shared, byte-for-byte, between the analyzer's brief and the report's
|
|
287
|
+
// own observed layer. The analyzer never authors a number that reaches
|
|
288
|
+
// either.
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
function observedMeasurementLines(summary) {
|
|
291
|
+
const lines = [];
|
|
292
|
+
lines.push(`- Total elapsed: ${formatDuration(summary.totalElapsedMs)}.`);
|
|
293
|
+
lines.push("- By bucket:");
|
|
294
|
+
for (const bucket of summary.buckets) {
|
|
295
|
+
if (bucket.spanCount === 0)
|
|
296
|
+
continue;
|
|
297
|
+
lines.push(` - \`${bucket.bucket}\`: ${formatDuration(bucket.durationMs)} (${bucket.percent.toFixed(1)}%, ${plural(bucket.spanCount, "span")})`);
|
|
298
|
+
}
|
|
299
|
+
const topStage = summary.rankings.byStage[0];
|
|
300
|
+
if (topStage) {
|
|
301
|
+
lines.push(`- Largest stage: \`${topStage.key}\` — ${formatDuration(topStage.durationMs)} across ${plural(topStage.count, "span")}.`);
|
|
302
|
+
}
|
|
303
|
+
const topWorkstream = summary.rankings.byWorkstream[0];
|
|
304
|
+
if (topWorkstream) {
|
|
305
|
+
lines.push(`- Largest workstream: \`${topWorkstream.key}\` — ${formatDuration(topWorkstream.durationMs)}.`);
|
|
306
|
+
}
|
|
307
|
+
const attempt = summary.rankings.largestAgentAttempt;
|
|
308
|
+
if (attempt) {
|
|
309
|
+
lines.push(`- Largest single agent attempt: \`${attempt.stage}\`` +
|
|
310
|
+
(attempt.workstream ? ` (${attempt.workstream}` : " (") +
|
|
311
|
+
`${attempt.attemptSeat ? `, ${attempt.attemptSeat}` : ""}` +
|
|
312
|
+
`${attempt.attemptIndex !== undefined ? ` #${attempt.attemptIndex}` : ""}) — ${formatDuration(attempt.durationMs)}.`);
|
|
313
|
+
}
|
|
314
|
+
const topCommand = summary.rankings.byVerifyCommand[0];
|
|
315
|
+
if (topCommand) {
|
|
316
|
+
lines.push(`- Largest verification command: \`${topCommand.command}\` — ${formatDuration(topCommand.totalMs)} across ${plural(topCommand.invocations, "invocation")}.`);
|
|
317
|
+
}
|
|
318
|
+
lines.push(`- Attempts by disposition: ${summary.dispositions.map((d) => `${d.outcome} ${plural(d.count, "attempt")} (${formatDuration(d.durationMs)})`).join(", ")}.`);
|
|
319
|
+
lines.push(`- Agent rerun time: ${formatDuration(summary.rerun.agent.totalMs)}` +
|
|
320
|
+
(summary.rerun.agent.byReason.length > 0
|
|
321
|
+
? `, by trigger: ${summary.rerun.agent.byReason.map((r) => `${r.key} ${plural(r.count, "attempt")} (${formatDuration(r.durationMs)})`).join(", ")}`
|
|
322
|
+
: "") +
|
|
323
|
+
".");
|
|
324
|
+
lines.push(`- Verification rerun time: ${formatDuration(summary.rerun.verification.totalMs)}` +
|
|
325
|
+
(summary.rerun.verification.byPhase.length > 0
|
|
326
|
+
? `, by phase: ${summary.rerun.verification.byPhase.map((p) => `${p.key} ${plural(p.count, "invocation")} (${formatDuration(p.durationMs)})`).join(", ")}`
|
|
327
|
+
: "") +
|
|
328
|
+
".");
|
|
329
|
+
lines.push(`- Observability coverage: ${summary.coverage.counts.observed} observed, ${summary.coverage.counts.estimated} estimated, ` +
|
|
330
|
+
`${summary.coverage.counts.unavailable} unavailable, ${summary.coverage.counts.incomplete} incomplete` +
|
|
331
|
+
(summary.coverage.unavailableRoles.length > 0
|
|
332
|
+
? ` (unavailable roles: ${summary.coverage.unavailableRoles.join(", ")} — unmeasured, not zero)`
|
|
333
|
+
: "") +
|
|
334
|
+
".");
|
|
335
|
+
return lines;
|
|
336
|
+
}
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// The brief (§3.3)
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
function successCriteriaLines(manifest) {
|
|
341
|
+
if (manifest.successCriteria.length === 0)
|
|
342
|
+
return ["None recorded in the manifest."];
|
|
343
|
+
return manifest.successCriteria.map((criterion) => `- **${criterion.id}**: ${criterion.description}`);
|
|
344
|
+
}
|
|
345
|
+
function rosterLines(manifest) {
|
|
346
|
+
return manifest.workstreams.map((workstream) => `- ${workstream.id} ${workstream.name} (spec: \`${workstream.taskFile}\`): ` +
|
|
347
|
+
`${workstream.scope?.summary ?? workstream.name}`);
|
|
348
|
+
}
|
|
349
|
+
function ledgerLines(ledger) {
|
|
350
|
+
if (ledger.decisions.length === 0 && ledger.findings.length === 0 && ledger.denials.length === 0) {
|
|
351
|
+
return ["No decisions, findings, or denials were journaled this run."];
|
|
352
|
+
}
|
|
353
|
+
const lines = [];
|
|
354
|
+
if (ledger.decisions.length > 0) {
|
|
355
|
+
lines.push("Decisions:");
|
|
356
|
+
for (const decision of ledger.decisions) {
|
|
357
|
+
lines.push(`- \`${decision.id}\` **${decision.decision.title}** (${decision.status}): ${decision.decision.rationale}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (ledger.findings.length > 0) {
|
|
361
|
+
lines.push("", "Findings:");
|
|
362
|
+
for (const findingRecord of ledger.findings) {
|
|
363
|
+
lines.push(`- \`${findingRecord.id}\` **${findingRecord.subject}** (${findingRecord.severity}, ${findingRecord.status}): ${findingRecord.message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (ledger.denials.length > 0) {
|
|
367
|
+
lines.push("", "Denied commands:");
|
|
368
|
+
for (const denial of ledger.denials) {
|
|
369
|
+
lines.push(`- \`${denial.id}\` \`${denial.command}\` (${denial.source}) in ${denial.workstream}/${denial.attempt}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return lines;
|
|
373
|
+
}
|
|
374
|
+
function timelineLines(workstreams) {
|
|
375
|
+
if (workstreams.length === 0)
|
|
376
|
+
return ["No workstream results were recorded."];
|
|
377
|
+
const lines = [];
|
|
378
|
+
for (const workstream of workstreams) {
|
|
379
|
+
lines.push(`- **${workstream.id} ${workstream.name}** — ${workstream.status}${workstream.reason ? `: ${workstream.reason}` : ""}`);
|
|
380
|
+
for (const diagnosis of workstream.failureDiagnoses ?? []) {
|
|
381
|
+
lines.push(` - after the ${diagnosis.attempt} attempt, the reviewer diagnosed: ${diagnosis.verdict}`);
|
|
382
|
+
}
|
|
383
|
+
if (workstream.testCritique) {
|
|
384
|
+
const tc = workstream.testCritique;
|
|
385
|
+
lines.push(` - test critique: ${tc.stopReason} after ${plural(tc.roundsRun, "round")}` +
|
|
386
|
+
(tc.openSubjects.length > 0 ? `; still open: ${tc.openSubjects.join(", ")}` : "") +
|
|
387
|
+
(tc.resolvedSubjects.length > 0 ? `; fixed: ${tc.resolvedSubjects.join(", ")}` : ""));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return lines;
|
|
391
|
+
}
|
|
392
|
+
const CAUSAL_ANALYSIS_CLIP_LIMIT = 60_000;
|
|
393
|
+
function clipForCausalAnalysis(text, label) {
|
|
394
|
+
if (text.length <= CAUSAL_ANALYSIS_CLIP_LIMIT)
|
|
395
|
+
return { text, clipped: false };
|
|
396
|
+
return {
|
|
397
|
+
text: `${text.slice(0, CAUSAL_ANALYSIS_CLIP_LIMIT)}\n… (${label} clipped at ` +
|
|
398
|
+
`${CAUSAL_ANALYSIS_CLIP_LIMIT} characters — the analyzer saw a partial ${label})`,
|
|
399
|
+
clipped: true,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const CAUSAL_CONTRACT = `
|
|
403
|
+
## Causal analysis (how to report your read)
|
|
404
|
+
|
|
405
|
+
End your reply with three fenced blocks, in this exact order:
|
|
406
|
+
\`\`\`interpretation\`\`\`, \`\`\`constraints\`\`\`, \`\`\`unresolved\`\`\`. There is
|
|
407
|
+
deliberately no \`observed\` block — the observed measurements above are
|
|
408
|
+
rendered by software from the run's own analytics; restating their values is
|
|
409
|
+
not your job, and nothing you write there would reach the report.
|
|
410
|
+
|
|
411
|
+
\`\`\`interpretation
|
|
412
|
+
[
|
|
413
|
+
{
|
|
414
|
+
"claim": "<a sentence tracing a measured cost toward a planner-actionable cause>",
|
|
415
|
+
"cites": ["<handle>", "<handle>"],
|
|
416
|
+
"from": "<optional handle — the measured cost this step starts from>",
|
|
417
|
+
"to": "<optional handle — the cause this step points toward>"
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
\`\`\`
|
|
421
|
+
|
|
422
|
+
Every \`cites\` entry must be an exact handle string copied from one of the
|
|
423
|
+
sections above (a span id, a point id, a ledger event id, or an evidence
|
|
424
|
+
ref — plus, for context only, a bucket name, workstream id, or
|
|
425
|
+
verify-command name). A step is only ever shown as a cause when at least one
|
|
426
|
+
of its citations is a concrete handle — a span, a point, a ledger event, or
|
|
427
|
+
an evidence ref. Citing only a bucket/workstream/verify-command name, citing
|
|
428
|
+
nothing, or citing a handle that does not appear above is checked
|
|
429
|
+
mechanically and never rendered as a fact. Fabricating a handle is pointless:
|
|
430
|
+
it will not resolve.
|
|
431
|
+
|
|
432
|
+
\`\`\`constraints
|
|
433
|
+
[
|
|
434
|
+
{ "constraint": "<a rule a future plan should carry forward>", "cites": ["<handle>"] }
|
|
435
|
+
]
|
|
436
|
+
\`\`\`
|
|
437
|
+
|
|
438
|
+
Gated the same way as interpretation steps.
|
|
439
|
+
|
|
440
|
+
\`\`\`unresolved
|
|
441
|
+
[
|
|
442
|
+
"<a plain-language question naming the missing or conflicting evidence>"
|
|
443
|
+
]
|
|
444
|
+
\`\`\`
|
|
445
|
+
|
|
446
|
+
Where two evidence items disagree, where telemetry is unavailable, or where
|
|
447
|
+
closing a chain would require a guess, put the question here instead of
|
|
448
|
+
picking a side in \`interpretation\`. An empty array in any block is a fine
|
|
449
|
+
answer — this is a second opinion, not an obligation to manufacture a cause.
|
|
450
|
+
`.trim();
|
|
451
|
+
/** The ```interpretation/```constraints/```unresolved block-format instructions. */
|
|
452
|
+
export function causalAnalysisContract() {
|
|
453
|
+
return CAUSAL_CONTRACT;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Assembles the analyzer's read-only brief (§3.3): framing, the
|
|
457
|
+
* software-rendered observed measurements, the citable span/point/evidence/
|
|
458
|
+
* ledger handles (drawn straight from `registry`, so what is shown is
|
|
459
|
+
* exactly what is citable), the plan, the ledger, the verification/retry
|
|
460
|
+
* timeline, the clipped final diff, and the response contract. Pure;
|
|
461
|
+
* returns the per-component byte sizes for SC-07 prompt telemetry.
|
|
462
|
+
*/
|
|
463
|
+
export function causalAnalysisBrief(input) {
|
|
464
|
+
const builder = createBriefBuilder();
|
|
465
|
+
builder.push("framing", `# Causal analysis: ${input.manifest.program.name} (${input.programId})`, "", "The run for this program is finished. You are an optional, independent,", "read-only second opinion — not a re-measurement. Every number below is", "already final; your job is to interpret it with cited evidence, or say", "plainly where you cannot.", "", "Build a causal chain only as far as retained evidence carries it. Where", "telemetry is unavailable, where two evidence items conflict, or where", "closing the chain would require a guess, record an unresolved question", "instead of picking a side or inventing a cause — an unresolved result is", "the correct answer here, not a failure.", "", "This analysis runs before the run report is assembled, so it reasons", "over a snapshot taken just before you were spawned: your own cost is", "not in any of the figures below.", "");
|
|
466
|
+
builder.push("observed-measurements", "## Observed measurements", "", ...observedMeasurementLines(input.summary), "", "Cite these by the exact handle shown — do not restate their values.", "");
|
|
467
|
+
const spanLines = registryLines(input.registry, "span");
|
|
468
|
+
const pointLines = registryLines(input.registry, "point");
|
|
469
|
+
builder.push("span-point-handles", "## Span and point handles", "", spanLines.length === 0 ? "No spans were recorded." : spanLines.join("\n"), "", pointLines.length === 0 ? "No points were recorded." : pointLines.join("\n"), "");
|
|
470
|
+
const evidenceLines = registryLines(input.registry, "evidence");
|
|
471
|
+
builder.push("evidence-index", "## Retained evidence index", "", evidenceLines.length === 0
|
|
472
|
+
? "No local evidence references were recorded for this run."
|
|
473
|
+
: evidenceLines.join("\n"), "", "Every reference above resolves only on the machine that ran this build.", "");
|
|
474
|
+
builder.push("plan", "## Program plan", "", "### Program narrative", "", input.programNarrative && input.programNarrative.trim() !== ""
|
|
475
|
+
? input.programNarrative.trim()
|
|
476
|
+
: "(the program document could not be read; only the manifest's success criteria and roster below are available)", "", "### Success criteria", "", ...successCriteriaLines(input.manifest), "", "### Workstream roster", "", "Each entry names its spec file by path; the full text of every spec is", "reproduced below. Trust the repository's spec file and the diff below", "over this narrative and roster where they disagree.", "", ...rosterLines(input.manifest), "");
|
|
477
|
+
builder.push("workstream-specs", "### Workstream specifications", "", input.workstreamSpecsText.trim() === ""
|
|
478
|
+
? "(no workstream spec content was available)"
|
|
479
|
+
: input.workstreamSpecsText, "");
|
|
480
|
+
builder.push("ledger", "## Decision ledger", "", ...ledgerLines(input.ledger), "");
|
|
481
|
+
builder.push("timeline", "## Verification/retry timeline", "", ...timelineLines(input.workstreams), "");
|
|
482
|
+
builder.push("diff", "## Final diff", "", input.baseCommit ? `Base commit: ${input.baseCommit}` : "(no base commit)", "", "```diff", input.diff.trim() === "" ? "(no diff available)" : input.diff, "```", "");
|
|
483
|
+
builder.push("contract", CAUSAL_CONTRACT, "", summaryContract());
|
|
484
|
+
return { brief: builder.join(), components: builder.components() };
|
|
485
|
+
}
|
|
486
|
+
/** The outcome recorded when no reviewerAgent is configured. */
|
|
487
|
+
export function causalAnalysisReviewerAbsent() {
|
|
488
|
+
return {
|
|
489
|
+
ran: false,
|
|
490
|
+
status: "no-reviewer",
|
|
491
|
+
reason: "causal analysis is disabled — no reviewerAgent is configured, and the pass was " +
|
|
492
|
+
"not substituted with the implementer or skipped silently.",
|
|
493
|
+
unresolvedQuestions: [],
|
|
494
|
+
parseErrors: [],
|
|
495
|
+
inputClipped: false,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function noAnalyticsOutcome() {
|
|
499
|
+
return {
|
|
500
|
+
ran: false,
|
|
501
|
+
status: "no-analytics",
|
|
502
|
+
reason: "causal analysis found no analytics to interpret — the run recorder produced no " +
|
|
503
|
+
"usable snapshot at the point this stage ran.",
|
|
504
|
+
unresolvedQuestions: [],
|
|
505
|
+
parseErrors: [],
|
|
506
|
+
inputClipped: false,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function analyzerErrorOutcome(reason, inputClipped, note) {
|
|
510
|
+
return {
|
|
511
|
+
ran: false,
|
|
512
|
+
status: "analyzer-error",
|
|
513
|
+
reason,
|
|
514
|
+
unresolvedQuestions: [],
|
|
515
|
+
parseErrors: [],
|
|
516
|
+
inputClipped,
|
|
517
|
+
...(note === undefined ? {} : { note }),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Runs the pass once: resolve reviewer → build brief → spawn → parse →
|
|
522
|
+
* validate → outcome. Never throws — every failure becomes a status and a
|
|
523
|
+
* `reason` sentence on the returned outcome (§3.5).
|
|
524
|
+
*/
|
|
525
|
+
export async function runCausalAnalysis(options) {
|
|
526
|
+
const reviewerAgent = resolveReviewerAgent(options.config);
|
|
527
|
+
if (!reviewerAgent)
|
|
528
|
+
return causalAnalysisReviewerAbsent();
|
|
529
|
+
if (options.snapshot === undefined)
|
|
530
|
+
return noAnalyticsOutcome();
|
|
531
|
+
let summary;
|
|
532
|
+
try {
|
|
533
|
+
summary = aggregateRunAnalytics(options.snapshot);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return noAnalyticsOutcome();
|
|
537
|
+
}
|
|
538
|
+
const recorder = options.recorder ?? NOOP_RUN_RECORDER;
|
|
539
|
+
const log = options.log ?? ((_line) => { });
|
|
540
|
+
const permits = options.permits ?? NOOP_PERMITS_CONTEXT;
|
|
541
|
+
const registry = buildHandleRegistry(options.snapshot, summary, options.ledger);
|
|
542
|
+
const diffClip = clipForCausalAnalysis(options.diff, "diff");
|
|
543
|
+
recorder.point(clippedInputPoint("causal-analysis-diff", options.diff, diffClip.text, { role: "reviewerAgent" }));
|
|
544
|
+
const narrativeClip = clipForCausalAnalysis(options.programNarrative ?? "", "program narrative");
|
|
545
|
+
recorder.point(clippedInputPoint("causal-analysis-plan", options.programNarrative ?? "", narrativeClip.text, {
|
|
546
|
+
role: "reviewerAgent",
|
|
547
|
+
}));
|
|
548
|
+
const specsText = combineWorkstreamSpecs(options.workstreamSpecs ?? []);
|
|
549
|
+
const specsClip = clipForCausalAnalysis(specsText, "workstream specs");
|
|
550
|
+
recorder.point(clippedInputPoint("causal-analysis-specs", specsText, specsClip.text, { role: "reviewerAgent" }));
|
|
551
|
+
const inputClipped = diffClip.clipped || narrativeClip.clipped || specsClip.clipped;
|
|
552
|
+
const { brief, components } = causalAnalysisBrief({
|
|
553
|
+
manifest: options.manifest,
|
|
554
|
+
programId: options.programId,
|
|
555
|
+
summary,
|
|
556
|
+
registry,
|
|
557
|
+
ledger: options.ledger,
|
|
558
|
+
workstreams: options.workstreams,
|
|
559
|
+
diff: diffClip.text,
|
|
560
|
+
workstreamSpecsText: specsClip.text,
|
|
561
|
+
...(options.baseCommit === undefined ? {} : { baseCommit: options.baseCommit }),
|
|
562
|
+
...(options.programNarrative === undefined ? {} : { programNarrative: narrativeClip.text }),
|
|
563
|
+
});
|
|
564
|
+
for (const point of promptComponentSizePoints(components, { role: "reviewerAgent" })) {
|
|
565
|
+
recorder.point(point);
|
|
566
|
+
}
|
|
567
|
+
log(`causal analysis: briefing reviewer (${brief.length} bytes)`);
|
|
568
|
+
const observe = {
|
|
569
|
+
root: options.root,
|
|
570
|
+
programId: options.programId,
|
|
571
|
+
label: "causal-analyzer",
|
|
572
|
+
log,
|
|
573
|
+
};
|
|
574
|
+
let invocation;
|
|
575
|
+
try {
|
|
576
|
+
invocation = await invokeAgent(options.agentRunner, reviewerAgent, brief, options.root, permits, "reviewerAgent", observe, recorder, { stage: "causal-analysis" });
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
return analyzerErrorOutcome(`the causal analyzer invocation threw: ${error.message}`, inputClipped);
|
|
580
|
+
}
|
|
581
|
+
const agentSummary = resolveSummary(invocation.output);
|
|
582
|
+
if (invocation.exitCode !== 0) {
|
|
583
|
+
return analyzerErrorOutcome(`the causal analyzer exited with code ${invocation.exitCode}: ${tail(invocation.output)}`, inputClipped, agentSummary.text);
|
|
584
|
+
}
|
|
585
|
+
const reply = parseCausalAnalysis(invocation.output);
|
|
586
|
+
if (reply.blockless) {
|
|
587
|
+
return analyzerErrorOutcome(`the causal analyzer's reply carried no interpretation, constraints, or unresolved ` +
|
|
588
|
+
`block: ${tail(invocation.output)}`, inputClipped, agentSummary.text);
|
|
589
|
+
}
|
|
590
|
+
const validated = validateCausalEvidence(reply, registry);
|
|
591
|
+
const status = validated.supportedSteps.length > 0 ? "analyzed" : "unresolved";
|
|
592
|
+
return {
|
|
593
|
+
ran: true,
|
|
594
|
+
status,
|
|
595
|
+
...(status === "unresolved"
|
|
596
|
+
? {
|
|
597
|
+
reason: "the analyzer ran, but no claim it made cited retained evidence the gate could " +
|
|
598
|
+
"confirm — this is a valid answer when the evidence does not support a cause.",
|
|
599
|
+
}
|
|
600
|
+
: {}),
|
|
601
|
+
validated,
|
|
602
|
+
unresolvedQuestions: reply.unresolved,
|
|
603
|
+
parseErrors: reply.errors,
|
|
604
|
+
inputClipped,
|
|
605
|
+
note: agentSummary.text,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// Rendering (§3.6)
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
function renderClaimLines(claim, text) {
|
|
612
|
+
const lines = [`- ${text}`];
|
|
613
|
+
if (claim.resolved.length > 0) {
|
|
614
|
+
lines.push(` Cites: ${claim.resolved.map((citation) => `\`${citation.handle}\` [${citation.kind}] — ${citation.value}`).join("; ")}.`);
|
|
615
|
+
}
|
|
616
|
+
if (claim.unresolvedCites.length > 0) {
|
|
617
|
+
lines.push(` Unresolved citations (not shown as evidence): ${claim.unresolvedCites.map((c) => `\`${c}\``).join(", ")}.`);
|
|
618
|
+
}
|
|
619
|
+
return lines;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* A human-facing render of one completed (or absent) causal analysis, for
|
|
623
|
+
* the run report. Markdown lines, no heading — the caller splices them
|
|
624
|
+
* under its own `## Why the time went there` heading, immediately after
|
|
625
|
+
* WS-04's own section. Total, never-throwing.
|
|
626
|
+
*/
|
|
627
|
+
export function renderCausalAnalysis(outcome, summary) {
|
|
628
|
+
if (outcome.status === "no-reviewer") {
|
|
629
|
+
return [
|
|
630
|
+
"**Causal analysis:** disabled — no reviewer is configured, and the pass was not",
|
|
631
|
+
"substituted with the implementer or skipped silently.",
|
|
632
|
+
];
|
|
633
|
+
}
|
|
634
|
+
if (outcome.status === "no-analytics") {
|
|
635
|
+
return ["**Causal analysis:** no analytics were available to interpret.", outcome.reason ?? ""];
|
|
636
|
+
}
|
|
637
|
+
if (outcome.status === "analyzer-error") {
|
|
638
|
+
const lines = ["**Causal analysis:** the analyzer did not produce a readable reply.", outcome.reason ?? ""];
|
|
639
|
+
if (outcome.note !== undefined)
|
|
640
|
+
lines.push(`Analyzer's note: ${outcome.note}`);
|
|
641
|
+
return lines;
|
|
642
|
+
}
|
|
643
|
+
const lines = [
|
|
644
|
+
"This section traces measured run costs toward planner-actionable causes,",
|
|
645
|
+
"using an independent, read-only pass over the numbers above. Boundary:",
|
|
646
|
+
"it analyzes the run from start to the moment this analysis began — its",
|
|
647
|
+
"own cost is in the deterministic buckets above, but not in the figures",
|
|
648
|
+
"it cites below.",
|
|
649
|
+
"",
|
|
650
|
+
"### Observed measurements",
|
|
651
|
+
"",
|
|
652
|
+
...observedMeasurementLines(summary),
|
|
653
|
+
"",
|
|
654
|
+
"### Interpretation",
|
|
655
|
+
"",
|
|
656
|
+
];
|
|
657
|
+
const validated = outcome.validated;
|
|
658
|
+
if (outcome.status === "unresolved" || validated === undefined || validated.supportedSteps.length === 0) {
|
|
659
|
+
lines.push("No evidence-backed causal step could be established this run.", "");
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
for (const step of validated.supportedSteps) {
|
|
663
|
+
lines.push(...renderClaimLines(step, step.source.claim));
|
|
664
|
+
}
|
|
665
|
+
lines.push("");
|
|
666
|
+
}
|
|
667
|
+
lines.push("### Reusable planning constraints", "");
|
|
668
|
+
if (validated === undefined || validated.supportedConstraints.length === 0) {
|
|
669
|
+
lines.push("None with resolving evidence.", "");
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
for (const constraint of validated.supportedConstraints) {
|
|
673
|
+
lines.push(...renderClaimLines(constraint, constraint.source.constraint));
|
|
674
|
+
}
|
|
675
|
+
lines.push("");
|
|
676
|
+
}
|
|
677
|
+
lines.push("### Unresolved questions", "");
|
|
678
|
+
if (outcome.unresolvedQuestions.length === 0) {
|
|
679
|
+
lines.push("None recorded.", "");
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
for (const question of outcome.unresolvedQuestions)
|
|
683
|
+
lines.push(`- ${question}`);
|
|
684
|
+
lines.push("");
|
|
685
|
+
}
|
|
686
|
+
const unsupportedSteps = validated?.unsupportedSteps ?? [];
|
|
687
|
+
const unsupportedConstraints = validated?.unsupportedConstraints ?? [];
|
|
688
|
+
if (unsupportedSteps.length > 0 || unsupportedConstraints.length > 0) {
|
|
689
|
+
lines.push("Claims the analyzer made without retained evidence (reported for transparency,", "never treated as findings):", "");
|
|
690
|
+
for (const step of unsupportedSteps)
|
|
691
|
+
lines.push(`- ${step.source.claim}`);
|
|
692
|
+
for (const constraint of unsupportedConstraints)
|
|
693
|
+
lines.push(`- ${constraint.source.constraint}`);
|
|
694
|
+
lines.push("");
|
|
695
|
+
}
|
|
696
|
+
if (outcome.parseErrors.length > 0) {
|
|
697
|
+
lines.push(`Parse errors: ${outcome.parseErrors.join("; ")}`, "");
|
|
698
|
+
}
|
|
699
|
+
if (outcome.inputClipped) {
|
|
700
|
+
lines.push("Input was clipped for length before the analyzer saw it.", "");
|
|
701
|
+
}
|
|
702
|
+
if (outcome.note !== undefined) {
|
|
703
|
+
lines.push(`Analyzer's note: ${outcome.note}`, "");
|
|
704
|
+
}
|
|
705
|
+
return lines;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* The run's own fail-open isolation boundary for the causal-analysis section
|
|
709
|
+
* (mirrors `renderAnalyticsSection` in run-analytics-report.ts): always
|
|
710
|
+
* returns a `## Why the time went there` section, never throws. `artifact`
|
|
711
|
+
* accepts a thunk so an evaluation failure of `recorder.snapshot()` itself
|
|
712
|
+
* stays inside this function's own try/catch rather than escaping before it
|
|
713
|
+
* is reached.
|
|
714
|
+
*/
|
|
715
|
+
export function renderCausalAnalysisSection(outcome, artifact, deps = {}) {
|
|
716
|
+
const aggregate = deps.aggregate ?? aggregateRunAnalytics;
|
|
717
|
+
const heading = ["## Why the time went there", ""];
|
|
718
|
+
try {
|
|
719
|
+
const resolved = typeof artifact === "function" ? artifact() : artifact;
|
|
720
|
+
if (resolved === undefined) {
|
|
721
|
+
return [
|
|
722
|
+
...heading,
|
|
723
|
+
"Causal analysis unavailable — no analytics snapshot was available when the report was rendered.",
|
|
724
|
+
"",
|
|
725
|
+
];
|
|
726
|
+
}
|
|
727
|
+
return [...heading, ...renderCausalAnalysis(outcome, aggregate(resolved)), ""];
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
return [...heading, `Causal analysis unavailable — ${error.message}.`, ""];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
//# sourceMappingURL=causal-analysis.js.map
|