Setting the file. One moment. Gate Investigations · Vercel Optimize · vercel-labs/agent-skills · Skills Docsscripts/gate-investigations.mjs
JavaScript·166 lines·6 KB
;
11import { validateCandidates } from '../lib/gates/contract.mjs';
12import { applyHardGates } from '../lib/gates/hard-gates.mjs';
13import { selectLaunchCandidates } from '../lib/gates/select-candidates.mjs';
14import { routePathMatchScore } from '../lib/investigation-brief.mjs';
15
16const SCHEMA_VERSION = '1.1';
17
18async function main() {
19 const args = parseArgs(process.argv.slice(2));
20 if (!args.signalsPath) {
21 console.error('usage: node scripts/gate-investigations.mjs <signals.json> [--max-candidates N|all]');
22 console.error(' VERCEL_OPTIMIZE_MAX_CANDIDATES env var supported (same values)');
23 process.exit(1);
24 }
25 const budget = resolveBudget(args);
26 const signals = JSON.parse(await readFile(args.signalsPath, 'utf-8'));
27
28 const allSeeds = gates.flatMap((g) => {
29 try {
30 return g.gate(signals) ?? [];
31 } catch (err) {
32 console.error(`[gate-investigations] gate ${g.metadata?.id} threw: ${err.message}`);
33 return [];
34 }
35 });
36
37 const validSeeds = validateCandidates(allSeeds, { source: 'gate-output' });
38 const annotated = validSeeds.map(applyAuthDisqualifier);
39 const sorted = annotated.slice().sort(stableCompare);
40
41 // Next.js 16 segment-tree metric paths surface the same source file under
42 // many encoded labels (city variants, _tree/_index siblings, base64 flag
43 // prefixes). Without dedup the budget gets shredded ~4-10x per page.
44 const { deduped, dropped } = dedupeCandidates(sorted);
45 const displayAnnotated = deduped.map((candidate) => attachDisplayRoute(candidate, signals));
46 const hardGateResult = applyHardGates(displayAnnotated, signals);
47 const gateable = hardGateResult.allowed;
48
49 // Account-scope candidates don't compete with code-scope for the budget.
50 const codeScoped = gateable.filter((c) => !c.disqualified && c.scope !== 'account');
51 const platformScoped = gateable.filter((c) => !c.disqualified && c.scope === 'account');
52
53 const selection = selectLaunchCandidates(codeScoped, budget, {
54 diversify: args.budgetSource === 'default',
55 });
56 const toLaunch = selection.selected;
57 const skippedByBudget = selection.skipped;
58 const budgetLabel = budget === Infinity ? 'unlimited (all)' : String(budget);
59 const gated = [
60 ...gateable
61 .filter((c) => c.disqualified)
62 .map((c) => ({ ...c, gatedReason: c.disqualifyReason ?? 'disqualified' })),
63 ...hardGateResult.gated,
64 ...skippedByBudget.map((c) => ({
65 ...c,
66 gatedReason: `skippedByBudget (max-candidates=${budgetLabel}; raise with --max-candidates N or =all)`,
67 })),
68 ...dropped.map((d) => ({
69 ...d.candidate,
70 gatedReason: `coveredBy (${d.mergedInto}) — ${d.reason}`,
71 })),
72 ];
73
74 process.stdout.write(JSON.stringify({
75 schemaVersion: SCHEMA_VERSION,
76 gateVersion: GATE_VERSION,
77 appliedAt: new Date().toISOString(),
78 budget: {
79 maxCandidates: budget === Infinity ? 'all' : budget,
80 source: args.budgetSource,
81 selection: selection.selectionMode,
82 },
83 toLaunch,
84 platform: platformScoped,
85 gated,
86 gateMetadata: gates.map((g) => ({
87 id: g.metadata?.id,
88 threshold: g.metadata?.threshold,
89 billingDimension: g.metadata?.billingDimension,
90 sourceCitation: g.metadata?.sourceCitation,
91 })),
92 }, null, 2) + '\n');
93}
94
95function parseArgs(argv) {
96 const out = { positional: [] };
97 for (let i = 0; i < argv.length; i++) {
98 const a = argv[i];
99 if (a === '--max-candidates') out.maxCandidatesArg = argv[++i];
100 else if (a.startsWith('--max-candidates=')) out.maxCandidatesArg = a.slice('--max-candidates='.length);
101 else out.positional.push(a);
102 }
103 out.signalsPath = out.positional[0];
104 return out;
105}
106
107function resolveBudget(args) {
108 const raw = args.maxCandidatesArg ?? process.env.VERCEL_OPTIMIZE_MAX_CANDIDATES;
109 if (raw == null || raw === '') {
110 args.budgetSource = 'default';
111 return DEFAULT_MAX_CODE_CANDIDATES;
112 }
113 const trimmed = String(raw).trim().toLowerCase();
114 if (trimmed === 'all' || trimmed === 'unlimited' || trimmed === '-1') {
115 args.budgetSource = args.maxCandidatesArg != null ? 'flag' : 'env';
116 return Infinity;
117 }
118 const n = Number(trimmed);
119 if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
120 console.error(`[gate-investigations] bad budget value '${raw}'; expected positive integer or 'all'`);
121 process.exit(2);
122 }
123 args.budgetSource = args.maxCandidatesArg != null ? 'flag' : 'env';
124 return n;
125}
126
127// Total ordering: priority desc, kind asc, route asc. Underpins byte-identical output.
128function stableCompare(a, b) {
129 const pa = a.priority ?? 0;
130 const pb = b.priority ?? 0;
131 if (pa !== pb) return pb - pa;
132 const ka = String(a.kind ?? '');
133 const kb = String(b.kind ?? '');
134 if (ka !== kb) return ka.localeCompare(kb);
135 const ra = String(a.route ?? a.hostname ?? '');
136 const rb = String(b.route ?? b.hostname ?? '');
137 return ra.localeCompare(rb);
138}
139
140function attachDisplayRoute(candidate, signals) {
141 if (!candidate || candidate.scope !== 'route' || typeof candidate.route !== 'string') return candidate;
142 if (!candidate.route.includes('[*]')) return candidate;
143
144 const routes = (signals.codebase?.routes ?? [])
145 .map((route) => route?.routePath)
146 .filter((routePath) => typeof routePath === 'string' && routePath.length > 0);
147 if (routes.length === 0) return candidate;
148
149 let bestRoute = null;
150 let bestScore = 0;
151 for (const routePath of routes) {
152 const score = routePathMatchScore(routePath, candidate.route);
153 if (score > bestScore) {
154 bestRoute = routePath;
155 bestScore = score;
156 }
157 }
158
159 if (!bestRoute || bestScore <= 0 || bestRoute === candidate.route) return candidate;
160 return { ...candidate, displayRoute: bestRoute };
161}
162
163main().catch((err) => {
164 console.error('[gate-investigations] FAILED:', err.message);
165 process.exit(1);
166});