@wildorder/nightshift 0.16.0 → 0.18.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 +289 -1
- package/dist/agent-runner.d.ts +95 -5
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +442 -32
- package/dist/agent-runner.js.map +1 -1
- package/dist/agent-session.d.ts +123 -0
- package/dist/agent-session.d.ts.map +1 -0
- package/dist/agent-session.js +114 -0
- package/dist/agent-session.js.map +1 -0
- package/dist/atomic-write.d.ts +18 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +74 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/author.d.ts +118 -4
- package/dist/author.d.ts.map +1 -1
- package/dist/author.js +650 -163
- package/dist/author.js.map +1 -1
- package/dist/causal-analysis.d.ts +216 -0
- package/dist/causal-analysis.d.ts.map +1 -0
- package/dist/causal-analysis.js +786 -0
- package/dist/causal-analysis.js.map +1 -0
- package/dist/cli.js +22 -2
- package/dist/cli.js.map +1 -1
- package/dist/concurrency.d.ts +24 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +28 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/decider-review.d.ts +13 -1
- package/dist/decider-review.d.ts.map +1 -1
- package/dist/decider-review.js +21 -7
- package/dist/decider-review.js.map +1 -1
- package/dist/decision-ledger.d.ts +7 -0
- package/dist/decision-ledger.d.ts.map +1 -1
- package/dist/decision-ledger.js +25 -10
- package/dist/decision-ledger.js.map +1 -1
- package/dist/detect-package-manager.d.ts +10 -0
- package/dist/detect-package-manager.d.ts.map +1 -1
- package/dist/detect-package-manager.js +12 -0
- package/dist/detect-package-manager.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +24 -5
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +254 -108
- package/dist/manifest.js.map +1 -1
- package/dist/permits.d.ts +18 -0
- package/dist/permits.d.ts.map +1 -1
- package/dist/permits.js +155 -0
- package/dist/permits.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/review-pass.d.ts +52 -10
- package/dist/review-pass.d.ts.map +1 -1
- package/dist/review-pass.js +114 -30
- package/dist/review-pass.js.map +1 -1
- package/dist/run-analytics-report.d.ts +286 -0
- package/dist/run-analytics-report.d.ts.map +1 -0
- package/dist/run-analytics-report.js +1242 -0
- package/dist/run-analytics-report.js.map +1 -0
- package/dist/run-analytics.d.ts +844 -0
- package/dist/run-analytics.d.ts.map +1 -0
- package/dist/run-analytics.js +777 -0
- package/dist/run-analytics.js.map +1 -0
- package/dist/run-log.d.ts +16 -0
- package/dist/run-log.d.ts.map +1 -0
- package/dist/run-log.js +32 -0
- package/dist/run-log.js.map +1 -0
- package/dist/run-program.d.ts +268 -4
- package/dist/run-program.d.ts.map +1 -1
- package/dist/run-program.js +2508 -762
- package/dist/run-program.js.map +1 -1
- package/dist/scheduler.d.ts +75 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +126 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/verify-cache.d.ts +64 -0
- package/dist/verify-cache.d.ts.map +1 -0
- package/dist/verify-cache.js +56 -0
- package/dist/verify-cache.js.map +1 -0
- 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/dist/worktree.d.ts +124 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +397 -0
- package/dist/worktree.js.map +1 -0
- package/dist/write-queue.d.ts +25 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +52 -0
- package/dist/write-queue.js.map +1 -0
- package/package.json +2 -2
- package/skills/plan-program/SKILL.md +37 -5
- package/dist/worktree-guard.d.ts +0 -40
- package/dist/worktree-guard.d.ts.map +0 -1
- package/dist/worktree-guard.js +0 -91
- package/dist/worktree-guard.js.map +0 -1
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
import { BUCKETS, KNOWN_OUTCOMES, KNOWN_WAIT_CAUSES, SCHEDULING_POINT_KINDS, } from "./run-analytics.js";
|
|
2
|
+
function compareIntervals(a, b) {
|
|
3
|
+
return a.start - b.start || a.end - b.end || a.span.id.localeCompare(b.span.id);
|
|
4
|
+
}
|
|
5
|
+
function attemptKeyOf(dims) {
|
|
6
|
+
if (dims.workstream === undefined || dims.attemptSeat === undefined || dims.attemptIndex === undefined) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
return `${dims.workstream} ${dims.attemptSeat} ${dims.attemptIndex}`;
|
|
10
|
+
}
|
|
11
|
+
function bump(map, key, durationMs) {
|
|
12
|
+
const cur = map.get(key) ?? { count: 0, durationMs: 0 };
|
|
13
|
+
cur.count += 1;
|
|
14
|
+
cur.durationMs += durationMs;
|
|
15
|
+
map.set(key, cur);
|
|
16
|
+
}
|
|
17
|
+
function rankedFromGroups(map) {
|
|
18
|
+
return [...map.entries()]
|
|
19
|
+
.map(([key, v]) => ({ key, durationMs: v.durationMs, count: v.count }))
|
|
20
|
+
.sort((a, b) => b.durationMs - a.durationMs || a.key.localeCompare(b.key));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Walks a span's `parentId` chain until it reaches a top-level span id.
|
|
24
|
+
* Returns `undefined` for an orphan chain (a dangling `parentId` — already
|
|
25
|
+
* flagged as its own diagnostic — or a chain that never reaches a top-level
|
|
26
|
+
* ancestor), never throws, and is bounded against a cyclic `parentId` a
|
|
27
|
+
* malformed artifact might otherwise loop on forever.
|
|
28
|
+
*/
|
|
29
|
+
function findTopLevelAncestor(span, byId, topLevelIds) {
|
|
30
|
+
let parentId = span.parentId;
|
|
31
|
+
let hops = 0;
|
|
32
|
+
while (parentId !== undefined && hops < 10_000) {
|
|
33
|
+
if (topLevelIds.has(parentId))
|
|
34
|
+
return parentId;
|
|
35
|
+
const parent = byId.get(parentId);
|
|
36
|
+
if (parent === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
parentId = parent.parentId;
|
|
39
|
+
hops += 1;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sweeps the sorted endpoints of every top-level interval and assigns each
|
|
45
|
+
* covered elementary sub-interval to exactly one owner — the covering span
|
|
46
|
+
* that sorts first by `(startOffsetMs, endOffsetMs, id)` — so
|
|
47
|
+
* `Σ(bucket durations)` equals the interval union exactly, even when
|
|
48
|
+
* top-level spans overlap (D2).
|
|
49
|
+
*
|
|
50
|
+
* An `overlap` diagnostic fires only when at least two of the covering spans
|
|
51
|
+
* share the same *defined* `dimensions.workstream` (§3.2): a workstream's own
|
|
52
|
+
* stages are sequential by construction, so overlap within one workstream is
|
|
53
|
+
* still a bug worth a note. Overlap between different workstreams — or
|
|
54
|
+
* involving a span with no workstream at all — is the expected shape of a
|
|
55
|
+
* concurrent run and produces no diagnostic. Top-level spans are direct
|
|
56
|
+
* children of the `run` span, which itself carries no workstream (WS-01), so
|
|
57
|
+
* a top-level span's own `dimensions.workstream` already *is* its effective
|
|
58
|
+
* workstream — no ancestor walk is needed here.
|
|
59
|
+
*/
|
|
60
|
+
function sweepBucketOwnership(intervals, diagnostics) {
|
|
61
|
+
const totals = new Map(BUCKETS.map((bucket) => [bucket, 0]));
|
|
62
|
+
if (intervals.length === 0)
|
|
63
|
+
return totals;
|
|
64
|
+
const boundarySet = new Set();
|
|
65
|
+
for (const interval of intervals) {
|
|
66
|
+
boundarySet.add(interval.start);
|
|
67
|
+
boundarySet.add(interval.end);
|
|
68
|
+
}
|
|
69
|
+
const boundaries = [...boundarySet].sort((a, b) => a - b);
|
|
70
|
+
const seenOverlaps = new Set();
|
|
71
|
+
for (let i = 0; i < boundaries.length - 1; i++) {
|
|
72
|
+
const a = boundaries[i];
|
|
73
|
+
const b = boundaries[i + 1];
|
|
74
|
+
if (b <= a)
|
|
75
|
+
continue;
|
|
76
|
+
const covering = intervals.filter((interval) => interval.start <= a && interval.end >= b);
|
|
77
|
+
if (covering.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
covering.sort(compareIntervals);
|
|
80
|
+
const owner = covering[0];
|
|
81
|
+
totals.set(owner.span.bucket, (totals.get(owner.span.bucket) ?? 0) + (b - a));
|
|
82
|
+
if (covering.length > 1) {
|
|
83
|
+
const workstreams = new Set(covering.map((interval) => interval.span.dimensions.workstream).filter((ws) => ws !== undefined));
|
|
84
|
+
const sameWorkstream = [...workstreams].find((ws) => covering.filter((interval) => interval.span.dimensions.workstream === ws).length > 1);
|
|
85
|
+
if (sameWorkstream !== undefined) {
|
|
86
|
+
const ids = covering.map((interval) => interval.span.id).sort();
|
|
87
|
+
const key = ids.join(",");
|
|
88
|
+
if (!seenOverlaps.has(key)) {
|
|
89
|
+
seenOverlaps.add(key);
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
kind: "overlap",
|
|
92
|
+
message: `${sameWorkstream}'s top-level spans overlap in [${a}, ${b}]ms: ${ids.join(", ")} — ` +
|
|
93
|
+
`attributed once, to ${owner.span.id} (${owner.span.bucket})`,
|
|
94
|
+
spanIds: ids,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return totals;
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// WS-06: scheduling-observation reconciliation, the derived dependency-landing
|
|
104
|
+
// wait, the concurrency sweep, and the critical-path walk.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
/** Two adjacent-but-distinct `monotonic()` reads can differ by a scheduler
|
|
107
|
+
* tick; this is orders of magnitude below any wait worth reporting and
|
|
108
|
+
* orders of magnitude above that jitter (§3.5). */
|
|
109
|
+
const SCHEDULING_IDENTITY_TOLERANCE_MS = 50;
|
|
110
|
+
const SCHEDULING_POINT_KIND_SET = new Set(SCHEDULING_POINT_KINDS);
|
|
111
|
+
function isFiniteNonNegative(value) {
|
|
112
|
+
return value !== undefined && Number.isFinite(value) && value >= 0;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Reconciles the three `SCHEDULING_POINT_KINDS` points into a per-workstream
|
|
116
|
+
* schedule, fail-open (§3.5): every malformed or contradictory observation is
|
|
117
|
+
* diagnosed and either skipped (malformed) or summed/kept-earliest-or-latest
|
|
118
|
+
* (a genuine duplicate) — never silently dropped, never thrown.
|
|
119
|
+
*/
|
|
120
|
+
function reconcileScheduling(artifact, diagnostics) {
|
|
121
|
+
const readyPoints = new Map();
|
|
122
|
+
const scheduledPoints = new Map();
|
|
123
|
+
const waitPoints = new Map(); // key: `${workstream} ${cause}`
|
|
124
|
+
for (const point of artifact.points) {
|
|
125
|
+
if (!SCHEDULING_POINT_KIND_SET.has(point.kind))
|
|
126
|
+
continue;
|
|
127
|
+
const workstream = point.dimensions?.workstream;
|
|
128
|
+
if (workstream === undefined) {
|
|
129
|
+
diagnostics.push({
|
|
130
|
+
kind: "scheduling-observation",
|
|
131
|
+
message: `${point.kind} point ${point.id} has no dimensions.workstream and cannot be attributed to any subject — skipped`,
|
|
132
|
+
spanIds: [],
|
|
133
|
+
pointIds: [point.id],
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (point.kind === "workstream-waiting") {
|
|
138
|
+
const cause = point.dimensions?.waitCause;
|
|
139
|
+
const value = point.value;
|
|
140
|
+
if (cause === undefined || !isFiniteNonNegative(value)) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
kind: "scheduling-observation",
|
|
143
|
+
message: `workstream-waiting point ${point.id} for ${workstream} has no waitCause, or a missing/negative/non-finite ` +
|
|
144
|
+
"value — skipped",
|
|
145
|
+
spanIds: [],
|
|
146
|
+
pointIds: [point.id],
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const key = `${workstream} ${cause}`;
|
|
151
|
+
const list = waitPoints.get(key) ?? [];
|
|
152
|
+
list.push({ pointId: point.id, atOffsetMs: point.atOffsetMs, waitedMs: value });
|
|
153
|
+
waitPoints.set(key, list);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (!isFiniteNonNegative(point.atOffsetMs)) {
|
|
157
|
+
diagnostics.push({
|
|
158
|
+
kind: "scheduling-observation",
|
|
159
|
+
message: `${point.kind} point ${point.id} for ${workstream} has a missing, negative, or non-finite atOffsetMs — skipped`,
|
|
160
|
+
spanIds: [],
|
|
161
|
+
pointIds: [point.id],
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const map = point.kind === "workstream-ready" ? readyPoints : scheduledPoints;
|
|
166
|
+
const list = map.get(workstream) ?? [];
|
|
167
|
+
list.push({ pointId: point.id, atOffsetMs: point.atOffsetMs });
|
|
168
|
+
map.set(workstream, list);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function resolveInstant(list, pick) {
|
|
172
|
+
const sorted = [...list].sort((a, b) => a.atOffsetMs - b.atOffsetMs || a.pointId.localeCompare(b.pointId));
|
|
173
|
+
return pick === "earliest" ? sorted[0] : sorted[sorted.length - 1];
|
|
174
|
+
}
|
|
175
|
+
const readyByWorkstream = new Map();
|
|
176
|
+
for (const [workstream, list] of readyPoints) {
|
|
177
|
+
if (list.length > 1) {
|
|
178
|
+
diagnostics.push({
|
|
179
|
+
kind: "duplicate-scheduling-point",
|
|
180
|
+
message: `workstream ${workstream} emitted ${list.length} workstream-ready points; the earliest is kept and the rest diagnosed`,
|
|
181
|
+
spanIds: [],
|
|
182
|
+
pointIds: [...list.map((p) => p.pointId)].sort(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
readyByWorkstream.set(workstream, resolveInstant(list, "earliest"));
|
|
186
|
+
}
|
|
187
|
+
const scheduledByWorkstream = new Map();
|
|
188
|
+
for (const [workstream, list] of scheduledPoints) {
|
|
189
|
+
if (list.length > 1) {
|
|
190
|
+
diagnostics.push({
|
|
191
|
+
kind: "duplicate-scheduling-point",
|
|
192
|
+
message: `workstream ${workstream} emitted ${list.length} workstream-scheduled points; the latest is kept and the rest diagnosed`,
|
|
193
|
+
spanIds: [],
|
|
194
|
+
pointIds: [...list.map((p) => p.pointId)].sort(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
scheduledByWorkstream.set(workstream, resolveInstant(list, "latest"));
|
|
198
|
+
}
|
|
199
|
+
const resolvedWaits = [];
|
|
200
|
+
for (const [key, list] of waitPoints) {
|
|
201
|
+
const sep = key.indexOf(" ");
|
|
202
|
+
const workstream = key.slice(0, sep);
|
|
203
|
+
const cause = key.slice(sep + 1);
|
|
204
|
+
if (list.length > 1) {
|
|
205
|
+
diagnostics.push({
|
|
206
|
+
kind: "duplicate-scheduling-point",
|
|
207
|
+
message: `workstream ${workstream} emitted ${list.length} workstream-waiting points for cause "${cause}"; ` +
|
|
208
|
+
"their durations were summed and the anomaly diagnosed",
|
|
209
|
+
spanIds: [],
|
|
210
|
+
pointIds: [...list.map((p) => p.pointId)].sort(),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const waitedMs = list.reduce((sum, p) => sum + p.waitedMs, 0);
|
|
214
|
+
const endMs = list.reduce((max, p) => Math.max(max, p.atOffsetMs), 0);
|
|
215
|
+
resolvedWaits.push({
|
|
216
|
+
workstream,
|
|
217
|
+
cause,
|
|
218
|
+
waitedMs,
|
|
219
|
+
endMs,
|
|
220
|
+
derived: false,
|
|
221
|
+
pointIds: [...list.map((p) => p.pointId)].sort(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const allScheduleWorkstreams = new Set([
|
|
225
|
+
...readyByWorkstream.keys(),
|
|
226
|
+
...scheduledByWorkstream.keys(),
|
|
227
|
+
...resolvedWaits.map((w) => w.workstream),
|
|
228
|
+
]);
|
|
229
|
+
// The `scheduled − ready === concurrency-cap` identity (rule 4, §3.3).
|
|
230
|
+
for (const workstream of allScheduleWorkstreams) {
|
|
231
|
+
const ready = readyByWorkstream.get(workstream);
|
|
232
|
+
const scheduled = scheduledByWorkstream.get(workstream);
|
|
233
|
+
if (scheduled === undefined)
|
|
234
|
+
continue;
|
|
235
|
+
if (ready === undefined) {
|
|
236
|
+
diagnostics.push({
|
|
237
|
+
kind: "scheduling-identity",
|
|
238
|
+
message: `workstream ${workstream} has a workstream-scheduled point with no matching workstream-ready point`,
|
|
239
|
+
spanIds: [],
|
|
240
|
+
pointIds: [scheduled.pointId],
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const capWaitMs = resolvedWaits.find((w) => w.workstream === workstream && w.cause === "concurrency-cap")?.waitedMs ?? 0;
|
|
245
|
+
const diff = scheduled.atOffsetMs - ready.atOffsetMs;
|
|
246
|
+
if (Math.abs(diff - capWaitMs) > SCHEDULING_IDENTITY_TOLERANCE_MS) {
|
|
247
|
+
diagnostics.push({
|
|
248
|
+
kind: "scheduling-identity",
|
|
249
|
+
message: `workstream ${workstream}: scheduled − ready is ${diff}ms but the concurrency-cap wait is ${capWaitMs}ms ` +
|
|
250
|
+
`(beyond the ${SCHEDULING_IDENTITY_TOLERANCE_MS}ms tolerance)`,
|
|
251
|
+
spanIds: [],
|
|
252
|
+
pointIds: [ready.pointId, scheduled.pointId],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// The derived `dependency-landing` wait (§3.4).
|
|
257
|
+
const depsReadyByWorkstream = new Map();
|
|
258
|
+
for (const [workstream, ready] of readyByWorkstream) {
|
|
259
|
+
const specPendingMs = resolvedWaits.find((w) => w.workstream === workstream && w.cause === "spec-pending")?.waitedMs ?? 0;
|
|
260
|
+
depsReadyByWorkstream.set(workstream, ready.atOffsetMs - specPendingMs);
|
|
261
|
+
}
|
|
262
|
+
const schedulerStartMs = depsReadyByWorkstream.size > 0 ? Math.min(...depsReadyByWorkstream.values()) : undefined;
|
|
263
|
+
const derivedWaits = [];
|
|
264
|
+
if (schedulerStartMs !== undefined) {
|
|
265
|
+
for (const [workstream, depsReadyMs] of [...depsReadyByWorkstream].sort(([a], [b]) => a.localeCompare(b))) {
|
|
266
|
+
const landingMs = Math.max(0, depsReadyMs - schedulerStartMs);
|
|
267
|
+
if (landingMs > 0) {
|
|
268
|
+
derivedWaits.push({ workstream, cause: "dependency-landing", waitedMs: landingMs, endMs: depsReadyMs, derived: true, pointIds: [] });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const allWaits = [...resolvedWaits, ...derivedWaits];
|
|
273
|
+
const scheduleWorkstreams = new Set([...allScheduleWorkstreams, ...derivedWaits.map((w) => w.workstream)]);
|
|
274
|
+
const schedules = [...scheduleWorkstreams].sort().map((workstream) => {
|
|
275
|
+
const ready = readyByWorkstream.get(workstream);
|
|
276
|
+
const scheduled = scheduledByWorkstream.get(workstream);
|
|
277
|
+
const depsReadyMs = depsReadyByWorkstream.get(workstream);
|
|
278
|
+
const waits = allWaits
|
|
279
|
+
.filter((w) => w.workstream === workstream)
|
|
280
|
+
.sort((a, b) => a.cause.localeCompare(b.cause))
|
|
281
|
+
.map((w) => ({
|
|
282
|
+
workstream: w.workstream,
|
|
283
|
+
cause: w.cause,
|
|
284
|
+
waitedMs: w.waitedMs,
|
|
285
|
+
endMs: w.endMs,
|
|
286
|
+
derived: w.derived,
|
|
287
|
+
...(!w.derived && w.pointIds.length > 0 ? { pointId: w.pointIds[0] } : {}),
|
|
288
|
+
}));
|
|
289
|
+
return {
|
|
290
|
+
workstream,
|
|
291
|
+
...(ready !== undefined ? { readyMs: ready.atOffsetMs } : {}),
|
|
292
|
+
...(depsReadyMs !== undefined ? { depsReadyMs } : {}),
|
|
293
|
+
...(scheduled !== undefined ? { scheduledMs: scheduled.atOffsetMs } : {}),
|
|
294
|
+
...(ready !== undefined && scheduled !== undefined ? { queuedMs: scheduled.atOffsetMs - ready.atOffsetMs } : {}),
|
|
295
|
+
waits,
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
function waitCauseTier(cause) {
|
|
299
|
+
if (KNOWN_WAIT_CAUSES.includes(cause))
|
|
300
|
+
return 0;
|
|
301
|
+
if (cause === "dependency-landing")
|
|
302
|
+
return 1;
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
const causeTotals = new Map();
|
|
306
|
+
for (const w of allWaits) {
|
|
307
|
+
const entry = causeTotals.get(w.cause) ?? { totalMs: 0, workstreams: new Set(), derived: w.derived };
|
|
308
|
+
entry.totalMs += w.waitedMs;
|
|
309
|
+
entry.workstreams.add(w.workstream);
|
|
310
|
+
causeTotals.set(w.cause, entry);
|
|
311
|
+
}
|
|
312
|
+
const waitsByCause = [...causeTotals.entries()]
|
|
313
|
+
.map(([cause, v]) => ({ cause, totalMs: v.totalMs, workstreams: v.workstreams.size, derived: v.derived }))
|
|
314
|
+
.sort((a, b) => {
|
|
315
|
+
const tierDiff = waitCauseTier(a.cause) - waitCauseTier(b.cause);
|
|
316
|
+
if (tierDiff !== 0)
|
|
317
|
+
return tierDiff;
|
|
318
|
+
if (waitCauseTier(a.cause) === 0) {
|
|
319
|
+
return KNOWN_WAIT_CAUSES.indexOf(a.cause) - KNOWN_WAIT_CAUSES.indexOf(b.cause);
|
|
320
|
+
}
|
|
321
|
+
return a.cause.localeCompare(b.cause);
|
|
322
|
+
});
|
|
323
|
+
return { schedules, waitsByCause, ...(schedulerStartMs !== undefined ? { schedulerStartMs } : {}), allWaits };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Time-weighted distribution of how many *distinct* workstreams were live at
|
|
327
|
+
* once (§3.7), over top-level intervals that carry a `workstream` — run-level
|
|
328
|
+
* spans are excluded entirely. Reuses the boundary-sweep technique from
|
|
329
|
+
* `sweepBucketOwnership`, counting distinct workstream ids rather than spans
|
|
330
|
+
* so a same-workstream overlap (already diagnosed in §3.2) is never counted
|
|
331
|
+
* as parallelism.
|
|
332
|
+
*/
|
|
333
|
+
function sweepWorkstreamConcurrency(topLevelIntervals) {
|
|
334
|
+
const withWorkstream = topLevelIntervals.filter((interval) => interval.span.dimensions.workstream !== undefined);
|
|
335
|
+
const cumulativeWorkstreamMs = withWorkstream.reduce((sum, interval) => sum + (interval.end - interval.start), 0);
|
|
336
|
+
if (withWorkstream.length === 0) {
|
|
337
|
+
return { meanWorkstreamConcurrency: 0, peakWorkstreamConcurrency: 0, overlappedMs: 0, cumulativeWorkstreamMs: 0 };
|
|
338
|
+
}
|
|
339
|
+
const boundarySet = new Set();
|
|
340
|
+
for (const interval of withWorkstream) {
|
|
341
|
+
boundarySet.add(interval.start);
|
|
342
|
+
boundarySet.add(interval.end);
|
|
343
|
+
}
|
|
344
|
+
const boundaries = [...boundarySet].sort((a, b) => a - b);
|
|
345
|
+
let peak = 0;
|
|
346
|
+
let overlappedMs = 0;
|
|
347
|
+
let weightedSum = 0;
|
|
348
|
+
let coveredLength = 0;
|
|
349
|
+
for (let i = 0; i < boundaries.length - 1; i++) {
|
|
350
|
+
const a = boundaries[i];
|
|
351
|
+
const b = boundaries[i + 1];
|
|
352
|
+
if (b <= a)
|
|
353
|
+
continue;
|
|
354
|
+
const distinct = new Set(withWorkstream.filter((interval) => interval.start <= a && interval.end >= b).map((interval) => interval.span.dimensions.workstream));
|
|
355
|
+
const count = distinct.size;
|
|
356
|
+
if (count === 0)
|
|
357
|
+
continue;
|
|
358
|
+
const length = b - a;
|
|
359
|
+
peak = Math.max(peak, count);
|
|
360
|
+
if (count >= 2)
|
|
361
|
+
overlappedMs += length;
|
|
362
|
+
weightedSum += count * length;
|
|
363
|
+
coveredLength += length;
|
|
364
|
+
}
|
|
365
|
+
const meanWorkstreamConcurrency = coveredLength > 0 ? weightedSum / coveredLength : 0;
|
|
366
|
+
return { meanWorkstreamConcurrency, peakWorkstreamConcurrency: peak, overlappedMs, cumulativeWorkstreamMs };
|
|
367
|
+
}
|
|
368
|
+
function ownerKeyOf(workstream) {
|
|
369
|
+
return workstream ?? "\u0000run-level";
|
|
370
|
+
}
|
|
371
|
+
function covers(start, end, cursor) {
|
|
372
|
+
return start < cursor && end >= cursor;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Backward walk from the end of the run (§3.6): at each step, either the
|
|
376
|
+
* current owner's own work explains the instant before the cursor (step 1),
|
|
377
|
+
* or another flow was holding the clock instead — the *truthful* leg for a
|
|
378
|
+
* cap/mutex wait, since somebody else was running, not nobody (step 2) — or,
|
|
379
|
+
* only when nothing at all was running, a genuine idle wait (step 3) — or,
|
|
380
|
+
* failing all three, an unexplained gap (step 4). Every branch strictly
|
|
381
|
+
* decreases the cursor, and an explicit `budget` backstops pathological
|
|
382
|
+
* input against a hang or a silently truncated path.
|
|
383
|
+
*/
|
|
384
|
+
function computeCriticalPath(topLevelIntervals, allWaits, totalElapsedMs) {
|
|
385
|
+
// `start` is clamped into [0, totalElapsedMs] defensively: a well-formed
|
|
386
|
+
// artifact never has a negative startOffsetMs, but a pathological one must
|
|
387
|
+
// still tile [0, totalElapsedMs) exactly rather than producing a leg whose
|
|
388
|
+
// startMs is negative.
|
|
389
|
+
const stages = topLevelIntervals.map((interval) => ({
|
|
390
|
+
kind: "stage",
|
|
391
|
+
workstream: interval.span.dimensions.workstream,
|
|
392
|
+
stage: interval.span.stage,
|
|
393
|
+
spanId: interval.span.id,
|
|
394
|
+
start: Math.min(Math.max(interval.start, 0), totalElapsedMs),
|
|
395
|
+
end: Math.min(Math.max(interval.end, 0), totalElapsedMs),
|
|
396
|
+
}));
|
|
397
|
+
const waits = allWaits
|
|
398
|
+
.map((w) => {
|
|
399
|
+
const rawStart = w.endMs - w.waitedMs;
|
|
400
|
+
const start = Math.min(Math.max(rawStart, 0), totalElapsedMs);
|
|
401
|
+
const end = Math.min(Math.max(w.endMs, 0), totalElapsedMs);
|
|
402
|
+
return {
|
|
403
|
+
workstream: w.workstream,
|
|
404
|
+
cause: w.cause,
|
|
405
|
+
pointId: w.derived ? undefined : w.pointIds[0],
|
|
406
|
+
derived: w.derived,
|
|
407
|
+
start,
|
|
408
|
+
end,
|
|
409
|
+
};
|
|
410
|
+
})
|
|
411
|
+
.filter((w) => w.end > w.start);
|
|
412
|
+
const allEnds = [...stages.map((s) => s.end), ...waits.map((w) => w.end)];
|
|
413
|
+
const legs = [];
|
|
414
|
+
let cursor = totalElapsedMs;
|
|
415
|
+
let owner;
|
|
416
|
+
let budget = stages.length + waits.length + 8;
|
|
417
|
+
while (cursor > 0 && budget-- > 0) {
|
|
418
|
+
const ownerKey = ownerKeyOf(owner);
|
|
419
|
+
// Step 1: the owner's own work explains it.
|
|
420
|
+
let step1;
|
|
421
|
+
for (const s of stages) {
|
|
422
|
+
if (ownerKeyOf(s.workstream) !== ownerKey || !covers(s.start, s.end, cursor))
|
|
423
|
+
continue;
|
|
424
|
+
if (step1 === undefined ||
|
|
425
|
+
s.start > step1.start ||
|
|
426
|
+
(s.start === step1.start && (s.end < step1.end || (s.end === step1.end && s.spanId < step1.spanId)))) {
|
|
427
|
+
step1 = s;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (step1 !== undefined) {
|
|
431
|
+
legs.push({
|
|
432
|
+
startMs: step1.start,
|
|
433
|
+
endMs: cursor,
|
|
434
|
+
durationMs: cursor - step1.start,
|
|
435
|
+
kind: "stage",
|
|
436
|
+
...(step1.workstream !== undefined ? { workstream: step1.workstream } : {}),
|
|
437
|
+
stage: step1.stage,
|
|
438
|
+
spanId: step1.spanId,
|
|
439
|
+
});
|
|
440
|
+
cursor = step1.start;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
// Step 2: somebody else held the clock.
|
|
444
|
+
let step2;
|
|
445
|
+
for (const s of stages) {
|
|
446
|
+
if (ownerKeyOf(s.workstream) === ownerKey || !covers(s.start, s.end, cursor))
|
|
447
|
+
continue;
|
|
448
|
+
if (step2 === undefined ||
|
|
449
|
+
s.end < step2.end ||
|
|
450
|
+
(s.end === step2.end && (s.start > step2.start || (s.start === step2.start && s.spanId < step2.spanId)))) {
|
|
451
|
+
step2 = s;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (step2 !== undefined) {
|
|
455
|
+
const ownerWait = owner === undefined
|
|
456
|
+
? undefined
|
|
457
|
+
: waits
|
|
458
|
+
.filter((w) => w.workstream === owner && covers(w.start, w.end, cursor))
|
|
459
|
+
.sort((a, b) => b.start - a.start || a.end - b.end || a.cause.localeCompare(b.cause))[0];
|
|
460
|
+
const previousLeg = legs.length > 0 ? legs[legs.length - 1] : undefined;
|
|
461
|
+
if (previousLeg !== undefined && previousLeg.kind !== "gap") {
|
|
462
|
+
previousLeg.handoff = {
|
|
463
|
+
...(step2.workstream !== undefined ? { fromWorkstream: step2.workstream } : {}),
|
|
464
|
+
cause: ownerWait?.cause ?? "concurrent-work",
|
|
465
|
+
causeObserved: ownerWait !== undefined,
|
|
466
|
+
...(ownerWait?.pointId !== undefined ? { pointId: ownerWait.pointId } : {}),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
legs.push({
|
|
470
|
+
startMs: step2.start,
|
|
471
|
+
endMs: cursor,
|
|
472
|
+
durationMs: cursor - step2.start,
|
|
473
|
+
kind: "stage",
|
|
474
|
+
...(step2.workstream !== undefined ? { workstream: step2.workstream } : {}),
|
|
475
|
+
stage: step2.stage,
|
|
476
|
+
spanId: step2.spanId,
|
|
477
|
+
});
|
|
478
|
+
owner = step2.workstream;
|
|
479
|
+
cursor = step2.start;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
// Step 3: nothing was running — a genuine idle wait.
|
|
483
|
+
const step3 = owner === undefined
|
|
484
|
+
? undefined
|
|
485
|
+
: waits
|
|
486
|
+
.filter((w) => w.workstream === owner && covers(w.start, w.end, cursor))
|
|
487
|
+
.sort((a, b) => b.start - a.start || a.end - b.end || a.cause.localeCompare(b.cause))[0];
|
|
488
|
+
if (step3 !== undefined) {
|
|
489
|
+
legs.push({
|
|
490
|
+
startMs: step3.start,
|
|
491
|
+
endMs: cursor,
|
|
492
|
+
durationMs: cursor - step3.start,
|
|
493
|
+
kind: "wait",
|
|
494
|
+
workstream: step3.workstream,
|
|
495
|
+
waitCause: step3.cause,
|
|
496
|
+
...(step3.pointId !== undefined ? { pointId: step3.pointId } : {}),
|
|
497
|
+
});
|
|
498
|
+
cursor = step3.start;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
// Step 4: nothing explains it at all.
|
|
502
|
+
const prevEnd = allEnds.filter((end) => end < cursor).reduce((max, end) => Math.max(max, end), 0);
|
|
503
|
+
legs.push({ startMs: prevEnd, endMs: cursor, durationMs: cursor - prevEnd, kind: "gap" });
|
|
504
|
+
cursor = prevEnd;
|
|
505
|
+
}
|
|
506
|
+
if (cursor > 0) {
|
|
507
|
+
legs.push({ startMs: 0, endMs: cursor, durationMs: cursor, kind: "gap" });
|
|
508
|
+
}
|
|
509
|
+
return legs.reverse();
|
|
510
|
+
}
|
|
511
|
+
const AGENT_ATTEMPT_STAGES = new Set(["implementer", "recovery", "informed-retry", "fix-now", "test-critique-fix"]);
|
|
512
|
+
const AGENT_REPEAT_REASONS = new Set([
|
|
513
|
+
"verify-failure",
|
|
514
|
+
"informed-by-diagnosis",
|
|
515
|
+
"critique-finding",
|
|
516
|
+
"fix-now-finding",
|
|
517
|
+
]);
|
|
518
|
+
const ALWAYS_RERUN_VERIFY_PHASES = new Set(["post-recovery", "post-informed-retry", "test-critique-fix", "fix-now"]);
|
|
519
|
+
const VERIFY_COMMAND_STAGES = new Set(["verification-command", "baseline-verification"]);
|
|
520
|
+
const TELEMETRY_KINDS = new Set(["provider-telemetry", "prompt-component-size"]);
|
|
521
|
+
/**
|
|
522
|
+
* Deterministically reconciles a `RunAnalytics` artifact into conserved
|
|
523
|
+
* wall-clock and repetition analytics (SC-09). Pure: no clock, no I/O, no
|
|
524
|
+
* randomness. A shuffled `spans`/`points` array yields a byte-identical
|
|
525
|
+
* summary, because every grouping keys off span/point fields — never array
|
|
526
|
+
* position — and every list is explicitly sorted before it is returned.
|
|
527
|
+
*/
|
|
528
|
+
export function aggregateRunAnalytics(artifact) {
|
|
529
|
+
const diagnostics = [];
|
|
530
|
+
const totalElapsedMs = artifact.anchors.finalizedOffsetMs ?? 0;
|
|
531
|
+
const byId = new Map(artifact.spans.map((span) => [span.id, span]));
|
|
532
|
+
for (const span of artifact.spans) {
|
|
533
|
+
if (span.parentId !== undefined && !byId.has(span.parentId)) {
|
|
534
|
+
diagnostics.push({
|
|
535
|
+
kind: "unresolved-parent",
|
|
536
|
+
message: `span ${span.id} (${span.stage}) has parentId "${span.parentId}", which does not resolve`,
|
|
537
|
+
spanIds: [span.id],
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (span.endOffsetMs !== undefined && span.endOffsetMs < span.startOffsetMs) {
|
|
541
|
+
diagnostics.push({
|
|
542
|
+
kind: "invalid-interval",
|
|
543
|
+
message: `span ${span.id} (${span.stage}) has endOffsetMs < startOffsetMs`,
|
|
544
|
+
spanIds: [span.id],
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const runSpan = artifact.spans.find((span) => span.stage === "run");
|
|
549
|
+
const runId = runSpan?.id;
|
|
550
|
+
if (runSpan === undefined) {
|
|
551
|
+
diagnostics.push({
|
|
552
|
+
kind: "unresolved-parent",
|
|
553
|
+
message: "no run-stage span was found; treating root spans (no parentId) as top-level",
|
|
554
|
+
spanIds: [],
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const topLevelSpans = artifact.spans.filter((span) => span.stage !== "run" && (runId !== undefined ? span.parentId === runId : span.parentId === undefined));
|
|
558
|
+
const topLevelIntervals = topLevelSpans.map((span) => {
|
|
559
|
+
if (span.endOffsetMs === undefined) {
|
|
560
|
+
diagnostics.push({
|
|
561
|
+
kind: "open-span",
|
|
562
|
+
// No raw span id in the human-facing message (the report never
|
|
563
|
+
// prints internal ids — `spanIds` below carries it for machine
|
|
564
|
+
// consumers/tests instead): this fires on effectively every real
|
|
565
|
+
// run, since `report-assembly` is deliberately still open when the
|
|
566
|
+
// snapshot is taken (§2.3/§3.2), not just on a genuine crash.
|
|
567
|
+
message: `the \`${span.stage}\` span has no endOffsetMs — open (still in progress, or interrupted) at ` +
|
|
568
|
+
`snapshot/finalize time, clamped to the total elapsed ${Math.round(totalElapsedMs)}ms`,
|
|
569
|
+
spanIds: [span.id],
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const rawEnd = span.endOffsetMs ?? totalElapsedMs;
|
|
573
|
+
let end = rawEnd;
|
|
574
|
+
if (end > totalElapsedMs) {
|
|
575
|
+
diagnostics.push({
|
|
576
|
+
kind: "interval-exceeds-total",
|
|
577
|
+
message: `span ${span.id} (${span.stage}) ends at ${end}ms, past the total elapsed ${totalElapsedMs}ms`,
|
|
578
|
+
spanIds: [span.id],
|
|
579
|
+
});
|
|
580
|
+
end = totalElapsedMs;
|
|
581
|
+
}
|
|
582
|
+
const start = span.startOffsetMs;
|
|
583
|
+
return { span, start, end: Math.max(start, end) };
|
|
584
|
+
});
|
|
585
|
+
const bucketTotals = sweepBucketOwnership(topLevelIntervals, diagnostics);
|
|
586
|
+
let attributedMs = 0;
|
|
587
|
+
for (const bucket of BUCKETS) {
|
|
588
|
+
if (bucket !== "unattributed")
|
|
589
|
+
attributedMs += bucketTotals.get(bucket) ?? 0;
|
|
590
|
+
}
|
|
591
|
+
const unattributedMs = Math.max(0, totalElapsedMs - attributedMs);
|
|
592
|
+
bucketTotals.set("unattributed", unattributedMs);
|
|
593
|
+
const spanCountByBucket = new Map(BUCKETS.map((bucket) => [bucket, 0]));
|
|
594
|
+
// `cumulativeByBucket` (§3.1) is computed from the exact same clamped
|
|
595
|
+
// `topLevelIntervals` the conserved `durationMs` uses — never the raw
|
|
596
|
+
// `endOffsetMs − startOffsetMs` — so an open or overrunning span's
|
|
597
|
+
// cumulative figure matches its owned duration on a serial run, keeping
|
|
598
|
+
// `cumulativeMs === durationMs` there for every bucket (the identity the
|
|
599
|
+
// renderer's self-gating suffix relies on).
|
|
600
|
+
const cumulativeByBucket = new Map(BUCKETS.map((bucket) => [bucket, 0]));
|
|
601
|
+
for (const interval of topLevelIntervals) {
|
|
602
|
+
spanCountByBucket.set(interval.span.bucket, (spanCountByBucket.get(interval.span.bucket) ?? 0) + 1);
|
|
603
|
+
cumulativeByBucket.set(interval.span.bucket, (cumulativeByBucket.get(interval.span.bucket) ?? 0) + (interval.end - interval.start));
|
|
604
|
+
}
|
|
605
|
+
// `unattributed` is a residual, not a set of spans (§3.1): its cumulative
|
|
606
|
+
// figure is its own duration, and its span count stays 0.
|
|
607
|
+
cumulativeByBucket.set("unattributed", unattributedMs);
|
|
608
|
+
const buckets = BUCKETS.map((bucket) => ({
|
|
609
|
+
bucket,
|
|
610
|
+
durationMs: bucketTotals.get(bucket) ?? 0,
|
|
611
|
+
cumulativeMs: cumulativeByBucket.get(bucket) ?? 0,
|
|
612
|
+
percent: totalElapsedMs > 0 ? ((bucketTotals.get(bucket) ?? 0) / totalElapsedMs) * 100 : 0,
|
|
613
|
+
spanCount: spanCountByBucket.get(bucket) ?? 0,
|
|
614
|
+
}));
|
|
615
|
+
// Drill-downs: every non-top-level, non-run span, grouped under its
|
|
616
|
+
// nearest top-level ancestor — detail, never summed into any bucket.
|
|
617
|
+
const topLevelIds = new Set(topLevelIntervals.map((interval) => interval.span.id));
|
|
618
|
+
const drillGroups = new Map();
|
|
619
|
+
for (const span of artifact.spans) {
|
|
620
|
+
if (span.id === runId || topLevelIds.has(span.id))
|
|
621
|
+
continue;
|
|
622
|
+
const owner = findTopLevelAncestor(span, byId, topLevelIds);
|
|
623
|
+
if (owner === undefined)
|
|
624
|
+
continue;
|
|
625
|
+
const durationMs = Math.max(0, (span.endOffsetMs ?? totalElapsedMs) - span.startOffsetMs);
|
|
626
|
+
const list = drillGroups.get(owner) ?? [];
|
|
627
|
+
list.push({
|
|
628
|
+
id: span.id,
|
|
629
|
+
stage: span.stage,
|
|
630
|
+
...(span.dimensions.workstream !== undefined ? { workstream: span.dimensions.workstream } : {}),
|
|
631
|
+
...(span.dimensions.role !== undefined ? { role: span.dimensions.role } : {}),
|
|
632
|
+
...(span.dimensions.outcome !== undefined ? { outcome: span.dimensions.outcome } : {}),
|
|
633
|
+
durationMs,
|
|
634
|
+
});
|
|
635
|
+
drillGroups.set(owner, list);
|
|
636
|
+
}
|
|
637
|
+
const drillDowns = [...drillGroups.entries()]
|
|
638
|
+
.map(([ownerSpanId, spans]) => ({
|
|
639
|
+
ownerSpanId,
|
|
640
|
+
ownerStage: byId.get(ownerSpanId)?.stage ?? "",
|
|
641
|
+
spans: [...spans].sort((a, b) => b.durationMs - a.durationMs || a.id.localeCompare(b.id)),
|
|
642
|
+
}))
|
|
643
|
+
.sort((a, b) => a.ownerSpanId.localeCompare(b.ownerSpanId));
|
|
644
|
+
// Dispositions: the five point-backed outcomes join `attempt-outcome`
|
|
645
|
+
// points to their spawn span on (workstream, attemptSeat, attemptIndex);
|
|
646
|
+
// `interrupted` is span-derived (D3) since WS-02 emits no point for it.
|
|
647
|
+
// Restricted to the actual attempt-spawn stages: a `verification-command`
|
|
648
|
+
// span carries the very same (workstream, attemptSeat, attemptIndex) triple
|
|
649
|
+
// as the attempt that provoked it (WS-02 §3.4/§3.6, so a rerun pass can be
|
|
650
|
+
// correlated back to its trigger) — without this filter it would collide
|
|
651
|
+
// with, and could nondeterministically shadow, the real spawn span.
|
|
652
|
+
const spawnByAttemptKey = new Map();
|
|
653
|
+
for (const interval of topLevelIntervals) {
|
|
654
|
+
if (!AGENT_ATTEMPT_STAGES.has(interval.span.stage))
|
|
655
|
+
continue;
|
|
656
|
+
const key = attemptKeyOf(interval.span.dimensions);
|
|
657
|
+
if (key === undefined)
|
|
658
|
+
continue;
|
|
659
|
+
const existing = spawnByAttemptKey.get(key);
|
|
660
|
+
if (existing === undefined || interval.span.id < existing.span.id)
|
|
661
|
+
spawnByAttemptKey.set(key, interval);
|
|
662
|
+
}
|
|
663
|
+
const dispositionTotals = new Map(KNOWN_OUTCOMES.map((outcome) => [outcome, { count: 0, durationMs: 0 }]));
|
|
664
|
+
const coveredAttemptKeys = new Set();
|
|
665
|
+
for (const point of artifact.points) {
|
|
666
|
+
if (point.kind !== "attempt-outcome")
|
|
667
|
+
continue;
|
|
668
|
+
const outcome = point.dimensions?.outcome ?? "unknown";
|
|
669
|
+
const key = attemptKeyOf(point.dimensions ?? {});
|
|
670
|
+
if (key !== undefined)
|
|
671
|
+
coveredAttemptKeys.add(key);
|
|
672
|
+
const matched = key === undefined ? undefined : spawnByAttemptKey.get(key);
|
|
673
|
+
const durationMs = matched === undefined ? 0 : matched.end - matched.start;
|
|
674
|
+
bump(dispositionTotals, outcome, durationMs);
|
|
675
|
+
}
|
|
676
|
+
let interruptedCount = 0;
|
|
677
|
+
let interruptedMs = 0;
|
|
678
|
+
for (const [key, interval] of spawnByAttemptKey) {
|
|
679
|
+
if (coveredAttemptKeys.has(key))
|
|
680
|
+
continue;
|
|
681
|
+
const isOpen = interval.span.endOffsetMs === undefined;
|
|
682
|
+
if (interval.span.dimensions.outcome === "interrupted" || isOpen) {
|
|
683
|
+
interruptedCount += 1;
|
|
684
|
+
interruptedMs += interval.end - interval.start;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
dispositionTotals.set("interrupted", { count: interruptedCount, durationMs: interruptedMs });
|
|
688
|
+
// `bump` above may have created outcome keys outside the known vocabulary
|
|
689
|
+
// (SC-04's open outcome vocabulary) — surfaced after the known six, sorted.
|
|
690
|
+
const extraOutcomes = [...dispositionTotals.keys()]
|
|
691
|
+
.filter((outcome) => !KNOWN_OUTCOMES.includes(outcome))
|
|
692
|
+
.sort();
|
|
693
|
+
const dispositions = [...KNOWN_OUTCOMES, ...extraOutcomes].map((outcome) => ({
|
|
694
|
+
outcome,
|
|
695
|
+
...(dispositionTotals.get(outcome) ?? { count: 0, durationMs: 0 }),
|
|
696
|
+
}));
|
|
697
|
+
// Rerun cost, grouped by trigger — never a single "waste" number (SC-06).
|
|
698
|
+
const agentRerunByReason = new Map();
|
|
699
|
+
let agentRerunTotalMs = 0;
|
|
700
|
+
for (const interval of topLevelIntervals) {
|
|
701
|
+
if (!AGENT_ATTEMPT_STAGES.has(interval.span.stage))
|
|
702
|
+
continue;
|
|
703
|
+
const reason = interval.span.dimensions.attemptReason;
|
|
704
|
+
if (reason === undefined || !AGENT_REPEAT_REASONS.has(reason))
|
|
705
|
+
continue;
|
|
706
|
+
const durationMs = interval.end - interval.start;
|
|
707
|
+
bump(agentRerunByReason, reason, durationMs);
|
|
708
|
+
agentRerunTotalMs += durationMs;
|
|
709
|
+
}
|
|
710
|
+
const verifyByWorkstreamPhase = new Map();
|
|
711
|
+
for (const interval of topLevelIntervals) {
|
|
712
|
+
if (interval.span.stage !== "verification-command")
|
|
713
|
+
continue;
|
|
714
|
+
const workstream = interval.span.dimensions.workstream ?? "";
|
|
715
|
+
const phase = interval.span.dimensions.attemptReason ?? "";
|
|
716
|
+
const key = `${workstream} ${phase}`;
|
|
717
|
+
const list = verifyByWorkstreamPhase.get(key) ?? [];
|
|
718
|
+
list.push(interval);
|
|
719
|
+
verifyByWorkstreamPhase.set(key, list);
|
|
720
|
+
}
|
|
721
|
+
const verifyRerunByPhase = new Map();
|
|
722
|
+
const verifyRerunByCommand = new Map();
|
|
723
|
+
let verifyRerunTotalMs = 0;
|
|
724
|
+
for (const [key, group] of verifyByWorkstreamPhase) {
|
|
725
|
+
const phase = key.split(" ")[1] ?? "";
|
|
726
|
+
const sorted = [...group].sort(compareIntervals);
|
|
727
|
+
sorted.forEach((interval, index) => {
|
|
728
|
+
// `post-implementer` verifies the initial attempt — only a rerun if it
|
|
729
|
+
// somehow fires again for the same workstream; every other phase this
|
|
730
|
+
// runner emits (post-recovery, post-informed-retry, test-critique-fix,
|
|
731
|
+
// fix-now) exists only because a prior attempt failed, so every
|
|
732
|
+
// occurrence counts (WS-02's phase vocabulary, §3.4).
|
|
733
|
+
const isRerun = phase === "post-implementer" ? index > 0 : ALWAYS_RERUN_VERIFY_PHASES.has(phase);
|
|
734
|
+
if (!isRerun)
|
|
735
|
+
return;
|
|
736
|
+
const durationMs = interval.end - interval.start;
|
|
737
|
+
verifyRerunTotalMs += durationMs;
|
|
738
|
+
bump(verifyRerunByPhase, phase, durationMs);
|
|
739
|
+
bump(verifyRerunByCommand, interval.span.dimensions.verifyCommand ?? "(unknown)", durationMs);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
// Rankings — largest measured contributors, never graded (§3.5). Only
|
|
743
|
+
// top-level spans participate, so parent/child durations are never summed.
|
|
744
|
+
const stageGroups = new Map();
|
|
745
|
+
const workstreamGroups = new Map();
|
|
746
|
+
const roleGroups = new Map();
|
|
747
|
+
const verifyCommandGroups = new Map();
|
|
748
|
+
let largestAgentAttempt;
|
|
749
|
+
for (const interval of topLevelIntervals) {
|
|
750
|
+
const durationMs = interval.end - interval.start;
|
|
751
|
+
bump(stageGroups, interval.span.stage, durationMs);
|
|
752
|
+
bump(workstreamGroups, interval.span.dimensions.workstream ?? "run-level", durationMs);
|
|
753
|
+
if (interval.span.dimensions.role !== undefined)
|
|
754
|
+
bump(roleGroups, interval.span.dimensions.role, durationMs);
|
|
755
|
+
if (VERIFY_COMMAND_STAGES.has(interval.span.stage) && interval.span.dimensions.verifyCommand !== undefined) {
|
|
756
|
+
const command = interval.span.dimensions.verifyCommand;
|
|
757
|
+
const cur = verifyCommandGroups.get(command) ?? { count: 0, durationMs: 0, largestMs: 0 };
|
|
758
|
+
cur.count += 1;
|
|
759
|
+
cur.durationMs += durationMs;
|
|
760
|
+
cur.largestMs = Math.max(cur.largestMs, durationMs);
|
|
761
|
+
verifyCommandGroups.set(command, cur);
|
|
762
|
+
}
|
|
763
|
+
if (AGENT_ATTEMPT_STAGES.has(interval.span.stage) && interval.span.dimensions.attemptSeat !== undefined) {
|
|
764
|
+
if (largestAgentAttempt === undefined ||
|
|
765
|
+
durationMs > largestAgentAttempt.durationMs ||
|
|
766
|
+
(durationMs === largestAgentAttempt.durationMs && interval.span.id < largestAgentAttempt.spanId)) {
|
|
767
|
+
largestAgentAttempt = {
|
|
768
|
+
spanId: interval.span.id,
|
|
769
|
+
stage: interval.span.stage,
|
|
770
|
+
...(interval.span.dimensions.workstream !== undefined ? { workstream: interval.span.dimensions.workstream } : {}),
|
|
771
|
+
...(interval.span.dimensions.attemptSeat !== undefined ? { attemptSeat: interval.span.dimensions.attemptSeat } : {}),
|
|
772
|
+
...(interval.span.dimensions.attemptIndex !== undefined ? { attemptIndex: interval.span.dimensions.attemptIndex } : {}),
|
|
773
|
+
durationMs,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const byVerifyCommand = [...verifyCommandGroups.entries()]
|
|
779
|
+
.map(([command, v]) => ({ command, invocations: v.count, totalMs: v.durationMs, largestMs: v.largestMs }))
|
|
780
|
+
.sort((a, b) => b.totalMs - a.totalMs || a.command.localeCompare(b.command));
|
|
781
|
+
// Observability coverage (§3.6) — over WS-03's own points only.
|
|
782
|
+
const coverageCounts = { observed: 0, estimated: 0, unavailable: 0, incomplete: 0 };
|
|
783
|
+
const unavailableRoles = new Set();
|
|
784
|
+
const providerPoints = [...artifact.points]
|
|
785
|
+
.filter((point) => point.kind === "provider-telemetry")
|
|
786
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
787
|
+
const roleInfo = new Map();
|
|
788
|
+
for (const point of artifact.points) {
|
|
789
|
+
if (!TELEMETRY_KINDS.has(point.kind))
|
|
790
|
+
continue;
|
|
791
|
+
coverageCounts[point.coverage] += 1;
|
|
792
|
+
if (point.kind === "provider-telemetry" && point.coverage === "unavailable" && point.dimensions?.role !== undefined) {
|
|
793
|
+
unavailableRoles.add(point.dimensions.role);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const point of providerPoints) {
|
|
797
|
+
const role = point.dimensions?.role;
|
|
798
|
+
if (role === undefined || (point.label !== "provider" && point.label !== "model"))
|
|
799
|
+
continue;
|
|
800
|
+
const info = roleInfo.get(role) ?? { role };
|
|
801
|
+
const field = point.label === "provider" ? "provider" : "model";
|
|
802
|
+
if (info[field] === undefined) {
|
|
803
|
+
info[field] = { coverage: point.coverage, ...(point.detail !== undefined ? { value: point.detail } : {}) };
|
|
804
|
+
}
|
|
805
|
+
roleInfo.set(role, info);
|
|
806
|
+
}
|
|
807
|
+
const byRoleCoverage = [...roleInfo.values()].sort((a, b) => a.role.localeCompare(b.role));
|
|
808
|
+
// Provider/model duration totals (§3.5): each role's already-computed
|
|
809
|
+
// duration total (`roleGroups`) is attributed to the provider/model
|
|
810
|
+
// identity WS-03 observed for that role — labelled `"unavailable"`, never
|
|
811
|
+
// dropped or blanked, when a role's provider telemetry is unavailable
|
|
812
|
+
// (e.g. a non-Claude role), so an entire role's measured time is never
|
|
813
|
+
// silently missing from these totals.
|
|
814
|
+
const providerGroups = new Map();
|
|
815
|
+
const modelGroups = new Map();
|
|
816
|
+
function labelFor(field) {
|
|
817
|
+
return field === undefined || field.coverage === "unavailable" ? "unavailable" : (field.value ?? "unavailable");
|
|
818
|
+
}
|
|
819
|
+
for (const [role, group] of roleGroups) {
|
|
820
|
+
const info = roleInfo.get(role);
|
|
821
|
+
const providerKey = labelFor(info?.provider);
|
|
822
|
+
const modelKey = labelFor(info?.model);
|
|
823
|
+
for (const [target, key] of [
|
|
824
|
+
[providerGroups, providerKey],
|
|
825
|
+
[modelGroups, modelKey],
|
|
826
|
+
]) {
|
|
827
|
+
const cur = target.get(key) ?? { count: 0, durationMs: 0 };
|
|
828
|
+
cur.count += group.count;
|
|
829
|
+
cur.durationMs += group.durationMs;
|
|
830
|
+
target.set(key, cur);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const bytesByLabel = new Map();
|
|
834
|
+
for (const point of artifact.points) {
|
|
835
|
+
if (point.kind !== "prompt-component-size" || point.unit !== "bytes")
|
|
836
|
+
continue;
|
|
837
|
+
bytesByLabel.set(point.label ?? "(unlabeled)", (bytesByLabel.get(point.label ?? "(unlabeled)") ?? 0) + (point.value ?? 0));
|
|
838
|
+
}
|
|
839
|
+
const promptComponents = [...bytesByLabel.entries()]
|
|
840
|
+
.map(([label, bytes]) => ({ label, bytes }))
|
|
841
|
+
.sort((a, b) => b.bytes - a.bytes || a.label.localeCompare(b.label))
|
|
842
|
+
.slice(0, 5);
|
|
843
|
+
const overheadPoints = artifact.points.filter((point) => point.kind === "analytics-overhead");
|
|
844
|
+
const analyticsOverheadMs = overheadPoints.length > 0 ? overheadPoints.reduce((sum, point) => sum + (point.value ?? 0), 0) : undefined;
|
|
845
|
+
const publishPoints = artifact.points.filter((point) => point.kind === "publish-duration");
|
|
846
|
+
const publish = publishPoints.length > 0
|
|
847
|
+
? { durationMs: publishPoints.reduce((sum, point) => sum + (point.value ?? 0), 0), invocations: publishPoints.length }
|
|
848
|
+
: undefined;
|
|
849
|
+
const evidenceMap = new Map();
|
|
850
|
+
for (const span of artifact.spans) {
|
|
851
|
+
for (const ref of span.dimensions.evidence ?? [])
|
|
852
|
+
evidenceMap.set(`${ref.kind}:${ref.ref}`, ref);
|
|
853
|
+
}
|
|
854
|
+
for (const point of artifact.points) {
|
|
855
|
+
for (const ref of point.evidence ?? [])
|
|
856
|
+
evidenceMap.set(`${ref.kind}:${ref.ref}`, ref);
|
|
857
|
+
}
|
|
858
|
+
const sortedEvidence = [...evidenceMap.values()].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
859
|
+
const evidence = sortedEvidence.slice(0, 8).map((ref) => ({
|
|
860
|
+
kind: ref.kind,
|
|
861
|
+
locality: ref.locality,
|
|
862
|
+
ref: ref.ref,
|
|
863
|
+
...(ref.note !== undefined ? { note: ref.note } : {}),
|
|
864
|
+
}));
|
|
865
|
+
// WS-06: cumulative (non-conserved) agent/verification time, at any span
|
|
866
|
+
// depth. `role` is not the same as "agent" — a verification-command span
|
|
867
|
+
// stamped with `role` (verifyAttempt) must land in the verification total,
|
|
868
|
+
// never the agent one, or every verify pass would inflate agent time.
|
|
869
|
+
let cumulativeAgentMs = 0;
|
|
870
|
+
let agentSpanCount = 0;
|
|
871
|
+
let cumulativeVerificationMs = 0;
|
|
872
|
+
let verificationSpanCount = 0;
|
|
873
|
+
for (const span of artifact.spans) {
|
|
874
|
+
const durationMs = Math.max(0, (span.endOffsetMs ?? totalElapsedMs) - span.startOffsetMs);
|
|
875
|
+
if (VERIFY_COMMAND_STAGES.has(span.stage)) {
|
|
876
|
+
// Role is not required here: an ordinary roleless verification-stage
|
|
877
|
+
// span (a deterministic check the runner ran itself) still counts.
|
|
878
|
+
// Role matters only to keep verification spans out of the agent total.
|
|
879
|
+
cumulativeVerificationMs += durationMs;
|
|
880
|
+
verificationSpanCount += 1;
|
|
881
|
+
}
|
|
882
|
+
else if (span.dimensions.role !== undefined) {
|
|
883
|
+
cumulativeAgentMs += durationMs;
|
|
884
|
+
agentSpanCount += 1;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// WS-06: reconcile the scheduling observations, sweep parallelism, and walk
|
|
888
|
+
// the critical path — all pure functions of the artifact already parsed
|
|
889
|
+
// above; see each function's own doc comment for the contract.
|
|
890
|
+
const scheduling = reconcileScheduling(artifact, diagnostics);
|
|
891
|
+
const parallelism = sweepWorkstreamConcurrency(topLevelIntervals);
|
|
892
|
+
const criticalPath = computeCriticalPath(topLevelIntervals, scheduling.allWaits, totalElapsedMs);
|
|
893
|
+
const concurrency = {
|
|
894
|
+
meanWorkstreamConcurrency: parallelism.meanWorkstreamConcurrency,
|
|
895
|
+
peakWorkstreamConcurrency: parallelism.peakWorkstreamConcurrency,
|
|
896
|
+
overlappedMs: parallelism.overlappedMs,
|
|
897
|
+
cumulativeAgentMs,
|
|
898
|
+
agentSpanCount,
|
|
899
|
+
cumulativeVerificationMs,
|
|
900
|
+
verificationSpanCount,
|
|
901
|
+
cumulativeWorkstreamMs: parallelism.cumulativeWorkstreamMs,
|
|
902
|
+
...(scheduling.schedulerStartMs !== undefined ? { schedulerStartMs: scheduling.schedulerStartMs } : {}),
|
|
903
|
+
schedules: scheduling.schedules,
|
|
904
|
+
waitsByCause: scheduling.waitsByCause,
|
|
905
|
+
criticalPath,
|
|
906
|
+
};
|
|
907
|
+
// Diagnostics are pushed as each condition is discovered while walking
|
|
908
|
+
// `artifact.spans` in array order (unresolved-parent, invalid-interval,
|
|
909
|
+
// interval-exceeds-total); `sweepBucketOwnership`'s overlap diagnostics are
|
|
910
|
+
// already order-independent (found via sorted interval boundaries), but the
|
|
911
|
+
// other three are not — sorted here so the full summary stays
|
|
912
|
+
// order-independent (SC-09): a shuffled `spans` array must never change
|
|
913
|
+
// which diagnostics were found or their relative order, only the pass that
|
|
914
|
+
// happened to find them first. `reconcileScheduling`'s diagnostics are
|
|
915
|
+
// already order-independent too (grouped by (workstream, kind[, cause])
|
|
916
|
+
// key, never by array position), but the shared sort still needs a
|
|
917
|
+
// `pointIds` tie-break so two point-derived diagnostics with empty
|
|
918
|
+
// `spanIds` sort deterministically against each other.
|
|
919
|
+
diagnostics.sort((a, b) => a.kind.localeCompare(b.kind) ||
|
|
920
|
+
a.spanIds.join(",").localeCompare(b.spanIds.join(",")) ||
|
|
921
|
+
(a.pointIds ?? []).join(",").localeCompare((b.pointIds ?? []).join(",")) ||
|
|
922
|
+
a.message.localeCompare(b.message));
|
|
923
|
+
return {
|
|
924
|
+
totalElapsedMs,
|
|
925
|
+
attributedMs,
|
|
926
|
+
unattributedMs,
|
|
927
|
+
buckets,
|
|
928
|
+
diagnostics,
|
|
929
|
+
drillDowns,
|
|
930
|
+
dispositions,
|
|
931
|
+
rerun: {
|
|
932
|
+
agent: { totalMs: agentRerunTotalMs, byReason: rankedFromGroups(agentRerunByReason).map((r) => ({ key: r.key, count: r.count, durationMs: r.durationMs })) },
|
|
933
|
+
verification: {
|
|
934
|
+
totalMs: verifyRerunTotalMs,
|
|
935
|
+
byPhase: rankedFromGroups(verifyRerunByPhase).map((r) => ({ key: r.key, count: r.count, durationMs: r.durationMs })),
|
|
936
|
+
byCommand: rankedFromGroups(verifyRerunByCommand).map((r) => ({ key: r.key, count: r.count, durationMs: r.durationMs })),
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
rankings: {
|
|
940
|
+
byStage: rankedFromGroups(stageGroups),
|
|
941
|
+
byWorkstream: rankedFromGroups(workstreamGroups),
|
|
942
|
+
byRole: rankedFromGroups(roleGroups),
|
|
943
|
+
byProvider: rankedFromGroups(providerGroups),
|
|
944
|
+
byModel: rankedFromGroups(modelGroups),
|
|
945
|
+
byVerifyCommand,
|
|
946
|
+
...(largestAgentAttempt !== undefined ? { largestAgentAttempt } : {}),
|
|
947
|
+
},
|
|
948
|
+
coverage: { counts: coverageCounts, unavailableRoles: [...unavailableRoles].sort(), byRole: byRoleCoverage },
|
|
949
|
+
promptComponents,
|
|
950
|
+
...(analyticsOverheadMs !== undefined ? { analyticsOverheadMs } : {}),
|
|
951
|
+
...(publish !== undefined ? { publish } : {}),
|
|
952
|
+
evidence,
|
|
953
|
+
evidenceTotalCount: evidenceMap.size,
|
|
954
|
+
concurrency,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
// ---------------------------------------------------------------------------
|
|
958
|
+
// Rendering
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
function formatDuration(ms) {
|
|
961
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
962
|
+
ms = 0;
|
|
963
|
+
if (ms < 1000)
|
|
964
|
+
return `${Math.round(ms)}ms`;
|
|
965
|
+
const totalSeconds = ms / 1000;
|
|
966
|
+
if (totalSeconds < 60)
|
|
967
|
+
return `${totalSeconds.toFixed(1)}s`;
|
|
968
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
969
|
+
const seconds = Math.round(totalSeconds - minutes * 60);
|
|
970
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
971
|
+
}
|
|
972
|
+
function plural(count, noun) {
|
|
973
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* One clause per known wait cause, for the "Where the time went" wait table.
|
|
977
|
+
* A cause with no entry here still renders — with no gloss — because the
|
|
978
|
+
* cause vocabulary is open (§3.3, §3.9): forgetting to add a gloss for a new
|
|
979
|
+
* cause degrades legibility, never correctness.
|
|
980
|
+
*/
|
|
981
|
+
const WAIT_CAUSE_GLOSS = {
|
|
982
|
+
dependency: "an author deferred re-authoring on an in-flight sibling's spec",
|
|
983
|
+
"concurrency-cap": "the shared --max-parallel budget was already saturated",
|
|
984
|
+
"integration-mutex": "another workstream held the serialized integration step",
|
|
985
|
+
"spec-pending": "dependencies had integrated but this workstream's own spec was still being authored",
|
|
986
|
+
"author-concurrency-cap": "the authoring half of the shared --max-parallel budget was saturated",
|
|
987
|
+
"dependency-landing": "derived by this report from the scheduler's derived start — not measured by the runner",
|
|
988
|
+
};
|
|
989
|
+
const CRITICAL_PATH_LEG_LIMIT = 40;
|
|
990
|
+
function describeLeg(leg) {
|
|
991
|
+
switch (leg.kind) {
|
|
992
|
+
case "stage":
|
|
993
|
+
return leg.workstream !== undefined ? `${leg.workstream} ${leg.stage}` : `run-level ${leg.stage}`;
|
|
994
|
+
case "wait":
|
|
995
|
+
return `${leg.workstream} waiting (${leg.waitCause})`;
|
|
996
|
+
case "gap":
|
|
997
|
+
return "unexplained gap — nothing observed covers this interval";
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function describeHandoff(leg) {
|
|
1001
|
+
if (leg.handoff === undefined)
|
|
1002
|
+
return "";
|
|
1003
|
+
const from = leg.handoff.fromWorkstream ?? "run-level work";
|
|
1004
|
+
const reason = leg.handoff.causeObserved
|
|
1005
|
+
? `that flow's own recorded wait was on \`${leg.handoff.cause}\``
|
|
1006
|
+
: "no wait was recorded across this boundary";
|
|
1007
|
+
return ` — took the wall clock over from ${from} (${reason})`;
|
|
1008
|
+
}
|
|
1009
|
+
/** Renders the gated "Concurrency and the critical path" subsection (§3.9) — only ever called when `peakWorkstreamConcurrency > 1`. */
|
|
1010
|
+
function renderConcurrencySection(summary) {
|
|
1011
|
+
const c = summary.concurrency;
|
|
1012
|
+
const lines = ["### Concurrency and the critical path", ""];
|
|
1013
|
+
lines.push("Two or more workstreams ran at once in this run. The bucket table above is a partition of wall-clock " +
|
|
1014
|
+
"time, so it says how the run's length was spent, not how much work was done — concurrent work shares " +
|
|
1015
|
+
"the same wall clock.", "");
|
|
1016
|
+
lines.push(`Parallelism: **${c.meanWorkstreamConcurrency.toFixed(1)}×** mean workstream concurrency (peak ` +
|
|
1017
|
+
`${c.peakWorkstreamConcurrency}). ${formatDuration(c.overlappedMs)} of the ${formatDuration(summary.totalElapsedMs)} ` +
|
|
1018
|
+
"elapsed had two or more workstreams live.", "");
|
|
1019
|
+
lines.push(`Cumulative agent time: **${formatDuration(c.cumulativeAgentMs)}** across ${plural(c.agentSpanCount, "spawn")}. ` +
|
|
1020
|
+
`Cumulative verification time: **${formatDuration(c.cumulativeVerificationMs)}** across ` +
|
|
1021
|
+
`${plural(c.verificationSpanCount, "invocation")}. Both are sums over concurrent work and are deliberately ` +
|
|
1022
|
+
"non-conserved — they can exceed the total elapsed above because the run did more work than it spent time.", "");
|
|
1023
|
+
lines.push("The chain below is reconstructed from observed time: the run records why a workstream was waiting, not " +
|
|
1024
|
+
"which workstream released it, so each step names the flow that held the wall clock before it rather than " +
|
|
1025
|
+
"a recorded cause.", "");
|
|
1026
|
+
const legs = c.criticalPath;
|
|
1027
|
+
const shown = legs.length > CRITICAL_PATH_LEG_LIMIT ? legs.slice(0, CRITICAL_PATH_LEG_LIMIT - 1) : legs;
|
|
1028
|
+
for (const leg of shown) {
|
|
1029
|
+
lines.push(`- ${formatDuration(leg.startMs)}–${formatDuration(leg.endMs)}: ${describeLeg(leg)}${describeHandoff(leg)}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (legs.length > CRITICAL_PATH_LEG_LIMIT) {
|
|
1032
|
+
const rest = legs.slice(CRITICAL_PATH_LEG_LIMIT - 1);
|
|
1033
|
+
const restMs = rest.reduce((sum, leg) => sum + leg.durationMs, 0);
|
|
1034
|
+
lines.push(`- other: ${plural(rest.length, "further leg")}, chronologically after the above, spanning ` +
|
|
1035
|
+
`${formatDuration(restMs)} combined (not shown individually)`);
|
|
1036
|
+
}
|
|
1037
|
+
lines.push("");
|
|
1038
|
+
lines.push("Wait totals, by cause — per-cause totals, **not a partition of the wall clock**: two of these causes are " +
|
|
1039
|
+
"measured on the author item and lie inside the same workstream's `spec-pending` window, so these rows " +
|
|
1040
|
+
"must never be summed together or into a run total.", "");
|
|
1041
|
+
if (c.waitsByCause.length === 0) {
|
|
1042
|
+
lines.push("No scheduling waits were observed.");
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
for (const wc of c.waitsByCause) {
|
|
1046
|
+
const gloss = WAIT_CAUSE_GLOSS[wc.cause];
|
|
1047
|
+
lines.push(`- \`${wc.cause}\`${wc.derived ? " (derived)" : ""}: ${formatDuration(wc.totalMs)} across ` +
|
|
1048
|
+
`${plural(wc.workstreams, "workstream")}${gloss ? ` — ${gloss}` : ""}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
lines.push("Per-workstream schedule:", "");
|
|
1053
|
+
if (c.schedules.length === 0) {
|
|
1054
|
+
lines.push("No scheduling observations were recorded for any workstream.");
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
for (const s of c.schedules) {
|
|
1058
|
+
const parts = [];
|
|
1059
|
+
parts.push(s.readyMs !== undefined ? `ready at ${formatDuration(s.readyMs)}` : "no ready instant observed");
|
|
1060
|
+
if (s.scheduledMs !== undefined) {
|
|
1061
|
+
parts.push(`scheduled at ${formatDuration(s.scheduledMs)}`);
|
|
1062
|
+
if (s.queuedMs !== undefined)
|
|
1063
|
+
parts.push(`queued ${formatDuration(s.queuedMs)}`);
|
|
1064
|
+
}
|
|
1065
|
+
else if (s.readyMs !== undefined) {
|
|
1066
|
+
parts.push("became ready but was never scheduled in this run");
|
|
1067
|
+
}
|
|
1068
|
+
lines.push(`- **${s.workstream}**: ${parts.join(", ")}`);
|
|
1069
|
+
for (const w of s.waits) {
|
|
1070
|
+
lines.push(` - \`${w.cause}\`: ${formatDuration(w.waitedMs)}${w.derived ? " (derived by this report, not measured by the runner)" : ""}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
lines.push("");
|
|
1075
|
+
return lines;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Renders the aggregated summary into the "Where the time went" section
|
|
1079
|
+
* (SC-10, SC-11) — human-first, concise, and never a score or a grade.
|
|
1080
|
+
*/
|
|
1081
|
+
export function renderWhereTheTimeWent(summary) {
|
|
1082
|
+
const lines = ["## Where the time went", ""];
|
|
1083
|
+
lines.push(`Total elapsed: **${formatDuration(summary.totalElapsedMs)}** — run start to the analytics snapshot taken ` +
|
|
1084
|
+
"just before finalize and the report commit. The report write, the report commit, and publish are " +
|
|
1085
|
+
"excluded from this total and, when observed, recorded separately (see Publish below).", "");
|
|
1086
|
+
lines.push("### Time by bucket", "");
|
|
1087
|
+
for (const bucket of summary.buckets) {
|
|
1088
|
+
const cumulativeSuffix = bucket.cumulativeMs !== bucket.durationMs ? ` (cumulative ${formatDuration(bucket.cumulativeMs)})` : "";
|
|
1089
|
+
lines.push(`- **${bucket.bucket}**: ${formatDuration(bucket.durationMs)} (${bucket.percent.toFixed(1)}%, ${plural(bucket.spanCount, "span")})${cumulativeSuffix}`);
|
|
1090
|
+
}
|
|
1091
|
+
lines.push("");
|
|
1092
|
+
// WS-06: gated on observed overlap — a serial run (including a
|
|
1093
|
+
// `--max-parallel 1` run) always has `peakWorkstreamConcurrency <= 1`, so
|
|
1094
|
+
// this section, and the bucket-line cumulative suffix above, are never
|
|
1095
|
+
// present there (SC-09, SC-11).
|
|
1096
|
+
if (summary.concurrency.peakWorkstreamConcurrency > 1) {
|
|
1097
|
+
lines.push(...renderConcurrencySection(summary));
|
|
1098
|
+
}
|
|
1099
|
+
lines.push("### Largest contributors", "");
|
|
1100
|
+
const topStage = summary.rankings.byStage[0];
|
|
1101
|
+
lines.push(topStage
|
|
1102
|
+
? `- Stage: \`${topStage.key}\` — ${formatDuration(topStage.durationMs)} across ${plural(topStage.count, "span")}`
|
|
1103
|
+
: "- Stage: none observed.");
|
|
1104
|
+
const topWorkstream = summary.rankings.byWorkstream[0];
|
|
1105
|
+
lines.push(topWorkstream
|
|
1106
|
+
? `- Workstream: ${topWorkstream.key} — ${formatDuration(topWorkstream.durationMs)}`
|
|
1107
|
+
: "- Workstream: none observed.");
|
|
1108
|
+
const attempt = summary.rankings.largestAgentAttempt;
|
|
1109
|
+
lines.push(attempt
|
|
1110
|
+
? `- Largest single agent attempt: \`${attempt.stage}\`` +
|
|
1111
|
+
(attempt.workstream ? ` (${attempt.workstream}` : " (") +
|
|
1112
|
+
`${attempt.attemptSeat ? `, ${attempt.attemptSeat}` : ""}` +
|
|
1113
|
+
`${attempt.attemptIndex !== undefined ? ` #${attempt.attemptIndex}` : ""})` +
|
|
1114
|
+
` — ${formatDuration(attempt.durationMs)}`
|
|
1115
|
+
: "- Largest single agent attempt: none observed.");
|
|
1116
|
+
const topCommand = summary.rankings.byVerifyCommand[0];
|
|
1117
|
+
lines.push(topCommand
|
|
1118
|
+
? `- Largest verification command: \`${topCommand.command}\` — ${formatDuration(topCommand.totalMs)} across ` +
|
|
1119
|
+
`${plural(topCommand.invocations, "invocation")} (largest single run ${formatDuration(topCommand.largestMs)})`
|
|
1120
|
+
: "- Largest verification command: none observed.");
|
|
1121
|
+
lines.push("");
|
|
1122
|
+
lines.push("### Attempts", "");
|
|
1123
|
+
for (const disposition of summary.dispositions) {
|
|
1124
|
+
lines.push(`- ${disposition.outcome}: ${plural(disposition.count, "attempt")} (${formatDuration(disposition.durationMs)})`);
|
|
1125
|
+
}
|
|
1126
|
+
lines.push("", `Agent time spent re-running after a prior attempt: **${formatDuration(summary.rerun.agent.totalMs)}**, by trigger:`);
|
|
1127
|
+
if (summary.rerun.agent.byReason.length === 0) {
|
|
1128
|
+
lines.push("- none observed");
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
for (const group of summary.rerun.agent.byReason) {
|
|
1132
|
+
lines.push(`- ${group.key}: ${plural(group.count, "attempt")} (${formatDuration(group.durationMs)})`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
lines.push("");
|
|
1136
|
+
lines.push("### Verification", "");
|
|
1137
|
+
lines.push(`Verification rerun time (passes caused by a retry, recovery, or fix — never the first pass over fresh ` +
|
|
1138
|
+
`work): **${formatDuration(summary.rerun.verification.totalMs)}**`);
|
|
1139
|
+
if (summary.rerun.verification.byPhase.length > 0) {
|
|
1140
|
+
lines.push("", "By phase:");
|
|
1141
|
+
for (const group of summary.rerun.verification.byPhase) {
|
|
1142
|
+
lines.push(`- ${group.key}: ${plural(group.count, "invocation")} (${formatDuration(group.durationMs)})`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (summary.rankings.byVerifyCommand.length > 0) {
|
|
1146
|
+
lines.push("", "Every configured command, all invocations:");
|
|
1147
|
+
for (const command of summary.rankings.byVerifyCommand) {
|
|
1148
|
+
lines.push(`- \`${command.command}\`: ${plural(command.invocations, "invocation")}, ${formatDuration(command.totalMs)} total`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
lines.push("");
|
|
1152
|
+
lines.push("### Observability gaps", "");
|
|
1153
|
+
lines.push(`Provider/prompt telemetry: ${summary.coverage.counts.observed} observed, ${summary.coverage.counts.estimated} ` +
|
|
1154
|
+
`estimated, ${summary.coverage.counts.unavailable} unavailable, ${summary.coverage.counts.incomplete} incomplete.`);
|
|
1155
|
+
if (summary.coverage.unavailableRoles.length > 0) {
|
|
1156
|
+
lines.push(`Roles reporting unavailable provider telemetry (expected coverage for a non-Claude provider, never a ` +
|
|
1157
|
+
`missing-time signal — their elapsed spans are still fully attributed above): ` +
|
|
1158
|
+
`${summary.coverage.unavailableRoles.join(", ")}.`);
|
|
1159
|
+
}
|
|
1160
|
+
if (summary.rankings.byProvider.length > 0) {
|
|
1161
|
+
lines.push("", "Time by provider (`unavailable` when a role's provider telemetry is unavailable, never a dropped total):");
|
|
1162
|
+
for (const p of summary.rankings.byProvider) {
|
|
1163
|
+
lines.push(`- ${p.key}: ${formatDuration(p.durationMs)} across ${plural(p.count, "span")}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (summary.rankings.byModel.length > 0) {
|
|
1167
|
+
lines.push("", "Time by model:");
|
|
1168
|
+
for (const m of summary.rankings.byModel) {
|
|
1169
|
+
lines.push(`- ${m.key}: ${formatDuration(m.durationMs)} across ${plural(m.count, "span")}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (summary.promptComponents.length > 0) {
|
|
1173
|
+
lines.push("", "Largest initial-context components: " +
|
|
1174
|
+
summary.promptComponents.map((c) => `${c.label} (${c.bytes.toLocaleString("en-US")} bytes)`).join(", ") +
|
|
1175
|
+
".");
|
|
1176
|
+
}
|
|
1177
|
+
if (summary.diagnostics.length > 0) {
|
|
1178
|
+
lines.push("", "Interval-consistency notes:");
|
|
1179
|
+
for (const diagnostic of summary.diagnostics)
|
|
1180
|
+
lines.push(`- ${diagnostic.message}`);
|
|
1181
|
+
}
|
|
1182
|
+
if (summary.analyticsOverheadMs !== undefined) {
|
|
1183
|
+
lines.push("", `Analytics-recorder overhead (a diagnostic footnote, not part of the total above): ${formatDuration(summary.analyticsOverheadMs)}.`);
|
|
1184
|
+
}
|
|
1185
|
+
lines.push("");
|
|
1186
|
+
lines.push("### Evidence", "");
|
|
1187
|
+
if (summary.evidence.length === 0) {
|
|
1188
|
+
lines.push("No canonical evidence references were recorded for this run.");
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
for (const ref of summary.evidence) {
|
|
1192
|
+
lines.push(`- local ${ref.kind}: \`${ref.ref}\`${ref.note ? ` — ${ref.note}` : ""}`);
|
|
1193
|
+
}
|
|
1194
|
+
if (summary.evidenceTotalCount > summary.evidence.length) {
|
|
1195
|
+
lines.push(`(showing ${summary.evidence.length} of ${summary.evidenceTotalCount} recorded references)`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
lines.push("", "Every reference above resolves only on the machine that ran this build — it is local evidence, not a " +
|
|
1199
|
+
"durable CI artifact.", "");
|
|
1200
|
+
if (summary.publish !== undefined) {
|
|
1201
|
+
lines.push("### Publish", "", `Publish ran after this report was already committed, so it is never reconciled into the total above: ` +
|
|
1202
|
+
`${formatDuration(summary.publish.durationMs)} across ${plural(summary.publish.invocations, "invocation")}.`, "");
|
|
1203
|
+
}
|
|
1204
|
+
return lines;
|
|
1205
|
+
}
|
|
1206
|
+
function unavailableSection(reason) {
|
|
1207
|
+
return ["## Where the time went", "", `Analytics unavailable — ${reason}.`, ""];
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* The run's own fail-open isolation boundary for analytics (D4, SC-13):
|
|
1211
|
+
* always returns a `## Where the time went` section, never throws. A throw
|
|
1212
|
+
* from `aggregate` (real or injected via `deps`, for tests), an `undefined`
|
|
1213
|
+
* snapshot, or a throw while *obtaining* the snapshot all degrade to an
|
|
1214
|
+
* explicit "analytics unavailable" placeholder — never suppress the rest of
|
|
1215
|
+
* the report, which is built independently of this function's output.
|
|
1216
|
+
*
|
|
1217
|
+
* `artifact` accepts either a plain value (the common case, and every
|
|
1218
|
+
* existing pure test) or a thunk. The thunk form matters at the real call
|
|
1219
|
+
* site: `recorder.snapshot()` is documented as non-throwing, but evaluating
|
|
1220
|
+
* it as a plain argument — `renderAnalyticsSection(recorder.snapshot())` —
|
|
1221
|
+
* would run it *before* this function's own try/catch is reached, so any
|
|
1222
|
+
* violation of that contract (a bug, or a test double standing in for the
|
|
1223
|
+
* recorder) would still escape this boundary and, through the caller's own
|
|
1224
|
+
* try/catch, suppress the whole report. Wrapping it in a thunk —
|
|
1225
|
+
* `renderAnalyticsSection(() => recorder.snapshot())` — keeps that call
|
|
1226
|
+
* inside the try below, so this function's "never throws" is true no matter
|
|
1227
|
+
* what actually produced the artifact.
|
|
1228
|
+
*/
|
|
1229
|
+
export function renderAnalyticsSection(artifact, deps = {}) {
|
|
1230
|
+
const aggregate = deps.aggregate ?? aggregateRunAnalytics;
|
|
1231
|
+
try {
|
|
1232
|
+
const resolved = typeof artifact === "function" ? artifact() : artifact;
|
|
1233
|
+
if (resolved === undefined) {
|
|
1234
|
+
return unavailableSection("no analytics snapshot was available when the report was rendered");
|
|
1235
|
+
}
|
|
1236
|
+
return renderWhereTheTimeWent(aggregate(resolved));
|
|
1237
|
+
}
|
|
1238
|
+
catch (error) {
|
|
1239
|
+
return unavailableSection(error.message);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
//# sourceMappingURL=run-analytics-report.js.map
|