Setting the file. One moment.
Collect Sub Agent Outputs · Vercel Optimize · vercel-labs/agent-skills · Skills Docs
ContentsBack to the top of the page 245
function normalizeOutput
— line 245
This file
Number 7.67
Position 67 of 155
Type JavaScript
Size 10 KB
Lines 296 scripts/ collect-sub-agent-outputs.mjs
JavaScript · 296 lines · 10 KB
,
...
a);
10
11 async function main () {
12 const args = parseArgs (process.argv. slice ( 2 ));
13 if (args.inputs. length === 0 && ! args.manifestPath) {
14 console. error ( 'usage: node scripts/collect-sub-agent-outputs.mjs [--manifest briefs/manifest.json] <output-file-or-dir...> [--out recommendations.json] [--strict]' );
15 process. exit ( 1 );
16 }
17
18 const manifest = args.manifestPath
19 ? JSON . parse ( await readFile (args.manifestPath, 'utf-8' ))
20 : null ;
21 const expected = manifest ? readExpectedBriefs (manifest) : [];
22 const preResolvedRecords = manifest ? readPreResolvedRecords (manifest) : [];
23 const files = args.inputs. length > 0 ? await collectInputFiles (args.inputs) : [];
24 const collected = [];
25 const summary = {
26 files: files. length ,
27 kept: 0 ,
28 abstained: 0 ,
29 parseFailed: 0 ,
30 nonObject: 0 ,
31 missingCandidateRef: 0 ,
32 };
33 const errors = [];
34
35 for ( const file of files) {
36 const raw = await readFile (file, 'utf-8' );
37 const extracted = extractJsonValue (raw);
38 if ( ! extracted.ok) {
39 summary.parseFailed ++ ;
40 const msg = `${ file }: ${ extracted . reason }` ;
41 if (args.strict) errors. push (msg);
42 else log ( `warn: ${ msg }` );
43 continue ;
44 }
45
46 const records = normalizeOutput (extracted.value);
47 if (records. length === 0 ) {
48 summary.nonObject ++ ;
49 const msg = `${ file }: JSON did not contain a recommendation or abstention object` ;
50 if (args.strict) errors. push (msg);
51 else log ( `warn: ${ msg }` );
52 continue ;
53 }
54
55 for ( const record of records) {
56 const candidateRef = record.candidateRef ?? inferCandidateRefFromFile (file, expected, records. length );
57 if ( ! candidateRef) {
58 summary.missingCandidateRef ++ ;
59 errors. push ( `${ file }: output is missing candidateRef` );
60 continue ;
61 }
62 collected. push ({
63 sourcePath: file,
64 record: record.candidateRef ? record : { ... record, candidateRef },
65 });
66 }
67 }
68
69 let ordered = collected;
70 if (expected. length > 0 ) {
71 const byRef = new Map ();
72 for ( const item of collected) {
73 const ref = item.record.candidateRef;
74 if ( ! expected. some (( b ) => b.candidateRef === ref)) {
75 errors. push ( `${ item . sourcePath }: unknown candidateRef ${ ref }` );
76 continue ;
77 }
78 if (byRef. has (ref)) {
79 errors. push ( `${ item . sourcePath }: duplicate output for candidateRef ${ ref }` );
80 continue ;
81 }
82 byRef. set (ref, item);
83 }
84 const missing = expected. filter (( b ) => ! byRef. has (b.candidateRef));
85 for ( const b of missing) errors. push ( `missing output for candidateRef ${ b . candidateRef }` );
86 ordered = expected. map (( b ) => byRef. get (b.candidateRef)). filter (Boolean);
87 } else {
88 ordered = collected. sort (( a , b ) => a.sourcePath. localeCompare (b.sourcePath));
89 }
90
91 const records = [ ... preResolvedRecords, ... ordered. map (( item ) => item.record)];
92 summary.kept = records. filter (( r ) => r?.abstain !== true ). length ;
93 summary.abstained = records. filter (( r ) => r?.abstain === true ). length ;
94
95 if (errors. length > 0 ) {
96 for ( const e of errors) log ( `error: ${ e }` );
97 process. exit ( 2 );
98 }
99 if (records. length === 0 ) {
100 log ( 'error: no recommendation or abstention records collected' );
101 process. exit ( 2 );
102 }
103
104 const serialized = JSON . stringify (records, null , 2 ) + ' \n ' ;
105 if (args.outPath) {
106 await mkdir ( dirname (args.outPath), { recursive: true });
107 await writeFile (args.outPath, serialized, 'utf-8' );
108 log ( `wrote ${ serialized . length }B → ${ args . outPath }` );
109 } else {
110 process.stdout. write (serialized);
111 }
112 log ( `done: ${ summary . files } files, ${ summary . kept } recommendation draft(s), ${ summary . abstained } found no supported change, ${ summary . parseFailed } parse failed, ${ summary . nonObject } invalid output(s)` );
113 }
114
115 function parseArgs ( argv ) {
116 const out = { inputs: [] };
117 for ( let i = 0 ; i < argv. length ; i ++ ) {
118 const a = argv[i];
119 if (a === '--manifest' ) out.manifestPath = resolve (argv[ ++ i]);
120 else if (a. startsWith ( '--manifest=' )) out.manifestPath = resolve (a. slice ( '--manifest=' . length ));
121 else if (a === '--out' ) out.outPath = resolve (argv[ ++ i]);
122 else if (a. startsWith ( '--out=' )) out.outPath = resolve (a. slice ( '--out=' . length ));
123 else if (a === '--strict' ) out.strict = true ;
124 else out.inputs. push ( resolve (a));
125 }
126 return out;
127 }
128
129 async function collectInputFiles ( paths ) {
130 const out = [];
131 for ( const p of paths) {
132 const s = await stat (p);
133 if (s. isDirectory ()) out. push ( ...await walkDir (p));
134 else if (s. isFile ()) out. push (p);
135 }
136 return out. sort (( a , b ) => a. localeCompare (b));
137 }
138
139 async function walkDir ( dir ) {
140 const entries = await readdir (dir, { withFileTypes: true });
141 const out = [];
142 for ( const e of entries. sort (( a , b ) => a.name. localeCompare (b.name))) {
143 if (e.name. startsWith ( '.' )) continue ;
144 const p = resolve (dir, e.name);
145 if (e. isDirectory ()) out. push ( ...await walkDir (p));
146 else if (e. isFile ()) out. push (p);
147 }
148 return out;
149 }
150
151 function readExpectedBriefs ( manifest ) {
152 if ( ! manifest || typeof manifest !== 'object' || ! Array. isArray (manifest.briefs)) {
153 throw new TypeError ( 'manifest must contain a briefs array' );
154 }
155 return manifest.briefs. map (( b , i ) => {
156 if ( ! b?.candidateRef) throw new TypeError ( `manifest.briefs[${ i }].candidateRef is required` );
157 return {
158 group: b.group ?? null ,
159 index: b.index ?? i,
160 candidateRef: b.candidateRef,
161 };
162 });
163 }
164
165 function readPreResolvedRecords ( manifest ) {
166 if ( ! manifest || ! Array. isArray (manifest.preResolvedRecords)) return [];
167 return manifest.preResolvedRecords. map (( r , i ) => {
168 if ( ! isRecordObject (r)) {
169 throw new TypeError ( `manifest.preResolvedRecords[${ i }] must be a recommendation or no-recommendation record` );
170 }
171 if ( ! r.candidateRef) {
172 throw new TypeError ( `manifest.preResolvedRecords[${ i }].candidateRef is required` );
173 }
174 return r;
175 });
176 }
177
178 function extractJsonValue ( raw ) {
179 for ( const block of extractFenceBlocks (raw)) {
180 const parsed = tryParseJson (block);
181 if (parsed.ok) return parsed;
182 }
183 const full = tryParseJson (raw);
184 if (full.ok) return full;
185 for ( const span of findBalancedJsonSpans (raw)) {
186 const parsed = tryParseJson (span);
187 if (parsed.ok) return parsed;
188 }
189 return { ok: false , reason: 'no valid JSON object or array found' };
190 }
191
192 function extractFenceBlocks ( raw ) {
193 const out = [];
194 const re = /```(?:json | JSON) ? \s * \n ( [\s\S] *? )```/ g ;
195 let m;
196 while ((m = re. exec (raw)) !== null ) out. push (m[ 1 ]. trim ());
197 return out;
198 }
199
200 function tryParseJson ( raw ) {
201 try {
202 return { ok: true , value: JSON . parse (raw. trim ()) };
203 } catch (err) {
204 return { ok: false , reason: err.message };
205 }
206 }
207
208 function findBalancedJsonSpans ( raw ) {
209 const spans = [];
210 for ( let i = 0 ; i < raw. length ; i ++ ) {
211 const ch = raw[i];
212 if (ch !== '{' && ch !== '[' ) continue ;
213 const closeFor = ch === '{' ? '}' : ']' ;
214 const stack = [closeFor];
215 let inString = false ;
216 let escape = false ;
217 for ( let j = i + 1 ; j < raw. length ; j ++ ) {
218 const c = raw[j];
219 if (inString) {
220 if (escape) escape = false ;
221 else if (c === ' \\ ' ) escape = true ;
222 else if (c === '"' ) inString = false ;
223 continue ;
224 }
225 if (c === '"' ) {
226 inString = true ;
227 continue ;
228 }
229 if (c === '{' ) stack. push ( '}' );
230 else if (c === '[' ) stack. push ( ']' );
231 else if (c === '}' || c === ']' ) {
232 if (stack. at ( - 1 ) !== c) break ;
233 stack. pop ();
234 if (stack. length === 0 ) {
235 spans. push (raw. slice (i, j + 1 ));
236 i = j;
237 break ;
238 }
239 }
240 }
241 }
242 return spans;
243 }
244
245 function normalizeOutput ( value ) {
246 const unwrapped = unwrapEnvelope (value);
247 if (Array. isArray (unwrapped)) return unwrapped. filter (isRecordObject);
248 if ( isRecordObject (unwrapped)) return [unwrapped];
249 if (unwrapped && typeof unwrapped === 'object' ) {
250 if ( isRecordObject (unwrapped.recommendation)) return [unwrapped.recommendation];
251 if (Array. isArray (unwrapped.recommendations)) return unwrapped.recommendations. filter (isRecordObject);
252 }
253 return [];
254 }
255
256 function unwrapEnvelope ( value ) {
257 let current = value;
258 for ( let depth = 0 ; depth < 2 ; depth ++ ) {
259 if ( ! current || typeof current !== 'object' || Array. isArray (current)) return current;
260 if (Array. isArray (current.recommendations) || current.recommendation) return current;
261 const keys = Object. keys (current);
262 const envelopeKey = [ 'data' , 'result' , 'insights' ]. find (( k ) => keys. length === 1 && k in current);
263 if ( ! envelopeKey) return current;
264 current = current[envelopeKey];
265 }
266 return current;
267 }
268
269 function isRecordObject ( value ) {
270 if ( ! value || typeof value !== 'object' || Array. isArray (value)) return false ;
271 if (value.abstain === true ) return true ;
272 return [ 'what' , 'why' , 'fix' , 'bucket' , 'affectedFiles' , 'citations' ]. some (( k ) => k in value);
273 }
274
275 function inferCandidateRefFromFile ( file , expected , recordCount ) {
276 if (recordCount !== 1 || expected. length === 0 ) return null ;
277 if (expected. length === 1 ) return expected[ 0 ].candidateRef;
278 const name = basename (file);
279 const matches = expected. filter (( b ) => {
280 if ( ! b.group && b.index == null ) return false ;
281 const group = escapeRegExp ( String (b.group ?? '' ));
282 const index = escapeRegExp ( String (b.index));
283 return new RegExp ( `(?:^|[^A-Za-z0-9])${ group }[-_.]?${ index }(?:[^A-Za-z0-9]|$)` ). test (name);
284 });
285 return matches. length === 1 ? matches[ 0 ].candidateRef : null ;
286 }
287
288 function escapeRegExp ( value ) {
289 return String (value). replace ( / [.*+?^${}()|[ \]\\ ] / g , ' \\ $&' );
290 }
291
292 main (). catch (( err ) => {
293 console. error ( '[collect-sub-agent-outputs] FAILED:' , err.message);
294 console. error (err.stack);
295 process. exit ( 1 );
296 });