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.73
Position 73 of 155
Type JavaScript
Size 17 KB
Lines 437 scripts/ render-report.mjs
JavaScript · 437 lines · 17 KB
;
10 import { hasUnsupportedCacheLifeCdnText, splitCustomerSafeObservations } from '../lib/observation-safety.mjs' ;
11
12 const log = ( ... a ) => console. error ( '[render-report]' , ... a);
13 const HARD_REGEN_TRIGGERS = new Set ([
14 'project_config_contradiction' ,
15 'cache_vary_safety' ,
16 'semantic_safety' ,
17 ]);
18
19 async function main () {
20 const args = parseArgs (process.argv. slice ( 2 ));
21 if ( ! args.recsPath || ! args.gatePath || ! args.signalsPath) {
22 console. error ( 'usage: node scripts/render-report.mjs <recommendations.json> <gate.json> <signals.json> [--project NAME] [--out FILE] [--message-out FILE] [--no-timestamp] [--debug-out FILE]' );
23 process. exit ( 1 );
24 }
25
26 const [ recsRaw , gateRaw , signalsRaw ] = await Promise . all ([
27 readFile (args.recsPath, 'utf-8' ). then ( JSON .parse),
28 readFile (args.gatePath, 'utf-8' ). then ( JSON .parse),
29 readFile (args.signalsPath, 'utf-8' ). then ( JSON .parse),
30 ]);
31
32 // Accept either a raw rec array OR the verify-and-regen wrapper
33 // {recsGraded, qualityDropped, ...}. Stale-rec defense: when verify-and-regen
34 // flagged a hard-safety issue but the orchestrator skipped re-spawn, the
35 // original rec is still in recsGraded. Filter it here so it can't ship; it
36 // surfaces in "Investigated, no change recommended" instead.
37 const hardRegenRefs = new Set (
38 Array. isArray (recsRaw.regenPlan)
39 ? recsRaw.regenPlan
40 . filter (( p ) => HARD_REGEN_TRIGGERS . has (p.regenTrigger))
41 . map (( p ) => p.candidateRef)
42 . filter (Boolean)
43 : []
44 );
45 const activeCandidates = [
46 ... (Array. isArray (gateRaw.toLaunch) ? gateRaw.toLaunch : []),
47 ... (Array. isArray (gateRaw.platform) ? gateRaw.platform : []),
48 ];
49 const enforceCurrentGate = ! Array. isArray (recsRaw) && activeCandidates. length > 0 ;
50 const staleRecommendationDrops = [];
51 const wrapperRecommendations = Array. isArray (recsRaw.renderableRecommendations)
52 ? recsRaw.renderableRecommendations
53 : (recsRaw.recsGraded ?? []);
54 const needsReviewDrops = [];
55 const candidateRecommendations = Array. isArray (recsRaw)
56 ? recsRaw. filter (( r ) => r?.abstain !== true )
57 : wrapperRecommendations
58 . filter (( r , i ) => (r.quality?.overall ?? 0 ) >= 0.55 )
59 . filter (( r ) => ! hardRegenRefs. has (r.candidateRef));
60 const recommendationsRaw = candidateRecommendations
61 . filter (( r ) => {
62 if (r?.abstain === true || r?.needsReview !== true ) return true ;
63 needsReviewDrops. push ({
64 candidateRef: r.candidateRef ?? null ,
65 reason: 'This recommendation needs a manual safety review before it is ready to apply.' ,
66 });
67 return false ;
68 })
69 . filter (( r ) => {
70 if ( ! enforceCurrentGate) return true ;
71 if ( recommendationMatchesActiveCandidate (r, activeCandidates)) return true ;
72 staleRecommendationDrops. push ({
73 candidateRef: r.candidateRef ?? null ,
74 reason: 'This recommendation came from a candidate that is not in the current run output. Re-run from a clean run directory before applying it.' ,
75 });
76 return false ;
77 });
78 const recommendations = dedupeRecommendations (recommendationsRaw);
79 const readyTargets = new Set (
80 recommendations
81 . map (( r ) => candidateTarget (r?.candidateRef))
82 . filter (Boolean)
83 );
84 const droppedContradictions = ! Array. isArray (recsRaw)
85 ? (recsRaw.recsGraded ?? [])
86 . map (( r , i ) => ({ r, i }))
87 . filter (({ r }) => hardRegenRefs. has (r.candidateRef))
88 . map (({ r , i }) => ({
89 candidateRef: r.candidateRef ?? null ,
90 reason: publicHardRegenReason (recsRaw.regenPlan?. find (( p ) => p.index === i || p.candidateRef === r.candidateRef)),
91 }))
92 : [];
93
94 const gated = Array. isArray (gateRaw.gated) ? gateRaw.gated : [];
95
96 // No-change findings are first-class investigation outputs ("the hypothesis didn't hold").
97 // Contradiction-dropped recs ride alongside them so customers see WHY a rec
98 // was held back instead of it silently disappearing.
99 const baseAbstentions = Array. isArray (recsRaw)
100 ? recsRaw. filter (( r ) => r?.abstain === true ). map (( r ) => ({
101 candidateRef: r.candidateRef ?? null ,
102 reason: publicNoChangeReason (r.reason ?? '(no reason recorded)' ),
103 }))
104 : (recsRaw.abstentions ?? []). map (( r ) => ({
105 ... r,
106 reason: publicNoChangeReason (r.reason ?? '(no reason recorded)' ),
107 }));
108 const publicBaseAbstentions = baseAbstentions. filter (( r ) => ! readyTargets. has ( candidateTarget (r?.candidateRef)));
109 // Observations: no-change findings carrying a structured non-perf finding
110 // (deployment regression, error storm, etc.).
111 const flattenedObservations = Array. isArray (recsRaw)
112 ? flattenObservations (recsRaw. filter (( r ) => r?.abstain === true ))
113 : flattenObservations ([
114 ... (Array. isArray (recsRaw.observations) ? recsRaw.observations : []),
115 ... (Array. isArray (recsRaw.abstentions) ? recsRaw.abstentions : []),
116 ]);
117 const { observations : safeObservations , heldBackObservations } = splitCustomerSafeObservations (flattenedObservations, baseAbstentions, signalsRaw);
118 const observations = suppressReadyCoveredObservations (safeObservations, recommendations);
119
120 const abstentions = [
121 ... publicBaseAbstentions,
122 ... droppedContradictions,
123 ... staleRecommendationDrops,
124 ... needsReviewDrops,
125 ... (Array. isArray (recsRaw.withheldRecommendations) ? recsRaw.withheldRecommendations. map (( d ) => ({
126 candidateRef: d.candidateRef ?? null ,
127 reason: publicWithheldReason (d),
128 needsEvidence: true ,
129 })) : []),
130 ... (Array. isArray (recsRaw.sanitizerDropped) ? recsRaw.sanitizerDropped. map (( d ) => ({
131 candidateRef: d.candidateRef ?? null ,
132 reason: `This needs a closer review before it is safe to apply: ${ d . reason ?? 'review required'}.` ,
133 needsEvidence: true ,
134 })) : []),
135 ... (Array. isArray (recsRaw.heldBackObservations) ? recsRaw.heldBackObservations. map (( d ) => ({
136 ... d,
137 needsEvidence: true ,
138 })) : []),
139 ... heldBackObservations,
140 ];
141
142 // Full catalog lets the renderer recover o11ySignal + aliasRoutes that recs
143 // didn't propagate, and canonicalize segment-tree candidateRefs.
144 const allCandidates = [
145 ... activeCandidates,
146 ... gated,
147 ];
148
149 const md = renderReport ({
150 recommendations,
151 gated,
152 abstentions,
153 observations,
154 signals: signalsRaw,
155 candidates: allCandidates,
156 opts: {
157 projectName: args.projectName,
158 generatedAt: args.noTimestamp ? null : new Date (). toISOString (),
159 heldBackCount: (Number. isInteger (recsRaw.summary?.withheldRecommendations)
160 ? recsRaw.summary.withheldRecommendations
161 : (Array. isArray (recsRaw.regenPlan) ? recsRaw.regenPlan. length : 0 ) +
162 (Array. isArray (recsRaw.qualityDropped) ? recsRaw.qualityDropped. length : 0 )) +
163 (Array. isArray (recsRaw.sanitizerDropped) ? recsRaw.sanitizerDropped. length : 0 ) +
164 (Array. isArray (recsRaw.heldBackObservations) ? recsRaw.heldBackObservations. length : 0 ) +
165 heldBackObservations. length ,
166 noChangeCount: Number. isInteger (recsRaw.summary?.abstentions)
167 ? Math. min (recsRaw.summary.abstentions, publicBaseAbstentions. length )
168 : publicBaseAbstentions. length ,
169 },
170 });
171
172 if (args.debugOutPath) {
173 const debugArtifact = buildDebugArtifact ({
174 recsRaw,
175 recommendationsRaw,
176 recommendations,
177 gateRaw,
178 abstentions,
179 observations,
180 heldBackObservations,
181 staleRecommendationDrops,
182 droppedContradictions,
183 });
184 const serializedDebug = JSON . stringify (debugArtifact, null , 2 ) + ' \n ' ;
185 await mkdir ( dirname (args.debugOutPath), { recursive: true });
186 await writeFile (args.debugOutPath, serializedDebug, 'utf-8' );
187 log ( `wrote debug ${ serializedDebug . length }B → ${ args . debugOutPath }` );
188 }
189
190 if (args.messageOutPath) {
191 const messageArtifact = buildFinalReportMessage ({
192 reportPath: args.outPath ?? '(stdout)' ,
193 markdown: md,
194 recommendations,
195 signals: signalsRaw,
196 });
197 const serializedMessage = JSON . stringify (messageArtifact, null , 2 ) + ' \n ' ;
198 await mkdir ( dirname (args.messageOutPath), { recursive: true });
199 await writeFile (args.messageOutPath, serializedMessage, 'utf-8' );
200 log ( `wrote final message ${ serializedMessage . length }B → ${ args . messageOutPath }` );
201 }
202
203 if (args.outPath) {
204 await mkdir ( dirname (args.outPath), { recursive: true });
205 await writeFile (args.outPath, md + ' \n ' , 'utf-8' );
206 log ( `wrote ${ md . length }B → ${ args . outPath }` );
207 } else {
208 process.stdout. write (md + ' \n ' );
209 }
210 }
211
212 function parseArgs ( argv ) {
213 const out = { positional: [] };
214 for ( let i = 0 ; i < argv. length ; i ++ ) {
215 const a = argv[i];
216 if (a === '--project' ) out.projectName = argv[ ++ i];
217 else if (a. startsWith ( '--project=' )) out.projectName = a. slice ( '--project=' . length );
218 else if (a === '--out' ) out.outPath = resolve (argv[ ++ i]);
219 else if (a. startsWith ( '--out=' )) out.outPath = resolve (a. slice ( '--out=' . length ));
220 else if (a === '--message-out' ) out.messageOutPath = resolve (argv[ ++ i]);
221 else if (a. startsWith ( '--message-out=' )) out.messageOutPath = resolve (a. slice ( '--message-out=' . length ));
222 else if (a === '--no-timestamp' ) out.noTimestamp = true ;
223 else if (a === '--debug-out' ) out.debugOutPath = resolve (argv[ ++ i]);
224 else if (a. startsWith ( '--debug-out=' )) out.debugOutPath = resolve (a. slice ( '--debug-out=' . length ));
225 else if (a === '--debug' ) {
226 console. error ( '[render-report] --debug no longer writes internal details into customer markdown; use --debug-out FILE' );
227 }
228 else out.positional. push (a);
229 }
230 out.recsPath = out.positional[ 0 ];
231 out.gatePath = out.positional[ 1 ];
232 out.signalsPath = out.positional[ 2 ];
233 return out;
234 }
235
236 function publicWithheldReason ( record ) {
237 switch (record?.reason) {
238 case 'needs_review' :
239 return 'Automated checks added a safety caveat, so this run kept the recommendation out of the ready-to-apply list.' ;
240 case 'quality_floor' :
241 return 'The recommendation did not meet the evidence bar for this report.' ;
242 case 'project_config_contradiction' :
243 case 'cache_vary_safety' :
244 case 'semantic_safety' :
245 return publicHardRegenReason ({ regenTrigger: record.reason });
246 default :
247 return 'This recommendation needs stronger evidence before it is safe to apply.' ;
248 }
249 }
250
251 function publicHardRegenReason ( plan ) {
252 switch (plan?.regenTrigger) {
253 case 'project_config_contradiction' :
254 return 'The recommendation tried to turn on a project setting that is already enabled. Re-run the investigation with refreshed project-config evidence.' ;
255 case 'cache_vary_safety' :
256 return 'The recommendation added shared CDN caching to output that varies by request geography without the required Vary header. Re-run the investigation with the cache-safety failure in scope.' ;
257 case 'semantic_safety' :
258 return 'This recommendation needs stronger framework evidence before it is safe to apply. Re-run the investigation with that evidence in scope.' ;
259 default :
260 return 'This recommendation needs stronger evidence before it is safe to apply. Re-run the investigation with those checks in scope.' ;
261 }
262 }
263
264 function recommendationMatchesActiveCandidate ( rec , candidates ) {
265 const ref = parseCandidateRef (rec?.candidateRef);
266 if ( ! ref) return true ;
267 return candidates. some (( candidate ) => candidateMatchesRef (candidate, ref));
268 }
269
270 function parseCandidateRef ( ref ) {
271 if ( typeof ref !== 'string' || ref. length === 0 ) return null ;
272 const [ kind , ... targetParts ] = ref. split ( ':' );
273 if ( ! kind) return null ;
274 return { kind, target: targetParts. join ( ':' ) };
275 }
276
277 function candidateMatchesRef ( candidate , ref ) {
278 if ( ! candidate || candidate.kind !== ref.kind) return false ;
279 if (candidate.scope === 'account' || ref.target === '<account>' ) return true ;
280
281 const candidateTarget = candidate.route ?? candidate.hostname ?? candidate.file ?? candidate.target ?? null ;
282 if ( ! candidateTarget || ! ref.target) return false ;
283
284 const a = String (candidateTarget);
285 const b = String (ref.target);
286 return a === b || canonicalizeRoute (a) === canonicalizeRoute (b);
287 }
288
289 function suppressReadyCoveredObservations ( observations , recommendations = []) {
290 if ( ! Array. isArray (observations) || observations. length === 0 ) return [];
291 const readyFamiliesByTarget = new Map ();
292 for ( const rec of recommendations) {
293 const parsed = parseCandidateRef (rec?.candidateRef);
294 const target = candidateTarget (rec?.candidateRef);
295 const family = candidateFamily (parsed?.kind);
296 if ( ! target || ! family) continue ;
297 const set = readyFamiliesByTarget. get (target) ?? new Set ();
298 set. add (family);
299 readyFamiliesByTarget. set (target, set);
300 }
301
302 return observations. filter (( observation ) => {
303 const parsed = parseCandidateRef (observation?.candidateRef);
304 const target = candidateTarget (observation?.candidateRef);
305 const family = candidateFamily (parsed?.kind);
306 if ( ! target || ! family) return true ;
307 return ! readyFamiliesByTarget. get (target)?. has (family);
308 });
309 }
310
311 function candidateFamily ( kind ) {
312 switch (kind) {
313 case 'uncached_route' :
314 case 'cache_header_gap' :
315 case 'missing_cache_headers' :
316 case 'max_age_without_s_maxage' :
317 return 'cache' ;
318 case 'slow_route' :
319 case 'cold_start' :
320 case 'external_api_slow' :
321 case 'cwv_poor' :
322 return 'performance' ;
323 case 'route_errors' :
324 return 'reliability' ;
325 case 'isr_overrevalidation' :
326 return 'isr' ;
327 case 'middleware_heavy' :
328 return 'middleware' ;
329 case 'build_minutes_fanout' :
330 return 'build' ;
331 default :
332 return kind || null ;
333 }
334 }
335
336 function candidateTarget ( ref ) {
337 if ( typeof ref !== 'string' ) return null ;
338 const idx = ref. indexOf ( ':' );
339 if (idx === - 1 ) return null ;
340 return ref. slice (idx + 1 );
341 }
342
343 function publicNoChangeReason ( reason ) {
344 if ( hasUnsupportedCacheLifeCdnText (reason)) {
345 return 'This candidate overlapped a cache-lifetime draft that did not meet the framework evidence bar. No supported change shipped from this run.' ;
346 }
347 return reason;
348 }
349
350 function buildDebugArtifact ({
351 recsRaw ,
352 recommendationsRaw ,
353 recommendations ,
354 gateRaw ,
355 abstentions = [],
356 observations = [],
357 heldBackObservations = [],
358 staleRecommendationDrops = [],
359 droppedContradictions = [],
360 }) {
361 const wrapper = Array. isArray (recsRaw) ? null : recsRaw;
362 const sourceRecords = Array. isArray (recsRaw)
363 ? recsRaw
364 : (recsRaw.recsGraded ?? []);
365 const summary = wrapper?.summary
366 ? {
367 ... wrapper.summary,
368 rawRecommendationCount: recommendationsRaw. length ,
369 renderedRecommendationCount: recommendations. length ,
370 }
371 : null ;
372 return {
373 schemaVersion: '1.0' ,
374 summary,
375 regenPlan: wrapper?.regenPlan ?? [],
376 qualityDropped: wrapper?.qualityDropped ?? [],
377 withheldRecommendations: wrapper?.withheldRecommendations ?? [],
378 abstentions,
379 observations,
380 heldBackObservations,
381 staleRecommendationDrops,
382 droppedContradictions,
383 sanitizerDropped: wrapper?.sanitizerDropped ?? [],
384 renderedRecommendationCount: recommendations. length ,
385 rawRecommendationCount: recommendationsRaw. length ,
386 gateBudget: gateRaw?.budget ?? null ,
387 recommendations: sourceRecords
388 . filter (( record ) => record && record.abstain !== true )
389 . map (( record ) => ({
390 candidateRef: record.candidateRef ?? null ,
391 what: record.what ?? null ,
392 verification: record.verification ?? null ,
393 quality: record.quality ?? null ,
394 passRate: record.passRate ?? record.verification?.passRate ?? null ,
395 avgQuality: record.avgQuality ?? null ,
396 needsReview: record.needsReview === true ,
397 sanitizerTrail: Array. isArray (record.sanitizerTrail) ? record.sanitizerTrail : [],
398 })),
399 };
400 }
401
402 function flattenObservations ( records ) {
403 const out = [];
404 for ( const record of records) {
405 if ( ! record || typeof record !== 'object' ) continue ;
406 if (record.observation && typeof record.observation === 'object' ) {
407 out. push ({
408 candidateRef: record.candidateRef ?? null ,
409 summary: coerceOptionalString (record.observation.summary),
410 evidence: record.observation.evidence ?? null ,
411 suggestedAction: record.observation.suggestedAction ?? null ,
412 kind: record.observation.kind ?? 'other' ,
413 });
414 continue ;
415 }
416 if ( 'summary' in record || 'evidence' in record || 'suggestedAction' in record || 'kind' in record) {
417 out. push ({
418 candidateRef: record.candidateRef ?? null ,
419 summary: coerceOptionalString (record.summary),
420 evidence: record.evidence ?? null ,
421 suggestedAction: record.suggestedAction ?? null ,
422 kind: record.kind ?? 'other' ,
423 });
424 }
425 }
426 return out;
427 }
428
429 function coerceOptionalString ( value ) {
430 return value == null ? value : String (value);
431 }
432
433 main (). catch (( err ) => {
434 console. error ( '[render-report] FAILED:' , err.message);
435 console. error (err.stack);
436 process. exit ( 1 );
437 });