@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/README.md +80 -15
- package/extension/implementer-reporting.js +215 -0
- package/extension/planner.js +106 -2
- package/package.json +2 -2
- package/spec/workflow.md +68 -27
- package/src/attempt.ts +104 -0
- package/src/cli.ts +340 -25
- package/src/metrics.ts +148 -5
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
|
-
|
|
152
|
-
|
|
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
|
}
|