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/skill-usage.ts — Component 1+2 of the pragmatic SkDD pivot.
|
|
2
2
|
//
|
|
3
3
|
// Pure functions for deterministic skill-usage tracking. Per the
|
|
4
4
|
// strategic pivot (2026-05-30): replace Zak Elfassi's speculative
|
|
@@ -32,19 +32,49 @@
|
|
|
32
32
|
// No automation acts on this output. It's a READ-ONLY dashboard.
|
|
33
33
|
// Humans read; humans decide; humans act.
|
|
34
34
|
|
|
35
|
+
import { spawn } from 'node:child_process';
|
|
36
|
+
|
|
37
|
+
// Per-skill delta record (one review's contribution).
|
|
38
|
+
export interface SkillDelta {
|
|
39
|
+
loads: number;
|
|
40
|
+
citations: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Per-skill usage record (accumulated across reviews).
|
|
44
|
+
export interface SkillUsageEntry {
|
|
45
|
+
loads: number;
|
|
46
|
+
citations: number;
|
|
47
|
+
last_cited: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Map keyed by skill slug.
|
|
51
|
+
export type SkillDeltaMap = Record<string, SkillDelta>;
|
|
52
|
+
export type SkillUsageMap = Record<string, SkillUsageEntry>;
|
|
53
|
+
|
|
54
|
+
// Shape of one finding entry the JSON delta extracts.
|
|
55
|
+
interface FindingLike {
|
|
56
|
+
skill?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PerSkillScanLike {
|
|
60
|
+
skill?: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface DedicatedSectionLike {
|
|
64
|
+
findings?: FindingLike[] | null | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ReviewJsonShape {
|
|
68
|
+
per_skill_scan?: PerSkillScanLike[] | null | undefined;
|
|
69
|
+
critical_findings?: FindingLike[] | null | undefined;
|
|
70
|
+
minor_findings?: FindingLike[] | null | undefined;
|
|
71
|
+
preexisting_findings?: FindingLike[] | null | undefined;
|
|
72
|
+
dedicated_sections?: DedicatedSectionLike[] | null | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
35
75
|
/**
|
|
36
76
|
* Compute per-skill usage delta from a single review's structured JSON.
|
|
37
77
|
*
|
|
38
|
-
* @param {object} reviewJson - Parsed structured-output JSON from one
|
|
39
|
-
* clud-bug review. Expected shape (subset of review-schema.js):
|
|
40
|
-
* - per_skill_scan: [{ skill, outcome }, ...]
|
|
41
|
-
* - critical_findings: [{ skill, ... }, ...]
|
|
42
|
-
* - minor_findings: [{ skill, ... }, ...]
|
|
43
|
-
* - dedicated_sections: [{ skill, findings: [...] }, ...]
|
|
44
|
-
*
|
|
45
|
-
* @returns {object} - Per-skill delta:
|
|
46
|
-
* { "<slug>": { loads: 1, citations: 0|1 } }
|
|
47
|
-
*
|
|
48
78
|
* Rules:
|
|
49
79
|
* - loads = 1 for every skill in per_skill_scan (the skill was in
|
|
50
80
|
* context for this review).
|
|
@@ -55,13 +85,14 @@
|
|
|
55
85
|
*
|
|
56
86
|
* Returns {} on missing / malformed input (defensive — never throws).
|
|
57
87
|
*/
|
|
58
|
-
export function computeSkillUsageDelta(reviewJson) {
|
|
88
|
+
export function computeSkillUsageDelta(reviewJson: unknown): SkillDeltaMap {
|
|
59
89
|
if (!reviewJson || typeof reviewJson !== 'object') return {};
|
|
90
|
+
const review = reviewJson as ReviewJsonShape;
|
|
60
91
|
|
|
61
|
-
const delta = {};
|
|
92
|
+
const delta: SkillDeltaMap = {};
|
|
62
93
|
|
|
63
94
|
// Loads — one per skill that scanned.
|
|
64
|
-
for (const entry of
|
|
95
|
+
for (const entry of review.per_skill_scan || []) {
|
|
65
96
|
if (!entry || typeof entry.skill !== 'string') continue;
|
|
66
97
|
const slug = entry.skill;
|
|
67
98
|
if (!delta[slug]) delta[slug] = { loads: 0, citations: 0 };
|
|
@@ -69,16 +100,16 @@ export function computeSkillUsageDelta(reviewJson) {
|
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
// Citations — collect unique skill slugs across all finding buckets.
|
|
72
|
-
const cited = new Set();
|
|
73
|
-
const collect = (findings) => {
|
|
103
|
+
const cited = new Set<string>();
|
|
104
|
+
const collect = (findings: FindingLike[] | null | undefined) => {
|
|
74
105
|
for (const f of findings || []) {
|
|
75
106
|
if (f && typeof f.skill === 'string') cited.add(f.skill);
|
|
76
107
|
}
|
|
77
108
|
};
|
|
78
|
-
collect(
|
|
79
|
-
collect(
|
|
80
|
-
collect(
|
|
81
|
-
for (const section of
|
|
109
|
+
collect(review.critical_findings);
|
|
110
|
+
collect(review.minor_findings);
|
|
111
|
+
collect(review.preexisting_findings);
|
|
112
|
+
for (const section of review.dedicated_sections || []) {
|
|
82
113
|
collect(section?.findings);
|
|
83
114
|
}
|
|
84
115
|
|
|
@@ -93,16 +124,6 @@ export function computeSkillUsageDelta(reviewJson) {
|
|
|
93
124
|
/**
|
|
94
125
|
* Merge a per-review delta into a persistent usage block.
|
|
95
126
|
*
|
|
96
|
-
* @param {object} existing - Current usage block (may be empty/missing).
|
|
97
|
-
* Shape: { "<slug>": { loads: int, citations: int, last_cited: string|null } }
|
|
98
|
-
* @param {object} delta - From computeSkillUsageDelta (above).
|
|
99
|
-
* @param {string|null} timestamp - ISO 8601 timestamp of THIS review
|
|
100
|
-
* (e.g., "2026-05-30T16:22:26Z"). Used to update last_cited when the
|
|
101
|
-
* skill is cited in this review. Pass null to skip the timestamp
|
|
102
|
-
* update (rarely useful — tests primarily).
|
|
103
|
-
*
|
|
104
|
-
* @returns {object} - New merged usage block (does NOT mutate inputs).
|
|
105
|
-
*
|
|
106
127
|
* Semantics:
|
|
107
128
|
* - existing.loads + delta.loads → new.loads (accumulates forever)
|
|
108
129
|
* - existing.citations + delta.citations → new.citations
|
|
@@ -110,45 +131,58 @@ export function computeSkillUsageDelta(reviewJson) {
|
|
|
110
131
|
* in THIS review). Stays at the prior value otherwise.
|
|
111
132
|
* - New skills (not in existing) get initialized fresh.
|
|
112
133
|
*/
|
|
113
|
-
export function mergeSkillUsage(
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
export function mergeSkillUsage(
|
|
135
|
+
existing: unknown,
|
|
136
|
+
delta: SkillDeltaMap | null | undefined,
|
|
137
|
+
timestamp: string | null,
|
|
138
|
+
): SkillUsageMap {
|
|
139
|
+
const safeExisting: Record<string, unknown> =
|
|
140
|
+
(existing && typeof existing === 'object') ? (existing as Record<string, unknown>) : {};
|
|
141
|
+
const result: SkillUsageMap = {};
|
|
116
142
|
|
|
117
143
|
// Copy all existing skills first (preserve skills NOT in this delta).
|
|
118
144
|
for (const [slug, entry] of Object.entries(safeExisting)) {
|
|
119
145
|
if (entry && typeof entry === 'object') {
|
|
146
|
+
const e = entry as { loads?: unknown; citations?: unknown; last_cited?: unknown };
|
|
120
147
|
result[slug] = {
|
|
121
|
-
loads: Number(
|
|
122
|
-
citations: Number(
|
|
123
|
-
last_cited:
|
|
148
|
+
loads: Number(e.loads) || 0,
|
|
149
|
+
citations: Number(e.citations) || 0,
|
|
150
|
+
last_cited: typeof e.last_cited === 'string' ? e.last_cited : null,
|
|
124
151
|
};
|
|
125
152
|
}
|
|
126
153
|
}
|
|
127
154
|
|
|
128
155
|
// Merge delta.
|
|
129
156
|
for (const [slug, d] of Object.entries(delta || {})) {
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
let row = result[slug];
|
|
158
|
+
if (!row) {
|
|
159
|
+
row = { loads: 0, citations: 0, last_cited: null };
|
|
160
|
+
result[slug] = row;
|
|
132
161
|
}
|
|
133
|
-
|
|
134
|
-
|
|
162
|
+
row.loads += Number(d.loads) || 0;
|
|
163
|
+
row.citations += Number(d.citations) || 0;
|
|
135
164
|
if ((Number(d.citations) || 0) > 0 && timestamp) {
|
|
136
|
-
|
|
165
|
+
row.last_cited = timestamp;
|
|
137
166
|
}
|
|
138
167
|
}
|
|
139
168
|
|
|
140
169
|
return result;
|
|
141
170
|
}
|
|
142
171
|
|
|
172
|
+
export type SkillHealthStatus = 'archive-candidate' | 'stale' | 'new' | 'healthy';
|
|
173
|
+
|
|
174
|
+
export interface SkillHealthRow {
|
|
175
|
+
slug: string;
|
|
176
|
+
status: SkillHealthStatus;
|
|
177
|
+
loads: number;
|
|
178
|
+
citations: number;
|
|
179
|
+
last_cited: string | null;
|
|
180
|
+
days_since_cited: number | null;
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
/**
|
|
144
184
|
* Apply deterministic skill-health thresholds to a usage block.
|
|
145
185
|
*
|
|
146
|
-
* @param {object} usage - The usage block from mergeSkillUsage.
|
|
147
|
-
* @param {Date} now - The current time (injected for testability).
|
|
148
|
-
*
|
|
149
|
-
* @returns {object[]} - Sorted array of:
|
|
150
|
-
* { slug, status, loads, citations, last_cited, days_since_cited }
|
|
151
|
-
*
|
|
152
186
|
* Status values:
|
|
153
187
|
* - "archive-candidate": citations == 0 AND loads >= 5
|
|
154
188
|
* → loaded enough to judge, never cited → propose for removal
|
|
@@ -162,21 +196,23 @@ export function mergeSkillUsage(existing, delta, timestamp) {
|
|
|
162
196
|
* Sorted by status priority (archive > stale > new > healthy), then
|
|
163
197
|
* by loads desc within each group. Highest-noise skills surface first.
|
|
164
198
|
*/
|
|
165
|
-
export function assessSkillHealth(usage, now) {
|
|
166
|
-
const safeUsage
|
|
199
|
+
export function assessSkillHealth(usage: unknown, now: Date | null | undefined): SkillHealthRow[] {
|
|
200
|
+
const safeUsage: Record<string, unknown> =
|
|
201
|
+
(usage && typeof usage === 'object') ? (usage as Record<string, unknown>) : {};
|
|
167
202
|
const safeNow = (now instanceof Date) ? now : new Date();
|
|
168
203
|
const sixtyDaysAgoMs = safeNow.getTime() - (60 * 24 * 60 * 60 * 1000);
|
|
169
204
|
|
|
170
|
-
const rows = [];
|
|
205
|
+
const rows: SkillHealthRow[] = [];
|
|
171
206
|
for (const [slug, entry] of Object.entries(safeUsage)) {
|
|
172
207
|
if (!entry || typeof entry !== 'object') continue;
|
|
208
|
+
const e = entry as { loads?: unknown; citations?: unknown; last_cited?: unknown };
|
|
173
209
|
|
|
174
|
-
const loads = Number(
|
|
175
|
-
const citations = Number(
|
|
176
|
-
const last_cited =
|
|
210
|
+
const loads = Number(e.loads) || 0;
|
|
211
|
+
const citations = Number(e.citations) || 0;
|
|
212
|
+
const last_cited: string | null = typeof e.last_cited === 'string' ? e.last_cited : null;
|
|
177
213
|
|
|
178
|
-
let status;
|
|
179
|
-
let days_since_cited = null;
|
|
214
|
+
let status: SkillHealthStatus;
|
|
215
|
+
let days_since_cited: number | null = null;
|
|
180
216
|
|
|
181
217
|
if (loads < 5) {
|
|
182
218
|
status = 'new';
|
|
@@ -202,7 +238,12 @@ export function assessSkillHealth(usage, now) {
|
|
|
202
238
|
|
|
203
239
|
// Sort: archive-candidates first, then stale, then new, then healthy.
|
|
204
240
|
// Within each group, by loads descending (loudest first).
|
|
205
|
-
const statusOrder
|
|
241
|
+
const statusOrder: Record<SkillHealthStatus, number> = {
|
|
242
|
+
'archive-candidate': 0,
|
|
243
|
+
'stale': 1,
|
|
244
|
+
'new': 2,
|
|
245
|
+
'healthy': 3,
|
|
246
|
+
};
|
|
206
247
|
rows.sort((a, b) => {
|
|
207
248
|
const da = statusOrder[a.status] ?? 99;
|
|
208
249
|
const db = statusOrder[b.status] ?? 99;
|
|
@@ -216,11 +257,8 @@ export function assessSkillHealth(usage, now) {
|
|
|
216
257
|
|
|
217
258
|
/**
|
|
218
259
|
* Render the health dashboard as a 3-column table for the CLI.
|
|
219
|
-
*
|
|
220
|
-
* @param {object[]} rows - Output of assessSkillHealth.
|
|
221
|
-
* @returns {string} - Multi-line markdown-ish table for stdout.
|
|
222
260
|
*/
|
|
223
|
-
export function formatHealthDashboard(rows) {
|
|
261
|
+
export function formatHealthDashboard(rows: SkillHealthRow[] | null | undefined): string {
|
|
224
262
|
if (!rows || rows.length === 0) {
|
|
225
263
|
return (
|
|
226
264
|
'Skill health: no usage data yet.\n\n' +
|
|
@@ -231,14 +269,14 @@ export function formatHealthDashboard(rows) {
|
|
|
231
269
|
);
|
|
232
270
|
}
|
|
233
271
|
|
|
234
|
-
const STATUS_GLYPH = {
|
|
272
|
+
const STATUS_GLYPH: Record<SkillHealthStatus, string> = {
|
|
235
273
|
'archive-candidate': '🟥 archive?',
|
|
236
274
|
'stale': '🟨 stale',
|
|
237
275
|
'new': '🟦 new',
|
|
238
276
|
'healthy': '🟩 healthy',
|
|
239
277
|
};
|
|
240
278
|
|
|
241
|
-
const lines = [];
|
|
279
|
+
const lines: string[] = [];
|
|
242
280
|
lines.push('Skill health (deterministic — read-only; no automation acts on this)');
|
|
243
281
|
lines.push('');
|
|
244
282
|
lines.push(' STATUS SLUG LOADS CITES LAST CITED');
|
|
@@ -275,6 +313,17 @@ export function formatHealthDashboard(rows) {
|
|
|
275
313
|
// widening, zero commit noise. v0.6.30 reads them back here.
|
|
276
314
|
// ---------------------------------------------------------------------------
|
|
277
315
|
|
|
316
|
+
export interface GhRunResult {
|
|
317
|
+
code: number | null;
|
|
318
|
+
stdout: string;
|
|
319
|
+
stderr: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface GhRunner {
|
|
323
|
+
json: (args: string[]) => Promise<unknown>;
|
|
324
|
+
run: (args: string[]) => Promise<GhRunResult>;
|
|
325
|
+
}
|
|
326
|
+
|
|
278
327
|
/**
|
|
279
328
|
* Default `gh` runner — spawns the local gh CLI. Tests inject a mock.
|
|
280
329
|
*
|
|
@@ -283,12 +332,11 @@ export function formatHealthDashboard(rows) {
|
|
|
283
332
|
* - run(args): returns {code, stdout, stderr}. For commands that
|
|
284
333
|
* download files etc. — no JSON parsing.
|
|
285
334
|
*/
|
|
286
|
-
async function defaultGhJson(args) {
|
|
287
|
-
|
|
288
|
-
return new Promise((resolve) => {
|
|
335
|
+
async function defaultGhJson(args: string[]): Promise<unknown> {
|
|
336
|
+
return new Promise<unknown>((resolve) => {
|
|
289
337
|
const child = spawn('gh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
290
338
|
let stdout = '';
|
|
291
|
-
child.stdout
|
|
339
|
+
child.stdout!.on('data', (d: Buffer | string) => { stdout += d; });
|
|
292
340
|
child.on('error', () => resolve(null));
|
|
293
341
|
child.on('close', (code) => {
|
|
294
342
|
if (code !== 0) return resolve(null);
|
|
@@ -297,38 +345,53 @@ async function defaultGhJson(args) {
|
|
|
297
345
|
});
|
|
298
346
|
}
|
|
299
347
|
|
|
300
|
-
async function defaultGhRun(args) {
|
|
301
|
-
|
|
302
|
-
return new Promise((resolve) => {
|
|
348
|
+
async function defaultGhRun(args: string[]): Promise<GhRunResult> {
|
|
349
|
+
return new Promise<GhRunResult>((resolve) => {
|
|
303
350
|
const child = spawn('gh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
304
351
|
let stdout = '';
|
|
305
352
|
let stderr = '';
|
|
306
|
-
child.stdout
|
|
307
|
-
child.stderr
|
|
353
|
+
child.stdout!.on('data', (d: Buffer | string) => { stdout += d; });
|
|
354
|
+
child.stderr!.on('data', (d: Buffer | string) => { stderr += d; });
|
|
308
355
|
child.on('error', () => resolve({ code: 1, stdout: '', stderr: 'gh not on PATH' }));
|
|
309
356
|
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
310
357
|
});
|
|
311
358
|
}
|
|
312
359
|
|
|
313
|
-
export const DEFAULT_GH_RUNNER = {
|
|
360
|
+
export const DEFAULT_GH_RUNNER: GhRunner = {
|
|
314
361
|
json: defaultGhJson,
|
|
315
362
|
run: defaultGhRun,
|
|
316
363
|
};
|
|
317
364
|
|
|
365
|
+
export interface FetchUsageArtifactsOptions {
|
|
366
|
+
owner: string;
|
|
367
|
+
repo: string;
|
|
368
|
+
since?: Date | null | undefined;
|
|
369
|
+
ghRunner?: GhRunner | undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface UsageArtifactRecord {
|
|
373
|
+
prNumber: number;
|
|
374
|
+
artifactId: number;
|
|
375
|
+
usage: SkillUsageMap;
|
|
376
|
+
fetchedAt: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// One entry returned by `gh api .../actions/artifacts --jq '[...]'`.
|
|
380
|
+
interface ArtifactListItem {
|
|
381
|
+
id: number;
|
|
382
|
+
name: string;
|
|
383
|
+
workflow_run_id: number;
|
|
384
|
+
created_at: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
318
387
|
/**
|
|
319
388
|
* Fetch all per-PR skill-usage artifacts from a repo. Each artifact is
|
|
320
389
|
* downloaded to a temp dir, its `.clud-bug.json` is parsed, and the
|
|
321
390
|
* usage block is returned.
|
|
322
|
-
*
|
|
323
|
-
* @param {object} opts
|
|
324
|
-
* @param {string} opts.owner - GitHub repo owner.
|
|
325
|
-
* @param {string} opts.repo - GitHub repo name.
|
|
326
|
-
* @param {Date|null} opts.since - Filter to artifacts created on/after this date.
|
|
327
|
-
* @param {object} opts.ghRunner - Injected gh CLI runner (see DEFAULT_GH_RUNNER).
|
|
328
|
-
*
|
|
329
|
-
* @returns {Promise<{prNumber: number, artifactId: number, usage: object, fetchedAt: string}[]>}
|
|
330
391
|
*/
|
|
331
|
-
export async function fetchUsageArtifacts(
|
|
392
|
+
export async function fetchUsageArtifacts(
|
|
393
|
+
{ owner, repo, since = null, ghRunner = DEFAULT_GH_RUNNER }: FetchUsageArtifactsOptions,
|
|
394
|
+
): Promise<UsageArtifactRecord[]> {
|
|
332
395
|
if (!owner || !repo) {
|
|
333
396
|
throw new Error('fetchUsageArtifacts: owner + repo are required');
|
|
334
397
|
}
|
|
@@ -356,20 +419,23 @@ export async function fetchUsageArtifacts({ owner, repo, since = null, ghRunner
|
|
|
356
419
|
// `--jq '[...]'` wraps the stream into a single array. If the runner
|
|
357
420
|
// returns null (404, no auth, etc.), bail to empty list.
|
|
358
421
|
if (!Array.isArray(list)) return [];
|
|
422
|
+
const items = list as ArtifactListItem[];
|
|
359
423
|
|
|
360
424
|
const filtered = since
|
|
361
|
-
?
|
|
362
|
-
:
|
|
425
|
+
? items.filter((a) => new Date(a.created_at) >= since)
|
|
426
|
+
: items;
|
|
363
427
|
|
|
364
428
|
const fs = await import('node:fs/promises');
|
|
365
429
|
const path = await import('node:path');
|
|
366
430
|
const os = await import('node:os');
|
|
367
431
|
|
|
368
|
-
const results = [];
|
|
432
|
+
const results: UsageArtifactRecord[] = [];
|
|
369
433
|
for (const art of filtered) {
|
|
370
434
|
const prMatch = art.name.match(/^clud-bug-skill-usage-pr-(\d+)$/);
|
|
371
435
|
if (!prMatch) continue;
|
|
372
|
-
|
|
436
|
+
// prMatch[1] is `string | undefined` under noUncheckedIndexedAccess; the
|
|
437
|
+
// regex guarantees the capture exists when match succeeds.
|
|
438
|
+
const prNumber = Number(prMatch[1]!);
|
|
373
439
|
|
|
374
440
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'clud-bug-art-'));
|
|
375
441
|
try {
|
|
@@ -385,14 +451,19 @@ export async function fetchUsageArtifacts({ owner, repo, since = null, ghRunner
|
|
|
385
451
|
// path key). `gh run download -D <dir>` writes it to the dest as
|
|
386
452
|
// `<dir>/.clud-bug.json` (preserves the source path).
|
|
387
453
|
const jsonPath = path.join(tmpDir, '.clud-bug.json');
|
|
388
|
-
let parsed;
|
|
454
|
+
let parsed: unknown;
|
|
389
455
|
try {
|
|
390
456
|
const raw = await fs.readFile(jsonPath, 'utf-8');
|
|
391
457
|
parsed = JSON.parse(raw);
|
|
392
458
|
} catch {
|
|
393
459
|
continue; // artifact corrupted or layout unexpected
|
|
394
460
|
}
|
|
395
|
-
const
|
|
461
|
+
const parsedObj = (parsed && typeof parsed === 'object')
|
|
462
|
+
? (parsed as { usage?: unknown })
|
|
463
|
+
: {};
|
|
464
|
+
const usage: SkillUsageMap = (parsedObj.usage && typeof parsedObj.usage === 'object')
|
|
465
|
+
? (parsedObj.usage as SkillUsageMap)
|
|
466
|
+
: {};
|
|
396
467
|
results.push({
|
|
397
468
|
prNumber,
|
|
398
469
|
artifactId: art.id,
|
|
@@ -416,17 +487,20 @@ export async function fetchUsageArtifacts({ owner, repo, since = null, ghRunner
|
|
|
416
487
|
* AND keeps the LATEST timestamp it sees as last_cited (we sort
|
|
417
488
|
* ascending so newest wins on the final pass), out-of-order input
|
|
418
489
|
* produces an identical result.
|
|
419
|
-
*
|
|
420
|
-
* @param {{usage: object, fetchedAt: string}[]} artifacts
|
|
421
|
-
* @returns {object} accumulated usage block, shape matches mergeSkillUsage output
|
|
422
490
|
*/
|
|
423
|
-
export function aggregateUsageStream(
|
|
491
|
+
export function aggregateUsageStream(
|
|
492
|
+
artifacts: Array<{ usage: SkillUsageMap | null | undefined; fetchedAt: string }> | null | undefined,
|
|
493
|
+
): SkillUsageMap {
|
|
424
494
|
if (!Array.isArray(artifacts) || artifacts.length === 0) return {};
|
|
425
495
|
const sorted = [...artifacts].sort(
|
|
426
|
-
(a, b) => new Date(a.fetchedAt) - new Date(b.fetchedAt)
|
|
496
|
+
(a, b) => new Date(a.fetchedAt).getTime() - new Date(b.fetchedAt).getTime()
|
|
427
497
|
);
|
|
428
|
-
return sorted.reduce(
|
|
429
|
-
|
|
498
|
+
return sorted.reduce<SkillUsageMap>(
|
|
499
|
+
// mergeSkillUsage expects SkillDeltaMap-shape for the delta arg; usage
|
|
500
|
+
// here is SkillUsageMap (loads/citations counts match — only last_cited
|
|
501
|
+
// is extra, which mergeSkillUsage ignores from the delta side). The
|
|
502
|
+
// type assertion is safe and matches the JS prior behavior exactly.
|
|
503
|
+
(acc, art) => mergeSkillUsage(acc, (art.usage || {}) as unknown as SkillDeltaMap, art.fetchedAt),
|
|
430
504
|
{}
|
|
431
505
|
);
|
|
432
506
|
}
|