clud-bug 0.6.34 → 0.7.0-rc.1
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 +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.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.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/skills.d.ts +80 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +510 -0
- package/dist/core/skills.js.map +1 -0
- package/package.json +27 -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 +70 -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/{lib/review-schema.js → src/core/review-schema.ts} +68 -5
- package/{lib/skills.js → src/core/skills.ts} +172 -343
- package/templates/workflow-py.yml.tmpl +2 -2
- package/templates/workflow-ts.yml.tmpl +2 -2
- package/templates/workflow.yml.tmpl +17 -8
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join, extname } from 'node:path';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Lookup tables for the project-shape detectors. Each map is `as const` so
|
|
5
|
+
// downstream consumers can rely on the value types narrowing to the literal
|
|
6
|
+
// strings rather than `string` — `_internal.EXT_TO_LANG['.ts']` resolves to
|
|
7
|
+
// `'typescript'` for IDE navigation, not just `string`.
|
|
8
|
+
export const EXT_TO_LANG = {
|
|
5
9
|
'.ts': 'typescript', '.tsx': 'typescript',
|
|
6
10
|
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
7
11
|
'.py': 'python',
|
|
@@ -14,12 +18,12 @@ const EXT_TO_LANG = {
|
|
|
14
18
|
'.cs': 'csharp',
|
|
15
19
|
'.c': 'c', '.h': 'c',
|
|
16
20
|
'.cpp': 'cpp', '.cc': 'cpp', '.hpp': 'cpp',
|
|
17
|
-
}
|
|
21
|
+
} as const satisfies Record<string, string>;
|
|
18
22
|
|
|
19
23
|
// Dependency name → search term hint passed to skills.sh.
|
|
20
24
|
// Only well-known frameworks; obscure packages get filtered out so the
|
|
21
25
|
// skills.sh query doesn't get drowned in noise.
|
|
22
|
-
const DEP_TO_TERM = {
|
|
26
|
+
export const DEP_TO_TERM = {
|
|
23
27
|
'next': 'nextjs', 'react': 'react', 'vue': 'vue', 'svelte': 'svelte',
|
|
24
28
|
'@angular/core': 'angular', 'solid-js': 'solid',
|
|
25
29
|
'express': 'express', 'fastify': 'fastify', 'koa': 'koa', 'hono': 'hono',
|
|
@@ -29,16 +33,45 @@ const DEP_TO_TERM = {
|
|
|
29
33
|
'vitest': 'vitest', 'jest': 'jest', 'playwright': 'playwright',
|
|
30
34
|
'@playwright/test': 'playwright',
|
|
31
35
|
'typescript': 'typescript',
|
|
32
|
-
}
|
|
36
|
+
} as const satisfies Record<string, string>;
|
|
33
37
|
|
|
34
|
-
const PY_DEP_TO_TERM = {
|
|
38
|
+
export const PY_DEP_TO_TERM = {
|
|
35
39
|
'django': 'django', 'flask': 'flask', 'fastapi': 'fastapi',
|
|
36
40
|
'click': 'click', 'typer': 'typer',
|
|
37
41
|
'pytest': 'pytest', 'sqlalchemy': 'sqlalchemy',
|
|
38
42
|
'pydantic': 'pydantic', 'numpy': 'numpy', 'pandas': 'pandas',
|
|
39
|
-
}
|
|
43
|
+
} as const satisfies Record<string, string>;
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
// Result of running every detector + post-processing — the data shape callers
|
|
46
|
+
// (bin/clud-bug.js, lib/update.js) read from. `description` is nullable
|
|
47
|
+
// because the README fallback may not produce anything.
|
|
48
|
+
export interface DetectedSignals {
|
|
49
|
+
name: string | null;
|
|
50
|
+
description: string | null;
|
|
51
|
+
languages: string[];
|
|
52
|
+
histogram: Record<string, number>;
|
|
53
|
+
searchTerms: string[];
|
|
54
|
+
primaryLanguage: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Per-detector intermediate type — what each manifest-reader returns before
|
|
58
|
+
// we aggregate. `languages` is the languages each manifest implies (e.g.
|
|
59
|
+
// package.json implies ['javascript']) so we can union them in detect().
|
|
60
|
+
interface DetectorResult {
|
|
61
|
+
name: string | null;
|
|
62
|
+
description: string | null;
|
|
63
|
+
languages: string[];
|
|
64
|
+
terms: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface PackageJson {
|
|
68
|
+
name?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
dependencies?: Record<string, string>;
|
|
71
|
+
devDependencies?: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
42
75
|
try {
|
|
43
76
|
await stat(path);
|
|
44
77
|
return true;
|
|
@@ -47,15 +80,15 @@ async function fileExists(path) {
|
|
|
47
80
|
}
|
|
48
81
|
}
|
|
49
82
|
|
|
50
|
-
async function readJsonSafe(path) {
|
|
83
|
+
async function readJsonSafe<T = unknown>(path: string): Promise<T | null> {
|
|
51
84
|
try {
|
|
52
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
85
|
+
return JSON.parse(await readFile(path, 'utf8')) as T;
|
|
53
86
|
} catch {
|
|
54
87
|
return null;
|
|
55
88
|
}
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
async function readTextSafe(path) {
|
|
91
|
+
async function readTextSafe(path: string): Promise<string | null> {
|
|
59
92
|
try {
|
|
60
93
|
return await readFile(path, 'utf8');
|
|
61
94
|
} catch {
|
|
@@ -63,26 +96,27 @@ async function readTextSafe(path) {
|
|
|
63
96
|
}
|
|
64
97
|
}
|
|
65
98
|
|
|
66
|
-
async function detectFromPackageJson(root) {
|
|
67
|
-
const pkg = await readJsonSafe(join(root, 'package.json'));
|
|
99
|
+
async function detectFromPackageJson(root: string): Promise<DetectorResult | null> {
|
|
100
|
+
const pkg = await readJsonSafe<PackageJson>(join(root, 'package.json'));
|
|
68
101
|
if (!pkg) return null;
|
|
69
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
70
|
-
const terms = new Set();
|
|
102
|
+
const deps: Record<string, string> = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
103
|
+
const terms = new Set<string>();
|
|
71
104
|
for (const dep of Object.keys(deps)) {
|
|
72
|
-
|
|
105
|
+
const term = (DEP_TO_TERM as Record<string, string>)[dep];
|
|
106
|
+
if (term) terms.add(term);
|
|
73
107
|
}
|
|
74
108
|
return {
|
|
75
|
-
name: pkg.name,
|
|
109
|
+
name: pkg.name ?? null,
|
|
76
110
|
description: pkg.description || null,
|
|
77
111
|
languages: ['javascript', ...(deps.typescript || pkg.devDependencies?.typescript ? ['typescript'] : [])],
|
|
78
112
|
terms: [...terms],
|
|
79
113
|
};
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
async function detectFromPyproject(root) {
|
|
116
|
+
async function detectFromPyproject(root: string): Promise<DetectorResult | null> {
|
|
83
117
|
const text = await readTextSafe(join(root, 'pyproject.toml'));
|
|
84
118
|
if (!text) return null;
|
|
85
|
-
const terms = new Set();
|
|
119
|
+
const terms = new Set<string>();
|
|
86
120
|
for (const [dep, term] of Object.entries(PY_DEP_TO_TERM)) {
|
|
87
121
|
// crude but adequate match — full TOML parse would be overkill for the
|
|
88
122
|
// dependency-name lookup we actually need
|
|
@@ -91,58 +125,59 @@ async function detectFromPyproject(root) {
|
|
|
91
125
|
const nameMatch = text.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
92
126
|
const descMatch = text.match(/^\s*description\s*=\s*["']([^"']+)["']/m);
|
|
93
127
|
return {
|
|
94
|
-
name: nameMatch?.[1]
|
|
95
|
-
description: descMatch?.[1]
|
|
128
|
+
name: nameMatch?.[1] ?? null,
|
|
129
|
+
description: descMatch?.[1] ?? null,
|
|
96
130
|
languages: ['python'],
|
|
97
131
|
terms: [...terms],
|
|
98
132
|
};
|
|
99
133
|
}
|
|
100
134
|
|
|
101
|
-
async function detectFromRequirements(root) {
|
|
135
|
+
async function detectFromRequirements(root: string): Promise<DetectorResult | null> {
|
|
102
136
|
const text = await readTextSafe(join(root, 'requirements.txt'));
|
|
103
137
|
if (!text) return null;
|
|
104
|
-
const terms = new Set();
|
|
138
|
+
const terms = new Set<string>();
|
|
105
139
|
for (const line of text.split('\n')) {
|
|
106
|
-
const dep = line.split(/[<>=~ #]/)[0].trim().toLowerCase();
|
|
107
|
-
|
|
140
|
+
const dep = (line.split(/[<>=~ #]/)[0] ?? '').trim().toLowerCase();
|
|
141
|
+
const term = (PY_DEP_TO_TERM as Record<string, string>)[dep];
|
|
142
|
+
if (term) terms.add(term);
|
|
108
143
|
}
|
|
109
144
|
return { name: null, description: null, languages: ['python'], terms: [...terms] };
|
|
110
145
|
}
|
|
111
146
|
|
|
112
|
-
async function detectFromGoMod(root) {
|
|
147
|
+
async function detectFromGoMod(root: string): Promise<DetectorResult | null> {
|
|
113
148
|
const text = await readTextSafe(join(root, 'go.mod'));
|
|
114
149
|
if (!text) return null;
|
|
115
150
|
const moduleMatch = text.match(/^module\s+(\S+)/m);
|
|
116
151
|
return {
|
|
117
|
-
name: moduleMatch?.[1]?.split('/').pop()
|
|
152
|
+
name: moduleMatch?.[1]?.split('/').pop() ?? null,
|
|
118
153
|
description: null,
|
|
119
154
|
languages: ['go'],
|
|
120
155
|
terms: [],
|
|
121
156
|
};
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
async function detectFromCargo(root) {
|
|
159
|
+
async function detectFromCargo(root: string): Promise<DetectorResult | null> {
|
|
125
160
|
const text = await readTextSafe(join(root, 'Cargo.toml'));
|
|
126
161
|
if (!text) return null;
|
|
127
162
|
const nameMatch = text.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
128
163
|
const descMatch = text.match(/^\s*description\s*=\s*["']([^"']+)["']/m);
|
|
129
164
|
return {
|
|
130
|
-
name: nameMatch?.[1]
|
|
131
|
-
description: descMatch?.[1]
|
|
165
|
+
name: nameMatch?.[1] ?? null,
|
|
166
|
+
description: descMatch?.[1] ?? null,
|
|
132
167
|
languages: ['rust'],
|
|
133
168
|
terms: [],
|
|
134
169
|
};
|
|
135
170
|
}
|
|
136
171
|
|
|
137
|
-
async function detectFromGemfile(root) {
|
|
172
|
+
async function detectFromGemfile(root: string): Promise<DetectorResult | null> {
|
|
138
173
|
const text = await readTextSafe(join(root, 'Gemfile'));
|
|
139
174
|
if (!text) return null;
|
|
140
175
|
return { name: null, description: null, languages: ['ruby'], terms: [] };
|
|
141
176
|
}
|
|
142
177
|
|
|
143
|
-
async function fileHistogram(root) {
|
|
144
|
-
const counts = {};
|
|
145
|
-
async function walk(dir, depth) {
|
|
178
|
+
async function fileHistogram(root: string): Promise<Record<string, number>> {
|
|
179
|
+
const counts: Record<string, number> = {};
|
|
180
|
+
async function walk(dir: string, depth: number): Promise<void> {
|
|
146
181
|
if (depth > 3) return;
|
|
147
182
|
let entries;
|
|
148
183
|
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
@@ -154,7 +189,7 @@ async function fileHistogram(root) {
|
|
|
154
189
|
if (entry.isDirectory()) {
|
|
155
190
|
await walk(full, depth + 1);
|
|
156
191
|
} else {
|
|
157
|
-
const lang = EXT_TO_LANG[extname(entry.name)];
|
|
192
|
+
const lang = (EXT_TO_LANG as Record<string, string>)[extname(entry.name)];
|
|
158
193
|
if (lang) counts[lang] = (counts[lang] || 0) + 1;
|
|
159
194
|
}
|
|
160
195
|
}
|
|
@@ -163,7 +198,7 @@ async function fileHistogram(root) {
|
|
|
163
198
|
return counts;
|
|
164
199
|
}
|
|
165
200
|
|
|
166
|
-
function firstParagraph(readme) {
|
|
201
|
+
function firstParagraph(readme: string | null): string | null {
|
|
167
202
|
if (!readme) return null;
|
|
168
203
|
const lines = readme.split('\n').slice(0, 200);
|
|
169
204
|
const paragraphs = lines.join('\n').split(/\n\s*\n/);
|
|
@@ -174,20 +209,22 @@ function firstParagraph(readme) {
|
|
|
174
209
|
return null;
|
|
175
210
|
}
|
|
176
211
|
|
|
177
|
-
export async function detect(root) {
|
|
212
|
+
export async function detect(root: string): Promise<DetectedSignals> {
|
|
178
213
|
const detectors = [
|
|
179
214
|
detectFromPackageJson, detectFromPyproject, detectFromRequirements,
|
|
180
215
|
detectFromGoMod, detectFromCargo, detectFromGemfile,
|
|
181
216
|
];
|
|
182
|
-
const results = (await Promise.all(detectors.map(d => d(root)))).filter(
|
|
217
|
+
const results = (await Promise.all(detectors.map(d => d(root)))).filter(
|
|
218
|
+
(r): r is DetectorResult => r !== null,
|
|
219
|
+
);
|
|
183
220
|
const histogram = await fileHistogram(root);
|
|
184
221
|
const readme = await readTextSafe(join(root, 'README.md'))
|
|
185
222
|
|| await readTextSafe(join(root, 'README'));
|
|
186
223
|
|
|
187
|
-
const languages = new Set();
|
|
188
|
-
const terms = new Set();
|
|
189
|
-
let name = null;
|
|
190
|
-
let description = null;
|
|
224
|
+
const languages = new Set<string>();
|
|
225
|
+
const terms = new Set<string>();
|
|
226
|
+
let name: string | null = null;
|
|
227
|
+
let description: string | null = null;
|
|
191
228
|
for (const r of results) {
|
|
192
229
|
for (const lang of r.languages) languages.add(lang);
|
|
193
230
|
for (const term of r.terms) terms.add(term);
|
|
@@ -206,12 +243,22 @@ export async function detect(root) {
|
|
|
206
243
|
languages: sortedLangs,
|
|
207
244
|
histogram,
|
|
208
245
|
searchTerms: [...new Set([...terms, ...sortedLangs.slice(0, 2)])],
|
|
209
|
-
primaryLanguage: sortedLangs[0]
|
|
246
|
+
primaryLanguage: sortedLangs[0] ?? null,
|
|
210
247
|
};
|
|
211
248
|
}
|
|
212
249
|
|
|
213
|
-
|
|
214
|
-
|
|
250
|
+
// Input shape for buildDescriptionLine — a subset of DetectedSignals. We
|
|
251
|
+
// don't reuse DetectedSignals directly because the callers (templates,
|
|
252
|
+
// LLM-flow tests) often hand-build a subset rather than running detect().
|
|
253
|
+
export interface DescriptionLineSignals {
|
|
254
|
+
name?: string | null;
|
|
255
|
+
description?: string | null;
|
|
256
|
+
primaryLanguage?: string | null;
|
|
257
|
+
searchTerms?: string[];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function buildDescriptionLine(signals: DescriptionLineSignals): string {
|
|
261
|
+
const parts: string[] = [];
|
|
215
262
|
if (signals.name) parts.push(`This project is "${signals.name}".`);
|
|
216
263
|
if (signals.description) {
|
|
217
264
|
// v0.6.25 / issue #89: when signals.description comes from a
|
|
@@ -226,7 +273,7 @@ export function buildDescriptionLine(signals) {
|
|
|
226
273
|
parts.push(/[.!?]$/.test(desc) ? desc : `${desc}.`);
|
|
227
274
|
}
|
|
228
275
|
if (signals.primaryLanguage) {
|
|
229
|
-
const frameworks = [...new Set(signals.searchTerms)].filter(t =>
|
|
276
|
+
const frameworks = [...new Set(signals.searchTerms || [])].filter((t) =>
|
|
230
277
|
!['typescript', 'javascript', 'python', 'go', 'rust', 'ruby'].includes(t));
|
|
231
278
|
const frameworkPart = frameworks.length ? ` using ${frameworks.join(', ')}` : '';
|
|
232
279
|
parts.push(`It's primarily ${signals.primaryLanguage}${frameworkPart}.`);
|
|
@@ -235,5 +282,11 @@ export function buildDescriptionLine(signals) {
|
|
|
235
282
|
return parts.join(' ');
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
//
|
|
239
|
-
export const _internal = {
|
|
285
|
+
// Architect's anti-pattern fix (Phase 2): the JS source used a single
|
|
286
|
+
// `export const _internal = { … }` namespace as a test seam. The TS port
|
|
287
|
+
// promotes the table exports (EXT_TO_LANG, DEP_TO_TERM, PY_DEP_TO_TERM)
|
|
288
|
+
// to direct top-level exports, and exposes the two helper functions
|
|
289
|
+
// fileHistogram + firstParagraph as direct named exports too. Tests now
|
|
290
|
+
// import each symbol by name. No `_internal` re-export — that pattern is
|
|
291
|
+
// gone.
|
|
292
|
+
export { fileHistogram, firstParagraph };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Public API surface for `clud-bug/core` (consumed via the package's
|
|
2
|
+
// `./core` exports map → `dist/core/index.js`).
|
|
3
|
+
//
|
|
4
|
+
// Each line re-exports one core module's public symbols. Modules are
|
|
5
|
+
// added incrementally as the v0.7.0 TypeScript migration converts each
|
|
6
|
+
// lib/* JS file.
|
|
7
|
+
|
|
8
|
+
export { reviewPrompt, type ReviewPromptOptions, type ReviewPromptLanguage } from './prompts.js';
|
|
9
|
+
export {
|
|
10
|
+
REVIEW_SCHEMA,
|
|
11
|
+
serializedReviewSchema,
|
|
12
|
+
type ReviewData,
|
|
13
|
+
type ReviewFinding,
|
|
14
|
+
type ReviewSummaryCounts,
|
|
15
|
+
type ReviewStatusHeader,
|
|
16
|
+
type FindingSeverity,
|
|
17
|
+
type PerSkillScanItem,
|
|
18
|
+
type DedicatedSection,
|
|
19
|
+
} from './review-schema.js';
|
|
20
|
+
export { renderReview, SEVERITY_LABEL } from './render-review.js';
|
|
21
|
+
export {
|
|
22
|
+
detect,
|
|
23
|
+
buildDescriptionLine,
|
|
24
|
+
EXT_TO_LANG,
|
|
25
|
+
DEP_TO_TERM,
|
|
26
|
+
PY_DEP_TO_TERM,
|
|
27
|
+
fileHistogram,
|
|
28
|
+
firstParagraph,
|
|
29
|
+
type DetectedSignals,
|
|
30
|
+
type DescriptionLineSignals,
|
|
31
|
+
} from './detect.js';
|
|
32
|
+
export {
|
|
33
|
+
render,
|
|
34
|
+
renderFile,
|
|
35
|
+
pickTemplate,
|
|
36
|
+
templateLanguage,
|
|
37
|
+
DEFAULTS,
|
|
38
|
+
type RenderDefaults,
|
|
39
|
+
type RenderVars,
|
|
40
|
+
type TemplateLanguage,
|
|
41
|
+
} from './render.js';
|
|
42
|
+
export {
|
|
43
|
+
durationToGitSince,
|
|
44
|
+
renderAuditHeader,
|
|
45
|
+
type AuditHeaderInput,
|
|
46
|
+
} from './audit.js';
|
|
47
|
+
export {
|
|
48
|
+
API_BASE,
|
|
49
|
+
MAX_SKILLS,
|
|
50
|
+
SkillsClient,
|
|
51
|
+
normalizeList,
|
|
52
|
+
rankAndCap,
|
|
53
|
+
readReviewMode,
|
|
54
|
+
readAppliesTo,
|
|
55
|
+
appliesToPr,
|
|
56
|
+
partitionByReviewMode,
|
|
57
|
+
extractPerSkillLine,
|
|
58
|
+
selectReviewHeader,
|
|
59
|
+
extractFirstReviewHeaderLine,
|
|
60
|
+
selectReviewBody,
|
|
61
|
+
extractStatsHeader,
|
|
62
|
+
isCriticalReviewHeader,
|
|
63
|
+
classifyPerSkillOutcome,
|
|
64
|
+
type SkillDescriptor,
|
|
65
|
+
type RankableSkill,
|
|
66
|
+
type AppliesToRule,
|
|
67
|
+
type SkillWithOptionalContent,
|
|
68
|
+
type PrComment,
|
|
69
|
+
type ReviewStatsHeader,
|
|
70
|
+
} from './skills.js';
|
|
@@ -6,7 +6,20 @@
|
|
|
6
6
|
// per-section budgets, comment format updates) by editing one function
|
|
7
7
|
// instead of three templates.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
export type ReviewPromptLanguage = 'generic' | 'ts' | 'py';
|
|
10
|
+
|
|
11
|
+
export interface ReviewPromptOptions {
|
|
12
|
+
projectDescription: string;
|
|
13
|
+
language?: ReviewPromptLanguage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Internal input type: caller may pass a malformed object (e.g. missing
|
|
17
|
+
// projectDescription) and we throw at runtime. Use Partial so the type
|
|
18
|
+
// system tracks that all fields could be missing — the explicit `undefined`
|
|
19
|
+
// check below recovers a meaningful error message.
|
|
20
|
+
type ReviewPromptInput = Partial<ReviewPromptOptions>;
|
|
21
|
+
|
|
22
|
+
const LANGUAGE_HINT_BLOCKS: Record<ReviewPromptLanguage, readonly string[]> = {
|
|
10
23
|
generic: ['- Broken or missing test coverage for new code'],
|
|
11
24
|
ts: [
|
|
12
25
|
'- Broken or missing test coverage for new code',
|
|
@@ -33,7 +46,8 @@ const LANGUAGE_HINT_BLOCKS = {
|
|
|
33
46
|
// - 'generic' (default): just "test coverage"
|
|
34
47
|
// - 'ts': test coverage + 4 TypeScript-specific bullets
|
|
35
48
|
// - 'py': 4 Python-specific bullets (replaces "test coverage")
|
|
36
|
-
export function reviewPrompt(
|
|
49
|
+
export function reviewPrompt(options: ReviewPromptInput = {}): string {
|
|
50
|
+
const { projectDescription, language = 'generic' } = options;
|
|
37
51
|
if (projectDescription === undefined) {
|
|
38
52
|
throw new Error('reviewPrompt: projectDescription is required');
|
|
39
53
|
}
|
|
@@ -15,21 +15,52 @@
|
|
|
15
15
|
// rendered shape is wrong). Centralising the markdown shape here means a
|
|
16
16
|
// future format tweak edits one function rather than the prompt.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import type {
|
|
19
|
+
DedicatedSection,
|
|
20
|
+
FindingSeverity,
|
|
21
|
+
PerSkillScanItem,
|
|
22
|
+
ReviewData,
|
|
23
|
+
ReviewFinding,
|
|
24
|
+
ReviewSummaryCounts,
|
|
25
|
+
} from './review-schema.js';
|
|
26
|
+
|
|
27
|
+
// Emoji constants: use explicit Unicode escape literals (`\u{HHHHH}`) so
|
|
28
|
+
// every step of the TS→JS toolchain — tsc, vitest's transformer, the
|
|
29
|
+
// publisher's tarball — emits the same byte sequence regardless of
|
|
30
|
+
// editor encoding settings. Per SPEC §6 byte-identical contract:
|
|
31
|
+
// \u{1F534} = 🔴 (red circle, critical / "important")
|
|
32
|
+
// \u{1F7E1} = 🟡 (yellow circle, minor / "nit")
|
|
33
|
+
// \u{1F7E3} = 🟣 (purple circle, pre-existing)
|
|
34
|
+
// \u{1F41B} = 🐛 (bug, H2 anchor for `## 🐛 Clud Bug review`)
|
|
35
|
+
const SEVERITY_EMOJI: Record<FindingSeverity, string> = {
|
|
36
|
+
critical: '\u{1F534}',
|
|
37
|
+
minor: '\u{1F7E1}',
|
|
38
|
+
preexisting: '\u{1F7E3}',
|
|
39
|
+
};
|
|
40
|
+
const SEVERITY_LABEL: Record<FindingSeverity, string> = {
|
|
20
41
|
critical: 'important',
|
|
21
42
|
minor: 'nit',
|
|
22
43
|
preexisting: 'pre-existing',
|
|
23
44
|
};
|
|
45
|
+
// SEVERITY_LABEL is retained for callers that import the constant table
|
|
46
|
+
// (the JS version exported it implicitly via module scope; keep the
|
|
47
|
+
// export so future renderer extensions can reuse it).
|
|
48
|
+
export { SEVERITY_LABEL };
|
|
49
|
+
|
|
50
|
+
// Renderer input type — schema-aligned but defensively typed. The renderer
|
|
51
|
+
// is the last line of defense against malformed JSON, so it accepts an
|
|
52
|
+
// "unknown-ish" shape and degrades gracefully rather than throwing on
|
|
53
|
+
// missing fields.
|
|
54
|
+
type RenderReviewInput = Partial<ReviewData> & Record<string, unknown>;
|
|
24
55
|
|
|
25
56
|
// Render the full summary comment markdown. `data` is the parsed JSON
|
|
26
|
-
// matching the schema (see schema.
|
|
27
|
-
// `gh pr comment --body`.
|
|
28
|
-
export function renderReview(data) {
|
|
57
|
+
// matching the schema (see review-schema.ts). Returns a string suitable
|
|
58
|
+
// for `gh pr comment --body`.
|
|
59
|
+
export function renderReview(data: RenderReviewInput | null | undefined): string {
|
|
29
60
|
if (!data || typeof data !== 'object') {
|
|
30
61
|
throw new TypeError('renderReview: data must be an object');
|
|
31
62
|
}
|
|
32
|
-
const out = [];
|
|
63
|
+
const out: string[] = [];
|
|
33
64
|
out.push(renderHeader(data));
|
|
34
65
|
out.push('');
|
|
35
66
|
out.push(renderStatusLine(data.summary_counts));
|
|
@@ -77,9 +108,9 @@ export function renderReview(data) {
|
|
|
77
108
|
return out.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '') + '\n';
|
|
78
109
|
}
|
|
79
110
|
|
|
80
|
-
function renderHeader(data) {
|
|
111
|
+
function renderHeader(data: RenderReviewInput): string {
|
|
81
112
|
const verdict = data.status_header;
|
|
82
|
-
const base = '##
|
|
113
|
+
const base = '## \u{1F41B} Clud Bug review';
|
|
83
114
|
if (verdict === 'critical findings') return `${base} — critical findings`;
|
|
84
115
|
if (verdict === 'clean') return `${base} — clean`;
|
|
85
116
|
// 'bare' (non-strict-mode default) OR an unexpected verdict — render
|
|
@@ -87,7 +118,7 @@ function renderHeader(data) {
|
|
|
87
118
|
return base;
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
function renderStatusLine(counts) {
|
|
121
|
+
function renderStatusLine(counts: ReviewSummaryCounts | undefined): string {
|
|
91
122
|
const c = sanitizeCounts(counts);
|
|
92
123
|
return `**This round:** ${c.critical} critical · ${c.minor} minor · ${c.resolved_from_prior} resolved from prior · ${c.still_open} still open`;
|
|
93
124
|
}
|
|
@@ -95,13 +126,13 @@ function renderStatusLine(counts) {
|
|
|
95
126
|
// Severity-emoji stats header. Counts pre-existing in 🟣 even though
|
|
96
127
|
// it's not in summary_counts (the prompt counts pre-existing separately
|
|
97
128
|
// in preexisting_findings.length).
|
|
98
|
-
function renderStatsHeader(counts) {
|
|
129
|
+
function renderStatsHeader(counts: ReviewSummaryCounts | undefined): string {
|
|
99
130
|
const c = sanitizeCounts(counts);
|
|
100
|
-
return `Found: ${c.critical}
|
|
131
|
+
return `Found: ${c.critical} \u{1F534} / ${c.minor} \u{1F7E1} / ${c.preexisting} \u{1F7E3}`;
|
|
101
132
|
}
|
|
102
133
|
|
|
103
|
-
function renderPerSkillScan(scan) {
|
|
104
|
-
const out = ['### Per-skill scan'];
|
|
134
|
+
function renderPerSkillScan(scan: PerSkillScanItem[] | undefined): string[] {
|
|
135
|
+
const out: string[] = ['### Per-skill scan'];
|
|
105
136
|
if (!Array.isArray(scan) || scan.length === 0) {
|
|
106
137
|
out.push('- (no skills loaded — review proceeded against the baseline.)');
|
|
107
138
|
return out;
|
|
@@ -116,14 +147,14 @@ function renderPerSkillScan(scan) {
|
|
|
116
147
|
return out;
|
|
117
148
|
}
|
|
118
149
|
|
|
119
|
-
function renderDedicatedSection(section) {
|
|
150
|
+
function renderDedicatedSection(section: DedicatedSection | undefined): string[] {
|
|
120
151
|
if (!section || typeof section !== 'object') return [];
|
|
121
152
|
const name = String(section.section_name || '').trim();
|
|
122
153
|
const skill = String(section.skill || '').trim();
|
|
123
154
|
const header = skill && name
|
|
124
155
|
? `### ${name} [${skill}]`
|
|
125
156
|
: `### ${name || skill || 'Dedicated section'}`;
|
|
126
|
-
const out = [header, ''];
|
|
157
|
+
const out: string[] = [header, ''];
|
|
127
158
|
if (Array.isArray(section.findings) && section.findings.length > 0) {
|
|
128
159
|
// Dedicated-section findings use the same emoji-prefix block.
|
|
129
160
|
// Default severity for dedicated sections is "critical" — they're
|
|
@@ -135,9 +166,10 @@ function renderDedicatedSection(section) {
|
|
|
135
166
|
return out;
|
|
136
167
|
}
|
|
137
168
|
|
|
138
|
-
function renderFindings(findings, severity) {
|
|
139
|
-
const emoji = SEVERITY_EMOJI[severity] ||
|
|
140
|
-
const out = [];
|
|
169
|
+
function renderFindings(findings: ReviewFinding[] | undefined, severity: FindingSeverity): string[] {
|
|
170
|
+
const emoji = SEVERITY_EMOJI[severity] || SEVERITY_EMOJI.critical;
|
|
171
|
+
const out: string[] = [];
|
|
172
|
+
if (!Array.isArray(findings)) return out;
|
|
141
173
|
for (const f of findings) {
|
|
142
174
|
if (!f || typeof f !== 'object') continue;
|
|
143
175
|
const skill = String(f.skill || '').trim();
|
|
@@ -163,7 +195,7 @@ function renderFindings(findings, severity) {
|
|
|
163
195
|
return out;
|
|
164
196
|
}
|
|
165
197
|
|
|
166
|
-
function renderSkillsReferenced(skills) {
|
|
198
|
+
function renderSkillsReferenced(skills: string[] | undefined): string {
|
|
167
199
|
if (!Array.isArray(skills) || skills.length === 0) {
|
|
168
200
|
return 'Skills referenced: [none] — no installed skill applied to this diff.';
|
|
169
201
|
}
|
|
@@ -172,12 +204,12 @@ function renderSkillsReferenced(skills) {
|
|
|
172
204
|
|
|
173
205
|
// --- helpers ---
|
|
174
206
|
|
|
175
|
-
function nonEmpty(arr) {
|
|
207
|
+
function nonEmpty<T>(arr: T[] | undefined): arr is T[] {
|
|
176
208
|
return Array.isArray(arr) && arr.length > 0;
|
|
177
209
|
}
|
|
178
210
|
|
|
179
|
-
function sanitizeCounts(counts) {
|
|
180
|
-
const c = counts && typeof counts === 'object' ? counts : {};
|
|
211
|
+
function sanitizeCounts(counts: ReviewSummaryCounts | undefined): ReviewSummaryCounts {
|
|
212
|
+
const c = counts && typeof counts === 'object' ? counts : ({} as Partial<ReviewSummaryCounts>);
|
|
181
213
|
return {
|
|
182
214
|
critical: numOrZero(c.critical),
|
|
183
215
|
minor: numOrZero(c.minor),
|
|
@@ -187,18 +219,18 @@ function sanitizeCounts(counts) {
|
|
|
187
219
|
};
|
|
188
220
|
}
|
|
189
221
|
|
|
190
|
-
function numOrZero(v) {
|
|
222
|
+
function numOrZero(v: unknown): number {
|
|
191
223
|
const n = Number(v);
|
|
192
224
|
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
|
193
225
|
}
|
|
194
226
|
|
|
195
|
-
function locationAnchor(f) {
|
|
227
|
+
function locationAnchor(f: ReviewFinding): string | null {
|
|
196
228
|
const file = String(f.file || '').trim();
|
|
197
229
|
if (!file) return null;
|
|
198
230
|
const line = Number(f.line);
|
|
199
231
|
return Number.isFinite(line) && line > 0 ? `${file}:${line}` : file;
|
|
200
232
|
}
|
|
201
233
|
|
|
202
|
-
function stripTrailingPunctuation(s) {
|
|
234
|
+
function stripTrailingPunctuation(s: string): string {
|
|
203
235
|
return s.replace(/[.!?]+$/, '');
|
|
204
236
|
}
|
|
@@ -12,11 +12,28 @@ const PLACEHOLDER_RE = /\{\{([A-Z_]+)\}\}/g;
|
|
|
12
12
|
// structured output to markdown. Pinning to the version that ran
|
|
13
13
|
// `clud-bug init` guarantees the renderer's output shape matches the
|
|
14
14
|
// prompt's expectations.
|
|
15
|
+
//
|
|
16
|
+
// IMPORTANT (v0.7.0 TS migration): the JS source lived at lib/render.js,
|
|
17
|
+
// so `join(__dirname, '..', 'package.json')` reached the package root
|
|
18
|
+
// from one level up. The TS port compiles to dist/core/render.js, which
|
|
19
|
+
// is TWO levels deep (dist/core/) — so we walk up TWO levels to find
|
|
20
|
+
// package.json. Without this adjustment, module-load would throw at
|
|
21
|
+
// runtime with "ENOENT dist/package.json". Verify on rebuild via:
|
|
22
|
+
// node -e "import('./dist/core/render.js').then(m => console.log(m.DEFAULTS.CLUD_BUG_VERSION))"
|
|
15
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const PKG_VERSION =
|
|
17
|
-
readFileSync(join(__dirname, '..', 'package.json'), 'utf8')
|
|
24
|
+
const PKG_VERSION: string = (
|
|
25
|
+
JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')) as { version: string }
|
|
18
26
|
).version;
|
|
19
27
|
|
|
28
|
+
// Public shape of the DEFAULTS map. Tests assert presence of CCA_VERSION
|
|
29
|
+
// and CLUD_BUG_VERSION; REVIEW_SCHEMA is the serialized schema string the
|
|
30
|
+
// templates inject via the {{REVIEW_SCHEMA}} placeholder.
|
|
31
|
+
export interface RenderDefaults {
|
|
32
|
+
CCA_VERSION: string;
|
|
33
|
+
CLUD_BUG_VERSION: string;
|
|
34
|
+
REVIEW_SCHEMA: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
// Default values for substitution tokens that every template uses.
|
|
21
38
|
// Callers can override per-render by passing the same key in `vars`.
|
|
22
39
|
//
|
|
@@ -26,19 +43,26 @@ const PKG_VERSION = JSON.parse(
|
|
|
26
43
|
// workflows mid-cycle. Bumping the pin requires a clud-bug release, which
|
|
27
44
|
// makes the upgrade visible + lets users opt out by pinning a different
|
|
28
45
|
// version in their own forked workflow.
|
|
29
|
-
export const DEFAULTS = {
|
|
46
|
+
export const DEFAULTS: RenderDefaults = {
|
|
30
47
|
CCA_VERSION: 'v1.0.133',
|
|
31
48
|
CLUD_BUG_VERSION: PKG_VERSION,
|
|
32
49
|
REVIEW_SCHEMA: serializedReviewSchema(),
|
|
33
50
|
};
|
|
34
51
|
|
|
52
|
+
// Caller-supplied substitution vars: any extra placeholders the template
|
|
53
|
+
// uses (REVIEW_PROMPT, PROJECT_DESCRIPTION, etc.). Values may be strings,
|
|
54
|
+
// numbers, or anything String()-coercible. We accept a wider unknown type
|
|
55
|
+
// because the JS callers freely pass numbers, arrays, etc., and the
|
|
56
|
+
// String(value) coercion below handles them uniformly.
|
|
57
|
+
export type RenderVars = Partial<RenderDefaults> & Record<string, unknown>;
|
|
58
|
+
|
|
35
59
|
// Multi-line value substitution preserves YAML/Markdown indentation by
|
|
36
60
|
// applying the placeholder line's leading whitespace to every
|
|
37
61
|
// continuation line. Single-line values pass through unchanged so
|
|
38
62
|
// existing tokens (CCA_VERSION, PROJECT_DESCRIPTION) keep current behavior.
|
|
39
|
-
export function render(template, vars) {
|
|
40
|
-
const merged = { ...DEFAULTS, ...vars };
|
|
41
|
-
return template.replace(PLACEHOLDER_RE, (
|
|
63
|
+
export function render(template: string, vars: RenderVars): string {
|
|
64
|
+
const merged: Record<string, unknown> = { ...DEFAULTS, ...vars };
|
|
65
|
+
return template.replace(PLACEHOLDER_RE, (_match, key: string, offset: number) => {
|
|
42
66
|
if (!(key in merged)) {
|
|
43
67
|
throw new Error(`Missing template variable: ${key}`);
|
|
44
68
|
}
|
|
@@ -48,7 +72,7 @@ export function render(template, vars) {
|
|
|
48
72
|
}
|
|
49
73
|
const lineStart = template.lastIndexOf('\n', offset - 1) + 1;
|
|
50
74
|
const leadingWhitespaceMatch = template.slice(lineStart, offset).match(/^(\s*)/);
|
|
51
|
-
const indent = leadingWhitespaceMatch ? leadingWhitespaceMatch[1] : '';
|
|
75
|
+
const indent = leadingWhitespaceMatch ? (leadingWhitespaceMatch[1] ?? '') : '';
|
|
52
76
|
return value
|
|
53
77
|
.split('\n')
|
|
54
78
|
.map((line, i) => (i === 0 || line === '' ? line : indent + line))
|
|
@@ -56,12 +80,12 @@ export function render(template, vars) {
|
|
|
56
80
|
});
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
export async function renderFile(path, vars) {
|
|
83
|
+
export async function renderFile(path: string, vars: RenderVars): Promise<string> {
|
|
60
84
|
const tmpl = await readFile(path, 'utf8');
|
|
61
85
|
return render(tmpl, vars);
|
|
62
86
|
}
|
|
63
87
|
|
|
64
|
-
export function pickTemplate(languages) {
|
|
88
|
+
export function pickTemplate(languages: string[]): string {
|
|
65
89
|
if (languages.includes('typescript') || languages.includes('javascript')) {
|
|
66
90
|
return 'workflow-ts.yml.tmpl';
|
|
67
91
|
}
|
|
@@ -74,7 +98,9 @@ export function pickTemplate(languages) {
|
|
|
74
98
|
// Map a pickTemplate() filename to the language key that `reviewPrompt`
|
|
75
99
|
// accepts. Keeps the mapping in one place so callers don't repeat the
|
|
76
100
|
// switch when computing the REVIEW_PROMPT token.
|
|
77
|
-
export
|
|
101
|
+
export type TemplateLanguage = 'ts' | 'py' | 'generic';
|
|
102
|
+
|
|
103
|
+
export function templateLanguage(tmplName: string): TemplateLanguage {
|
|
78
104
|
if (tmplName === 'workflow-ts.yml.tmpl') return 'ts';
|
|
79
105
|
if (tmplName === 'workflow-py.yml.tmpl') return 'py';
|
|
80
106
|
return 'generic';
|