Setting the file. One moment.
Render Report · Vercel Optimize · vercel-labs/agent-skills · Skills Docs
ContentsBack to the top of the page This file
Number 7.116
Position 116 of 155
Type JavaScript
Size 38 KB
Lines 955 lib/ render-report.mjs
JavaScript · 955 lines · 38 KB
as
registeredGates }
from
'./gates/index.mjs'
;
9 import { formatCandidateLabel, formatKind, formatPublicText, formatRoute, formatSignal } from './display-labels.mjs' ;
10 import { splitCustomerSafeObservations } from './observation-safety.mjs' ;
11
12 const PLATFORM_CAP = 3 ;
13 const GATED_TARGET_PREVIEW = 5 ;
14
15 export function renderReport ({ recommendations = [], gated = [], abstentions = [], observations = [], signals = {}, candidates = [], opts = {} } = {}) {
16 const safety = splitCustomerSafeObservations (observations, abstentions, signals);
17 observations = safety.observations;
18 abstentions = [ ... abstentions, ... safety.heldBackObservations];
19 assertValidObservations (observations);
20
21 const projectName = opts.projectName ?? signals.project?.name ?? '<project>' ;
22 const stack = signals.stack ?? signals.codebase?.stack ?? {};
23 const usage = signals.usage ?? null ;
24 const plan = signals.plan ?? { plan: 'unknown' , reason: '(not detected)' };
25
26 // Sub-agents don't always propagate o11ySignal/aliasRoutes — look them up by candidateRef and canonicalize the displayed ref.
27 recommendations = recommendations. map (( r ) => enrichRecFromCandidates (r, candidates));
28 const { needsEvidenceRows , noChangeRows } = splitInvestigationOutcomes (abstentions);
29
30 const lines = [];
31 lines. push ( `# Vercel Optimization Report — ${ projectName }` );
32 lines. push ( '' );
33 lines. push ( renderMetadataLine (stack, plan, usage, signals));
34 const coverageLine = renderCoverageLine (candidates, recommendations, signals, {
35 abstentions,
36 heldBackCount: opts.heldBackCount,
37 noChangeCount: opts.noChangeCount,
38 });
39 if (coverageLine) lines. push (coverageLine);
40 if (opts.generatedAt) {
41 lines. push ( '' );
42 lines. push ( `_Generated ${ opts . generatedAt }_` );
43 }
44 lines. push ( '' );
45
46 lines. push ( ... renderCostHeader (signals));
47 lines. push ( '' );
48 lines. push ( ... renderCostBreakdown (usage, signals));
49 if (usage) {
50 const coverage = computeCostCoverage (usage, registeredGates);
51 lines. push ( ... renderCostCoverageMarkdown (coverage));
52 }
53 lines. push ( '' );
54
55 const platformRecs = recommendations. filter (isPlatformScope). slice ( 0 , PLATFORM_CAP );
56 const codeRecs = recommendations. filter (( r ) => ! isPlatformScope (r));
57 const sorted = sortRecs (codeRecs);
58 const high = sorted. filter (( r ) => r.impactTier === 'high' );
59 const medium = sorted. filter (( r ) => r.impactTier === 'medium' );
60 const low = sorted. filter (( r ) => r.impactTier === 'low' || ! r.impactTier);
61 lines. push ( '## Highest-impact recommendations' );
62 lines. push ( '' );
63 if (sorted. length === 0 ) {
64 lines. push ( '_No recommendations are ready to apply from this run._' );
65 } else {
66 const top = sorted. slice ( 0 , 5 );
67 top. forEach (( rec , i ) => {
68 const candidate = candidateForDisplay (rec);
69 const signal = formatSignal (rec.o11ySignal ?? signalFromRec (rec) ?? '' , candidate);
70 lines. push ( `${ i + 1 }. **${ formatCandidateLabel ( candidate ) }** — ${ signal }` );
71 lines. push ( ` - **What to do**: ${ formatRecommendationText ( rec . what ?? '' ) }` );
72 lines. push ( ` - **Impact**: ${ formatRecommendationText ( impactString ( rec , signals )) }` );
73 if (rec.effort) lines. push ( ` - **Effort**: ${ rec . effort }` );
74 const cites = asArray (rec.citations);
75 if (cites. length > 0 ) lines. push ( ` - **Citations**: ${ cites . join ( ', ' ) }` );
76 });
77 }
78 lines. push ( '' );
79
80 lines. push ( '## Recommendations' );
81 lines. push ( '' );
82 lines. push ( '### High impact' );
83 lines. push ( '' );
84 lines. push ( ... renderRecTable (high, signals));
85 lines. push ( '' );
86 lines. push ( '### Medium impact' );
87 lines. push ( '' );
88 lines. push ( ... renderRecTable (medium, signals));
89 lines. push ( '' );
90 if (low. length > 0 ) {
91 lines. push ( '### Low impact' );
92 lines. push ( '' );
93 lines. push ( ... renderRecTable (low, signals));
94 lines. push ( '' );
95 }
96
97 lines. push ( '## Detailed recommendations' );
98 lines. push ( '' );
99 if (sorted. length === 0 ) {
100 lines. push ( '_No recommendations are ready to apply from this run._' );
101 } else {
102 for ( const [ i , rec ] of sorted. entries ()) {
103 lines. push ( ... renderRecDetail (rec, i + 1 , { signals }));
104 }
105 }
106 lines. push ( '' );
107
108 lines. push ( '## Platform recommendations' );
109 lines. push ( '' );
110 if (platformRecs. length === 0 ) {
111 lines. push ( '_(none — the gate did not surface any platform-scope recommendations)_' );
112 } else {
113 for ( const [ i , rec ] of platformRecs. entries ()) {
114 lines. push ( ... renderRecDetail (rec, i + 1 , { compact: true , signals }));
115 }
116 }
117 lines. push ( '' );
118
119 // Observations carry actionable signal discovered during investigation.
120 if (observations. length > 0 ) {
121 lines. push ( '## Observations from investigation' );
122 lines. push ( '' );
123 lines. push ( 'These are real signals from the audit, but they are not ready-to-apply recommendations.' );
124 lines. push ( '' );
125 lines. push ( '| Candidate | Observation | Evidence | Suggested action | Kind |' );
126 lines. push ( '|---|---|---|---|---|' );
127 for ( const o of observations) {
128 const ref = o.candidateRef ?? '(unspecified)' ;
129 lines. push ( `| ${ escape ( displayCandidateRef ( ref )) } | ${ escape ( formatEvidenceText ( o . summary )) } | ${ escape ( formatEvidenceText ( o . evidence ?? '_(none recorded)_' )) } | ${ escape ( formatEvidenceText ( o . suggestedAction ?? '_(none recorded)_' )) } | ${ escape ( formatKind ( o . kind ?? 'other' )) } |` );
130 }
131 lines. push ( '' );
132 }
133
134 // Trust mechanism: customer sees what was investigated and why no rec emerged.
135 if (needsEvidenceRows. length > 0 ) {
136 lines. push ( '## Needs more evidence' );
137 lines. push ( '' );
138 lines. push ( 'These candidates were investigated, but automated checks kept the change out of the ready-to-apply list.' );
139 lines. push ( '' );
140 lines. push ( '| Candidate | Why it was held back |' );
141 lines. push ( '|---|---|' );
142 for ( const a of needsEvidenceRows) {
143 const ref = a.candidateRef ?? '(unspecified)' ;
144 const reason = publicNoRecommendationReason (a.reason ?? '(no reason recorded)' );
145 lines. push ( `| ${ escape ( displayCandidateRef ( ref )) } | ${ escape ( reason ) } |` );
146 }
147 lines. push ( '' );
148 }
149
150 if (noChangeRows. length > 0 ) {
151 lines. push ( '## Investigated, no change recommended' );
152 lines. push ( '' );
153 lines. push ( 'These candidates were checked and did not produce a supported change.' );
154 lines. push ( '' );
155 lines. push ( '| Candidate | Why no recommendation shipped |' );
156 lines. push ( '|---|---|' );
157 for ( const a of noChangeRows) {
158 const ref = a.candidateRef ?? '(unspecified)' ;
159 const reason = publicNoRecommendationReason (a.reason ?? '(no reason recorded)' );
160 lines. push ( `| ${ escape ( displayCandidateRef ( ref )) } | ${ escape ( reason ) } |` );
161 }
162 lines. push ( '' );
163 }
164
165 lines. push ( '## Not investigated in this run' );
166 lines. push ( '' );
167 lines. push ( ... renderGatedTable (gated));
168 lines. push ( '' );
169
170 lines. push ( '## Strengths' );
171 lines. push ( '' );
172 lines. push ( ... renderStrengths (signals));
173 lines. push ( '' );
174
175 const configNotes = renderConfigurationNotes (signals);
176 if (configNotes. length > 0 ) {
177 lines. push ( '## Configuration notes' );
178 lines. push ( '' );
179 lines. push ( ... configNotes);
180 lines. push ( '' );
181 }
182
183 lines. push ( '## Data gaps' );
184 lines. push ( '' );
185 lines. push ( ... renderDataGaps (signals));
186
187 return lines. join ( ' \n ' );
188 }
189
190 function assertValidObservations ( observations ) {
191 if ( ! Array. isArray (observations)) {
192 throw new TypeError ( 'renderReport observations must be an array' );
193 }
194 for ( const [ i , o ] of observations. entries ()) {
195 if ( ! o || typeof o !== 'object' ) {
196 throw new TypeError ( `renderReport observations[${ i }] must be an object` );
197 }
198 if ( typeof o.summary !== 'string' || o.summary. trim () === '' ) {
199 throw new TypeError ( `renderReport observations[${ i }].summary is required` );
200 }
201 }
202 }
203
204 export function buildFinalReportMessage ({ reportPath , markdown , recommendations = [], signals = {}, maxRecommendations = 10 } = {}) {
205 const destination = reportPath || 'report.md' ;
206 const coverageLine = extractCoverageLine (markdown);
207 const lines = [ `Report saved: ${ destination }` ];
208 if (coverageLine) {
209 lines. push ( '' );
210 lines. push ( stripDetailsLink (coverageLine));
211 } else {
212 lines. push ( '' );
213 lines. push ( 'Open the report for details. No coverage summary was available.' );
214 }
215 const readyPreview = renderFinalRecommendationPreview (recommendations, signals, maxRecommendations);
216 if (readyPreview. length > 0 ) {
217 lines. push ( '' );
218 lines. push ( ... readyPreview);
219 }
220 const body = lines. join ( ' \n ' );
221 return {
222 body,
223 lineCount: lines. length ,
224 sha256: createHash ( 'sha256' ). update (body). digest ( 'hex' ),
225 reportPath: destination,
226 coverageLine: coverageLine ?? null ,
227 recommendationsShown: readyPreview. filter (( line ) => / ^ \d + \. / . test (line)). length ,
228 };
229 }
230
231 function renderFinalRecommendationPreview ( recommendations , signals , maxRecommendations ) {
232 const ready = Array. isArray (recommendations)
233 ? sortRecs (recommendations. filter (( r ) => r && r.abstain !== true ))
234 : [];
235 if (ready. length === 0 ) return [];
236 const max = Math. max ( 1 , Math. min (Number. isInteger (maxRecommendations) ? maxRecommendations : 5 , 10 ));
237 const shown = ready. slice ( 0 , max);
238 const lines = [ 'Ready recommendations:' ];
239 for ( const [ i , rec ] of shown. entries ()) {
240 lines. push ( `${ i + 1 }. ${ compactFinalText ( rec . what ?? displayCandidate ( rec )) }` );
241 const impact = impactString (rec, signals);
242 if (impact && ! / ^ _ \( no impact framing recorded \) _ $ / . test (impact)) {
243 lines. push ( ` Impact: ${ compactFinalText ( impact ) }` );
244 }
245 }
246 const hidden = ready. length - shown. length ;
247 if (hidden > 0 ) {
248 lines. push ( `Open the report for ${ hidden } more ready recommendation${ hidden === 1 ? '' : 's'} and the full evidence.` );
249 }
250 return lines;
251 }
252
253 function compactFinalText ( value ) {
254 const text = formatRecommendationText ( String (value ?? '' ))
255 . replace ( / \s + / g , ' ' )
256 . trim ();
257 if (text. length <= 220 ) return text;
258 return `${ text . slice ( 0 , 217 ). trimEnd () }...` ;
259 }
260
261 function extractCoverageLine ( markdown ) {
262 if ( typeof markdown !== 'string' ) return null ;
263 return markdown
264 . split ( ' \n ' )
265 . find (( line ) => line. startsWith ( '**Coverage**:' )) ?? null ;
266 }
267
268 function stripDetailsLink ( line ) {
269 return String (line). replace ( / \s * · \s * \[ details \]\( #not-investigated-in-this-run \) \s *$ / , '' );
270 }
271
272 // Hidden when no candidates exist (e.g., observability blocker — nothing to cover).
273 function renderCoverageLine ( candidates , recommendations , signals , opts = {}) {
274 if ( ! Array. isArray (candidates) || candidates. length === 0 ) return null ;
275 const launched = candidates. filter (( c ) => ! c.gatedReason && ! c.disqualified && c.scope !== 'account' );
276 const skippedByBudget = candidates. filter (
277 ( c ) => typeof c.gatedReason === 'string' && c.gatedReason. startsWith ( 'skippedByBudget' )
278 );
279 const coveredByDedup = candidates. filter (
280 ( c ) => typeof c.gatedReason === 'string' && c.gatedReason. startsWith ( 'coveredBy' )
281 );
282 const disqualified = candidates. filter (
283 ( c ) => typeof c.gatedReason === 'string' && c.gatedReason === c.disqualifyReason
284 );
285 const total = launched. length + skippedByBudget. length ;
286 if (total === 0 ) return null ;
287 const parts = [];
288 parts. push ( `Found **${ total }** potential issue${ total === 1 ? '' : 's'} to check` );
289 parts. push ( `${ launched . length } investigated` );
290 if (skippedByBudget. length > 0 ) {
291 parts. push ( `${ skippedByBudget . length } left for a larger run — re-run with \` --max-candidates all \` to see the rest` );
292 }
293 if (coveredByDedup. length > 0 ) {
294 parts. push ( `${ coveredByDedup . length } similar route variant${ coveredByDedup . length === 1 ? '' : 's'} grouped` );
295 }
296 const recCount = (recommendations ?? []). filter (( r ) => ! r.abstain && ! isPlatformScope (r)). length ;
297 parts. push ( `${ recCount } recommendation${ recCount === 1 ? '' : 's'} ready` );
298 const rawHeldBackCount = Number. isInteger (opts.heldBackCount)
299 ? opts.heldBackCount
300 : (Array. isArray (opts.abstentions) ? opts.abstentions. filter (( a ) => a?.needsEvidence === true ). length : 0 );
301 const heldBackCount = Math. min (rawHeldBackCount, Math. max ( 0 , launched. length - recCount));
302 if (heldBackCount > 0 ) {
303 parts. push ( `${ heldBackCount } need more evidence` );
304 }
305 const rawNoChangeCount = Number. isInteger (opts.noChangeCount)
306 ? opts.noChangeCount
307 : (Array. isArray (opts.abstentions) ? opts.abstentions. length : 0 );
308 const noChangeCount = Math. min (rawNoChangeCount, Math. max ( 0 , launched. length - recCount - heldBackCount));
309 if (noChangeCount > 0 ) {
310 parts. push ( `${ noChangeCount } investigated, no change recommended` );
311 }
312 return `**Coverage**: ${ parts . join ( ' · ' ) } · [details](#not-investigated-in-this-run)` ;
313 }
314
315 function renderMetadataLine ( stack , plan , usage , signals = {}) {
316 const fw = `${ stack . framework ?? 'unknown'}@${ stack . frameworkVersion ?? '?'}` ;
317 const router = stack.hasAppRouter ? 'app-router' : stack.hasPagesRouter ? 'pages-router' : null ;
318 const orm = stack.orm && stack.orm !== 'none' ? stack.orm : null ;
319 const stackParts = [fw, router, orm]. filter (Boolean). join ( ' | ' );
320 const period = usage?.period
321 ? `${ usage . period . from ?? '?'} → ${ usage . period . to ?? '?'}`
322 : '(unavailable)' ;
323 const oplusLabel = observabilityLabel (signals, usage);
324 // Plan-inference reason is debug detail — only surface when plan is uncertain.
325 const planLabel = plan.plan === 'uncertain'
326 ? `${ plan . plan } (${ plan . reason ?? 'no signal'})`
327 : (plan.plan ?? 'unknown' );
328 return `**Stack**: ${ stackParts } · **Plan**: ${ planLabel } · **Period**: ${ period } · **Observability**: ${ oplusLabel }` ;
329 }
330
331 function observabilityLabel ( signals , usage ) {
332 if (signals.observabilityPlusUsable === true ) {
333 return 'Observability Plus enabled — per-route metrics included' ;
334 }
335 if (signals.observabilityPlusUsable === false ) {
336 if (usage) {
337 return 'Per-route metrics unavailable — analysis based on billing + scanner findings' ;
338 }
339 if (signals.usageError === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED' ) {
340 return 'Per-route metrics unavailable — audit paused before metric-backed route ranking' ;
341 }
342 return 'Per-route metrics unavailable — limited analysis based on scanner findings' ;
343 }
344 if (signals.observabilityPlus === true ) {
345 return 'Observability Plus enabled — per-route metrics included' ;
346 }
347 if (signals.usageError === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK' ) {
348 return 'Not checked — audit paused at unsupported-framework preflight' ;
349 }
350 if (signals.usageError === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED' ) {
351 return 'Per-route metrics unavailable — audit paused before metric-backed route ranking' ;
352 }
353 if (usage) {
354 return 'Not enabled — analysis based on billing + scanner findings' ;
355 }
356 if (signals.observabilityPlus === false ) {
357 return 'Not enabled — limited analysis only' ;
358 }
359 return 'Not checked — limited analysis only' ;
360 }
361
362 function renderCostHeader ( signals ) {
363 const scope = signals.usageScope;
364 if (scope === 'project' ) {
365 return [ '## Cost breakdown (this project)' ];
366 }
367 if (scope === 'team' && signals.usage) {
368 return [
369 '## Cost breakdown (team-wide — `vercel usage` has no per-project filter)' ,
370 '' ,
371 '_The Vercel CLI \' s `vercel usage` reports team-wide billing without a project filter (verified May 2026). This breakdown is the whole team \' s bill for the window. Per-route metrics in the rest of this report are project-scoped via `vercel metrics`._' ,
372 ];
373 }
374 return [ '## Cost breakdown' ];
375 }
376
377 function renderCostBreakdown ( usage , signals ) {
378 const lines = [];
379 const services = Array. isArray (usage?.services) ? usage.services : null ;
380 if (services && services. length > 0 ) {
381 const chargedRows = services. filter (( s ) => {
382 const cost = serviceCost (s);
383 return cost === null || costRoundsToCents (cost) > 0 ;
384 });
385 if (chargedRows. length > 0 ) {
386 return renderServiceCostRows (chargedRows, {
387 costLabel: 'Billed cost' ,
388 costOf: serviceCost,
389 omittedZeroRows: services. length - chargedRows. length ,
390 total: usage.totals?.billedCost,
391 totalLabel: 'Total billed' ,
392 totalSuffix: ' _(precise observed cost; future-savings framing is magnitude, never precise)_' ,
393 });
394 }
395
396 const effectiveRows = services. filter (( s ) => costRoundsToCents ( serviceEffectiveCost (s)) > 0 );
397 if (effectiveRows. length > 0 ) {
398 lines. push ( '_Net billed cost is $0.00 after included credits or allotments. Showing effective usage cost so active cost drivers are still visible._' );
399 lines. push ( '' );
400 return [
401 ... lines,
402 ... renderServiceCostRows (effectiveRows, {
403 costLabel: 'Effective cost' ,
404 costOf: serviceEffectiveCost,
405 omittedZeroRows: services. length - effectiveRows. length ,
406 total: usage.totals?.effectiveCost,
407 totalLabel: 'Total effective cost' ,
408 totalSuffix: ' _(usage cost before included-credit or allotment offsets)_' ,
409 }),
410 ];
411 }
412
413 if (chargedRows. length === 0 ) {
414 const scope = signals.usageScope === 'team' ? 'team-wide ' : '' ;
415 lines. push ( `_ \` vercel usage \` returned a ${ scope }billing payload, but every reported service cost was $0.00 for this window._` );
416 return lines;
417 }
418 }
419
420 // Fallback to o11y-derived ranking when usage payload missing.
421 const gbHr = signals.metrics?.fnGbHrByRoute?.rows ?? [];
422 const usageGap = missingUsageSentence (signals);
423 if (gbHr. length === 0 ) {
424 lines. push ( `_${ usageGap } Without per-route function GB-hour data, this report cannot rank cost drivers._` );
425 return lines;
426 }
427 const top = groupGbHoursByCanonicalRoute (gbHr)
428 . sort (( a , b ) => (b.value ?? 0 ) - (a.value ?? 0 ))
429 . slice ( 0 , 10 );
430 lines. push ( `_${ usageGap } Ranking by \` function_duration_gbhr \` instead. These do not translate to dollars directly, but they show which routes consume billable units._` );
431 lines. push ( '' );
432 lines. push ( '| Route | GB-hr (sum, 14d) |' );
433 lines. push ( '|---|---|' );
434 for ( const r of top) {
435 lines. push ( `| ${ escape ( r . route ?? '(unnamed)' ) } | ${ ( r . value ?? 0 ). toFixed ( 4 ) } |` );
436 }
437 return lines;
438 }
439
440 function renderServiceCostRows ( services , { costLabel , costOf , omittedZeroRows = 0 , total = null , totalLabel , totalSuffix = '' }) {
441 const lines = [];
442 const rows = services. slice (). sort (( a , b ) => ( costOf (b) ?? 0 ) - ( costOf (a) ?? 0 ));
443 // Drop Usage column when every cell is "(unspecified)" — happens when CLI emits pricingUnit=USD.
444 const usageCells = rows. map (( s ) => formatUsage (s));
445 const hasRealUsage = usageCells. some (( c ) => c !== '(unspecified)' );
446 if (hasRealUsage) {
447 lines. push ( `| Service | Usage | ${ costLabel } |` );
448 lines. push ( '|---|---|---|' );
449 for ( let i = 0 ; i < rows. length ; i ++ ) {
450 const s = rows[i];
451 const costValue = costOf (s);
452 const cost = typeof costValue === 'number' ? `$${ costValue . toFixed ( 2 ) }` : '(n/a)' ;
453 lines. push ( `| ${ escape ( s . name ?? '(unnamed)' ) } | ${ escape ( usageCells [ i ]) } | ${ cost } |` );
454 }
455 } else {
456 lines. push ( `| Service | ${ costLabel } |` );
457 lines. push ( '|---|---|' );
458 for ( const s of rows) {
459 const costValue = costOf (s);
460 const cost = typeof costValue === 'number' ? `$${ costValue . toFixed ( 2 ) }` : '(n/a)' ;
461 lines. push ( `| ${ escape ( s . name ?? '(unnamed)' ) } | ${ cost } |` );
462 }
463 }
464 if (omittedZeroRows > 0 ) {
465 lines. push ( '' );
466 lines. push ( `_${ omittedZeroRows } zero-cost service ${ omittedZeroRows === 1 ? 'row was' : 'rows were'} omitted._` );
467 }
468 if ( typeof total === 'number' ) {
469 lines. push ( '' );
470 lines. push ( `**${ totalLabel }: $${ total . toFixed ( 2 ) }**${ totalSuffix }` );
471 }
472 return lines;
473 }
474
475 function serviceCost ( service ) {
476 if ( typeof service?.billedCost === 'number' ) return service.billedCost;
477 if ( typeof service?.cost === 'number' ) return service.cost;
478 return null ;
479 }
480
481 function serviceEffectiveCost ( service ) {
482 if ( typeof service?.effectiveCost === 'number' ) return service.effectiveCost;
483 if ( typeof service?.pricingQuantity === 'number' && service?.pricingUnit === 'USD' ) return service.pricingQuantity;
484 return 0 ;
485 }
486
487 function costRoundsToCents ( cost ) {
488 if ( typeof cost !== 'number' || ! Number. isFinite (cost)) return 0 ;
489 return Math. round (cost * 100 ) / 100 ;
490 }
491
492 function renderRecTable ( recs , signals = {}) {
493 if (recs. length === 0 ) return [ '_(none)_' ];
494 const lines = [];
495 lines. push ( '| # | Bucket | What | Impact | Effort | Citations |' );
496 lines. push ( '|---|---|---|---|---|---|' );
497 recs. forEach (( r , i ) => {
498 const cites = asArray (r.citations). slice ( 0 , 2 ). join ( '<br>' );
499 lines. push ( `| ${ i + 1 } | ${ r . bucket ?? '?'} | ${ escape ( formatRecommendationText ( r . what ?? '' )) } | ${ escape ( formatRecommendationText ( impactString ( r , signals ))) } | ${ r . effort ?? '?'} | ${ cites } |` );
500 });
501 return lines;
502 }
503
504 function renderRecDetail ( rec , index , { compact = false , signals = {} } = {}) {
505 const lines = [];
506 lines. push ( `### ${ index }. ${ formatRecommendationText ( rec . what ?? '(no `what`)' ) }` );
507 lines. push ( '' );
508 const meta = [
509 rec.bucket ? `**${ rec . bucket }**` : null ,
510 rec.effort ? `effort: ${ rec . effort }` : null ,
511 rec.impactTier ? `impact tier: ${ rec . impactTier }` : null ,
512 rec.candidateRef ? `candidate: ${ displayCandidate ( rec ) }` : null ,
513 rec.corroborationCount > 1 ? `corroborated: ${ rec . corroborationCount }` : null ,
514 ]. filter (Boolean);
515 if (meta. length > 0 ) lines. push ( `_${ meta . join ( ' · ' ) }_` );
516 lines. push ( '' );
517 const appliesAlsoTo = asArray (rec.appliesAlsoTo);
518 if (appliesAlsoTo. length > 0 ) {
519 const refs = appliesAlsoTo
520 . map (( a ) => a?.candidateRef)
521 . filter (Boolean)
522 . slice ( 0 , 4 );
523 if (refs. length > 0 ) {
524 const suffix = appliesAlsoTo. length > refs. length ? `, +${ appliesAlsoTo . length - refs . length } more` : '' ;
525 lines. push ( `_Also applies to: ${ refs . map ( displayCandidateRef ). join ( ', ' ) }${ suffix }._` );
526 lines. push ( '' );
527 }
528 }
529 if (rec.why) {
530 lines. push ( '**Why**' );
531 lines. push ( '' );
532 lines. push ( formatRecommendationText (rec.why));
533 lines. push ( '' );
534 }
535 lines. push ( '**Impact**' );
536 lines. push ( '' );
537 lines. push ( formatRecommendationText ( impactString (rec, signals)));
538 lines. push ( '' );
539 if ( ! compact && rec.fix) {
540 lines. push ( '**Fix**' );
541 lines. push ( '' );
542 lines. push (rec.fix);
543 lines. push ( '' );
544 }
545 if ( ! compact && rec.currentBehavior) {
546 lines. push ( '**Before**' );
547 lines. push ( '' );
548 lines. push (rec.currentBehavior);
549 lines. push ( '' );
550 }
551 if ( ! compact && rec.desiredBehavior) {
552 lines. push ( '**After**' );
553 lines. push ( '' );
554 lines. push (rec.desiredBehavior);
555 lines. push ( '' );
556 }
557 if (rec.verify) {
558 lines. push ( '**Verify**' );
559 lines. push ( '' );
560 lines. push ( formatRecommendationText (rec.verify));
561 lines. push ( '' );
562 }
563 const cites = asArray (rec.citations);
564 if (cites. length > 0 ) {
565 lines. push ( '**Citations**' );
566 lines. push ( '' );
567 for ( const c of cites) lines. push ( `- \` ${ c } \` ` );
568 lines. push ( '' );
569 }
570 lines. push ( '' );
571 return lines;
572 }
573
574 function renderGatedTable ( gated ) {
575 if ( ! Array. isArray (gated) || gated. length === 0 ) {
576 return [ '_(no candidates were held back)_' ];
577 }
578 const groups = groupGatedCandidates (gated);
579 const lines = [];
580 lines. push ( '| Candidate type | Why not investigated | Targets | Count |' );
581 lines. push ( '|---|---|---|---:|' );
582 for ( const group of groups) {
583 lines. push ( `| ${ escape ( group . kind ) } | ${ escape ( group . reason ) } | ${ formatGatedTargets ( group . targets , group . count ) } | ${ group . count } |` );
584 }
585 return lines;
586 }
587
588 function formatGatedTargets ( targets , count ) {
589 const unique = [ ...new Set (targets. map (( t ) => String (t)))];
590 const shown = unique. slice ( 0 , GATED_TARGET_PREVIEW ). map (( target ) => escape (target));
591 const hidden = Math. max ( 0 , count - shown. length );
592 if (hidden > 0 ) shown. push ( `+${ hidden } more` );
593 return shown. join ( '<br>' );
594 }
595
596 function groupGatedCandidates ( gated ) {
597 const byKey = new Map ();
598 for ( const g of gated) {
599 const kind = formatKind (g.kind ?? '?' );
600 const reason = publicGatedReason (g.gatedReason ?? g.disqualifyReason ?? '(no reason recorded)' );
601 const target = formatRoute (g);
602 const key = `${ kind } \u0000 ${ reason }` ;
603 const existing = byKey. get (key);
604 if (existing) {
605 existing.count += 1 ;
606 existing.targets. push ( String (target));
607 } else {
608 byKey. set (key, { kind: String (kind), reason: String (reason), targets: [ String (target)], count: 1 });
609 }
610 }
611 return Array. from (byKey. values ());
612 }
613
614 function publicNoRecommendationReason ( reason ) {
615 return formatEvidenceText ( String (reason))
616 . replace ( / \b Dropped at render: \s * / gi , '' )
617 . replace ( / \b verifier flagged for regen, but no regen happened \b / gi , 'needs stronger evidence before it is safe to apply' )
618 . replace ( / \b Re-run with a refreshed brief \. ? / gi , 'Re-run the investigation after refreshing the evidence.' )
619 . replace ( / \b regen \b / gi , 're-check' )
620 . replace ( / \b verifier \b / gi , 'verification' )
621 . replace ( / \b rec \b / gi , 'recommendation' )
622 . replace ( / \b sub [- ] agent \b / gi , 'investigation' )
623 . replace ( / \b abstentions ?\b / gi , 'no-change findings' )
624 . replace ( / \b abstaining \b / gi , 'not recommending a change' )
625 . replace ( / \b abstain(?:ed) ?\b / gi , 'found no supported change' )
626 . replace ( / \b quality \s * \+ \s * verification \b / gi , 'verification' )
627 . replace ( / \b quality \b / gi , 'review' )
628 . replace ( / \b sanitizers ?\b / gi , 'checks' );
629 }
630
631 function splitInvestigationOutcomes ( abstentions ) {
632 const rows = Array. isArray (abstentions) ? abstentions : [];
633 return {
634 needsEvidenceRows: rows. filter (( a ) => a?.needsEvidence === true ),
635 noChangeRows: rows. filter (( a ) => a?.needsEvidence !== true ),
636 };
637 }
638
639 function publicGatedReason ( reason ) {
640 return formatPublicText ( String (reason))
641 . replace ( / \b hardGated: \s * / gi , '' )
642 . replace ( /skippedByBudget \s * \( max-candidates=( [ ^ );] + )(?:; [ ^ )] * ) ? \) / i , 'left for a larger run (max candidates: $1)' )
643 . replace ( /skippedByBudget \b / gi , 'left for a larger run' )
644 . replace ( / \s * ; \s * raise with --max-candidates N or =all/ gi , '' )
645 . replace ( /=all/ g , 'all' )
646 . replace ( / \b coveredBy \b / gi , 'covered by a higher-priority candidate' )
647 . replace ( / \b disqualified \b / gi , 'not eligible' );
648 }
649
650 function formatEvidenceText ( value ) {
651 const expanded = formatPublicText (value)
652 . replace ( / \b deepDive \b / gi , 'follow-up metric' )
653 . replace ( / \b deep-dive \b / gi , 'follow-up metric' )
654 . replace ( / \b latency p95 \b / gi , '95th percentile latency' )
655 . replace ( / \b ttfb p95 \b / gi , '95th percentile TTFB' )
656 . replace ( / \b cpu p95 \b / gi , '95th percentile CPU time' )
657 . replace ( / \b p95 \b / gi , '95th percentile' )
658 . replace ( / \b gate signal \b / gi , 'broad metric signal' )
659 . replace ( / \b o11ySignal \b / gi , 'observed signal' )
660 . replace ( / \b perDeployment \b / gi , 'per-deployment' )
661 . replace ( / \b startTypeSplit \b / gi , 'start-type breakdown' )
662 . replace ( / \b statusDistribution \b / gi , 'status distribution' )
663 . replace ( / \b cacheBreakdown \b / gi , 'cache breakdown' )
664 . replace ( / \b functionRoutes \b / gi , 'function routes' )
665 . replace ( / \b fnGbHrByRoute \b / gi , 'function duration by route' );
666 return formatPublicText (expanded);
667 }
668
669 function formatRecommendationText ( value ) {
670 return formatEvidenceText (value)
671 . replace ( / \b the gate fires \b / gi , 'this audit flags the signal' )
672 . replace ( / \b ships immediately \b / gi , 'can ship sooner' );
673 }
674
675 function displayCandidate ( value ) {
676 return formatCandidateLabel ( candidateForDisplay (value));
677 }
678
679 function candidateForDisplay ( value ) {
680 const parsed = parseCandidateRef (value?.candidateRef);
681 return parsed
682 ? displayCandidateObject (value, parsed)
683 : value;
684 }
685
686 function displayCandidateRef ( ref ) {
687 const parsed = parseCandidateRef (ref);
688 if ( ! parsed) return String (ref ?? '(unspecified)' );
689 return formatCandidateLabel ( displayCandidateObject ({}, parsed));
690 }
691
692 function parseCandidateRef ( ref ) {
693 if ( typeof ref !== 'string' ) return null ;
694 const [ kind , ... rest ] = ref. split ( ':' );
695 const target = rest. join ( ':' );
696 if ( ! kind || ! target) return null ;
697 return { kind, target };
698 }
699
700 function displayCandidateObject ( base , parsed ) {
701 if (parsed.target. startsWith ( '<account>#' )) {
702 return { ... base, kind: parsed.kind, files: [parsed.target. slice ( '<account>#' . length )] };
703 }
704 if (parsed.target === '<account>' ) {
705 return { ... base, kind: parsed.kind };
706 }
707 return { ... base, kind: parsed.kind, route: parsed.target };
708 }
709
710 function groupGbHoursByCanonicalRoute ( rows ) {
711 const byRoute = new Map ();
712 for ( const row of rows) {
713 const route = row?.route ? canonicalizeRoute (row.route) : '(unnamed)' ;
714 byRoute. set (route, (byRoute. get (route) ?? 0 ) + (row?.value ?? 0 ));
715 }
716 return [ ... byRoute. entries ()]. map (([ route , value ]) => ({ route, value }));
717 }
718
719 function renderStrengths ( signals ) {
720 const lines = [];
721
722 // Stops agent from emitting "verify Fluid is on" recs. Source: defaultResourceConfig from project API.
723 const projectFacts = deriveProjectFacts (signals);
724 for ( const f of projectFacts) {
725 if ( String (f.id ?? '' ). startsWith ( 'memory_' )) continue ;
726 lines. push ( `- ${ f . strength }` );
727 }
728
729 const cache = signals.metrics?.fdtByCache?.rows ?? [];
730 const hit = cache. find (( r ) => r.cache_result === 'HIT' || r.cache_result === 'STALE' );
731 const miss = cache. find (( r ) => r.cache_result === 'MISS' || r.cache_result === 'BYPASS' );
732 if (hit && miss && (hit.value ?? 0 ) > (miss.value ?? 0 )) {
733 lines. push ( `- Cache hit-rate is healthy at the bandwidth tier — HIT/STALE bandwidth (${ formatBytes ( hit . value ) }) exceeds MISS/BYPASS (${ formatBytes ( miss . value ) }).` );
734 }
735 const cold = signals.metrics?.fnStartTypeByRoute?.rows ?? [];
736 const totalInv = cold. reduce (( s , r ) => s + (r.total ?? 0 ), 0 );
737 const totalCold = cold. reduce (( s , r ) => s + (r.coldCount ?? 0 ), 0 );
738 if (totalInv > 1000 ) {
739 const coldPct = totalCold / totalInv;
740 if (coldPct < 0.02 ) lines. push ( `- Cold-start rate is very low (${ ( coldPct * 100 ). toFixed ( 2 ) }%) — Fluid Compute or warm-instance reuse is doing its job.` );
741 }
742 const errors = signals.metrics?.requestsByRouteStatus?.rows ?? [];
743 const total5xx = errors. filter (( r ) => / ^ 5/ . test (r.http_status ?? '' )). reduce (( s , r ) => s + (r.value ?? 0 ), 0 );
744 const totalReq = errors. reduce (( s , r ) => s + (r.value ?? 0 ), 0 );
745 if (totalReq > 1000 ) {
746 const rate = total5xx / totalReq;
747 if (rate < 0.001 ) lines. push ( `- 5xx rate is very low (${ ( rate * 100 ). toFixed ( 3 ) }%) on ${ formatNum ( totalReq ) } requests.` );
748 }
749 if (lines. length === 0 ) lines. push ( '_(no headline strengths to call out — see the gated table for signals we considered)_' );
750 return lines;
751 }
752
753 function renderConfigurationNotes ( signals ) {
754 const projectFacts = deriveProjectFacts (signals);
755 return projectFacts
756 . filter (( f ) => String (f.id ?? '' ). startsWith ( 'memory_' ))
757 . map (( f ) => `- ${ f . strength }` );
758 }
759
760 function renderDataGaps ( signals ) {
761 const lines = [];
762 const observabilityGap = observabilityDataGap (signals);
763 if (observabilityGap) lines. push (observabilityGap);
764 if ( ! signals.usage) lines. push ( `- ${ missingUsageSentence ( signals ) }` );
765 const cwvMetric = metricState (signals, 'cwvCount' );
766 if (cwvMetric.failed) {
767 lines. push ( `- Speed Insights metrics were not usable ( \` ${ cwvMetric . code } \` ), so LCP/INP/CLS analysis was skipped.` );
768 } else if (cwvMetric.collected) {
769 const cwv = cwvMetric.rows?.[ 0 ]?.value ?? 0 ;
770 if (cwv === 0 ) lines. push ( '- No Speed Insights measurements — Core Web Vitals analysis dormant. Wire up Speed Insights to enable LCP/INP/CLS recommendations.' );
771 }
772 const isrMetric = metricState (signals, 'isrReadsByRoute' );
773 if (isrMetric.collected) {
774 const isrR = isrMetric.rows ?? [];
775 if (isrR. length === 0 ) lines. push ( '- No ISR activity observed — either the project does not use ISR or no eligible routes had traffic in the window.' );
776 }
777 const imageMetric = metricState (signals, 'imageCount' );
778 if (imageMetric.collected) {
779 const images = imageMetric.rows?.[ 0 ]?.value ?? 0 ;
780 if (images === 0 ) lines. push ( '- No image transformations observed — either `next/image` is not used or no images served in the window.' );
781 }
782 const middlewareMetric = metricState (signals, 'middlewareCount' );
783 if (middlewareMetric.collected) {
784 const middleware = middlewareMetric.rows ?? [];
785 if (middleware. length === 0 ) lines. push ( '- No middleware invocations — either no `middleware.ts` is shipped or its matcher excludes all observed traffic.' );
786 }
787 if (lines. length === 0 ) lines. push ( '_(no relevant gaps — every signal had data)_' );
788 return lines;
789 }
790
791 function observabilityDataGap ( signals = {}) {
792 if (signals.usageError === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK' ) {
793 return '- Observability Plus was not checked because the audit paused at the unsupported-framework preflight.' ;
794 }
795 if (signals.observabilityPlusUsable === false ) {
796 const blocker = signals.observabilityPlusBlocker;
797 if (blocker === 'project_disabled' ) {
798 return '- Per-route metrics unavailable — Observability Plus is disabled for this project.' ;
799 }
800 if (blocker === 'forbidden' || blocker === 'project_not_found' ) {
801 return '- Per-route metrics unavailable — the authenticated Vercel scope cannot read this project.' ;
802 }
803 if (blocker === 'not_linked' ) {
804 return '- Per-route metrics unavailable — the app directory is not linked to the Vercel project.' ;
805 }
806 if (blocker === 'no_oplus_probe' ) {
807 return '- Per-route metrics unavailable — Observability Plus was not detected for this scope.' ;
808 }
809 if (blocker === 'payment_required' ) {
810 return '- Per-route metrics unavailable — Observability Plus metrics were not usable for this scope.' ;
811 }
812 if (blocker === 'daily_quota_exceeded' ) {
813 return '- Per-route metrics unavailable — the Observability Plus query quota is exhausted for today.' ;
814 }
815 if (blocker === 'no_traffic' ) {
816 return '- Per-route metrics sparse — no route-level traffic was returned in the metrics window.' ;
817 }
818 if (blocker === 'all_failed_other' ) {
819 return '- Per-route metrics unavailable — all Observability Plus metric queries failed.' ;
820 }
821 if (blocker) {
822 return `- Per-route metrics unavailable — Observability Plus metrics returned \` ${ blocker } \` .` ;
823 }
824 return '- Per-route metrics unavailable — Observability Plus data was not usable for this run.' ;
825 }
826 if (signals.observabilityPlus === false ) {
827 return '- Observability Plus not enabled — per-route latency / cache-hit / cold-start metrics unavailable.' ;
828 }
829 return null ;
830 }
831
832 function missingUsageSentence ( signals = {}) {
833 const code = signals.usageError;
834 if (code === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED' ) {
835 return '`vercel usage` was not collected because the audit paused before billing collection on the Observability Plus blocker.' ;
836 }
837 if (code === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK' ) {
838 return '`vercel usage` was not collected because the audit paused at the unsupported-framework preflight.' ;
839 }
840 if (code === 'USAGE_CONTEXT_MISMATCH' ) {
841 return '`vercel usage` returned data for a different team context, so the billing breakdown was not used.' ;
842 }
843 if (code === 'USAGE_UNAVAILABLE' ) {
844 return '`vercel usage` returned `USAGE_UNAVAILABLE`; no billing breakdown was available from the Vercel CLI.' ;
845 }
846 if ( typeof code === 'string' && code. trim () !== '' ) {
847 return ` \` vercel usage \` returned \` ${ code } \` ; no billing breakdown was available from the Vercel CLI.` ;
848 }
849 return '`vercel usage` did not return a billing payload.' ;
850 }
851
852 function metricState ( signals , id ) {
853 const metrics = signals.metrics ?? {};
854 if ( ! Object . prototype .hasOwnProperty. call (metrics, id)) {
855 return { collected: false , failed: false , rows: null , code: null };
856 }
857 const metric = metrics[id] ?? {};
858 const failed = metric.ok === false ;
859 return {
860 collected: ! failed,
861 failed,
862 rows: Array. isArray (metric.rows) ? metric.rows : [],
863 code: metric.code ?? 'UNKNOWN' ,
864 };
865 }
866
867 function sortRecs ( recs ) {
868 return recs. slice (). sort (( a , b ) => priorityScore (b) - priorityScore (a));
869 }
870
871 function priorityScore ( rec ) {
872 return typeof rec.priority === 'number' ? rec.priority : tierScore (rec.impactTier);
873 }
874 function tierScore ( t ) { return ({ high: 100 , medium: 50 , low: 10 })[t] ?? 0 ; }
875
876 function isPlatformScope ( rec ) {
877 const k = String (rec.candidateRef ?? '' ). split ( ':' )[ 0 ];
878 return k. startsWith ( 'platform_' ) || rec.scope === 'account' ;
879 }
880
881 function impactString ( rec , signals = {}) {
882 const label = computeImpactLabel (rec, signals);
883 if (label) return label;
884 return '_(no impact framing recorded)_' ;
885 }
886
887 function signalFromRec ( rec ) {
888 return rec.findingRefs?.[ 0 ] ?? null ;
889 }
890
891 function formatUsage ( s ) {
892 if ( typeof s.usage === 'string' ) return s.usage;
893 if ( typeof s.usage === 'number' ) return formatNum (s.usage) + (s.unit ? ` ${ s . unit }` : '' );
894 return '(unspecified)' ;
895 }
896
897 function formatNum ( n ) {
898 if ( ! Number. isFinite (n)) return String (n);
899 if (n >= 1_000_000 ) return (n / 1_000_000 ). toFixed ( 2 ) + 'M' ;
900 if (n >= 1_000 ) return (n / 1_000 ). toFixed ( 2 ) + 'K' ;
901 return String (n);
902 }
903
904 function formatBytes ( b ) {
905 if ( ! Number. isFinite (b)) return '(n/a)' ;
906 if (b >= 1e12 ) return (b / 1e12 ). toFixed ( 2 ) + ' TB' ;
907 if (b >= 1e9 ) return (b / 1e9 ). toFixed ( 2 ) + ' GB' ;
908 if (b >= 1e6 ) return (b / 1e6 ). toFixed ( 2 ) + ' MB' ;
909 if (b >= 1e3 ) return (b / 1e3 ). toFixed ( 2 ) + ' KB' ;
910 return Math. round (b) + ' B' ;
911 }
912
913 function escape ( s ) {
914 if ( typeof s !== 'string' ) return String (s ?? '' );
915 return s. replace ( / \| / g , ' \\ |' ). replace ( / \n / g , ' ' );
916 }
917
918 function asArray ( v ) { return Array. isArray (v) ? v : []; }
919
920 function enrichRecFromCandidates ( rec , candidates ) {
921 if ( ! rec || typeof rec !== 'object' ) return rec;
922 if ( ! Array. isArray (candidates) || candidates. length === 0 ) return rec;
923 const ref = rec.candidateRef ?? null ;
924 // Match on raw OR canonical kind:route so pre-dedup-canonicalization refs still resolve.
925 let match = null ;
926 if (ref) {
927 const [ kind , route ] = String (ref). split ( ':' );
928 const canonical = route ? canonicalizeRoute (route) : null ;
929 match = candidates. find (( c ) => {
930 if ( ! c || c.kind !== kind) return false ;
931 const cRoute = c.route ?? c.hostname ?? '<account>' ;
932 return cRoute === route || cRoute === canonical || canonicalizeRoute (cRoute) === canonical;
933 });
934 }
935 const canonicalRef = ref ? canonicalRefOf (ref) : ref;
936 const merged = { ... rec, candidateRef: canonicalRef };
937 if (match) {
938 if ( ! merged.o11ySignal && match.o11ySignal) merged.o11ySignal = match.o11ySignal;
939 if ( ! merged.displayRoute && match.displayRoute) merged.displayRoute = match.displayRoute;
940 if ( ! merged.aliasRoutes && Array. isArray (match.aliasRoutes) && match.aliasRoutes. length > 0 ) {
941 merged.aliasRoutes = match.aliasRoutes;
942 }
943 if ( ! merged.mergedCount && typeof match.mergedCount === 'number' ) {
944 merged.mergedCount = match.mergedCount;
945 }
946 }
947 return merged;
948 }
949
950 function canonicalRefOf ( ref ) {
951 const [ kind , ... rest ] = String (ref). split ( ':' );
952 const route = rest. join ( ':' );
953 if ( ! route) return ref;
954 return `${ kind }:${ canonicalizeRoute ( route ) }` ;
955 }