@dev-loops/core 1.0.2 → 1.0.4-pre.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.
@@ -0,0 +1,412 @@
1
+ /**
2
+ * execution-record.mjs — compact per-execution-unit telemetry record, the
3
+ * dev-loop execution-cap effort's final telemetry slice.
4
+ *
5
+ * Earlier execution-cap slices bounded the dev-loop's execution units (child
6
+ * launch, reviewer unit, role budget) and enforced single watcher ownership.
7
+ * None of them measure what a unit actually cost. This module adds one
8
+ * compact, honesty-gated telemetry RECORD per execution unit (coordinator
9
+ * phase, reviewer unit, judge round, fixer pass, watch cycle).
10
+ *
11
+ * Modeled on ./cache-telemetry-evidence.mjs: LOCAL/harness-observable
12
+ * metrics (prompt/context bytes, turns, tool calls, local tool time) are
13
+ * always measured — a genuine local zero is a real zero. PROVIDER-owned
14
+ * metrics (input/output/cache-read tokens) are honesty-gated: a harness
15
+ * that cannot observe a dimension must report it `{ available:false,
16
+ * reason }`, NEVER a coerced/estimated zero, and — the core fidelity guard
17
+ * — must never be handed a numeric value for a dimension its profile marks
18
+ * unavailable (fail closed). Child wall time (childWallTimeMs) is a
19
+ * wall-clock measurement, not provider telemetry: any harness may report
20
+ * it, regardless of its provider-token profile — measured when a finite
21
+ * non-negative value is supplied, else `{ available:false, reason }`,
22
+ * never a coerced zero.
23
+ *
24
+ * Pure and offline: no GitHub, no clock, no file reads except the writer.
25
+ */
26
+ import { mkdir, writeFile } from "node:fs/promises";
27
+ import path from "node:path";
28
+
29
+ import { HARNESS_VALUES } from "./role-budget-bound.mjs";
30
+
31
+ export const EXECUTION_RECORD_SCHEMA_VERSION = 1;
32
+
33
+ /** The five execution-unit kinds this record covers (superset of role-budget-bound's ROLE_VALUES). */
34
+ export const EXECUTION_UNIT_ROLES = Object.freeze([
35
+ "coordinator_phase",
36
+ "reviewer_unit",
37
+ "judge_round",
38
+ "fixer_pass",
39
+ "watch_cycle",
40
+ ]);
41
+
42
+ /**
43
+ * Per-harness provider-token-telemetry capability. Conservative honest
44
+ * defaults: `claude` observes provider token usage; `pi` and `codex` do not
45
+ * — we have no ground truth that either exposes per-unit provider token
46
+ * usage, so a record for them must never claim a measured token value (the
47
+ * dev-loop execution-cap telemetry non-goal: never claim telemetry a harness
48
+ * does not expose). Kept separate from review-dispatch-plan's own harness
49
+ * capability map on purpose (different concern: cache reuse vs. per-unit cost).
50
+ */
51
+ export const TELEMETRY_HARNESS_PROFILES = Object.freeze({
52
+ claude: Object.freeze({ providerTokens: "available" }),
53
+ codex: Object.freeze({ providerTokens: "unavailable" }),
54
+ pi: Object.freeze({ providerTokens: "unavailable" }),
55
+ });
56
+
57
+ const profileKeys = Object.keys(TELEMETRY_HARNESS_PROFILES).slice().sort();
58
+ const harnessKeys = HARNESS_VALUES.slice().sort();
59
+ if (profileKeys.length !== harnessKeys.length || profileKeys.some((k, i) => k !== harnessKeys[i])) {
60
+ throw new Error("execution-record.mjs: TELEMETRY_HARNESS_PROFILES must cover exactly HARNESS_VALUES");
61
+ }
62
+
63
+ /** @param {unknown} value @returns {boolean} */
64
+ function isNonEmptyString(value) {
65
+ return typeof value === "string" && value.trim().length > 0;
66
+ }
67
+ /** @param {unknown} value @returns {boolean} */
68
+ function isHexHeadSha(value) {
69
+ return typeof value === "string" && /^[0-9a-f]{7,64}$/i.test(value.trim());
70
+ }
71
+ /** @param {unknown} value @returns {boolean} discrete non-negative counter. */
72
+ function isNonNegativeInteger(value) {
73
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
74
+ }
75
+ /** @param {unknown} value @returns {boolean} a genuine measurable non-negative duration/count. */
76
+ function isNonNegativeFiniteNumber(value) {
77
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
78
+ }
79
+ /**
80
+ * Own-property-only harness profile lookup. A plain `[]` read plus
81
+ * truthiness would let an inherited name (`toString`, `constructor`,
82
+ * `__proto__`) resolve to a non-nullish value from Object.prototype and
83
+ * bypass the "unknown harness" fail-closed check — this gates strictly on
84
+ * the three real harness keys.
85
+ * @param {unknown} harness @returns {object|undefined}
86
+ */
87
+ function getHarnessProfile(harness) {
88
+ return typeof harness === "string" && Object.hasOwn(TELEMETRY_HARNESS_PROFILES, harness)
89
+ ? TELEMETRY_HARNESS_PROFILES[harness]
90
+ : undefined;
91
+ }
92
+ /** @param {unknown} value @returns {boolean} rejects an empty string, a path separator, or a ".." traversal segment — a fail-closed guard for any value interpolated into a filesystem path. */
93
+ function isSafePathSegment(value) {
94
+ return typeof value === "string" && value.trim().length > 0 && !value.includes("/") && !value.includes("\\") && !value.includes("..");
95
+ }
96
+
97
+ /**
98
+ * Recursively freeze a plain object/array's own nested plain objects/arrays.
99
+ * Mirrors reviewer-unit-bound.mjs / role-budget-bound.mjs / watcher-exclusivity.mjs.
100
+ * @param {unknown} value @param {WeakSet<object>} [seen] @returns {unknown}
101
+ */
102
+ function deepFreeze(value, seen = new WeakSet()) {
103
+ if (value === null || typeof value !== "object" || seen.has(value)) return value;
104
+ seen.add(value);
105
+ for (const key of Object.keys(value)) deepFreeze(value[key], seen);
106
+ return Object.freeze(value);
107
+ }
108
+
109
+ /**
110
+ * Resolve one PROVIDER-token dimension's honesty-gated value. `null` means
111
+ * "not observed" and is recorded as `{available:false, reason}` — never
112
+ * coerced to 0. A non-null value is only accepted when the harness profile
113
+ * marks the dimension observable; otherwise this is the core fidelity guard
114
+ * and fails closed (a harness that cannot observe the metric must never
115
+ * report a number for it).
116
+ */
117
+ function resolveProviderTokenDimension({ harness, dim, value, reason, role, observable }) {
118
+ if (value === null || value === undefined) {
119
+ const defaultReason = observable
120
+ ? `harness ${harness} exposes token telemetry but no value was reported for this ${role}`
121
+ : `harness ${harness} does not expose provider token usage telemetry`;
122
+ return Object.freeze({ available: false, value: null, reason: isNonEmptyString(reason) ? reason.trim() : defaultReason });
123
+ }
124
+ if (!observable) {
125
+ throw new Error(`harness ${harness} does not expose provider token usage telemetry — providerTokens.${dim} must never report a value, got ${JSON.stringify(value)}`);
126
+ }
127
+ if (!isNonNegativeFiniteNumber(value)) {
128
+ throw new TypeError(`providerTokens.${dim} must be a finite non-negative number or null, got ${JSON.stringify(value)}`);
129
+ }
130
+ return Object.freeze({ available: true, value, reason: null });
131
+ }
132
+
133
+ /**
134
+ * Resolve childWallTimeMs. Any harness may report it (a wall-clock
135
+ * measurement, not provider telemetry) — measured-or-unavailable-with-reason,
136
+ * never gated on the provider-token profile.
137
+ */
138
+ function resolveChildWallTime({ value, reason, role }) {
139
+ if (value === null || value === undefined) {
140
+ const defaultReason = `child wall time not reported by harness for this ${role}`;
141
+ return Object.freeze({ available: false, value: null, reason: isNonEmptyString(reason) ? reason.trim() : defaultReason });
142
+ }
143
+ if (!isNonNegativeFiniteNumber(value)) {
144
+ throw new TypeError(`childWallTimeMs must be a finite non-negative number or null, got ${JSON.stringify(value)}`);
145
+ }
146
+ return Object.freeze({ available: true, value, reason: null });
147
+ }
148
+
149
+ /**
150
+ * Build one compact per-execution-unit telemetry record.
151
+ *
152
+ * @param {object} input
153
+ * @param {"pi"|"claude"|"codex"} input.harness
154
+ * @param {"coordinator_phase"|"reviewer_unit"|"judge_round"|"fixer_pass"|"watch_cycle"} input.role
155
+ * @param {{headSha:string, phase?, round?, unit?, unitId?}} input.identity
156
+ * @param {number} input.promptBytes @param {number} input.contextBytes
157
+ * @param {number} input.turns @param {number} input.toolCalls
158
+ * @param {{input:number|null, output:number|null, cacheRead:number|null, reasons?:object}} input.providerTokens
159
+ * @param {number} input.localToolTimeMs @param {number|null} input.childWallTimeMs
160
+ * @param {string|null} input.waitOwner @param {string} input.outcome
161
+ * @returns {object} frozen record.
162
+ */
163
+ export function buildExecutionUnitRecord({
164
+ harness,
165
+ role,
166
+ identity,
167
+ promptBytes,
168
+ contextBytes,
169
+ turns,
170
+ toolCalls,
171
+ providerTokens = {},
172
+ localToolTimeMs,
173
+ childWallTimeMs = null,
174
+ childWallTimeMsReason,
175
+ waitOwner = null,
176
+ outcome,
177
+ } = {}) {
178
+ if (!HARNESS_VALUES.includes(harness)) {
179
+ throw new TypeError(`buildExecutionUnitRecord requires harness to be one of ${HARNESS_VALUES.join(", ")}, got ${JSON.stringify(harness)}`);
180
+ }
181
+ if (!EXECUTION_UNIT_ROLES.includes(role)) {
182
+ throw new TypeError(`buildExecutionUnitRecord requires role to be one of ${EXECUTION_UNIT_ROLES.join(", ")}, got ${JSON.stringify(role)}`);
183
+ }
184
+ if (!identity || typeof identity !== "object" || !isHexHeadSha(identity.headSha)) {
185
+ throw new TypeError("buildExecutionUnitRecord requires identity.headSha to be a hex string (7-64 chars)");
186
+ }
187
+ const headSha = identity.headSha.trim().toLowerCase();
188
+ const derivedUnitId = identity.unitId ?? identity.unit ?? identity.round ?? identity.phase;
189
+ if (derivedUnitId === null || derivedUnitId === undefined || (typeof derivedUnitId === "string" && derivedUnitId.trim().length === 0)) {
190
+ throw new TypeError("buildExecutionUnitRecord requires identity to carry at least one of unitId/unit/round/phase");
191
+ }
192
+ const unitId = String(derivedUnitId).trim();
193
+ if (!isNonEmptyString(outcome)) {
194
+ throw new TypeError("buildExecutionUnitRecord requires a non-empty outcome");
195
+ }
196
+ if (waitOwner !== null && !isNonEmptyString(waitOwner)) {
197
+ throw new TypeError("buildExecutionUnitRecord requires waitOwner to be a non-empty string or null");
198
+ }
199
+
200
+ const localMetric = (value, label) => {
201
+ if (!isNonNegativeInteger(value)) {
202
+ throw new TypeError(`${label} must be a finite non-negative integer, got ${JSON.stringify(value)}`);
203
+ }
204
+ return value;
205
+ };
206
+ if (!isNonNegativeFiniteNumber(localToolTimeMs)) {
207
+ throw new TypeError(`localToolTimeMs must be a finite non-negative number, got ${JSON.stringify(localToolTimeMs)}`);
208
+ }
209
+ const metrics = Object.freeze({
210
+ promptBytes: localMetric(promptBytes, "promptBytes"),
211
+ contextBytes: localMetric(contextBytes, "contextBytes"),
212
+ turns: localMetric(turns, "turns"),
213
+ toolCalls: localMetric(toolCalls, "toolCalls"),
214
+ localToolTimeMs,
215
+ });
216
+
217
+ const observable = getHarnessProfile(harness)?.providerTokens === "available";
218
+ const reasons = providerTokens.reasons ?? {};
219
+ const providerTokensNorm = Object.freeze({
220
+ input: resolveProviderTokenDimension({ harness, dim: "input", value: providerTokens.input ?? null, reason: reasons.input, role, observable }),
221
+ output: resolveProviderTokenDimension({ harness, dim: "output", value: providerTokens.output ?? null, reason: reasons.output, role, observable }),
222
+ cacheRead: resolveProviderTokenDimension({ harness, dim: "cacheRead", value: providerTokens.cacheRead ?? null, reason: reasons.cacheRead, role, observable }),
223
+ });
224
+ const childWallTimeNorm = resolveChildWallTime({ value: childWallTimeMs, reason: childWallTimeMsReason, role });
225
+
226
+ const availability = Object.freeze({
227
+ providerTokensInput: Object.freeze({ available: providerTokensNorm.input.available, reason: providerTokensNorm.input.reason }),
228
+ providerTokensOutput: Object.freeze({ available: providerTokensNorm.output.available, reason: providerTokensNorm.output.reason }),
229
+ providerTokensCacheRead: Object.freeze({ available: providerTokensNorm.cacheRead.available, reason: providerTokensNorm.cacheRead.reason }),
230
+ childWallTimeMs: Object.freeze({ available: childWallTimeNorm.available, reason: childWallTimeNorm.reason }),
231
+ });
232
+ const hasUnavailableProviderMetric = !providerTokensNorm.input.available
233
+ || !providerTokensNorm.output.available
234
+ || !providerTokensNorm.cacheRead.available;
235
+
236
+ return deepFreeze({
237
+ schemaVersion: EXECUTION_RECORD_SCHEMA_VERSION,
238
+ role,
239
+ harness,
240
+ headSha,
241
+ unitId,
242
+ identity: {
243
+ headSha,
244
+ unitId,
245
+ phase: identity.phase ?? null,
246
+ round: identity.round ?? null,
247
+ unit: identity.unit ?? null,
248
+ },
249
+ metrics,
250
+ providerTokens: providerTokensNorm,
251
+ childWallTimeMs: childWallTimeNorm,
252
+ waitOwner,
253
+ outcome,
254
+ availability,
255
+ hasUnavailableProviderMetric,
256
+ });
257
+ }
258
+
259
+ /**
260
+ * Fail-closed validation (never throws). Re-derives provider-token
261
+ * availability from `record.harness`'s profile — NOT from a stored
262
+ * `available:true` flag — so a hand-edited record that flips availability
263
+ * while the harness profile says unavailable fails closed, mirroring
264
+ * validateCacheTelemetryEvidence's capability re-derivation.
265
+ *
266
+ * @param {object} input @param {object} input.record
267
+ * @returns {{ok:boolean, failures:Array<{check:string, reason:string}>}}
268
+ */
269
+ export function validateExecutionUnitRecord({ record } = {}) {
270
+ const failures = [];
271
+ if (!record || typeof record !== "object") {
272
+ return { ok: false, failures: [{ check: "record", reason: "missing execution-unit record" }] };
273
+ }
274
+ if (record.schemaVersion !== EXECUTION_RECORD_SCHEMA_VERSION) {
275
+ failures.push({ check: "schema_version", reason: `schemaVersion must be ${EXECUTION_RECORD_SCHEMA_VERSION}, got ${JSON.stringify(record.schemaVersion)}` });
276
+ }
277
+ if (!EXECUTION_UNIT_ROLES.includes(record.role)) {
278
+ failures.push({ check: "role", reason: `role must be one of ${EXECUTION_UNIT_ROLES.join(", ")}, got ${JSON.stringify(record.role)}` });
279
+ }
280
+ const profile = getHarnessProfile(record.harness);
281
+ if (!profile) {
282
+ failures.push({ check: "harness", reason: `harness must be one of ${HARNESS_VALUES.join(", ")}, got ${JSON.stringify(record.harness)}` });
283
+ }
284
+ if (!isHexHeadSha(record.headSha)) {
285
+ failures.push({ check: "head_sha", reason: `headSha must be a hex string (7-64 chars), got ${JSON.stringify(record.headSha)}` });
286
+ }
287
+ if (!isNonEmptyString(record.unitId)) {
288
+ failures.push({ check: "unit_id", reason: `unitId must be a non-empty string, got ${JSON.stringify(record.unitId)}` });
289
+ }
290
+ if (!record.identity || typeof record.identity !== "object" || record.identity.headSha !== record.headSha || record.identity.unitId !== record.unitId) {
291
+ failures.push({ check: "identity", reason: "identity.headSha/identity.unitId must equal the record's own headSha/unitId" });
292
+ }
293
+ const metrics = record.metrics ?? {};
294
+ for (const dim of ["promptBytes", "contextBytes", "turns", "toolCalls"]) {
295
+ if (!isNonNegativeInteger(metrics[dim])) {
296
+ failures.push({ check: "local_metric", reason: `metrics.${dim} must be a finite non-negative integer, got ${JSON.stringify(metrics[dim])}` });
297
+ }
298
+ }
299
+ if (!isNonNegativeFiniteNumber(metrics.localToolTimeMs)) {
300
+ failures.push({ check: "local_metric", reason: `metrics.localToolTimeMs must be a finite non-negative number, got ${JSON.stringify(metrics.localToolTimeMs)}` });
301
+ }
302
+
303
+ const checkHonestyGatedDim = (label, dimRecord, { gateOnProfile } = {}) => {
304
+ if (!dimRecord || typeof dimRecord !== "object" || typeof dimRecord.available !== "boolean") {
305
+ failures.push({ check: `${label}_shape`, reason: `${label} must be an object with a boolean available field` });
306
+ return;
307
+ }
308
+ if (dimRecord.available) {
309
+ if (gateOnProfile && profile && profile.providerTokens !== "available") {
310
+ failures.push({ check: `${label}_honesty`, reason: `${label}.available=true but harness ${record.harness} does not expose provider token telemetry — an unavailable harness must never claim a measured value` });
311
+ }
312
+ if (!isNonNegativeFiniteNumber(dimRecord.value)) {
313
+ failures.push({ check: `${label}_value`, reason: `${label}.value must be a finite non-negative number when available=true, got ${JSON.stringify(dimRecord.value)}` });
314
+ }
315
+ } else {
316
+ if (dimRecord.value !== null) {
317
+ failures.push({ check: `${label}_value`, reason: `${label}.value must be null when available=false, got ${JSON.stringify(dimRecord.value)}` });
318
+ }
319
+ if (!isNonEmptyString(dimRecord.reason)) {
320
+ failures.push({ check: `${label}_reason`, reason: `${label}.reason must be a non-empty string when available=false` });
321
+ }
322
+ }
323
+ };
324
+ checkHonestyGatedDim("providerTokens.input", record.providerTokens?.input, { gateOnProfile: true });
325
+ checkHonestyGatedDim("providerTokens.output", record.providerTokens?.output, { gateOnProfile: true });
326
+ checkHonestyGatedDim("providerTokens.cacheRead", record.providerTokens?.cacheRead, { gateOnProfile: true });
327
+ checkHonestyGatedDim("childWallTimeMs", record.childWallTimeMs, {});
328
+
329
+ // Re-derive availability from the already-validated per-dimension records
330
+ // (never trust a stored availability object on its own) — deleting or
331
+ // forging this field must fail closed rather than silently pass.
332
+ const expectedAvailability = {
333
+ providerTokensInput: record.providerTokens?.input,
334
+ providerTokensOutput: record.providerTokens?.output,
335
+ providerTokensCacheRead: record.providerTokens?.cacheRead,
336
+ childWallTimeMs: record.childWallTimeMs,
337
+ };
338
+ for (const [key, dim] of Object.entries(expectedAvailability)) {
339
+ const avail = record.availability?.[key];
340
+ if (!avail || typeof avail !== "object" || typeof avail.available !== "boolean") {
341
+ failures.push({ check: `availability.${key}`, reason: `availability.${key} must be an object with a boolean available field` });
342
+ continue;
343
+ }
344
+ const expectedAvailable = dim && typeof dim === "object" ? dim.available : undefined;
345
+ const expectedReason = dim && typeof dim === "object" ? dim.reason : undefined;
346
+ if (avail.available !== expectedAvailable) {
347
+ failures.push({ check: `availability.${key}`, reason: `availability.${key}.available=${JSON.stringify(avail.available)} does not match the re-derived ${key} availability (expected ${JSON.stringify(expectedAvailable)})` });
348
+ } else if (avail.reason !== expectedReason) {
349
+ failures.push({ check: `availability.${key}`, reason: `availability.${key}.reason=${JSON.stringify(avail.reason)} does not match the re-derived ${key} reason (expected ${JSON.stringify(expectedReason)})` });
350
+ }
351
+ }
352
+
353
+ if (record.waitOwner !== null && !isNonEmptyString(record.waitOwner)) {
354
+ failures.push({ check: "wait_owner", reason: `waitOwner must be a non-empty string or null, got ${JSON.stringify(record.waitOwner)}` });
355
+ }
356
+ if (!isNonEmptyString(record.outcome)) {
357
+ failures.push({ check: "outcome", reason: `outcome must be a non-empty string, got ${JSON.stringify(record.outcome)}` });
358
+ }
359
+
360
+ const expectedHasUnavailable = ["input", "output", "cacheRead"].some((dim) => record.providerTokens?.[dim]?.available !== true);
361
+ if (record.hasUnavailableProviderMetric !== expectedHasUnavailable) {
362
+ failures.push({ check: "aggregate_consistency", reason: `hasUnavailableProviderMetric=${record.hasUnavailableProviderMetric} does not match the re-derived providerTokens availability (expected ${expectedHasUnavailable})` });
363
+ }
364
+
365
+ return { ok: failures.length === 0, failures };
366
+ }
367
+
368
+ /**
369
+ * Strict fail-closed enforcement surface (GATE-EXEC-EXECUTION-RECORD): throws
370
+ * when the record is missing or invalid, naming every failing check.
371
+ * @param {object} input @param {object} input.record
372
+ * @returns {true}
373
+ */
374
+ export function enforceExecutionUnitRecord({ record } = {}) {
375
+ const r = validateExecutionUnitRecord({ record });
376
+ if (!r.ok) {
377
+ throw new Error(
378
+ `GATE-EXEC-EXECUTION-RECORD: execution-unit record failed validation; refusing to proceed (${r.failures.map((f) => `${f.check}: ${f.reason}`).join("; ")})`,
379
+ );
380
+ }
381
+ return true;
382
+ }
383
+
384
+ /**
385
+ * Deterministic artifact path for one execution unit's record.
386
+ * @param {object} input @param {string} input.dir @param {string} input.role
387
+ * @param {string} input.headSha @param {string} input.unitId
388
+ * @returns {string}
389
+ */
390
+ export function executionRecordPath({ dir, role, headSha, unitId } = {}) {
391
+ if (typeof dir !== "string" || dir.length === 0) throw new Error("executionRecordPath requires a dir");
392
+ if (!EXECUTION_UNIT_ROLES.includes(role) || !isSafePathSegment(role)) {
393
+ throw new Error(`executionRecordPath requires role to be one of ${EXECUTION_UNIT_ROLES.join(", ")}`);
394
+ }
395
+ if (!isHexHeadSha(headSha) || !isSafePathSegment(headSha)) throw new Error("executionRecordPath requires a hex headSha");
396
+ if (!isSafePathSegment(unitId)) {
397
+ throw new Error("executionRecordPath requires a non-empty unitId without path separators or '..' segments");
398
+ }
399
+ return path.join(dir, `${role}-${String(unitId).trim()}-${headSha.trim().toLowerCase()}.execution-record.json`);
400
+ }
401
+
402
+ /**
403
+ * Persist the record to its deterministic path.
404
+ * @param {object} input @param {string} input.dir @param {object} input.record
405
+ * @returns {Promise<{path:string}>}
406
+ */
407
+ export async function writeExecutionUnitRecord({ dir, record } = {}) {
408
+ const target = executionRecordPath({ dir, role: record.role, headSha: record.headSha, unitId: record.unitId });
409
+ await mkdir(path.dirname(target), { recursive: true });
410
+ await writeFile(target, `${JSON.stringify(record, null, 2)}\n`, "utf8");
411
+ return { path: target };
412
+ }