#!/usr/bin/env node // Runs AFTER gate-investigations.mjs and BEFORE any sub-agent reads source. // Attaches per-candidate evidence.deepDive to gate.toLaunch + gate.platform. // Byte-stable apart from totalWallMs; each CLI query is isolated. import { readFile } from 'node:fs/promises'; import { queryMetric, readProjectJson, resolveCommandScope } from '../lib/vercel.mjs'; import { specsForCandidate, mergeIntoEvidence, SCANNER_KINDS, TIME_WINDOW } from '../lib/deep-dive.mjs'; const SCHEMA_VERSION = '1.0'; const log = (...a) => console.error('[deep-dive]', ...a); async function main() { // --cwd is load-bearing: the Vercel CLI resolves project/team from cwd's // .vercel/project.json. Outside the project, metric queries silently hit the // wrong team and look like "no traffic". We hard-fail on mismatch below. const positional = []; let explicitCwd = null; for (let i = 2; i < process.argv.length; i++) { const a = process.argv[i]; if (a === '--cwd' && i + 1 < process.argv.length) { explicitCwd = process.argv[++i]; } else if (a.startsWith('--cwd=')) { explicitCwd = a.slice('--cwd='.length); } else { positional.push(a); } } const mergedPath = positional[0]; const gatePath = positional[1]; if (!mergedPath || !gatePath) { console.error('usage: node scripts/deep-dive.mjs [--cwd ]'); process.exit(1); } const [merged, gate] = await Promise.all([ readFile(mergedPath, 'utf-8').then(JSON.parse), readFile(gatePath, 'utf-8').then(JSON.parse), ]); if (explicitCwd) { process.chdir(explicitCwd); log(`cwd: ${process.cwd()} (via --cwd)`); } const link = await readProjectJson(process.cwd()); if (!link) { console.error(`[deep-dive] FATAL: cwd ${process.cwd()} has no .vercel/project.json or .vercel/repo.json.`); console.error(' Re-run with --cwd pointing at the linked project, or cd into it first.'); console.error(' (The Vercel CLI resolves team/project from cwd; without a .vercel/ linkage every query returns empty rows for the wrong team.)'); process.exit(2); } if (merged.projectId && link.projectId !== merged.projectId) { console.error('[deep-dive] FATAL: cwd .vercel/ links a different project than merged.json.'); console.error(' Re-run with --cwd .'); process.exit(2); } if (merged.orgId && link.orgId && link.orgId !== merged.orgId) { console.error('[deep-dive] FATAL: cwd .vercel/ links the project to a different Vercel scope than signals.json.'); console.error(' Re-run with --cwd , or rerun collect-signals.mjs from the intended app directory.'); process.exit(2); } log(`cwd link OK (source ${link.source})`); const commandScope = await resolveDeepDiveCommandScope(merged, link); if (!commandScope.ok) { console.error(`[deep-dive] FATAL: could not resolve a CLI-safe Vercel scope (${commandScope.detail ?? commandScope.error ?? 'unknown'}).`); console.error(' Re-run collect-signals.mjs with the current skill, run `vercel switch `, or re-link with `vercel link --yes --project --team `.'); process.exit(2); } if (typeof commandScope.cliScope === 'string' && /^(team|usr)_/.test(commandScope.cliScope)) { console.error('[deep-dive] FATAL: commandScope.cliScope is a raw account ID, not a CLI-safe scope.'); console.error(' Re-run collect-signals.mjs with the current skill so deep-dive queries use the same team as the broad pass.'); process.exit(2); } const commandAccountId = commandScope.teamId ?? commandScope.userId ?? null; if (commandAccountId && link.orgId && link.orgId !== commandAccountId) { console.error('[deep-dive] FATAL: cwd .vercel/ links the project to a different Vercel scope than commandScope.'); console.error(' Re-run with --cwd , or rerun collect-signals.mjs from the intended app directory.'); process.exit(2); } const scope = commandScope.cliScope || undefined; log(`command scope resolved (source=${commandScope.source}; scoped=${scope ? 'yes' : 'no'})`); const toLaunch = Array.isArray(gate.toLaunch) ? gate.toLaunch : []; const platform = Array.isArray(gate.platform) ? gate.platform : []; log(`enriching ${toLaunch.length} toLaunch + ${platform.length} platform candidate(s) (window=${TIME_WINDOW})`); const t0 = Date.now(); const errors = []; // Flatten {candidate, spec}, fire all CLI calls in one Promise.all, re-group. // Avoids per-candidate sequentiality. const allCandidates = [...toLaunch.map((c, i) => ({ c, group: 'toLaunch', i })), ...platform.map((c, i) => ({ c, group: 'platform', i }))]; const flatJobs = []; const skipNotes = new Map(); for (const entry of allCandidates) { const specs = specsForCandidate(entry.c); if (specs.length === 0) { if (SCANNER_KINDS.has(entry.c.kind)) { skipNotes.set(`${entry.group}:${entry.i}`, 'scanner-driven (no deep-dive needed)'); } else if (entry.c.kind === 'platform_fluid_compute') { skipNotes.set(`${entry.group}:${entry.i}`, 'reused from broad pass (fnStartTypeByRoute)'); } else { skipNotes.set(`${entry.group}:${entry.i}`, `no deep-dive spec for kind=${entry.c.kind}`); } continue; } for (const spec of specs) { flatJobs.push({ entry, spec }); } } // Cut CLI calls two ways: (1) extract per-route slices already collected in // the broad pass; (2) dedupe identical queries across candidates (same route // can fire multiple gates wanting the same metric). let extractedFromBroadPass = 0; let dedupedQueryHits = 0; const broadPassResults = []; const remainingJobs = []; for (const job of flatJobs) { const extracted = tryExtractFromBroadPass(job.spec, merged); if (extracted) { broadPassResults.push({ entry: job.entry, spec: job.spec, ok: true, ...extracted }); extractedFromBroadPass++; } else { remainingJobs.push(job); } } // One CLI call per unique dedup key; jobs sharing a key share the result. const queryGroups = new Map(); for (const job of remainingJobs) { const key = queryKey(job.spec, scope); if (!queryGroups.has(key)) { queryGroups.set(key, { spec: job.spec, jobs: [] }); } queryGroups.get(key).jobs.push(job); } dedupedQueryHits = remainingJobs.length - queryGroups.size; const totalCliQueries = queryGroups.size; log(`${flatJobs.length} specs total: ${extractedFromBroadPass} extracted from broad-pass, ${dedupedQueryHits} deduped, ${totalCliQueries} CLI queries to run`); const groupResults = await Promise.all([...queryGroups.values()].map(async ({ spec, jobs }) => { const r = await queryMetric(spec.metricId, { aggregation: spec.aggregation, groupBy: spec.groupBy, filter: spec.filter, since: spec.since, limit: spec.limit, scope, }); return { spec, jobs, response: r }; })); const cliResults = []; for (const { spec, jobs, response: r } of groupResults) { if (!r.ok) { for (const job of jobs) { errors.push({ candidateGroup: job.entry.group, candidateIndex: job.entry.i, kind: job.entry.c.kind, route: job.entry.c.route ?? job.entry.c.hostname ?? null, specId: spec.id, code: r.code, }); cliResults.push({ entry: job.entry, spec, ok: false, error: r.code }); } continue; } const norm = normalizeResponse(r.data, spec); for (const job of jobs) { cliResults.push({ entry: job.entry, spec, ok: true, ...norm }); } } const results = [...broadPassResults, ...cliResults]; const wallMs = Date.now() - t0; log(`done in ${wallMs}ms (${totalCliQueries} CLI queries, ${extractedFromBroadPass} extracted from broad-pass, ${dedupedQueryHits} deduped, ${errors.length} errors)`); const byCandidate = new Map(); for (const res of results) { const k = `${res.entry.group}:${res.entry.i}`; if (!byCandidate.has(k)) byCandidate.set(k, []); byCandidate.get(k).push(res); } function enrich(c, group, i) { const k = `${group}:${i}`; const note = skipNotes.get(k); if (note) { return { ...c, evidence: { ...(c.evidence ?? {}), deepDive: { note }, }, }; } const list = byCandidate.get(k) ?? []; const merged = mergeIntoEvidence(list); return { ...c, evidence: { ...(c.evidence ?? {}), deepDive: merged, }, }; } const enrichedToLaunch = toLaunch.map((c, i) => enrich(c, 'toLaunch', i)); const enrichedPlatform = platform.map((c, i) => enrich(c, 'platform', i)); const out = { schemaVersion: SCHEMA_VERSION, appliedAt: new Date().toISOString(), candidatesEnriched: toLaunch.length + platform.length, specsTotal: flatJobs.length, queriesRun: totalCliQueries, extractedFromBroadPass, dedupedQueryHits, totalWallMs: wallMs, errors, toLaunch: enrichedToLaunch, platform: enrichedPlatform, }; process.stdout.write(JSON.stringify(out, null, 2) + '\n'); } async function resolveDeepDiveCommandScope(merged, link) { const linkedOrgId = merged.orgId ?? link.orgId ?? null; if (merged.commandScope?.ok && (merged.commandScope.cliScope || !linkedOrgId)) { return merged.commandScope; } if (merged.commandScope && merged.commandScope.ok === false) return merged.commandScope; return await resolveCommandScope({ projectId: merged.projectId ?? link.projectId ?? null, orgId: merged.orgId ?? link.orgId ?? null, }); } // Reduce CLI response to {value} or {rows:[{value,...dims}]}. The per-metric // underscore field (e.g. vercel_function_invocation_count_sum) gets renamed // to `value` for compactness. function normalizeResponse(data, spec) { if (!data || !Array.isArray(data.summary)) return { value: null }; const field = `${spec.metricId.replace(/\./g, '_')}_${spec.aggregation}`; if (spec.groupBy.length === 0) { const first = data.summary[0]; if (!first) return { value: null }; const v = first[field]; return { value: typeof v === 'number' ? round4(v) : null }; } const rows = data.summary.map((row) => { const out = { value: typeof row[field] === 'number' ? round4(row[field]) : null }; for (const dim of spec.groupBy) { if (row[dim] !== undefined) out[dim] = row[dim]; } return out; }); return { rows }; } function round4(n) { if (!Number.isFinite(n)) return n; return Math.round(n * 10000) / 10000; } // Skip the CLI call when broad-pass already collected the same metric grouped // by [route, dim]. Returns {rows} on hit, null on miss. Cuts rate-limit pressure // for per-route slice specs (startTypeSplit, cacheBreakdown, methodDistribution). function tryExtractFromBroadPass(spec, merged) { const eq = spec.broadPassEquivalent; if (!eq) return null; const broadRows = merged?.metrics?.[eq.key]?.rows; if (!Array.isArray(broadRows)) return null; const rows = []; for (const row of broadRows) { if (row.route !== eq.routeFilter) continue; const out = { value: typeof row.value === 'number' ? row.value : null }; for (const dim of (eq.projectDims ?? [])) { if (row[dim] !== undefined) out[dim] = row[dim]; } rows.push(out); } // Zero rows ≠ "no data" — broad-pass row limit may have truncated the route. // Fall through to CLI so the caller gets a definitive answer. if (rows.length === 0) return null; return { rows }; } // Two specs sharing this key answer the same question — one CLI call serves both. // Must include everything that affects the CLI's arg list. function queryKey(spec, scope) { const groupBy = [...(spec.groupBy ?? [])].sort(); return JSON.stringify({ metricId: spec.metricId, aggregation: spec.aggregation, groupBy, filter: spec.filter ?? null, since: spec.since ?? null, limit: spec.limit ?? null, scope: scope ?? null, }); } main().catch((err) => { console.error('[deep-dive] FAILED:', err.message); console.error(err.stack); process.exit(1); });