clud-bug 0.7.0-rc.11 → 0.7.0-rc.13

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.
Files changed (55) hide show
  1. package/dist/cli/hooks.d.ts +40 -0
  2. package/dist/cli/hooks.d.ts.map +1 -0
  3. package/dist/cli/hooks.js +105 -0
  4. package/dist/cli/hooks.js.map +1 -0
  5. package/dist/cli/main.d.ts.map +1 -1
  6. package/dist/cli/main.js +61 -0
  7. package/dist/cli/main.js.map +1 -1
  8. package/dist/cli/review-prompt.d.ts +28 -0
  9. package/dist/cli/review-prompt.d.ts.map +1 -0
  10. package/dist/cli/review-prompt.js +169 -0
  11. package/dist/cli/review-prompt.js.map +1 -0
  12. package/dist/cli/update.d.ts.map +1 -1
  13. package/dist/cli/update.js +22 -0
  14. package/dist/cli/update.js.map +1 -1
  15. package/dist/core/budget-plan.d.ts +125 -0
  16. package/dist/core/budget-plan.d.ts.map +1 -0
  17. package/dist/core/budget-plan.js +211 -0
  18. package/dist/core/budget-plan.js.map +1 -0
  19. package/dist/core/index.d.ts +5 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +14 -0
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/multi-pass-aggregate.d.ts +131 -0
  24. package/dist/core/multi-pass-aggregate.d.ts.map +1 -0
  25. package/dist/core/multi-pass-aggregate.js +427 -0
  26. package/dist/core/multi-pass-aggregate.js.map +1 -0
  27. package/dist/core/plan-review.d.ts +57 -0
  28. package/dist/core/plan-review.d.ts.map +1 -0
  29. package/dist/core/plan-review.js +77 -0
  30. package/dist/core/plan-review.js.map +1 -0
  31. package/dist/core/review-plan.d.ts +219 -0
  32. package/dist/core/review-plan.d.ts.map +1 -0
  33. package/dist/core/review-plan.js +274 -0
  34. package/dist/core/review-plan.js.map +1 -0
  35. package/dist/core/review-writeback.d.ts +20 -0
  36. package/dist/core/review-writeback.d.ts.map +1 -1
  37. package/dist/core/review-writeback.js +16 -0
  38. package/dist/core/review-writeback.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/hooks.ts +122 -0
  43. package/src/cli/main.ts +56 -0
  44. package/src/cli/review-prompt.ts +206 -0
  45. package/src/cli/update.ts +22 -0
  46. package/src/core/budget-plan.ts +324 -0
  47. package/src/core/index.ts +64 -0
  48. package/src/core/multi-pass-aggregate.ts +545 -0
  49. package/src/core/plan-review.ts +136 -0
  50. package/src/core/review-plan.ts +485 -0
  51. package/src/core/review-writeback.ts +38 -0
  52. package/src/core/version.ts +1 -1
  53. package/templates/workflow-py.yml.tmpl +1 -1
  54. package/templates/workflow-ts.yml.tmpl +1 -1
  55. package/templates/workflow.yml.tmpl +1 -1
@@ -0,0 +1,427 @@
1
+ import { flattenFindings, } from './review-schema-zod.js';
2
+ /**
3
+ * Multi-pass aggregator.
4
+ *
5
+ * Takes N passes' review outputs + the active mode + the role definitions
6
+ * and produces a single unified `MultiPassReview` payload the renderer can
7
+ * format with per-pass attribution inline.
8
+ *
9
+ * The three modes are NOT interchangeable in their output shape — see the
10
+ * per-mode comments below.
11
+ *
12
+ * Design discipline:
13
+ * - Pure function. No I/O, no AI calls. Input = N validated reviews +
14
+ * metadata; output = aggregated findings list.
15
+ * - Order-stable. The renderer needs a deterministic finding order so the
16
+ * comment is stable across re-reviews (matters for thread continuity in
17
+ * D.2.6). We preserve Pass 1's order, then append later-pass findings.
18
+ * - Match semantics live here. Two findings are "the same" if their
19
+ * (file, line, skill) tuple matches. Summaries diverge stylistically
20
+ * between passes; key matching on summary is too brittle.
21
+ * - Resolution rules:
22
+ * cross-check → Pass 2 explicitly classified Pass-1 findings as
23
+ * agreed/disagreed via the cross-check schema; we use
24
+ * its verdict verbatim.
25
+ * consensus → Pass 2 ran independent. Same-tuple findings across
26
+ * passes are marked `agreed`; tuple-unique findings are
27
+ * attributed to their pass with provenance.
28
+ * independent → No merging. We emit a flat list with one entry per
29
+ * (pass, finding) — the renderer formats them as a
30
+ * side-by-side review block.
31
+ *
32
+ * The orchestrator owns the AI call orchestration upstream. This module just
33
+ * deals with shapes — call it once per skill (or once per shared bundle).
34
+ *
35
+ * Ported from clud-bug-app/lib/multi-pass-aggregator.ts. The result types
36
+ * (`MultiPassReview`, `UnifiedFinding`, `PassAttribution`, `PassSource`,
37
+ * `ReviewPassMode`) live in `./review-writeback.ts` and are imported here so
38
+ * core has a single declaration for each. The aggregator-internal contracts
39
+ * (`PassSource` is shared; `Consensus`, `CrossCheckVerdict`,
40
+ * `CrossCheckPassResult`, `AggregateInput`) are defined below.
41
+ */
42
+ // ---------------------------------------------------------------------------
43
+ // Public types
44
+ // ---------------------------------------------------------------------------
45
+ // `Consensus` (the SPEC §6.10.1 marker) is defined in `./review-writeback.ts`
46
+ // alongside `UnifiedFinding` and imported above — core keeps a single
47
+ // declaration so it rides on `MultiPassReview.findings[].consensus`.
48
+ /**
49
+ * Derive the SPEC §6.10.1 consensus marker from a finding's per-pass
50
+ * attribution list. Mapping rules:
51
+ *
52
+ * - any source === 'agreed' → '2-of-2' (consensus reached)
53
+ * - any source === 'disagreed' → 'arbitrated' (cross-check produced
54
+ * active dissent; the finding still made it through, which is the
55
+ * arbitration outcome we model today — see Consensus type doc)
56
+ * - otherwise (just 'first' / 'independent') → '1-of-N' (single-pass)
57
+ *
58
+ * Priority order: agreed > disagreed > default. If a finding has BOTH
59
+ * an `agreed` and a `disagreed` attribution (e.g., 3-pass setup with
60
+ * Pass 2 agreed + Pass 3 disagreed), `agreed` wins — at least one pass
61
+ * cross-validated the finding, so the gate has consensus.
62
+ */
63
+ export function deriveConsensus(attributions) {
64
+ let sawDisagreed = false;
65
+ for (const a of attributions) {
66
+ if (a.source === 'agreed')
67
+ return '2-of-2';
68
+ if (a.source === 'disagreed')
69
+ sawDisagreed = true;
70
+ }
71
+ return sawDisagreed ? 'arbitrated' : '1-of-N';
72
+ }
73
+ /**
74
+ * Merges the per-pass results into a single MultiPassReview.
75
+ */
76
+ export function aggregatePasses(input) {
77
+ switch (input.mode) {
78
+ case 'cross-check':
79
+ return aggregateCrossCheck(input);
80
+ case 'consensus':
81
+ return aggregateConsensus(input);
82
+ case 'independent':
83
+ return aggregateIndependent(input);
84
+ }
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Mode: cross-check
88
+ // ---------------------------------------------------------------------------
89
+ /**
90
+ * Cross-check: Pass 2 saw Pass 1's findings + the diff. Each Pass-1 finding
91
+ * gets a per-pass-2 verdict; Pass 2's independent findings are appended.
92
+ *
93
+ * Output order:
94
+ * 1. Pass-1 findings in original order, each with their pass-2 verdict
95
+ * attribution attached (one PassAttribution for Pass 1, one for Pass 2
96
+ * if applicable, etc.).
97
+ * 2. Pass-2 (and beyond) independent findings, in pass order.
98
+ */
99
+ function aggregateCrossCheck(input) {
100
+ const findings = [];
101
+ const firstRoleAttribution = (_f, source = 'first') => ({
102
+ passNumber: 1,
103
+ roleName: input.firstPassRole.name,
104
+ model: input.firstPassRole.model,
105
+ source,
106
+ });
107
+ // Initialize the unified list with Pass 1's findings. Wire-shape Review
108
+ // splits findings across 3 severity arrays; flatten to internal Finding[].
109
+ for (const f of flattenFindings(input.firstPass)) {
110
+ findings.push({
111
+ ...f,
112
+ attributions: [firstRoleAttribution(f)],
113
+ });
114
+ }
115
+ // Layer in each subsequent pass's verdicts + independent findings.
116
+ for (const pass of input.subsequentPasses) {
117
+ if (!pass.crossCheck)
118
+ continue; // mis-configured input; skip cleanly
119
+ // 1. Verdicts on Pass-1 findings — attach a PassAttribution per verdict.
120
+ for (const v of pass.crossCheck.verdicts) {
121
+ const target = findings[v.pass1Index];
122
+ if (!target)
123
+ continue; // out-of-range index — ignore
124
+ target.attributions.push({
125
+ passNumber: pass.passNumber,
126
+ roleName: pass.role.name,
127
+ model: pass.role.model,
128
+ source: v.verdict,
129
+ // exactOptionalPropertyTypes: omit `note` entirely when the pass
130
+ // supplied no rationale (vs. setting it to `undefined`). Behavior is
131
+ // identical for every consumer (renderer/tests read `note` only when
132
+ // present); the App relied on a looser tsconfig where `note:
133
+ // undefined` was allowed.
134
+ ...(v.rationale !== undefined ? { note: v.rationale } : {}),
135
+ });
136
+ }
137
+ // 2. Independent findings — append with provenance.
138
+ for (const f of pass.crossCheck.independentFindings) {
139
+ findings.push({
140
+ ...f,
141
+ attributions: [
142
+ {
143
+ passNumber: pass.passNumber,
144
+ roleName: pass.role.name,
145
+ model: pass.role.model,
146
+ source: 'independent',
147
+ },
148
+ ],
149
+ });
150
+ }
151
+ }
152
+ return finalize({
153
+ mode: 'cross-check',
154
+ findings,
155
+ firstPassRole: input.firstPassRole,
156
+ subsequentPasses: input.subsequentPasses,
157
+ });
158
+ }
159
+ // ---------------------------------------------------------------------------
160
+ // Mode: consensus
161
+ // ---------------------------------------------------------------------------
162
+ /**
163
+ * Consensus: every pass ran fully independent of the others. We diff the
164
+ * passes pairwise and label same-tuple findings as `agreed` (intersection)
165
+ * and tuple-unique findings as `first` / `independent` per pass.
166
+ *
167
+ * Output order:
168
+ * 1. Pass 1's findings in original order (each enriched with `agreed`
169
+ * attributions if other passes also raised the same tuple).
170
+ * 2. Tuple-unique findings from later passes, in pass-number order.
171
+ */
172
+ function aggregateConsensus(input) {
173
+ const findings = [];
174
+ // Index Pass 1's findings by tuple → list index.
175
+ const tupleToIndex = new Map();
176
+ flattenFindings(input.firstPass).forEach((f, i) => {
177
+ findings.push({
178
+ ...f,
179
+ attributions: [
180
+ {
181
+ passNumber: 1,
182
+ roleName: input.firstPassRole.name,
183
+ model: input.firstPassRole.model,
184
+ source: 'first',
185
+ },
186
+ ],
187
+ });
188
+ tupleToIndex.set(tupleKey(f), i);
189
+ });
190
+ for (const pass of input.subsequentPasses) {
191
+ if (!pass.review)
192
+ continue;
193
+ for (const f of flattenFindings(pass.review)) {
194
+ const key = tupleKey(f);
195
+ const existingIndex = tupleToIndex.get(key);
196
+ if (existingIndex !== undefined) {
197
+ // Same-tuple match → consensus. Append an "agreed" attribution.
198
+ const target = findings[existingIndex];
199
+ if (!target)
200
+ continue;
201
+ target.attributions.push({
202
+ passNumber: pass.passNumber,
203
+ roleName: pass.role.name,
204
+ model: pass.role.model,
205
+ source: 'agreed',
206
+ });
207
+ // Promote severity if this pass flagged it higher (e.g. Pass 1 minor,
208
+ // Pass 2 critical — we keep the more conservative critical).
209
+ if (severityRank(f.severity) > severityRank(target.severity)) {
210
+ target.severity = f.severity;
211
+ }
212
+ }
213
+ else {
214
+ // New tuple — append + index for future passes.
215
+ const newIdx = findings.length;
216
+ findings.push({
217
+ ...f,
218
+ attributions: [
219
+ {
220
+ passNumber: pass.passNumber,
221
+ roleName: pass.role.name,
222
+ model: pass.role.model,
223
+ source: 'independent',
224
+ },
225
+ ],
226
+ });
227
+ tupleToIndex.set(key, newIdx);
228
+ }
229
+ }
230
+ }
231
+ return finalize({
232
+ mode: 'consensus',
233
+ findings,
234
+ firstPassRole: input.firstPassRole,
235
+ subsequentPasses: input.subsequentPasses,
236
+ });
237
+ }
238
+ // ---------------------------------------------------------------------------
239
+ // Mode: independent
240
+ // ---------------------------------------------------------------------------
241
+ /**
242
+ * Independent: no merging. Every finding from every pass is emitted with
243
+ * `source: 'first'` (Pass 1) or `source: 'independent'` (Pass N > 1). The
244
+ * renderer is responsible for formatting as side-by-side blocks.
245
+ *
246
+ * Output order: Pass 1's findings, then Pass 2's, then Pass 3's — preserving
247
+ * each pass's internal order.
248
+ */
249
+ function aggregateIndependent(input) {
250
+ const findings = [];
251
+ for (const f of flattenFindings(input.firstPass)) {
252
+ findings.push({
253
+ ...f,
254
+ attributions: [
255
+ {
256
+ passNumber: 1,
257
+ roleName: input.firstPassRole.name,
258
+ model: input.firstPassRole.model,
259
+ source: 'first',
260
+ },
261
+ ],
262
+ });
263
+ }
264
+ for (const pass of input.subsequentPasses) {
265
+ if (!pass.review)
266
+ continue;
267
+ for (const f of flattenFindings(pass.review)) {
268
+ findings.push({
269
+ ...f,
270
+ attributions: [
271
+ {
272
+ passNumber: pass.passNumber,
273
+ roleName: pass.role.name,
274
+ model: pass.role.model,
275
+ source: 'independent',
276
+ },
277
+ ],
278
+ });
279
+ }
280
+ }
281
+ return finalize({
282
+ mode: 'independent',
283
+ findings,
284
+ firstPassRole: input.firstPassRole,
285
+ subsequentPasses: input.subsequentPasses,
286
+ });
287
+ }
288
+ // ---------------------------------------------------------------------------
289
+ // Internal helpers
290
+ // ---------------------------------------------------------------------------
291
+ /**
292
+ * Tuple-key used to detect "same finding" across passes.
293
+ *
294
+ * We key on (file, line, skill) instead of summary because:
295
+ * - Summaries vary stylistically between passes (Sonnet vs Opus phrasing).
296
+ * - File + line + cited skill is the SPEC §1.8.1 minimum identity unit.
297
+ * - File-level findings (no line) collapse to the same tuple regardless of
298
+ * phrasing, which is the desired consensus semantic.
299
+ */
300
+ function tupleKey(f) {
301
+ return `${f.file}::${f.line ?? '*'}::${f.skill}`;
302
+ }
303
+ function severityRank(s) {
304
+ switch (s) {
305
+ case 'critical':
306
+ return 3;
307
+ case 'minor':
308
+ return 2;
309
+ case 'preexisting':
310
+ return 1;
311
+ }
312
+ }
313
+ function finalize(input) {
314
+ const passCount = 1 + input.subsequentPasses.length;
315
+ const roles = [
316
+ {
317
+ passNumber: 1,
318
+ roleName: input.firstPassRole.name,
319
+ model: input.firstPassRole.model,
320
+ },
321
+ ...input.subsequentPasses.map((p) => ({
322
+ passNumber: p.passNumber,
323
+ roleName: p.role.name,
324
+ model: p.role.model,
325
+ })),
326
+ ];
327
+ // SPEC §6.10.1 consensus marker — single derivation point.
328
+ // Aggregators emit findings without `consensus`; this map adds it
329
+ // from the accumulated per-pass attributions. Renderer + auto-fix
330
+ // gate both consume `finding.consensus` downstream.
331
+ const findingsWithConsensus = input.findings.map((f) => ({
332
+ ...f,
333
+ consensus: deriveConsensus(f.attributions),
334
+ }));
335
+ // Derive summary counts + skills_referenced from the aggregated list.
336
+ const counts = {
337
+ critical: findingsWithConsensus.filter((f) => f.severity === 'critical').length,
338
+ minor: findingsWithConsensus.filter((f) => f.severity === 'minor').length,
339
+ preexisting: findingsWithConsensus.filter((f) => f.severity === 'preexisting')
340
+ .length,
341
+ resolved_from_prior: 0,
342
+ still_open: 0,
343
+ };
344
+ const skillsReferenced = uniqInOrder(findingsWithConsensus.map((f) => f.skill));
345
+ // Status header logic:
346
+ // - empty → "clean"
347
+ // - any critical → "critical findings"
348
+ // - else → "clean" (minor + preexisting alone don't headline as critical)
349
+ const status_header = counts.critical > 0
350
+ ? 'critical findings'
351
+ : 'clean';
352
+ // Verdict resolution — see resolveVerdict for the per-mode rules.
353
+ const verdict = resolveVerdict(input.mode, findingsWithConsensus, passCount);
354
+ return {
355
+ status_header,
356
+ summary_counts: counts,
357
+ skills_referenced: skillsReferenced,
358
+ findings: findingsWithConsensus,
359
+ mode: input.mode,
360
+ passCount,
361
+ roles,
362
+ verdict,
363
+ };
364
+ }
365
+ function uniqInOrder(items) {
366
+ const seen = new Set();
367
+ const out = [];
368
+ for (const it of items) {
369
+ if (seen.has(it))
370
+ continue;
371
+ seen.add(it);
372
+ out.push(it);
373
+ }
374
+ return out;
375
+ }
376
+ // ---------------------------------------------------------------------------
377
+ // Verdict resolution
378
+ // ---------------------------------------------------------------------------
379
+ /**
380
+ * Resolves the APPROVE / REQUEST_CHANGES verdict per the SPEC §1.8.5 table:
381
+ *
382
+ * mode | request_changes when…
383
+ * --------------+------------------------------------------------------
384
+ * strict (=cross-check default) | ANY pass flagged critical
385
+ * consensus | ≥2 passes flagged the same critical tuple
386
+ * independent | findings side-by-side — human decides per finding.
387
+ * | For automation purposes we treat ANY critical as
388
+ * | request_changes (safer default; humans can downgrade).
389
+ *
390
+ * We expose this both as a return field on `MultiPassReview.verdict` and
391
+ * separately (for tests + future D.2 wiring) as `resolveVerdict`.
392
+ *
393
+ * NOTE: The broader D.2 phase wires APPROVE/REQUEST_CHANGES into actual
394
+ * PR check states. D.2.5 only computes the verdict — surfacing it is left
395
+ * to the renderer (which prefixes the header line with the verdict label).
396
+ */
397
+ export function resolveVerdict(mode, findings, passCount) {
398
+ if (findings.length === 0)
399
+ return 'clean';
400
+ const criticals = findings.filter((f) => f.severity === 'critical');
401
+ if (criticals.length === 0)
402
+ return 'review_only';
403
+ switch (mode) {
404
+ case 'cross-check':
405
+ case 'independent':
406
+ // Strict: any critical → request_changes.
407
+ return 'request_changes';
408
+ case 'consensus':
409
+ // ≥2 passes must agree on the same critical tuple.
410
+ // Single-pass consensus is degenerate but valid — any critical fires.
411
+ if (passCount < 2)
412
+ return 'request_changes';
413
+ for (const f of criticals) {
414
+ // Spec: "≥2 passes flagged the same critical tuple."
415
+ // Each attribution represents exactly one pass, so the correct
416
+ // count is f.attributions.length. The earlier filter on
417
+ // `source === 'first' | 'agreed'` silently dropped findings
418
+ // raised by Pass 2 + confirmed by Pass 3 but missed by Pass 1
419
+ // (attributions = [independent, agreed], neither matches → the
420
+ // consensus gate never fired).
421
+ if (f.attributions.length >= 2)
422
+ return 'request_changes';
423
+ }
424
+ return 'review_only';
425
+ }
426
+ }
427
+ //# sourceMappingURL=multi-pass-aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multi-pass-aggregate.js","sourceRoot":"","sources":["../../src/core/multi-pass-aggregate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { type ReviewPlanSkill, type ReviewPassesConfig, type ResolvedReviewPasses, type ReviewRole, type ApplyTo } from './review-plan.js';
2
+ import { type BudgetVerdict } from './budget-plan.js';
3
+ /** Trigger context that selects plan depth (SPEC §11.5). */
4
+ export type ReviewTrigger = 'commit' | 'push' | 'pr';
5
+ /** Why a plan was tiered down from its fully-resolved passes. */
6
+ export type TierDownReason = 'commit' | 'large-diff';
7
+ /**
8
+ * Diff size (bytes) at/above which the planner auto-tiers down to a single
9
+ * fast pass to protect cost + latency. A very large diff rarely benefits from
10
+ * multi-pass depth as much as it costs.
11
+ */
12
+ export declare const LARGE_DIFF_THRESHOLD_BYTES = 50000;
13
+ export interface PlanReviewInput {
14
+ /** Loaded skills — the caller does the consumer-specific I/O to load them. */
15
+ skills: ReviewPlanSkill[];
16
+ /** Parsed `.clud-bug.json` `reviewPasses` block (may be null). */
17
+ config: ReviewPassesConfig | null;
18
+ /** Raw SKILL.md text per slug, for the frontmatter `review_passes` override. */
19
+ rawSkillMd?: Record<string, string>;
20
+ /** Trigger context. Defaults to `pr` (the full plan). */
21
+ trigger?: ReviewTrigger;
22
+ /** Diff size under review, in bytes — enables large-diff auto-tiering. */
23
+ diffSizeBytes?: number;
24
+ /** Optional per-PR USD cap forwarded to the budget gate. */
25
+ perPrCapUsd?: number;
26
+ /** Billing-exempt installs bypass the budget cap. */
27
+ billingExempt?: boolean;
28
+ }
29
+ export interface ReviewPlan {
30
+ /** Effective per-skill passes (after any tier-down). */
31
+ perSkill: ResolvedReviewPasses[];
32
+ /** Role tiers in pass order — the consumer binds each tier → a concrete model. */
33
+ roles: ReviewRole[];
34
+ /** Multi-pass apply scope. */
35
+ applyTo: ApplyTo;
36
+ /** Layer-1 budget verdict over the effective (post-tiering) plan. */
37
+ budget: BudgetVerdict;
38
+ /** The trigger this plan was computed for. */
39
+ trigger: ReviewTrigger;
40
+ /** Present when the resolved passes were tiered down, with the reason. */
41
+ tieredDown?: TierDownReason;
42
+ /** One-line human summary. */
43
+ summary: string;
44
+ }
45
+ /**
46
+ * Plan a review. Tiering (a commit-time review runs on every commit, and a very
47
+ * large diff shouldn't pay for full multi-pass depth):
48
+ * - `trigger: 'commit'` → a single fast pass per skill.
49
+ * - `diffSizeBytes` >= `LARGE_DIFF_THRESHOLD_BYTES` → a single fast pass per skill.
50
+ * - otherwise (push/pr, normal diff) → the fully-resolved plan.
51
+ * `commit` takes precedence over diff size.
52
+ *
53
+ * Tiering to one pass means pass 1 / role[0] (the `beetle` fast tier) runs — so
54
+ * "fast model for commits" falls out of the tier system, never hand-picked.
55
+ */
56
+ export declare function planReview(input: PlanReviewInput): ReviewPlan;
57
+ //# sourceMappingURL=plan-review.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-review.d.ts","sourceRoot":"","sources":["../../src/core/plan-review.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,OAAO,EACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtE,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,CAAC;AAErD,iEAAiE;AACjE,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,YAAY,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,kEAAkE;IAClE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAClC,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,yDAAyD;IACzD,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,kFAAkF;IAClF,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,MAAM,EAAE,aAAa,CAAC;IACtB,8CAA8C;IAC9C,OAAO,EAAE,aAAa,CAAC;IACvB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,UAAU,CAyD7D"}
@@ -0,0 +1,77 @@
1
+ // The single shared review planner (SPEC §11.5). Composes the per-skill pass
2
+ // resolver (`review-plan.ts`) and the Layer-1 budget gate (`budget-plan.ts`),
3
+ // then applies trigger / diff-size tiering. Every consumer — the hosted bot,
4
+ // the npm workflow, and local mode — runs THIS function so they plan
5
+ // identically; only the tier → concrete-model binding differs per consumer.
6
+ //
7
+ // Lives in its own module (not in `review-plan.ts`) because it imports
8
+ // `estimateBudget`, and `budget-plan.ts` already imports from `review-plan.ts`
9
+ // — keeping `planReview` here avoids a review-plan ↔ budget-plan cycle.
10
+ import { resolveReviewPasses, } from './review-plan.js';
11
+ import { estimateBudget } from './budget-plan.js';
12
+ /**
13
+ * Diff size (bytes) at/above which the planner auto-tiers down to a single
14
+ * fast pass to protect cost + latency. A very large diff rarely benefits from
15
+ * multi-pass depth as much as it costs.
16
+ */
17
+ export const LARGE_DIFF_THRESHOLD_BYTES = 50_000;
18
+ /**
19
+ * Plan a review. Tiering (a commit-time review runs on every commit, and a very
20
+ * large diff shouldn't pay for full multi-pass depth):
21
+ * - `trigger: 'commit'` → a single fast pass per skill.
22
+ * - `diffSizeBytes` >= `LARGE_DIFF_THRESHOLD_BYTES` → a single fast pass per skill.
23
+ * - otherwise (push/pr, normal diff) → the fully-resolved plan.
24
+ * `commit` takes precedence over diff size.
25
+ *
26
+ * Tiering to one pass means pass 1 / role[0] (the `beetle` fast tier) runs — so
27
+ * "fast model for commits" falls out of the tier system, never hand-picked.
28
+ */
29
+ export function planReview(input) {
30
+ const trigger = input.trigger ?? 'pr';
31
+ const resolved = resolveReviewPasses({
32
+ skills: input.skills,
33
+ config: input.config,
34
+ ...(input.rawSkillMd !== undefined ? { rawSkillMd: input.rawSkillMd } : {}),
35
+ });
36
+ let tieredDown;
37
+ if (trigger === 'commit') {
38
+ tieredDown = 'commit';
39
+ }
40
+ else if (input.diffSizeBytes !== undefined &&
41
+ input.diffSizeBytes >= LARGE_DIFF_THRESHOLD_BYTES) {
42
+ tieredDown = 'large-diff';
43
+ }
44
+ const perSkill = tieredDown
45
+ ? resolved.perSkill.map((p) => ({ ...p, count: 1 }))
46
+ : resolved.perSkill;
47
+ // Cost only the passes that actually run: a tiered plan runs a single
48
+ // (role[0] / beetle) pass, so don't bill the skipped deeper tiers.
49
+ const roleModels = (tieredDown ? resolved.roles.slice(0, 1) : resolved.roles).map((r) => r.model);
50
+ const budget = estimateBudget({
51
+ resolved: perSkill,
52
+ roleModels,
53
+ ...(input.perPrCapUsd !== undefined ? { perPrCapUsd: input.perPrCapUsd } : {}),
54
+ ...(input.billingExempt !== undefined ? { billingExempt: input.billingExempt } : {}),
55
+ });
56
+ // Report the per-skill pass DEPTH (max), not the summed call count — the
57
+ // recipe branches on this same depth, so "1-pass" here agrees with "single
58
+ // pass" there. (Total call count drives the budget below, not the summary.)
59
+ const passesPerSkill = perSkill.length
60
+ ? Math.max(...perSkill.map((p) => p.count))
61
+ : 0;
62
+ const tierNote = tieredDown ? `, tiered down (${tieredDown})` : '';
63
+ const budgetNote = budget.verdict === 'deny'
64
+ ? `; budget exceeded ($${budget.estimate.estimatedCostUsd.toFixed(2)} > $${budget.estimate.capUsd.toFixed(2)})`
65
+ : `; est $${budget.estimate.estimatedCostUsd.toFixed(2)}`;
66
+ const summary = `${passesPerSkill}-pass review across ${perSkill.length} skill(s) [${trigger}]${tierNote}${budgetNote}`;
67
+ return {
68
+ perSkill,
69
+ roles: resolved.roles,
70
+ applyTo: resolved.applyTo,
71
+ budget,
72
+ trigger,
73
+ ...(tieredDown !== undefined ? { tieredDown } : {}),
74
+ summary,
75
+ };
76
+ }
77
+ //# sourceMappingURL=plan-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-review.js","sourceRoot":"","sources":["../../src/core/plan-review.ts"],"names":[],"mappings":"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"}