clud-bug 0.6.34 → 0.7.0-rc.11
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 +20 -0
- package/bin/clud-bug.js +10 -1353
- package/data/canonical-v1.json +37 -0
- 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/configure-github.d.ts +58 -0
- package/dist/cli/configure-github.d.ts.map +1 -0
- package/dist/cli/configure-github.js +242 -0
- package/dist/cli/configure-github.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 +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +19 -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 +2067 -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 +205 -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/auto-resolve.d.ts +235 -0
- package/dist/core/auto-resolve.d.ts.map +1 -0
- package/dist/core/auto-resolve.js +293 -0
- package/dist/core/auto-resolve.js.map +1 -0
- package/dist/core/configure-github.d.ts +179 -0
- package/dist/core/configure-github.d.ts.map +1 -0
- package/dist/core/configure-github.js +289 -0
- package/dist/core/configure-github.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/diff-findings.d.ts +96 -0
- package/dist/core/diff-findings.d.ts.map +1 -0
- package/dist/core/diff-findings.js +261 -0
- package/dist/core/diff-findings.js.map +1 -0
- package/dist/core/formal-review.d.ts +79 -0
- package/dist/core/formal-review.d.ts.map +1 -0
- package/dist/core/formal-review.js +71 -0
- package/dist/core/formal-review.js.map +1 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +66 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/inline-threads.d.ts +160 -0
- package/dist/core/inline-threads.d.ts.map +1 -0
- package/dist/core/inline-threads.js +369 -0
- package/dist/core/inline-threads.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 +62 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/resolve-verifier.d.ts +46 -0
- package/dist/core/resolve-verifier.d.ts.map +1 -0
- package/dist/core/resolve-verifier.js +187 -0
- package/dist/core/resolve-verifier.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 +193 -0
- package/dist/core/review-writeback.d.ts.map +1 -0
- package/dist/core/review-writeback.js +364 -0
- package/dist/core/review-writeback.js.map +1 -0
- package/dist/core/skills.d.ts +147 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +726 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +4 -0
- package/dist/core/version.js.map +1 -0
- package/package.json +37 -5
- 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/src/cli/configure-github.ts +329 -0
- package/{lib/edit-workflow.js → src/cli/edit-workflow.ts} +32 -11
- package/src/cli/index.ts +107 -0
- package/src/cli/main.ts +2204 -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} +87 -27
- package/{lib/usage.js → src/cli/usage.ts} +167 -76
- package/src/core/audit.ts +53 -0
- package/src/core/auto-resolve.ts +488 -0
- package/src/core/configure-github.ts +497 -0
- package/{lib/detect.js → src/core/detect.ts} +100 -47
- package/src/core/diff-findings.ts +323 -0
- package/src/core/formal-review.ts +168 -0
- package/src/core/index.ts +250 -0
- package/src/core/inline-threads.ts +471 -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} +37 -17
- package/src/core/resolve-verifier.ts +228 -0
- 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 +565 -0
- package/{lib/skills.js → src/core/skills.ts} +456 -342
- package/src/core/version.ts +3 -0
- package/templates/clud-bug-review.md.tmpl +115 -0
- package/templates/workflow-py.yml.tmpl +80 -3
- package/templates/workflow-ts.yml.tmpl +80 -3
- package/templates/workflow.yml.tmpl +175 -8
|
@@ -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
|
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
2
|
import { serializedReviewSchema } from './review-schema.js';
|
|
3
|
+
import { PKG_VERSION } from './version.js';
|
|
6
4
|
|
|
7
5
|
const PLACEHOLDER_RE = /\{\{([A-Z_]+)\}\}/g;
|
|
8
6
|
|
|
9
|
-
// CLUD_BUG_VERSION
|
|
10
|
-
//
|
|
7
|
+
// CLUD_BUG_VERSION is baked at build time by scripts/gen-version.mjs into
|
|
8
|
+
// src/core/version.ts (a git-ignored file, regenerated on every `npm run
|
|
9
|
+
// build` via the "prebuild" hook). The rendered workflow uses
|
|
11
10
|
// `npx --yes clud-bug@<CLUD_BUG_VERSION>` in the post-step that renders
|
|
12
11
|
// structured output to markdown. Pinning to the version that ran
|
|
13
12
|
// `clud-bug init` guarantees the renderer's output shape matches the
|
|
14
13
|
// prompt's expectations.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
//
|
|
15
|
+
// History (v0.7.0-rc.5): previously this module did
|
|
16
|
+
// `readFileSync(package.json)` at module-load. Under Next.js bundling in
|
|
17
|
+
// clud-bug-app, `import.meta.url` resolved to `.next/server/...` not the
|
|
18
|
+
// node_modules layout, so the fs call threw ENOENT (Phase 5.1 hotfix in
|
|
19
|
+
// clud-bug-app PR #50 worked around with `serverExternalPackages`).
|
|
20
|
+
// Codegen eliminates the runtime fs lookup entirely.
|
|
21
|
+
|
|
22
|
+
// Public shape of the DEFAULTS map. Tests assert presence of CCA_VERSION
|
|
23
|
+
// and CLUD_BUG_VERSION; REVIEW_SCHEMA is the serialized schema string the
|
|
24
|
+
// templates inject via the {{REVIEW_SCHEMA}} placeholder.
|
|
25
|
+
export interface RenderDefaults {
|
|
26
|
+
CCA_VERSION: string;
|
|
27
|
+
CLUD_BUG_VERSION: string;
|
|
28
|
+
REVIEW_SCHEMA: string;
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
// Default values for substitution tokens that every template uses.
|
|
21
32
|
// Callers can override per-render by passing the same key in `vars`.
|
|
@@ -26,19 +37,26 @@ const PKG_VERSION = JSON.parse(
|
|
|
26
37
|
// workflows mid-cycle. Bumping the pin requires a clud-bug release, which
|
|
27
38
|
// makes the upgrade visible + lets users opt out by pinning a different
|
|
28
39
|
// version in their own forked workflow.
|
|
29
|
-
export const DEFAULTS = {
|
|
40
|
+
export const DEFAULTS: RenderDefaults = {
|
|
30
41
|
CCA_VERSION: 'v1.0.133',
|
|
31
42
|
CLUD_BUG_VERSION: PKG_VERSION,
|
|
32
43
|
REVIEW_SCHEMA: serializedReviewSchema(),
|
|
33
44
|
};
|
|
34
45
|
|
|
46
|
+
// Caller-supplied substitution vars: any extra placeholders the template
|
|
47
|
+
// uses (REVIEW_PROMPT, PROJECT_DESCRIPTION, etc.). Values may be strings,
|
|
48
|
+
// numbers, or anything String()-coercible. We accept a wider unknown type
|
|
49
|
+
// because the JS callers freely pass numbers, arrays, etc., and the
|
|
50
|
+
// String(value) coercion below handles them uniformly.
|
|
51
|
+
export type RenderVars = Partial<RenderDefaults> & Record<string, unknown>;
|
|
52
|
+
|
|
35
53
|
// Multi-line value substitution preserves YAML/Markdown indentation by
|
|
36
54
|
// applying the placeholder line's leading whitespace to every
|
|
37
55
|
// continuation line. Single-line values pass through unchanged so
|
|
38
56
|
// 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, (
|
|
57
|
+
export function render(template: string, vars: RenderVars): string {
|
|
58
|
+
const merged: Record<string, unknown> = { ...DEFAULTS, ...vars };
|
|
59
|
+
return template.replace(PLACEHOLDER_RE, (_match, key: string, offset: number) => {
|
|
42
60
|
if (!(key in merged)) {
|
|
43
61
|
throw new Error(`Missing template variable: ${key}`);
|
|
44
62
|
}
|
|
@@ -48,7 +66,7 @@ export function render(template, vars) {
|
|
|
48
66
|
}
|
|
49
67
|
const lineStart = template.lastIndexOf('\n', offset - 1) + 1;
|
|
50
68
|
const leadingWhitespaceMatch = template.slice(lineStart, offset).match(/^(\s*)/);
|
|
51
|
-
const indent = leadingWhitespaceMatch ? leadingWhitespaceMatch[1] : '';
|
|
69
|
+
const indent = leadingWhitespaceMatch ? (leadingWhitespaceMatch[1] ?? '') : '';
|
|
52
70
|
return value
|
|
53
71
|
.split('\n')
|
|
54
72
|
.map((line, i) => (i === 0 || line === '' ? line : indent + line))
|
|
@@ -56,12 +74,12 @@ export function render(template, vars) {
|
|
|
56
74
|
});
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
export async function renderFile(path, vars) {
|
|
77
|
+
export async function renderFile(path: string, vars: RenderVars): Promise<string> {
|
|
60
78
|
const tmpl = await readFile(path, 'utf8');
|
|
61
79
|
return render(tmpl, vars);
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
export function pickTemplate(languages) {
|
|
82
|
+
export function pickTemplate(languages: string[]): string {
|
|
65
83
|
if (languages.includes('typescript') || languages.includes('javascript')) {
|
|
66
84
|
return 'workflow-ts.yml.tmpl';
|
|
67
85
|
}
|
|
@@ -74,7 +92,9 @@ export function pickTemplate(languages) {
|
|
|
74
92
|
// Map a pickTemplate() filename to the language key that `reviewPrompt`
|
|
75
93
|
// accepts. Keeps the mapping in one place so callers don't repeat the
|
|
76
94
|
// switch when computing the REVIEW_PROMPT token.
|
|
77
|
-
export
|
|
95
|
+
export type TemplateLanguage = 'ts' | 'py' | 'generic';
|
|
96
|
+
|
|
97
|
+
export function templateLanguage(tmplName: string): TemplateLanguage {
|
|
78
98
|
if (tmplName === 'workflow-ts.yml.tmpl') return 'ts';
|
|
79
99
|
if (tmplName === 'workflow-py.yml.tmpl') return 'py';
|
|
80
100
|
return 'generic';
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Wave 5b — D.2.6 per-thread fix-verifier prompt + response parser.
|
|
2
|
+
//
|
|
3
|
+
// On every fix-push, the CLI verb asks Claude DIRECTLY for each prior
|
|
4
|
+
// thread: "the prior reviewer said X. Here is the BEFORE / AFTER /
|
|
5
|
+
// DIFF. Did the new commit address the original finding?" Verdicts:
|
|
6
|
+
//
|
|
7
|
+
// ADDRESSED — fix unambiguously resolves the original concern
|
|
8
|
+
// NOT_ADDRESSED — original concern still applies
|
|
9
|
+
// UNCERTAIN — can't tell; routes through human review
|
|
10
|
+
//
|
|
11
|
+
// This module is PURE — prompt builder + response parser. The actual
|
|
12
|
+
// Anthropic Messages API call lives in `src/cli/main.ts::runResolveThreads`
|
|
13
|
+
// (raw `fetch()` — no `@anthropic-ai/sdk` dep so the npm package stays
|
|
14
|
+
// dep-light). Tests inject mocked verifier outcomes via the
|
|
15
|
+
// `runAutoResolve` `verifier` callback in `./auto-resolve`.
|
|
16
|
+
//
|
|
17
|
+
// Ported from `clud-bug-app/lib/resolve-verifier.ts:162-260`. Differences:
|
|
18
|
+
// - No `@ai-sdk/anthropic` / `zod` dependency. Response parsing is
|
|
19
|
+
// hand-rolled JSON extraction with manual validation.
|
|
20
|
+
// - DROP `verifySingleFinding` (App-side IO entry point). The CLI
|
|
21
|
+
// verb owns the API call directly.
|
|
22
|
+
// - DROP `aggregateMultiPassVerdicts` — OSS is single-pass.
|
|
23
|
+
|
|
24
|
+
import type { PriorFinding, VerifyOutcome } from './auto-resolve.js';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// System prompt — hard-coded, NOT derived from user content (anti-injection)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const VERIFIER_SYSTEM = [
|
|
31
|
+
'You are a code-review fix verifier. Your only job is to decide whether a',
|
|
32
|
+
'specific code change addressed a specific prior reviewer concern.',
|
|
33
|
+
'',
|
|
34
|
+
'You are NOT writing a new code review. You are NOT looking for new issues.',
|
|
35
|
+
'You are NOT judging style. You are answering one question: did the change',
|
|
36
|
+
'between BEFORE and AFTER resolve the concern raised in the prior finding?',
|
|
37
|
+
'',
|
|
38
|
+
'Your verdict MUST be exactly one of: ADDRESSED, NOT_ADDRESSED, UNCERTAIN.',
|
|
39
|
+
'',
|
|
40
|
+
'Rules:',
|
|
41
|
+
'- ADDRESSED: the AFTER code unambiguously resolves the original concern.',
|
|
42
|
+
' Partial fixes that leave the core problem unsolved are NOT ADDRESSED.',
|
|
43
|
+
'- NOT_ADDRESSED: the AFTER code still has the original problem, OR the',
|
|
44
|
+
' change is unrelated to the concern, OR the change made the problem worse.',
|
|
45
|
+
'- UNCERTAIN: you cannot tell from the BEFORE / AFTER alone. Use this when',
|
|
46
|
+
' the context is too narrow, the change is subtle and could go either way,',
|
|
47
|
+
' or the concern depends on code you cannot see. Never guess — say',
|
|
48
|
+
' UNCERTAIN when you would be guessing.',
|
|
49
|
+
'',
|
|
50
|
+
'Treat the prior finding as a black box: it might be wrong, but you are not',
|
|
51
|
+
'judging the finding. You are judging whether the new code resolves what it',
|
|
52
|
+
'said. If the finding asked for X and the change does X, mark ADDRESSED even',
|
|
53
|
+
'if X is unnecessary.',
|
|
54
|
+
'',
|
|
55
|
+
'Reply with a JSON object on a single line, no markdown fence, no surrounding',
|
|
56
|
+
'prose. Shape:',
|
|
57
|
+
' {"verdict":"ADDRESSED"|"NOT_ADDRESSED"|"UNCERTAIN","rationale":"<one sentence>"}',
|
|
58
|
+
'Rationale MUST be one sentence, max 500 characters.',
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Input shape
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export interface VerifySingleFindingInput {
|
|
66
|
+
finding: PriorFinding;
|
|
67
|
+
/** Code at the finding's anchor BEFORE the fix-push. Empty when deleted. */
|
|
68
|
+
codeBefore: string;
|
|
69
|
+
/** Code at the finding's anchor AFTER the fix-push. Empty when deleted. */
|
|
70
|
+
codeAfter: string;
|
|
71
|
+
/** Optional: unified-diff hunk at the anchor. */
|
|
72
|
+
diffAtAnchor?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Prompt builder — pure (no env, no Date.now, no timestamps)
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Builds the user-facing prompt body.
|
|
81
|
+
*
|
|
82
|
+
* Prompt-injection defense:
|
|
83
|
+
* - System prompt is module-scope, NOT derived from user content.
|
|
84
|
+
* - User content (finding body, code, diff) is wrapped in fence-
|
|
85
|
+
* delimited blocks with clear labels. The model knows the
|
|
86
|
+
* boundaries.
|
|
87
|
+
* - Output is parsed with `parseVerifierResponse` which validates
|
|
88
|
+
* the verdict against a fixed enum + caps rationale at 500 chars.
|
|
89
|
+
* A model coerced to emit "ADDRESSED!" outside the JSON shape
|
|
90
|
+
* gets routed to UNCERTAIN+api-error (fail-closed).
|
|
91
|
+
*/
|
|
92
|
+
export function buildVerifierPrompt(input: VerifySingleFindingInput): string {
|
|
93
|
+
const f = input.finding;
|
|
94
|
+
const anchor = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
|
|
95
|
+
const sev = f.severity === 'critical' ? '🔴 critical' : '🟡 minor';
|
|
96
|
+
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
parts.push(
|
|
99
|
+
`A prior code review (skill: \`${f.skill}\`, severity: ${sev}) flagged \`${anchor}\` with this finding:`,
|
|
100
|
+
);
|
|
101
|
+
parts.push('');
|
|
102
|
+
parts.push('```');
|
|
103
|
+
parts.push('PRIOR FINDING:');
|
|
104
|
+
parts.push(f.body);
|
|
105
|
+
parts.push('```');
|
|
106
|
+
parts.push('');
|
|
107
|
+
parts.push(
|
|
108
|
+
'The PR author has since pushed a new commit. Here is the code BEFORE and AFTER:',
|
|
109
|
+
);
|
|
110
|
+
parts.push('');
|
|
111
|
+
parts.push('```');
|
|
112
|
+
parts.push('BEFORE:');
|
|
113
|
+
parts.push(input.codeBefore || '(empty — file did not exist or was empty)');
|
|
114
|
+
parts.push('```');
|
|
115
|
+
parts.push('');
|
|
116
|
+
parts.push('```');
|
|
117
|
+
parts.push('AFTER:');
|
|
118
|
+
parts.push(input.codeAfter || '(empty — file was deleted or emptied)');
|
|
119
|
+
parts.push('```');
|
|
120
|
+
|
|
121
|
+
if (input.diffAtAnchor) {
|
|
122
|
+
parts.push('');
|
|
123
|
+
parts.push('```');
|
|
124
|
+
parts.push('DIFF AT ANCHOR:');
|
|
125
|
+
parts.push(input.diffAtAnchor);
|
|
126
|
+
parts.push('```');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
parts.push('');
|
|
130
|
+
parts.push('Did this change ADDRESS the original finding?');
|
|
131
|
+
parts.push(
|
|
132
|
+
'Reply with the JSON object only — single line, no markdown fence, no prose.',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return parts.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Response parser — extracts `{verdict, rationale}` from the model text
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parses the model's text response into a `VerifyOutcome`. Fail-closed:
|
|
144
|
+
* any malformed input → UNCERTAIN+api-error with the failure mode in
|
|
145
|
+
* the rationale, so the caller's auto-resolve rules route through
|
|
146
|
+
* human review (never silently ADDRESSED).
|
|
147
|
+
*
|
|
148
|
+
* Tolerates:
|
|
149
|
+
* - Leading/trailing whitespace
|
|
150
|
+
* - The model wrapping the JSON in ```json fences (strips them)
|
|
151
|
+
* - Extra fields (ignored)
|
|
152
|
+
*
|
|
153
|
+
* Rejects:
|
|
154
|
+
* - Empty / non-string input
|
|
155
|
+
* - Non-JSON / malformed JSON
|
|
156
|
+
* - Missing or non-string `verdict`
|
|
157
|
+
* - `verdict` not in the allowed enum
|
|
158
|
+
* - Missing or empty `rationale`
|
|
159
|
+
* - `rationale` longer than 500 chars (caps at 500 in the outcome)
|
|
160
|
+
*/
|
|
161
|
+
export function parseVerifierResponse(text: unknown): VerifyOutcome {
|
|
162
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
163
|
+
return {
|
|
164
|
+
verdict: 'UNCERTAIN',
|
|
165
|
+
source: 'api-error',
|
|
166
|
+
rationale: 'verifier returned empty or non-string response',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Strip optional ```json … ``` or ``` … ``` fence.
|
|
171
|
+
let body = text.trim();
|
|
172
|
+
const fenced = body.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/);
|
|
173
|
+
if (fenced) body = fenced[1]!.trim();
|
|
174
|
+
|
|
175
|
+
let parsed: unknown;
|
|
176
|
+
try {
|
|
177
|
+
parsed = JSON.parse(body);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
verdict: 'UNCERTAIN',
|
|
181
|
+
source: 'api-error',
|
|
182
|
+
rationale: `verifier response was not valid JSON: ${
|
|
183
|
+
err instanceof Error ? err.message : String(err)
|
|
184
|
+
}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
189
|
+
return {
|
|
190
|
+
verdict: 'UNCERTAIN',
|
|
191
|
+
source: 'api-error',
|
|
192
|
+
rationale: 'verifier response was not a JSON object',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const obj = parsed as Record<string, unknown>;
|
|
197
|
+
const verdict = obj.verdict;
|
|
198
|
+
const rationale = obj.rationale;
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
verdict !== 'ADDRESSED' &&
|
|
202
|
+
verdict !== 'NOT_ADDRESSED' &&
|
|
203
|
+
verdict !== 'UNCERTAIN'
|
|
204
|
+
) {
|
|
205
|
+
return {
|
|
206
|
+
verdict: 'UNCERTAIN',
|
|
207
|
+
source: 'api-error',
|
|
208
|
+
rationale: `verifier emitted unknown verdict: ${JSON.stringify(verdict)}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof rationale !== 'string' || !rationale.trim()) {
|
|
213
|
+
return {
|
|
214
|
+
verdict: 'UNCERTAIN',
|
|
215
|
+
source: 'api-error',
|
|
216
|
+
rationale: 'verifier response missing rationale',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Cap rationale at 500 chars per the system prompt's contract.
|
|
221
|
+
const trimmed = rationale.trim().slice(0, 500);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
verdict,
|
|
225
|
+
rationale: trimmed,
|
|
226
|
+
source: 'model',
|
|
227
|
+
};
|
|
228
|
+
}
|