clud-bug 0.6.34 → 0.7.0-rc.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/bin/clud-bug.js +10 -1353
- package/dist/cli/agents-md.d.ts +16 -0
- package/dist/cli/agents-md.d.ts.map +1 -0
- package/dist/cli/agents-md.js +226 -0
- package/dist/cli/agents-md.js.map +1 -0
- package/dist/cli/audit.d.ts +13 -0
- package/dist/cli/audit.d.ts.map +1 -0
- package/dist/cli/audit.js +90 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/branch-protection.d.ts +57 -0
- package/dist/cli/branch-protection.d.ts.map +1 -0
- package/dist/cli/branch-protection.js +118 -0
- package/dist/cli/branch-protection.js.map +1 -0
- package/dist/cli/edit-workflow.d.ts +18 -0
- package/dist/cli/edit-workflow.d.ts.map +1 -0
- package/dist/cli/edit-workflow.js +43 -0
- package/dist/cli/edit-workflow.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +1336 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/skill-usage.d.ts +109 -0
- package/dist/cli/skill-usage.d.ts.map +1 -0
- package/dist/cli/skill-usage.js +380 -0
- package/dist/cli/skill-usage.js.map +1 -0
- package/dist/cli/skills.d.ts +56 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +292 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/update.d.ts +29 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +186 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/usage.d.ts +142 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +348 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/core/audit.d.ts +8 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +47 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/detect.d.ts +77 -0
- package/dist/core/detect.d.ts.map +1 -0
- package/dist/core/detect.js +262 -0
- package/dist/core/detect.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +31 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/prompt-builder.d.ts +164 -0
- package/dist/core/prompt-builder.d.ts.map +1 -0
- package/dist/core/prompt-builder.js +419 -0
- package/dist/core/prompt-builder.js.map +1 -0
- package/dist/core/prompts.d.ts +9 -0
- package/dist/core/prompts.d.ts.map +1 -0
- package/dist/core/prompts.js +401 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/render-review.d.ts +6 -0
- package/dist/core/render-review.d.ts.map +1 -0
- package/dist/core/render-review.js +219 -0
- package/dist/core/render-review.js.map +1 -0
- package/dist/core/render.d.ts +13 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +80 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/review-schema-zod.d.ts +240 -0
- package/dist/core/review-schema-zod.d.ts.map +1 -0
- package/dist/core/review-schema-zod.js +218 -0
- package/dist/core/review-schema-zod.js.map +1 -0
- package/dist/core/review-schema.d.ts +42 -0
- package/dist/core/review-schema.d.ts.map +1 -0
- package/dist/core/review-schema.js +156 -0
- package/dist/core/review-schema.js.map +1 -0
- package/dist/core/review-writeback.d.ts +139 -0
- package/dist/core/review-writeback.d.ts.map +1 -0
- package/dist/core/review-writeback.js +313 -0
- package/dist/core/review-writeback.js.map +1 -0
- package/dist/core/skills.d.ts +122 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +636 -0
- package/dist/core/skills.js.map +1 -0
- package/package.json +30 -4
- package/{lib/agents-md.js → src/cli/agents-md.ts} +25 -14
- package/{lib/audit.js → src/cli/audit.ts} +37 -44
- package/{lib/branch-protection.js → src/cli/branch-protection.ts} +75 -11
- package/{lib/edit-workflow.js → src/cli/edit-workflow.ts} +32 -11
- package/src/cli/index.ts +101 -0
- package/src/cli/main.ts +1376 -0
- package/{lib/skill-usage.js → src/cli/skill-usage.ts} +168 -94
- package/src/cli/skills.ts +386 -0
- package/{lib/update.js → src/cli/update.ts} +68 -27
- package/{lib/usage.js → src/cli/usage.ts} +167 -76
- package/src/core/audit.ts +53 -0
- package/{lib/detect.js → src/core/detect.ts} +100 -47
- package/src/core/index.ts +155 -0
- package/src/core/prompt-builder.ts +561 -0
- package/{lib/prompts.js → src/core/prompts.ts} +16 -2
- package/{lib/render-review.js → src/core/render-review.ts} +57 -25
- package/{lib/render.js → src/core/render.ts} +36 -10
- package/src/core/review-schema-zod.ts +262 -0
- package/{lib/review-schema.js → src/core/review-schema.ts} +68 -5
- package/src/core/review-writeback.ts +446 -0
- package/{lib/skills.js → src/core/skills.ts} +339 -342
- package/templates/workflow-py.yml.tmpl +2 -2
- package/templates/workflow-ts.yml.tmpl +2 -2
- package/templates/workflow.yml.tmpl +17 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// src/cli/usage.ts — Q7-clud-bug $/LOC compute.
|
|
2
2
|
//
|
|
3
3
|
// Pure functions, no I/O. Driven from bin/clud-bug.js which fetches workflow
|
|
4
4
|
// run JSON + PR metadata via gh CLI. Implementation of the 0.0.M.1 dashboard
|
|
@@ -20,9 +20,16 @@
|
|
|
20
20
|
// Q7-clud-bug enforcement: dashboard reports the 30-day rolling trend; the
|
|
21
21
|
// next Phase 0.5 PR ships when the trend stops declining.
|
|
22
22
|
|
|
23
|
+
export interface ModelPricing {
|
|
24
|
+
input: number;
|
|
25
|
+
output: number;
|
|
26
|
+
cacheRead: number;
|
|
27
|
+
cacheWrite: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
// Anthropic pricing as of 2026-05 (per MTok). Cache write is 1.25× input
|
|
24
31
|
// per Anthropic's published 5-min-TTL ephemeral cache rate.
|
|
25
|
-
export const PRICING = {
|
|
32
|
+
export const PRICING: Record<string, ModelPricing> = {
|
|
26
33
|
'claude-sonnet-4-6': {
|
|
27
34
|
input: 3.0, output: 15.0, cacheRead: 0.30, cacheWrite: 3.75,
|
|
28
35
|
},
|
|
@@ -39,19 +46,32 @@ export const PRICING = {
|
|
|
39
46
|
// update the table. The `unknown` flag in the result lets callers warn.
|
|
40
47
|
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
41
48
|
|
|
49
|
+
export interface TokenCounts {
|
|
50
|
+
input_tokens?: number | undefined;
|
|
51
|
+
output_tokens?: number | undefined;
|
|
52
|
+
cache_read_input_tokens?: number | undefined;
|
|
53
|
+
cache_creation_input_tokens?: number | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CostParts {
|
|
57
|
+
input: number;
|
|
58
|
+
output: number;
|
|
59
|
+
cacheRead: number;
|
|
60
|
+
cacheWrite: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ReviewCost {
|
|
64
|
+
total: number;
|
|
65
|
+
parts: CostParts;
|
|
66
|
+
model: string;
|
|
67
|
+
unknownModel: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
/**
|
|
43
71
|
* Compute the USD cost of a single clud-bug review from token counts +
|
|
44
72
|
* model. All four token classes are billed independently.
|
|
45
|
-
*
|
|
46
|
-
* Returns:
|
|
47
|
-
* {
|
|
48
|
-
* total: number USD,
|
|
49
|
-
* parts: { input, output, cacheRead, cacheWrite } USD breakdown,
|
|
50
|
-
* model: string (normalized),
|
|
51
|
-
* unknownModel: boolean (true if we used DEFAULT_MODEL pricing),
|
|
52
|
-
* }
|
|
53
73
|
*/
|
|
54
|
-
export function computeReviewCost(tokens, model) {
|
|
74
|
+
export function computeReviewCost(tokens: TokenCounts, model: string | null | undefined): ReviewCost {
|
|
55
75
|
const t = {
|
|
56
76
|
input: tokens.input_tokens || 0,
|
|
57
77
|
output: tokens.output_tokens || 0,
|
|
@@ -59,8 +79,11 @@ export function computeReviewCost(tokens, model) {
|
|
|
59
79
|
cacheWrite: tokens.cache_creation_input_tokens || 0,
|
|
60
80
|
};
|
|
61
81
|
const normalized = model && PRICING[model] ? model : DEFAULT_MODEL;
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
// PRICING[normalized] is guaranteed to exist (normalized is either a
|
|
83
|
+
// known key or DEFAULT_MODEL which is defined above). Non-null assert
|
|
84
|
+
// to satisfy noUncheckedIndexedAccess.
|
|
85
|
+
const p = PRICING[normalized]!;
|
|
86
|
+
const parts: CostParts = {
|
|
64
87
|
input: (t.input / 1e6) * p.input,
|
|
65
88
|
output: (t.output / 1e6) * p.output,
|
|
66
89
|
cacheRead: (t.cacheRead / 1e6) * p.cacheRead,
|
|
@@ -83,7 +106,7 @@ export function computeReviewCost(tokens, model) {
|
|
|
83
106
|
* docs-only / empty PRs); callers can filter zero-LOC reviews out of
|
|
84
107
|
* trend lines as outliers.
|
|
85
108
|
*/
|
|
86
|
-
export function costPerLOC(cost, additions, deletions) {
|
|
109
|
+
export function costPerLOC(cost: number, additions: number | null | undefined, deletions: number | null | undefined): number {
|
|
87
110
|
const loc = (additions || 0) + (deletions || 0);
|
|
88
111
|
if (loc === 0) return 0;
|
|
89
112
|
return cost / loc;
|
|
@@ -96,7 +119,7 @@ export function costPerLOC(cost, additions, deletions) {
|
|
|
96
119
|
* High hit rate proves the v0.6.3 caching layer is firing on
|
|
97
120
|
* re-reviews and fix-pushes.
|
|
98
121
|
*/
|
|
99
|
-
export function cacheHitRate(tokens) {
|
|
122
|
+
export function cacheHitRate(tokens: TokenCounts): number {
|
|
100
123
|
const read = tokens.cache_read_input_tokens || 0;
|
|
101
124
|
const write = tokens.cache_creation_input_tokens || 0;
|
|
102
125
|
const input = tokens.input_tokens || 0;
|
|
@@ -105,6 +128,12 @@ export function cacheHitRate(tokens) {
|
|
|
105
128
|
return read / denom;
|
|
106
129
|
}
|
|
107
130
|
|
|
131
|
+
export interface ExtractedTokens {
|
|
132
|
+
model: string | null;
|
|
133
|
+
tokens: Required<TokenCounts> | null;
|
|
134
|
+
ok: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
/**
|
|
109
138
|
* Parse the model + token counts from a clud-bug-review job log dump.
|
|
110
139
|
*
|
|
@@ -121,15 +150,8 @@ export function cacheHitRate(tokens) {
|
|
|
121
150
|
* the same number Anthropic charges. If no result event exists, the
|
|
122
151
|
* review didn't complete successfully — return ok:false so the caller
|
|
123
152
|
* skips this run rather than trusting partial token data.
|
|
124
|
-
*
|
|
125
|
-
* Returns:
|
|
126
|
-
* {
|
|
127
|
-
* model: string | null,
|
|
128
|
-
* tokens: { input, output, cacheRead, cacheWrite } | null,
|
|
129
|
-
* ok: boolean (false if no result event — partial / errored job),
|
|
130
|
-
* }
|
|
131
153
|
*/
|
|
132
|
-
export function extractTokensFromLog(logText) {
|
|
154
|
+
export function extractTokensFromLog(logText: unknown): ExtractedTokens {
|
|
133
155
|
if (typeof logText !== 'string' || logText.length === 0) {
|
|
134
156
|
return { model: null, tokens: null, ok: false };
|
|
135
157
|
}
|
|
@@ -138,15 +160,18 @@ export function extractTokensFromLog(logText) {
|
|
|
138
160
|
// uses the same model throughout). Captured before the usage parse
|
|
139
161
|
// so model is reported even when we can't find a result event.
|
|
140
162
|
const modelMatches = [...logText.matchAll(/"model"\s*:\s*"([^"]+)"/g)];
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
// matchAll yields RegExpMatchArray entries; capture group 1 is the
|
|
164
|
+
// model string. Under noUncheckedIndexedAccess every index is typed
|
|
165
|
+
// possibly-undefined — coalesce so the return type stays string|null.
|
|
166
|
+
const model: string | null = modelMatches.length > 0
|
|
167
|
+
? (modelMatches[modelMatches.length - 1]?.[1] ?? null)
|
|
143
168
|
: null;
|
|
144
169
|
|
|
145
170
|
// Locate the final result event. There may be multiple over the
|
|
146
171
|
// life of a long-running session — take the LAST one.
|
|
147
172
|
const resultMarkerRe = /"type"\s*:\s*"result"/g;
|
|
148
173
|
let lastResultIdx = -1;
|
|
149
|
-
let m;
|
|
174
|
+
let m: RegExpExecArray | null;
|
|
150
175
|
while ((m = resultMarkerRe.exec(logText)) !== null) {
|
|
151
176
|
lastResultIdx = m.index;
|
|
152
177
|
}
|
|
@@ -176,9 +201,9 @@ export function extractTokensFromLog(logText) {
|
|
|
176
201
|
? fromUsage.slice(0, iterationsIdx)
|
|
177
202
|
: fromUsage;
|
|
178
203
|
|
|
179
|
-
const pluck = (re) => {
|
|
204
|
+
const pluck = (re: RegExp): number => {
|
|
180
205
|
const match = usageOnly.match(re);
|
|
181
|
-
return match ? Number(match[1]) : 0;
|
|
206
|
+
return match && match[1] !== undefined ? Number(match[1]) : 0;
|
|
182
207
|
};
|
|
183
208
|
|
|
184
209
|
const input = pluck(/"input_tokens"\s*:\s*(\d+)/);
|
|
@@ -203,38 +228,88 @@ export function extractTokensFromLog(logText) {
|
|
|
203
228
|
};
|
|
204
229
|
}
|
|
205
230
|
|
|
231
|
+
export interface ReviewRecord {
|
|
232
|
+
repo: string;
|
|
233
|
+
pr: number;
|
|
234
|
+
createdAt: string;
|
|
235
|
+
model: string;
|
|
236
|
+
tokens: TokenCounts;
|
|
237
|
+
additions: number;
|
|
238
|
+
deletions: number;
|
|
239
|
+
cost: number;
|
|
240
|
+
costPerLOC: number;
|
|
241
|
+
cacheRate: number;
|
|
242
|
+
unknownModel?: boolean | undefined;
|
|
243
|
+
modelObserved?: string | undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface RollupGroupStats {
|
|
247
|
+
reviews: number;
|
|
248
|
+
cost: number;
|
|
249
|
+
loc: number;
|
|
250
|
+
costPerLOC: number;
|
|
251
|
+
cacheRate: number;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface RollupTotal {
|
|
255
|
+
reviews: number;
|
|
256
|
+
cost: number;
|
|
257
|
+
loc: number;
|
|
258
|
+
costPerLOC: number;
|
|
259
|
+
cacheRate: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface RollupTrend {
|
|
263
|
+
current: number;
|
|
264
|
+
previous: number | null;
|
|
265
|
+
slopePct: number | null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface RollupOutlier {
|
|
269
|
+
repo: string;
|
|
270
|
+
pr: number;
|
|
271
|
+
costPerLOC: number;
|
|
272
|
+
multiple: number;
|
|
273
|
+
cost: number;
|
|
274
|
+
reason: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface UnknownModelReview {
|
|
278
|
+
repo: string;
|
|
279
|
+
pr: number;
|
|
280
|
+
modelObserved: string | undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface Rollup {
|
|
284
|
+
total: RollupTotal;
|
|
285
|
+
perRepo: Record<string, RollupGroupStats>;
|
|
286
|
+
perModel: Record<string, RollupGroupStats>;
|
|
287
|
+
trend30d: RollupTrend;
|
|
288
|
+
outliers: RollupOutlier[];
|
|
289
|
+
unknownModelReviews: UnknownModelReview[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Internal mutable form used during group-by build-up — the *s arrays
|
|
293
|
+
// disappear before return; we type the in-progress shape separately.
|
|
294
|
+
interface GroupAccumulator {
|
|
295
|
+
reviews: number;
|
|
296
|
+
cost: number;
|
|
297
|
+
loc: number;
|
|
298
|
+
costPerLOCs: number[];
|
|
299
|
+
cacheRates: number[];
|
|
300
|
+
costPerLOC?: number;
|
|
301
|
+
cacheRate?: number;
|
|
302
|
+
}
|
|
303
|
+
|
|
206
304
|
/**
|
|
207
305
|
* Roll up an array of per-review records into a structured summary.
|
|
208
306
|
*
|
|
209
|
-
* Each review record:
|
|
210
|
-
* {
|
|
211
|
-
* repo: "owner/name",
|
|
212
|
-
* pr: number,
|
|
213
|
-
* createdAt: ISO 8601,
|
|
214
|
-
* model: string,
|
|
215
|
-
* tokens: { ... },
|
|
216
|
-
* additions: number,
|
|
217
|
-
* deletions: number,
|
|
218
|
-
* cost: number (USD, total),
|
|
219
|
-
* costPerLOC: number,
|
|
220
|
-
* cacheRate: number (0..1),
|
|
221
|
-
* }
|
|
222
|
-
*
|
|
223
|
-
* Returns:
|
|
224
|
-
* {
|
|
225
|
-
* total: { reviews, cost, loc, costPerLOC (median), cacheRate (median) },
|
|
226
|
-
* perRepo: { [repo]: { ... } },
|
|
227
|
-
* perModel: { [model]: { ... } },
|
|
228
|
-
* trend30d: { dailyMedians: [...], slopePct (MoM) },
|
|
229
|
-
* outliers: [{ review, severity }],
|
|
230
|
-
* }
|
|
231
|
-
*
|
|
232
307
|
* Pre-conditions: callers should drop zero-LOC reviews before passing in.
|
|
233
308
|
*/
|
|
234
|
-
export function rollup(reviews) {
|
|
309
|
+
export function rollup(reviews: ReviewRecord[]): Rollup {
|
|
235
310
|
const valid = reviews.filter((r) => r.costPerLOC > 0);
|
|
236
311
|
|
|
237
|
-
const total = {
|
|
312
|
+
const total: RollupTotal = {
|
|
238
313
|
reviews: valid.length,
|
|
239
314
|
cost: valid.reduce((a, r) => a + r.cost, 0),
|
|
240
315
|
loc: valid.reduce((a, r) => a + (r.additions + r.deletions), 0),
|
|
@@ -242,34 +317,44 @@ export function rollup(reviews) {
|
|
|
242
317
|
cacheRate: median(valid.map((r) => r.cacheRate)),
|
|
243
318
|
};
|
|
244
319
|
|
|
245
|
-
const groupBy = (key) => {
|
|
246
|
-
const out = {};
|
|
320
|
+
const groupBy = (key: 'repo' | 'model'): Record<string, RollupGroupStats> => {
|
|
321
|
+
const out: Record<string, GroupAccumulator> = {};
|
|
247
322
|
for (const r of valid) {
|
|
248
|
-
const k = r[key];
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
323
|
+
const k = String(r[key]);
|
|
324
|
+
let bucket = out[k];
|
|
325
|
+
if (!bucket) {
|
|
326
|
+
bucket = { reviews: 0, cost: 0, loc: 0, costPerLOCs: [], cacheRates: [] };
|
|
327
|
+
out[k] = bucket;
|
|
328
|
+
}
|
|
329
|
+
bucket.reviews += 1;
|
|
330
|
+
bucket.cost += r.cost;
|
|
331
|
+
bucket.loc += r.additions + r.deletions;
|
|
332
|
+
bucket.costPerLOCs.push(r.costPerLOC);
|
|
333
|
+
bucket.cacheRates.push(r.cacheRate);
|
|
255
334
|
}
|
|
335
|
+
const finalized: Record<string, RollupGroupStats> = {};
|
|
256
336
|
for (const k of Object.keys(out)) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
337
|
+
const bucket = out[k]!;
|
|
338
|
+
finalized[k] = {
|
|
339
|
+
reviews: bucket.reviews,
|
|
340
|
+
cost: bucket.cost,
|
|
341
|
+
loc: bucket.loc,
|
|
342
|
+
costPerLOC: median(bucket.costPerLOCs),
|
|
343
|
+
cacheRate: median(bucket.cacheRates),
|
|
344
|
+
};
|
|
261
345
|
}
|
|
262
|
-
return
|
|
346
|
+
return finalized;
|
|
263
347
|
};
|
|
264
348
|
|
|
265
349
|
const perRepo = groupBy('repo');
|
|
266
350
|
const perModel = groupBy('model');
|
|
267
351
|
|
|
268
352
|
// Outliers: > 2× total.costPerLOC.
|
|
269
|
-
const outliers = valid
|
|
353
|
+
const outliers: RollupOutlier[] = valid
|
|
270
354
|
.filter((r) => r.costPerLOC > total.costPerLOC * 2)
|
|
271
355
|
.map((r) => ({
|
|
272
|
-
repo: r.repo,
|
|
356
|
+
repo: r.repo,
|
|
357
|
+
pr: r.pr,
|
|
273
358
|
costPerLOC: r.costPerLOC,
|
|
274
359
|
multiple: r.costPerLOC / total.costPerLOC,
|
|
275
360
|
cost: r.cost,
|
|
@@ -287,23 +372,25 @@ export function rollup(reviews) {
|
|
|
287
372
|
// per-model table — exactly the false-good signal Q7 must NOT produce.
|
|
288
373
|
// Caller renders this as a loud warning so the dashboard reader knows
|
|
289
374
|
// to update the PRICING table.
|
|
290
|
-
const unknownModelReviews = valid
|
|
375
|
+
const unknownModelReviews: UnknownModelReview[] = valid
|
|
291
376
|
.filter((r) => r.unknownModel === true)
|
|
292
377
|
.map((r) => ({ repo: r.repo, pr: r.pr, modelObserved: r.modelObserved }));
|
|
293
378
|
|
|
294
379
|
return { total, perRepo, perModel, trend30d, outliers, unknownModelReviews };
|
|
295
380
|
}
|
|
296
381
|
|
|
297
|
-
function median(nums) {
|
|
382
|
+
function median(nums: number[]): number {
|
|
298
383
|
if (nums.length === 0) return 0;
|
|
299
384
|
const sorted = [...nums].sort((a, b) => a - b);
|
|
300
385
|
const mid = Math.floor(sorted.length / 2);
|
|
386
|
+
// sorted is non-empty here so sorted[mid] is defined; the `!`
|
|
387
|
+
// satisfies noUncheckedIndexedAccess and matches JS semantics.
|
|
301
388
|
return sorted.length % 2 === 0
|
|
302
|
-
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
303
|
-
: sorted[mid]
|
|
389
|
+
? (sorted[mid - 1]! + sorted[mid]!) / 2
|
|
390
|
+
: sorted[mid]!;
|
|
304
391
|
}
|
|
305
392
|
|
|
306
|
-
function computeTrend(reviews) {
|
|
393
|
+
function computeTrend(reviews: ReviewRecord[]): RollupTrend {
|
|
307
394
|
// PR #104 fix: distinguish "no prior window" (previous bucket empty)
|
|
308
395
|
// from "exactly flat trend" (current === previous > 0). The original
|
|
309
396
|
// code returned slopePct=0 for both, which masked the dangerous case
|
|
@@ -331,6 +418,10 @@ function computeTrend(reviews) {
|
|
|
331
418
|
return { current, previous, slopePct };
|
|
332
419
|
}
|
|
333
420
|
|
|
421
|
+
export interface FormatRollupOptions {
|
|
422
|
+
json?: boolean | undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
334
425
|
/**
|
|
335
426
|
* Render the rollup as a human-readable table. Mirrors the sample output
|
|
336
427
|
* from the Phase 0.5 plan.
|
|
@@ -338,11 +429,11 @@ function computeTrend(reviews) {
|
|
|
338
429
|
* Pass `{ json: true }` for the machine-readable form (the same data
|
|
339
430
|
* the rollup() function returns).
|
|
340
431
|
*/
|
|
341
|
-
export function formatRollup(rollup, opts = {}) {
|
|
432
|
+
export function formatRollup(rollup: Rollup, opts: FormatRollupOptions = {}): string {
|
|
342
433
|
if (opts.json) {
|
|
343
434
|
return JSON.stringify(rollup, null, 2);
|
|
344
435
|
}
|
|
345
|
-
const lines = [];
|
|
436
|
+
const lines: string[] = [];
|
|
346
437
|
const t = rollup.total;
|
|
347
438
|
const trend = rollup.trend30d;
|
|
348
439
|
// PR #104 fix: null slopePct = "no prior window" (prior 30d bucket
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Pure audit helpers — no FS, no git, no child_process.
|
|
2
|
+
//
|
|
3
|
+
// Split from lib/audit.js during the v0.7.0 TS migration: durationToGitSince
|
|
4
|
+
// and renderAuditHeader are pure functions safe to consume from any runtime
|
|
5
|
+
// (e.g. clud-bug-app's serverless review path), so they live in src/core/.
|
|
6
|
+
// The git-spawning siblings (gitLines, computeAuditFileSet) live in
|
|
7
|
+
// src/cli/audit.ts.
|
|
8
|
+
|
|
9
|
+
// Convert a duration like "7d", "2w", "1mo", "3mo", "1y" to a git --since arg.
|
|
10
|
+
// Returns null if the input is empty/undefined; throws on malformed input.
|
|
11
|
+
export function durationToGitSince(input: string | null | undefined): string | null {
|
|
12
|
+
if (!input) return null;
|
|
13
|
+
const m = String(input).trim().match(/^(\d+)\s*(d|w|mo|m|y)$/i);
|
|
14
|
+
if (!m) {
|
|
15
|
+
throw new Error(`Unrecognized duration "${input}". Examples: 7d, 2w, 1mo, 1y.`);
|
|
16
|
+
}
|
|
17
|
+
// Capture group 1 (\d+) is always present when m is truthy; same for group 2.
|
|
18
|
+
const n = Number(m[1]);
|
|
19
|
+
const unit = (m[2] as string).toLowerCase();
|
|
20
|
+
const map: Record<string, string> = { d: 'day', w: 'week', mo: 'month', m: 'month', y: 'year' };
|
|
21
|
+
return `${n} ${map[unit]}${n === 1 ? '' : 's'} ago`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AuditHeaderInput {
|
|
25
|
+
date: string;
|
|
26
|
+
scopeLabel: string;
|
|
27
|
+
files: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Render the audit report's initial markdown body. The Action's Claude run
|
|
31
|
+
// will append findings under a "## Findings" section after this header.
|
|
32
|
+
export function renderAuditHeader({ date, scopeLabel, files }: AuditHeaderInput): string {
|
|
33
|
+
const head = `# 🐛 Clud Bug audit — ${date}
|
|
34
|
+
|
|
35
|
+
A scheduled walk through the habitat. Scope: ${scopeLabel}.
|
|
36
|
+
Files surveyed: **${files.length}**.
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary>File manifest (${files.length})</summary>
|
|
40
|
+
|
|
41
|
+
\`\`\`
|
|
42
|
+
${files.join('\n')}
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Findings
|
|
50
|
+
|
|
51
|
+
`;
|
|
52
|
+
return head;
|
|
53
|
+
}
|