Setting the file. One moment. Prepare Investigation Brief · Vercel Optimize · vercel-labs/agent-skills · Skills Docsscripts/prepare-investigation-brief.mjs
JavaScript·231 lines·8 KB
13
resolveFiles,
14 citationSubset,
15} from '../lib/investigation-brief.mjs';
16import { supportTopicSubset } from '../lib/support-topics.mjs';
17import { candidateRefFor } from '../lib/reconcile-candidates.mjs';
18import { formatCandidateLabel } from '../lib/display-labels.mjs';
19import { fileURLToPath } from 'node:url';
20
21const HERE = dirname(fileURLToPath(import.meta.url));
22const PLAYBOOKS_DIR = join(HERE, '..', 'references', 'playbooks');
23
24const log = (...a) => console.error('[prepare-brief]', ...a);
25
26async function main() {
27 const args = parseArgs(process.argv.slice(2));
28 if (!args.mergedPath || !args.investigationPath) {
29 console.error('usage: node scripts/prepare-investigation-brief.mjs <merged.json> <investigation.json> [--index N] [--group toLaunch|platform] [--out FILE]');
30 console.error(' or: node scripts/prepare-investigation-brief.mjs <merged.json> <investigation.json> --list');
31 process.exit(1);
32 }
33
34 const [merged, investigation] = await Promise.all([
35 readFile(args.mergedPath, 'utf-8').then(JSON.parse),
36 readFile(args.investigationPath, 'utf-8').then(JSON.parse),
37 ]);
38
39 if (args.list) {
40 const manifest = buildManifest(merged, investigation);
41 process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
42 return;
43 }
44
45 const group = args.group ?? 'toLaunch';
46 const index = args.index ?? 0;
47 const pool = Array.isArray(investigation[group]) ? investigation[group] : [];
48 if (index < 0 || index >= pool.length) {
49 console.error(`[prepare-brief] FATAL: ${group}[${index}] out of range (${group} has ${pool.length} entries)`);
50 process.exit(2);
51 }
52 let candidate = pool[index];
53
54 // Scan output may live at merged.codebase (older shape) or merged.signals.codebase
55 // (current shape, after the jq merge nests it under signals). Resolve either.
56 const codebase = pickCodebase(merged);
57 const signals = {
58 ...merged,
59 codebase,
60 };
61 const files = resolveFiles(candidate, signals);
62 candidate = {
63 ...candidate,
64 candidateRef: candidate.candidateRef ?? candidateRefFor(candidate, files),
65 };
66 const playbookId = inferPlaybook(signals);
67 const playbookBody = playbookId ? await tryReadPlaybook(playbookId) : null;
68 const frameworkPlaybookId = inferFrameworkPlaybook(signals);
69 const frameworkPlaybookBody = frameworkPlaybookId ? await tryReadPlaybook(frameworkPlaybookId) : null;
70
71 const stack = signals.stack ?? signals.codebase?.stack ?? {};
72 const framework = stack.framework ?? 'unknown';
73 const version = stack.frameworkVersion ?? 'unknown';
74 const citations = await citationSubset(candidate.kind, framework, version);
75 const supportTopics = await supportTopicSubset({
76 candidate,
77 signals,
78 framework,
79 version,
80 profile: playbookId,
81 frameworkPlaybookId,
82 });
83
84 const brief = buildBrief({
85 candidate,
86 candidateIndex: index,
87 candidateGroup: group,
88 files,
89 signals,
90 citations,
91 playbookId,
92 playbookBody,
93 frameworkPlaybookId,
94 frameworkPlaybookBody,
95 supportTopics,
96 generatedAt: args.deterministic ? null : new Date().toISOString(),
97 });
98
99 if (args.outPath) {
100 await mkdir(dirname(args.outPath), { recursive: true });
101 await writeBriefFile(args.outPath, brief, { force: args.force });
102 log(`wrote ${brief.length}B → ${args.outPath}`);
103 } else {
104 process.stdout.write(brief + '\n');
105 }
106}
107
108function buildManifest(merged, investigation) {
109 const out = [];
110 const groups = ['toLaunch', 'platform'];
111 for (const group of groups) {
112 const pool = Array.isArray(investigation[group]) ? investigation[group] : [];
113 pool.forEach((c, i) => {
114 const files = resolveFiles(c, { ...merged, codebase: pickCodebase(merged) });
115 const candidateRef = c.candidateRef ?? candidateRefFor(c, files);
116 out.push({
117 group,
118 index: i,
119 kind: c.kind,
120 route: c.route ?? c.hostname ?? null,
121 scope: c.scope ?? null,
122 priority: c.priority ?? null,
123 confidence: c.confidence ?? null,
124 o11ySignal: c.o11ySignal ?? null,
125 files,
126 candidateRef,
127 label: formatCandidateLabel({ ...c, files }),
128 });
129 });
130 }
131 return {
132 schemaVersion: '1.0',
133 totalBriefs: out.length,
134 toLaunchCount: out.filter((b) => b.group === 'toLaunch').length,
135 platformCount: out.filter((b) => b.group === 'platform').length,
136 preResolvedRecords: Array.isArray(investigation.preResolvedRecords)
137 ? investigation.preResolvedRecords
138 : [],
139 fanoutPlan: buildFanoutPlan(out),
140 briefs: out,
141 };
142}
143
144function buildFanoutPlan(briefs) {
145 const groups = new Map();
146 for (const brief of briefs) {
147 const key = candidateFamilyKey(brief);
148 const existing = groups.get(key) ?? {
149 familyKey: key,
150 label: brief.label,
151 kind: brief.kind,
152 primaryBrief: { group: brief.group, index: brief.index, candidateRef: brief.candidateRef },
153 relatedBriefs: [],
154 };
155 if (existing.primaryBrief.candidateRef !== brief.candidateRef) {
156 existing.relatedBriefs.push({ group: brief.group, index: brief.index, candidateRef: brief.candidateRef });
157 }
158 groups.set(key, existing);
159 }
160 return {
161 totalFamilies: groups.size,
162 families: [...groups.values()].map((g) => ({
163 ...g,
164 totalBriefs: 1 + g.relatedBriefs.length,
165 })),
166 };
167}
168
169function candidateFamilyKey(brief) {
170 const file = Array.isArray(brief.files) && brief.files.length > 0 ? brief.files[0] : null;
171 const target = file ?? brief.route ?? brief.scope ?? '<account>';
172 return `${brief.kind ?? 'unknown'}:${target}`;
173}
174
175// Prefer merged.codebase, fall back to merged.signals.codebase, then empty.
176// Also accepts a fully-shaped scan doc directly (used in tests).
177function pickCodebase(merged) {
178 if (!merged || typeof merged !== 'object') return {};
179 if (merged.codebase && typeof merged.codebase === 'object' && (merged.codebase.routes || merged.codebase.findings)) {
180 return merged.codebase;
181 }
182 if (merged.signals?.codebase && typeof merged.signals.codebase === 'object') {
183 return merged.signals.codebase;
184 }
185 return {};
186}
187
188async function tryReadPlaybook(id) {
189 try {
190 return await readFile(join(PLAYBOOKS_DIR, `${id}.md`), 'utf-8');
191 } catch {
192 return null;
193 }
194}
195
196function parseArgs(argv) {
197 const out = { positional: [] };
198 for (let i = 0; i < argv.length; i++) {
199 const a = argv[i];
200 if (a === '--index') out.index = Number(argv[++i]);
201 else if (a.startsWith('--index=')) out.index = Number(a.slice('--index='.length));
202 else if (a === '--group') out.group = argv[++i];
203 else if (a.startsWith('--group=')) out.group = a.slice('--group='.length);
204 else if (a === '--out') out.outPath = resolve(argv[++i]);
205 else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
206 else if (a === '--list') out.list = true;
207 else if (a === '--deterministic') out.deterministic = true;
208 else if (a === '--force') out.force = true;
209 else out.positional.push(a);
210 }
211 out.mergedPath = out.positional[0];
212 out.investigationPath = out.positional[1];
213 return out;
214}
215
216async function writeBriefFile(outPath, brief, { force = false } = {}) {
217 try {
218 await writeFile(outPath, brief + '\n', { encoding: 'utf-8', flag: force ? 'w' : 'wx' });
219 } catch (err) {
220 if (err?.code === 'EEXIST') {
221 throw new Error(`output file already exists: ${outPath}. Use a fresh run directory or pass --force to overwrite.`);
222 }
223 throw err;
224 }
225}
226
227main().catch((err) => {
228 console.error('[prepare-brief] FAILED:', err.message);
229 console.error(err.stack);
230 process.exit(1);
231});