@dpeek/codeless 0.1.0 → 0.1.2

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/src/metrics.ts CHANGED
@@ -5,17 +5,21 @@ import {
5
5
  readFileSync,
6
6
  readdirSync,
7
7
  renameSync,
8
+ rmdirSync,
8
9
  unlinkSync,
9
10
  writeFileSync,
10
11
  } from "node:fs";
11
12
  import { dirname, join } from "node:path";
12
13
 
14
+ import { type Attempt, validAttempt } from "./attempt.ts";
15
+
13
16
  export type Metric = {
14
17
  stream: string;
15
18
  change: string;
16
19
  dispatchedAt?: string;
17
20
  landedAt?: string;
18
21
  landedCommit?: string;
22
+ attempts?: Record<string, Attempt>;
19
23
  };
20
24
 
21
25
  function metricPath(workspaceRoot: string, stream: string, change: string): string {
@@ -32,7 +36,16 @@ function readMetric(path: string): Metric {
32
36
  typeof metric["change"] !== "string" ||
33
37
  (metric["dispatchedAt"] !== undefined && typeof metric["dispatchedAt"] !== "string") ||
34
38
  (metric["landedAt"] !== undefined && typeof metric["landedAt"] !== "string") ||
35
- (metric["landedCommit"] !== undefined && typeof metric["landedCommit"] !== "string")
39
+ (metric["landedCommit"] !== undefined && typeof metric["landedCommit"] !== "string") ||
40
+ (metric["attempts"] !== undefined &&
41
+ (typeof metric["attempts"] !== "object" ||
42
+ metric["attempts"] === null ||
43
+ Array.isArray(metric["attempts"]) ||
44
+ !Object.entries(metric["attempts"] as Record<string, unknown>).every(
45
+ ([id, attempt]) =>
46
+ validAttempt(attempt, metric["stream"] as string, metric["change"] as string) &&
47
+ attempt.id === id,
48
+ )))
36
49
  )
37
50
  throw new Error(`${path} is not a metric record`);
38
51
  return metric as Metric;
@@ -73,6 +86,50 @@ export function recordDispatch(workspaceRoot: string, stream: string, change: st
73
86
  readMetric(path);
74
87
  }
75
88
 
89
+ function withMetricLock(path: string, action: () => void): void {
90
+ const lock = `${path}.lock`;
91
+ mkdirSync(dirname(path), { recursive: true });
92
+ for (let attempt = 0; attempt < 100; attempt += 1) {
93
+ try {
94
+ mkdirSync(lock);
95
+ } catch (error) {
96
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
97
+ Bun.sleepSync(10);
98
+ continue;
99
+ }
100
+ try {
101
+ action();
102
+ } finally {
103
+ rmdirSync(lock);
104
+ }
105
+ return;
106
+ }
107
+ throw new Error(`could not acquire metric lock ${lock}`);
108
+ }
109
+
110
+ export function recordAttempt(
111
+ workspaceRoot: string,
112
+ stream: string,
113
+ change: string,
114
+ attempt: Attempt,
115
+ ): void {
116
+ if (!validAttempt(attempt, stream, change))
117
+ throw new Error("attempt is not a valid metric attempt");
118
+ const path = metricPath(workspaceRoot, stream, change);
119
+ withMetricLock(path, () => {
120
+ const metric = existsSync(path) ? readMetric(path) : { stream, change };
121
+ if (metric.stream !== stream || metric.change !== change)
122
+ throw new Error(`${path} does not match ${stream} change ${change}`);
123
+ const existing = metric.attempts?.[attempt.id];
124
+ if (existing !== undefined) {
125
+ if (JSON.stringify(existing) !== JSON.stringify(attempt))
126
+ throw new Error(`attempt ${attempt.id} conflicts with its existing metric record`);
127
+ return;
128
+ }
129
+ writeMetric(path, { ...metric, attempts: { ...metric.attempts, [attempt.id]: attempt } });
130
+ });
131
+ }
132
+
76
133
  export function recordLanding(
77
134
  workspaceRoot: string,
78
135
  stream: string,
@@ -144,12 +201,98 @@ export function metricReport(workspaceRoot: string): string[] {
144
201
  return `${name}\t${value.landed}\t${value.unlanded}\t${value.coverage}\t${value.total}\t${value.average}`;
145
202
  };
146
203
  const rows = [...byStream.entries()].map(([stream, records]) => ({ stream, records }));
204
+ const allRecords = rows.flatMap(({ records }) => records);
205
+ const formatCost = (amounts: number[]) => {
206
+ const parts = amounts.map((amount) => {
207
+ const [coefficient, exponent = "0"] = String(amount).toLowerCase().split("e");
208
+ const [whole, fraction = ""] = coefficient!.split(".");
209
+ return { digits: BigInt(`${whole}${fraction}`), scale: fraction.length - Number(exponent) };
210
+ });
211
+ const scale = Math.max(0, ...parts.map((part) => part.scale));
212
+ const total = parts.reduce(
213
+ (sum, part) => sum + part.digits * 10n ** BigInt(scale - part.scale),
214
+ 0n,
215
+ );
216
+ const digits = total.toString().padStart(scale + 1, "0");
217
+ if (scale === 0) return digits;
218
+ const fraction = digits.slice(-scale).replace(/0+$/, "");
219
+ return fraction.length === 0
220
+ ? digits.slice(0, -scale)
221
+ : `${digits.slice(0, -scale)}.${fraction}`;
222
+ };
223
+ const attemptSummary = (records: Metric[]) => {
224
+ const attempts = records.flatMap((record) => Object.values(record.attempts ?? {}));
225
+ const usage = attempts.filter((attempt) => attempt.usage !== undefined);
226
+ const costs = attempts.filter((attempt) => attempt.cost !== undefined);
227
+ const outcomes = new Map<string, number>();
228
+ const currencies = new Map<string, number[]>();
229
+ for (const attempt of attempts) {
230
+ outcomes.set(attempt.outcome, (outcomes.get(attempt.outcome) ?? 0) + 1);
231
+ if (attempt.cost !== undefined)
232
+ currencies.set(attempt.cost.currency, [
233
+ ...(currencies.get(attempt.cost.currency) ?? []),
234
+ attempt.cost.amount,
235
+ ]);
236
+ }
237
+ const coverage = (measured: number) =>
238
+ `${measured} measured, ${attempts.length - measured} unavailable`;
239
+ return {
240
+ reworkedChanges: new Set(
241
+ records
242
+ .filter((record) =>
243
+ Object.values(record.attempts ?? {}).some((attempt) => attempt.kind === "rework"),
244
+ )
245
+ .map((record) => `${record.stream}\0${record.change}`),
246
+ ).size,
247
+ initial: attempts.filter((attempt) => attempt.kind === "initial").length,
248
+ rework: attempts.filter((attempt) => attempt.kind === "rework").length,
249
+ incomplete: attempts.filter((attempt) => attempt.incomplete).length,
250
+ outcomes:
251
+ outcomes.size === 0
252
+ ? "none"
253
+ : [...outcomes.entries()]
254
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
255
+ .map(([outcome, count]) => `${outcome}: ${count}`)
256
+ .join(", "),
257
+ toolErrors: attempts.reduce((total, attempt) => total + attempt.errorCount, 0),
258
+ usageCoverage: coverage(usage.length),
259
+ input:
260
+ usage.length === 0
261
+ ? "unavailable"
262
+ : usage.reduce((total, attempt) => total + attempt.usage!.input, 0),
263
+ output:
264
+ usage.length === 0
265
+ ? "unavailable"
266
+ : usage.reduce((total, attempt) => total + attempt.usage!.output, 0),
267
+ cacheRead:
268
+ usage.length === 0
269
+ ? "unavailable"
270
+ : usage.reduce((total, attempt) => total + attempt.usage!.cacheRead, 0),
271
+ cacheWrite:
272
+ usage.length === 0
273
+ ? "unavailable"
274
+ : usage.reduce((total, attempt) => total + attempt.usage!.cacheWrite, 0),
275
+ costCoverage: coverage(costs.length),
276
+ costs:
277
+ currencies.size === 0
278
+ ? "unavailable"
279
+ : [...currencies.entries()]
280
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
281
+ .map(([currency, amounts]) => `${currency} ${formatCost(amounts)}`)
282
+ .join(", "),
283
+ };
284
+ };
285
+ const attemptRow = (name: string, records: Metric[]) => {
286
+ const value = attemptSummary(records);
287
+ return `${name}\t${value.reworkedChanges}\t${value.initial}\t${value.rework}\t${value.incomplete}\t${value.outcomes}\t${value.toolErrors}\t${value.usageCoverage}\t${value.input}\t${value.output}\t${value.cacheRead}\t${value.cacheWrite}\t${value.costCoverage}\t${value.costs}`;
288
+ };
147
289
  return [
148
290
  "Stream\tLanded\tNot landed\tElapsed coverage\tDispatch-to-land wall clock total\tAverage",
149
291
  ...rows.map(({ stream, records }) => row(stream, records)),
150
- row(
151
- "Project total",
152
- rows.flatMap(({ records }) => records),
153
- ),
292
+ row("Project total", allRecords),
293
+ "",
294
+ "Stream\tChanges with rework\tInitial attempts\tRework attempts\tIncomplete collection\tTerminal outcomes\tTool errors\tUsage coverage\tInput tokens\tOutput tokens\tCache-read tokens\tCache-write tokens\tCost coverage\tCost totals",
295
+ ...rows.map(({ stream, records }) => attemptRow(stream, records)),
296
+ attemptRow("Project total", allRecords),
154
297
  ];
155
298
  }