📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-06-03 16:02:56 +00:00
parent ac7fffe532
commit 5cb67428bd
803 changed files with 78437 additions and 401 deletions
@@ -0,0 +1,56 @@
#!/usr/bin/env node
// Mid-flow checkpoint: should the orchestrator ask the user to raise the budget?
// JSON output is the contract the orchestrator parses; markdown is human-only.
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { buildBudgetSummary, renderBudgetSummaryMarkdown } from '../lib/budget-summary.mjs';
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.gatePath) {
console.error('usage: node scripts/budget-summary.mjs <gate.json> [--format json|markdown] [--no-prompt]');
process.exit(1);
}
const gate = JSON.parse(await readFile(args.gatePath, 'utf-8'));
const summary = buildBudgetSummary(gate);
// --no-prompt: CI / non-interactive hosts collapse the checkpoint to a logging hop.
if (args.noPrompt) {
summary.shouldAsk = false;
summary.reason = 'forced false via --no-prompt (non-interactive host)';
summary.printContract = null;
summary.questionText = '';
summary.questionPayload = null;
summary.options = [];
summary.chatPreview = `Audit scope: no question needed — ${summary.reason}.`;
summary.exactChatMessage = {
body: summary.chatPreview,
lineCount: summary.chatPreview.split('\n').length,
sha256: createHash('sha256').update(summary.chatPreview).digest('hex'),
};
summary.printCheck = null;
}
if (args.format === 'markdown') {
process.stdout.write(renderBudgetSummaryMarkdown(summary) + '\n');
} else {
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
}
}
function parseArgs(argv) {
const out = { positional: [], format: 'json' };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--format') out.format = argv[++i];
else if (a.startsWith('--format=')) out.format = a.slice('--format='.length);
else if (a === '--no-prompt') out.noPrompt = true;
else out.positional.push(a);
}
out.gatePath = out.positional[0];
return out;
}
main().catch((err) => {
console.error('[budget-summary] FAILED:', err.message);
process.exit(1);
});
@@ -0,0 +1,74 @@
#!/usr/bin/env node
// Regenerates references/{scanner-patterns,candidates}.md from lib/{scanners,gates}/*
// metadata. The .mjs files are the source of truth; check-docs-fresh.mjs blocks
// PRs where the regenerated output diverges from what's checked in.
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { scanners } from '../lib/scanners/index.mjs';
import { gates, MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const REFS = join(HERE, '..', 'references');
const GENERATED_BANNER =
'<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->\n' +
'<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->\n' +
'<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->\n\n';
async function main() {
await writeFile(join(REFS, 'scanner-patterns.md'), renderScanners());
await writeFile(join(REFS, 'candidates.md'), renderCandidates());
console.error('[build-docs] wrote scanner-patterns.md + candidates.md');
}
function renderScanners() {
const sorted = scanners.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Scanner patterns\n\n';
out += 'AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares `trafficIndependent: true`.\n\n';
out += `Total scanners: ${sorted.length}.\n\n`;
out += '## Patterns\n\n';
for (const s of sorted) {
const m = s.metadata;
out += `### \`${m.id}\`${m.title}\n\n`;
out += `- **Severity**: ${m.severity}\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Traffic-independent**: ${m.trafficIndependent ? 'yes (cold-path findings survive the doctrine drop)' : 'no (cold-path findings get dropped)'}\n\n`;
out += `**Description.** ${m.description}\n\n`;
out += `**Fix.** ${m.fix}\n\n`;
if (m.citations?.length) {
out += `**Citations:**\n${m.citations.map((c) => `- \`${c}\``).join('\n')}\n\n`;
}
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function renderCandidates() {
const sorted = gates.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Candidate gates\n\n';
out += 'The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.\n\n';
out += `Total gates: ${sorted.length}. Budget cap: \`MAX_CODE_CANDIDATES = ${MAX_CODE_CANDIDATES}\`. Gate version: \`${GATE_VERSION}\`.\n\n`;
out += '## Gates\n\n';
for (const g of sorted) {
const m = g.metadata;
out += `### \`${m.id}\`\n\n`;
out += `- **Threshold**: \`${m.threshold}\`\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Scope**: ${m.scope}\n`;
out += `- **Source citation**: \`${m.sourceCitation}\`\n\n`;
out += `${m.description}\n\n`;
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function trimTrailingBlankLine(value) {
return value.replace(/\n{2,}$/, '\n');
}
main().catch((err) => {
console.error('[build-docs] FAILED:', err.message);
process.exit(1);
});
@@ -0,0 +1,81 @@
#!/usr/bin/env node
// Offline citation-library consistency checks. This intentionally does not
// fetch URLs; it validates the local allow-list contract used by sanitizers.
import { loadLibrary, matchesFrameworkVersion } from '../lib/citations.mjs';
const URL_RE = /^https:\/\/[A-Za-z0-9.-]+\/\S*$/;
const SKILL_REF_RE = /^[\w-]+:[\w-]+$/;
const BANNED_STALE_URLS = new Set([
'https://nextjs.org/docs/app/api-reference/functions/cache-life',
'https://nextjs.org/docs/app/api-reference/functions/cache-tag',
'https://nextjs.org/docs/app/api-reference/functions/revalidate-tag',
'https://nextjs.org/docs/app/api-reference/functions/revalidate-path',
'https://nextjs.org/docs/app/api-reference/functions/cache',
]);
async function main() {
const lib = await loadLibrary();
const errors = [];
if (!Array.isArray(lib.urls)) errors.push('docs-library.urls must be an array');
if (!Array.isArray(lib.ruleSkillRefs)) errors.push('docs-library.ruleSkillRefs must be an array');
for (const [i, entry] of (lib.urls ?? []).entries()) {
const label = `urls[${i}]`;
if (!URL_RE.test(entry?.url ?? '')) errors.push(`${label}.url must be an https URL`);
if (BANNED_STALE_URLS.has(entry?.url)) {
errors.push(`${label}.url uses a stale Next.js docs path: ${entry.url}`);
}
if (typeof entry.topic !== 'string' || entry.topic.trim() === '') errors.push(`${label}.topic is required`);
if (!Array.isArray(entry.appliesTo)) errors.push(`${label}.appliesTo must be an array`);
validateFrameworks(entry.applicableFrameworks, `${label}.applicableFrameworks`, errors);
}
const seenRules = new Set();
for (const [i, entry] of (lib.ruleSkillRefs ?? []).entries()) {
const label = `ruleSkillRefs[${i}]`;
const ref = `${entry?.skill ?? ''}:${entry?.rule ?? ''}`;
if (!SKILL_REF_RE.test(ref)) errors.push(`${label} must contain skill + rule identifiers`);
if (seenRules.has(ref)) errors.push(`${label} duplicate: ${ref}`);
seenRules.add(ref);
if (typeof (entry.description ?? entry.topic) !== 'string' || (entry.description ?? entry.topic).trim() === '') {
errors.push(`${label}.topic or .description is required`);
}
validateFrameworks(entry.applicableFrameworks, `${label}.applicableFrameworks`, errors);
}
if (errors.length > 0) {
for (const error of errors) console.error(`[check-citations] ${error}`);
process.exit(1);
}
console.error(`[check-citations] OK — ${lib.urls.length} URLs, ${lib.ruleSkillRefs.length} skill-rule refs`);
}
function validateFrameworks(patterns, label, errors) {
if (!Array.isArray(patterns) || patterns.length === 0) {
errors.push(`${label} must be a non-empty array`);
return;
}
for (const pattern of patterns) {
if (typeof pattern !== 'string' || pattern.trim() === '') {
errors.push(`${label} contains an empty pattern`);
continue;
}
if (pattern === '*') continue;
// Smoke-check parser coverage with a modern Next version. Unknown framework
// patterns are still valid as long as the syntax is recognizable.
if (!/^[\w-]+@(?:\*|\d+(?:\.\d+){0,2}|[<>]=?\s*\d+(?:\.\d+){0,2})(?:\s*\|\|\s*[\w-]+@(?:\*|\d+(?:\.\d+){0,2}|[<>]=?\s*\d+(?:\.\d+){0,2}))*$/.test(pattern)) {
errors.push(`${label} has unsupported pattern: ${pattern}`);
continue;
}
matchesFrameworkVersion(pattern, 'next', '16.0.0');
}
}
main().catch((err) => {
console.error('[check-citations] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
@@ -0,0 +1,93 @@
#!/usr/bin/env node
// CI gate: regenerate the reference docs in memory and diff against what's
// checked in. Non-zero exit forces contributors to run build-docs.mjs.
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { scanners } from '../lib/scanners/index.mjs';
import { gates, MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const REFS = join(HERE, '..', 'references');
const GENERATED_BANNER =
'<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->\n' +
'<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->\n' +
'<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->\n\n';
async function main() {
const expected = {
'scanner-patterns.md': renderScanners(),
'candidates.md': renderCandidates(),
};
let stale = false;
for (const [name, content] of Object.entries(expected)) {
let actual;
try { actual = await readFile(join(REFS, name), 'utf-8'); }
catch {
console.error(`[check-docs-fresh] ${name} does not exist. Run \`node scripts/build-docs.mjs\`.`);
stale = true;
continue;
}
if (actual !== content) {
console.error(`[check-docs-fresh] ${name} is stale. Run \`node scripts/build-docs.mjs\` and commit.`);
stale = true;
}
}
if (stale) process.exit(1);
console.error('[check-docs-fresh] OK — generated docs match source');
}
// MUST stay byte-identical to build-docs.mjs renderers — duplication is the contract.
function renderScanners() {
const sorted = scanners.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Scanner patterns\n\n';
out += 'AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares `trafficIndependent: true`.\n\n';
out += `Total scanners: ${sorted.length}.\n\n`;
out += '## Patterns\n\n';
for (const s of sorted) {
const m = s.metadata;
out += `### \`${m.id}\`${m.title}\n\n`;
out += `- **Severity**: ${m.severity}\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Traffic-independent**: ${m.trafficIndependent ? 'yes (cold-path findings survive the doctrine drop)' : 'no (cold-path findings get dropped)'}\n\n`;
out += `**Description.** ${m.description}\n\n`;
out += `**Fix.** ${m.fix}\n\n`;
if (m.citations?.length) {
out += `**Citations:**\n${m.citations.map((c) => `- \`${c}\``).join('\n')}\n\n`;
}
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function renderCandidates() {
const sorted = gates.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Candidate gates\n\n';
out += 'The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.\n\n';
out += `Total gates: ${sorted.length}. Budget cap: \`MAX_CODE_CANDIDATES = ${MAX_CODE_CANDIDATES}\`. Gate version: \`${GATE_VERSION}\`.\n\n`;
out += '## Gates\n\n';
for (const g of sorted) {
const m = g.metadata;
out += `### \`${m.id}\`\n\n`;
out += `- **Threshold**: \`${m.threshold}\`\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Scope**: ${m.scope}\n`;
out += `- **Source citation**: \`${m.sourceCitation}\`\n\n`;
out += `${m.description}\n\n`;
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function trimTrailingBlankLine(value) {
return value.replace(/\n{2,}$/, '\n');
}
main().catch((err) => {
console.error('[check-docs-fresh] FAILED:', err.message);
process.exit(1);
});
@@ -0,0 +1,576 @@
#!/usr/bin/env node
// Emits signals.json: Vercel CLI capability probe + project config + plan +
// usage + codebase stack + metric queries. Status → stderr, JSON → stdout.
// Degrades gracefully when capabilities are missing.
import {
checkCliVersion,
checkAuth,
resolveProjectId,
resolveCommandScope,
hasObservabilityPlus,
checkObservabilityPlusConfiguration,
getMetricsSchema,
getProjectConfig,
getAccountPlan,
getContract,
getUsage,
filterUsageByProject,
inferPlan,
queryMetric,
detectStack,
redactSensitiveText,
} from '../lib/vercel.mjs';
import { classifyFrameworkSupport } from '../lib/framework-support.mjs';
import { QUERIES, TIME_WINDOW, normalizerFor } from '../lib/queries.mjs';
const SCHEMA_VERSION = '1.2';
const log = (...args) => console.error('[collect-signals]', ...args);
function parseArgs(argv) {
let explicitProjectId = null;
let continueWithoutObservability = process.env.VERCEL_OPTIMIZE_CONTINUE_WITHOUT_OBSERVABILITY === '1';
let continueUnsupportedFramework = process.env.VERCEL_OPTIMIZE_CONTINUE_UNSUPPORTED_FRAMEWORK === '1';
for (const arg of argv) {
if (arg === '--continue-without-observability') {
continueWithoutObservability = true;
continue;
}
if (arg === '--continue-unsupported-framework') {
continueUnsupportedFramework = true;
continue;
}
if (arg.startsWith('--')) {
throw new Error(`UNKNOWN_ARG: ${arg}`);
}
if (!explicitProjectId) {
explicitProjectId = arg;
continue;
}
throw new Error(`UNKNOWN_ARG: ${arg}`);
}
return { explicitProjectId, continueWithoutObservability, continueUnsupportedFramework };
}
async function main() {
const { explicitProjectId, continueWithoutObservability, continueUnsupportedFramework } = parseArgs(process.argv.slice(2));
log('checking Vercel CLI version…');
const cli = await checkCliVersion();
log(`vercel CLI v${cli.join('.')} OK`);
log('checking auth…');
await checkAuth();
log('auth OK');
log('resolving project id…');
const project = await resolveProjectId(explicitProjectId);
if (!project) {
throw new Error(
'NO_PROJECT_ID: pass one as argv, set VERCEL_PROJECT_ID, or run `vercel link` in this directory.'
);
}
log(`project link resolved (source=${project.source}; teamScope=${project.orgId ? 'yes' : 'no'})`);
if (!project.orgId) {
throw new Error('PROJECT_SCOPE_UNRESOLVED: the project was resolved without an owner account. Ask the user which Vercel team or personal scope owns the project, then rerun from a linked app directory or set VERCEL_PROJECT_ID with VERCEL_ORG_ID for that scope.');
}
log('checking framework support…');
const stack = await detectStack();
const frameworkSupport = classifyFrameworkSupport(stack);
log(`framework=${stack.framework}@${stack.frameworkVersion ?? '?'} support=${frameworkSupport.status}`);
if (!frameworkSupport.ok && !continueUnsupportedFramework) {
writeOutput({
schemaVersion: SCHEMA_VERSION,
collectedAt: new Date().toISOString(),
timeWindow: TIME_WINDOW,
projectId: project.projectId,
orgId: project.orgId,
projectIdSource: project.source,
commandScope: null,
frameworkSupport,
frameworkSupportBlocker: frameworkSupport.blocker,
frameworkSupportDetail: frameworkSupport.detail,
observabilityPlus: null,
observabilityPlusPreflight: null,
observabilityPlusUsable: null,
observabilityPlusBlocker: null,
observabilityPlusBlockerDetail: null,
plan: {
plan: 'uncertain',
reason: 'not collected before unsupported-framework confirmation',
},
project: null,
contract: null,
usage: null,
usageScope: null,
usageTeamTotal: null,
usageError: 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK',
stack,
metrics: {},
metricsSchema: null,
}, { usable: true, blocker: null, detail: 'Observability Plus was not checked.' }, frameworkSupport);
return;
}
if (!frameworkSupport.ok && continueUnsupportedFramework) {
log('continuing after unsupported framework blocker because --continue-unsupported-framework was set');
}
log('resolving Vercel CLI command scope…');
const commandScope = await resolveCommandScope(project);
if (!commandScope.ok) {
throw new Error(`SCOPE_UNRESOLVED: ${commandScope.detail} Run \`vercel switch <team>\` or re-link with \`vercel link --yes --project <project-name-or-id> --team <team-slug>\`.`);
}
const scope = commandScope.cliScope || undefined;
log(`command scope resolved (source=${commandScope.source}; scoped=${scope ? 'yes' : 'no'})`);
log('validating linked project belongs to the resolved scope…');
const projectCfg = await getProjectConfig(project.projectId, project.orgId);
const projectScope = validateProjectScope(projectCfg, project);
if (!projectScope.ok) {
throw new Error(`PROJECT_SCOPE_MISMATCH: ${projectScope.detail} Ask the user to confirm the exact Vercel project and team/personal scope, then rerun after \`vercel link --yes --project <project-name-or-id> --team <team-slug>\` or after setting both VERCEL_PROJECT_ID and VERCEL_ORG_ID for the intended scope.`);
}
log(`project scope verified (source=${projectScope.source})`);
log('checking Observability Plus configuration…');
const observabilityPlusConfig = await checkObservabilityPlusConfiguration({
orgId: project.orgId,
projectId: project.projectId,
});
log(`observabilityPlusPreflight=${observabilityPlusConfig.access === true ? 'enabled' : observabilityPlusConfig.blocker ?? 'unknown'} (${observabilityPlusConfig.source})`);
let oplus = observabilityPlusConfig.access === true;
if (observabilityPlusConfig.access == null) {
log('Observability Plus configuration preflight inconclusive; falling back to metrics schema probe…');
oplus = await hasObservabilityPlus(scope);
}
log(`observabilityPlus=${oplus}`);
const schema = oplus ? await getMetricsSchema(scope) : null;
if (oplus && schema) {
const count = Array.isArray(schema) ? schema.length : (schema.metrics?.length ?? 0);
log(`metric catalog: ${count} metrics available`);
}
// Check one cheap metric before pulling slower project context. If this fails,
// the orchestrator can ask the user immediately instead of waiting on billing.
let metrics = {};
let metricsCanaryOk = false;
if (oplus) {
log(`checking Observability Plus metrics access (window=${TIME_WINDOW})…`);
const t0 = Date.now();
const canary = await queryMetric('vercel.request.count', {
aggregation: 'sum',
since: TIME_WINDOW,
limit: 1,
scope,
});
metricsCanaryOk = !!canary?.ok;
if (!metricsCanaryOk) {
metrics = {
observabilityPlusCanary: {
...canary,
metricId: 'vercel.request.count',
aggregation: 'sum',
},
};
log(`metrics access check failed: ${canary?.code ?? 'unknown'} — skipping full metrics fan-out`);
} else {
log(`metrics access check passed in ${Date.now() - t0}ms`);
}
} else {
log('skipping metric queries (Observability Plus preflight did not confirm access)');
}
let oplusDiag = observabilityPlusConfig.access === false
? {
usable: false,
blocker: observabilityPlusConfig.blocker,
detail: observabilityPlusConfig.detail,
}
: (metricsCanaryOk
? { usable: true, blocker: null, detail: 'Observability Plus metrics access check passed.' }
: diagnoseObservabilityPlus(metrics, oplus));
if (!oplusDiag.usable && !continueWithoutObservability) {
writeOutput({
schemaVersion: SCHEMA_VERSION,
collectedAt: new Date().toISOString(),
timeWindow: TIME_WINDOW,
projectId: project.projectId,
orgId: project.orgId,
projectIdSource: project.source,
commandScope,
observabilityPlus: oplus,
observabilityPlusPreflight: observabilityPlusConfig,
observabilityPlusUsable: oplusDiag.usable,
observabilityPlusBlocker: oplusDiag.blocker,
observabilityPlusBlockerDetail: oplusDiag.detail,
frameworkSupport,
frameworkSupportBlocker: frameworkSupport.blocker,
frameworkSupportDetail: frameworkSupport.detail,
plan: {
plan: 'uncertain',
reason: 'not collected before Observability Plus blocker confirmation',
},
project: projectCfg,
contract: null,
usage: null,
usageScope: null,
usageTeamTotal: null,
usageError: 'NOT_COLLECTED_OBSERVABILITY_BLOCKED',
stack: null,
metrics,
metricsSchema: schema,
}, oplusDiag);
return;
}
if (!oplusDiag.usable && continueWithoutObservability) {
log('continuing after Observability Plus blocker because --continue-without-observability was set');
}
log('pulling account plan + contract + usage in parallel…');
const [accountPlan, contract, usageResult] = await Promise.all([
getAccountPlan(project.orgId || scope),
getContract(scope),
getUsage({ days: 14, scope }),
]);
let usage = null;
let usageContextMismatch = false;
let usageTotalCost = null;
let usageScope = 'team';
let usageTeamTotal = null;
if (usageResult?.ok) {
usage = usageResult.data;
const contractContext = contract?.context;
if (usage?.context && contractContext && usage.context !== contractContext) {
usageContextMismatch = true;
log(`usage: WARNING context mismatch — returned context=${usage.context} but project team=${contractContext}; treating usage as unavailable for this project`);
usage = null;
} else {
// Capture team total pre-filter so the report can label "this project vs team-wide" honestly.
usageTeamTotal = sumUsageCosts(usage);
const filterResult = filterUsageByProject(usage, project.projectId, projectCfg?.name);
if (filterResult.matched) {
usage = filterResult.filtered;
usageScope = 'project';
usageTotalCost = sumUsageCosts(usage);
log(`usage: filtered to project — ~$${usageTotalCost.toFixed(2)} (team-wide ~$${usageTeamTotal.toFixed(2)}; unattributed ~$${filterResult.unattributedTotal.toFixed(2)})`);
} else {
usageTotalCost = usageTeamTotal;
log(`usage: ~$${usageTotalCost.toFixed(2)} billed across services (team-wide — no per-project usage rows matched the linked project; report will label this team-wide)`);
}
}
} else {
log(`usage: unavailable (${usageResult?.code ?? 'unknown'}) — degrading to scanner+metrics-only mode`);
}
const planInfo = inferPlan(contract, { accountPlan, usageTotalCost });
log(`plan=${planInfo.plan} (${planInfo.reason})`);
if (projectCfg?.error) {
log(`project config: failed (${projectCfg.error}) — gates that need it will skip`);
}
log(`stack: ${stack.framework}@${stack.frameworkVersion ?? '?'} ${stack.hasAppRouter ? 'app-router' : ''}${stack.hasPagesRouter ? ' pages-router' : ''}${stack.orm !== 'none' ? ` orm=${stack.orm}` : ''}`);
// Each query is wrapped; one failure degrades only that metric.
if (oplus && metricsCanaryOk) {
log(`querying observability metrics (${QUERIES.length} metrics in parallel)…`);
const t0 = Date.now();
metrics = await collectMetrics(scope);
const wallMs = Date.now() - t0;
const counts = Object.fromEntries(
Object.entries(metrics).map(([k, v]) => {
if (!v) return [k, 'null'];
if (!v.ok) return [k, `err:${v.code}`];
const rows = Array.isArray(v.rows) ? v.rows.length : 0;
return [k, `${rows} rows`];
})
);
log(`metrics collected in ${wallMs}ms: ${JSON.stringify(counts)}`);
}
// The `vercel metrics schema` probe alone is NOT a reliable usability signal:
// it can return OK while per-route queries fail with payment_required (metrics
// unavailable for the team) or FORBIDDEN (auth-scope mismatch). Diagnose AFTER
// running queries by counting failure codes so the orchestrator can PAUSE and
// surface the choice before falling back to scanner-only mode.
oplusDiag = observabilityPlusConfig.access === false
? {
usable: false,
blocker: observabilityPlusConfig.blocker,
detail: observabilityPlusConfig.detail,
}
: diagnoseObservabilityPlus(metrics, oplus);
const output = {
schemaVersion: SCHEMA_VERSION,
collectedAt: new Date().toISOString(),
timeWindow: TIME_WINDOW,
projectId: project.projectId,
orgId: project.orgId,
projectIdSource: project.source,
commandScope,
observabilityPlus: oplus,
observabilityPlusPreflight: observabilityPlusConfig,
observabilityPlusUsable: oplusDiag.usable,
observabilityPlusBlocker: oplusDiag.blocker,
observabilityPlusBlockerDetail: oplusDiag.detail,
frameworkSupport,
frameworkSupportBlocker: frameworkSupport.blocker,
frameworkSupportDetail: frameworkSupport.detail,
plan: planInfo,
project: projectCfg,
contract,
usage,
usageScope,
usageTeamTotal,
usageError: usageResult?.ok
? (usageContextMismatch ? 'USAGE_CONTEXT_MISMATCH' : null)
: (usageResult?.code ?? 'UNKNOWN'),
stack,
metrics,
metricsSchema: schema,
};
writeOutput(output, oplusDiag);
}
function writeOutput(output, oplusDiag, frameworkSupport = output.frameworkSupport) {
if (frameworkSupport?.blocker) {
log(`⚠ Framework is not supported for metric-backed route-to-file optimization: ${frameworkSupport.detail}`);
log(' The orchestrator should PAUSE and ask whether to continue with a limited platform/scanner audit.');
}
if (!oplusDiag.usable) {
log(`⚠ Observability Plus is NOT usable on this project: blocker=${oplusDiag.blocker} (${oplusDiag.detail})`);
log(' The orchestrator should PAUSE and follow the blocker-specific remediation before proceeding.');
}
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
log('done');
}
function validateProjectScope(projectCfg, project) {
if (!projectCfg || projectCfg.error) {
return {
ok: false,
source: 'project-api',
detail: `The resolved account could not read the resolved project (project API error=${projectCfg?.error ?? 'unknown'}).`,
};
}
if (projectCfg.id && String(projectCfg.id) !== String(project.projectId)) {
return {
ok: false,
source: 'project-api',
detail: 'The project API returned a different project than the collector resolved from the link or environment.',
};
}
const ownerId = firstString(
projectCfg.accountId,
projectCfg.orgId,
projectCfg.ownerId,
projectCfg.teamId,
projectCfg.team?.id,
projectCfg.account?.id,
projectCfg.owner?.id,
);
if (ownerId && project.orgId && String(ownerId) !== String(project.orgId)) {
return {
ok: false,
source: 'project-api',
detail: 'The project API returned an owner account that differs from the collector-resolved account.',
};
}
return {
ok: true,
source: ownerId ? 'project-api-owner' : 'project-api-readable',
};
}
function firstString(...values) {
return values.find((value) => typeof value === 'string' && value.trim() !== '') ?? null;
}
async function collectMetrics(scope) {
const results = await Promise.all(
QUERIES.map(async (entry) => {
const r = await queryMetric(entry.metricId, {
aggregation: entry.aggregation,
groupBy: entry.groupBy,
filter: entry.filter,
since: TIME_WINDOW,
limit: entry.limit,
scope,
});
return [entry, r];
})
);
const out = {};
for (const [entry, result] of results) {
out[entry.id] = enrichEntry(entry, result);
}
return out;
}
function enrichEntry(entry, result) {
if (!result?.ok) {
return {
...result,
metricId: entry.metricId,
aggregation: entry.aggregation,
groupBy: entry.groupBy,
};
}
const normalize = normalizerFor(entry);
const { rows } = normalize(result.data);
return {
...result,
rows,
metricId: entry.metricId,
aggregation: entry.aggregation,
groupBy: entry.groupBy,
};
}
// `vercel usage --format json` shape is documented but not stable across CLI
// versions; try several roots, return null if none match.
function sumUsageCosts(usage) {
if (!usage) return null;
if (typeof usage.totalCost === 'number') return usage.totalCost;
if (typeof usage.totals?.billedCost === 'number') return usage.totals.billedCost;
if (Array.isArray(usage.services)) {
return usage.services.reduce((s, x) => s + (x.billedCost ?? x.cost ?? 0), 0);
}
if (Array.isArray(usage.breakdown?.data)) {
return usage.breakdown.data.reduce((s, d) => {
if (Array.isArray(d.services)) {
return s + d.services.reduce((ss, x) => ss + (x.billedCost ?? x.cost ?? 0), 0);
}
return s + (d.billedCost ?? d.cost ?? 0);
}, 0);
}
return null;
}
// Returns { usable, blocker, detail }. `blocker` enum:
// null | 'no_oplus_probe' | 'project_disabled' | 'payment_required' |
// 'forbidden' | 'daily_quota_exceeded' | 'project_not_found' |
// 'not_linked' | 'all_failed_other' | 'no_traffic'
export function diagnoseObservabilityPlus(metrics, oplusProbe) {
if (!oplusProbe) {
return {
usable: false,
blocker: 'no_oplus_probe',
detail: 'vercel metrics schema returned non-OK; the team does not have Observability Plus enabled.',
};
}
const entries = Object.values(metrics);
if (entries.length === 0) {
return { usable: false, blocker: 'no_oplus_probe', detail: 'No metrics were attempted.' };
}
const failures = entries.filter((m) => m && m.ok === false);
const successes = entries.filter((m) => m && m.ok !== false);
if (successes.length === 0) {
const codeCounts = new Map();
for (const f of failures) {
const code = String(f.code ?? 'unknown').toLowerCase();
codeCounts.set(code, (codeCounts.get(code) ?? 0) + 1);
}
const top = [...codeCounts.entries()].sort((a, b) => b[1] - a[1])[0];
const topCode = top?.[0] ?? 'unknown';
if (/daily_quota_exceeded/.test(topCode)) {
return {
usable: false,
blocker: 'daily_quota_exceeded',
detail: `${top[1]}/${entries.length} metric queries hit the daily Observability query limit. Retry after the next UTC midnight reset.`,
};
}
if (/payment_required/.test(topCode)) {
const text = failures
.map((f) => `${f.message ?? ''}\n${f.stderr ?? ''}`)
.join('\n')
.toLowerCase();
if (
/subscription to observability plus[\s\S]{0,160}required/.test(text) ||
/observability plus[\s\S]{0,160}not enabled/.test(text)
) {
return {
usable: false,
blocker: 'no_oplus_probe',
detail: `${top[1]}/${entries.length} metric queries need route-level Observability Plus data. Enable Observability Plus, then re-run the metric-backed audit.`,
};
}
return {
usable: false,
blocker: 'payment_required',
detail: `${top[1]}/${entries.length} metric queries returned payment_required. Route-level metrics were recognized for this team, but these queries are not usable. Check the team's Observability Plus subscription or event quota.`,
};
}
if (/forbidden|not_authorized|403/.test(topCode)) {
return {
usable: false,
blocker: 'forbidden',
detail: `${top[1]}/${entries.length} metric queries returned FORBIDDEN. Auth-scope mismatch — likely logged in to the wrong team (run \`vercel switch\`).`,
};
}
if (/project_not_found/.test(topCode)) {
return {
usable: false,
blocker: 'project_not_found',
detail: `Project ID not visible to the auth'd team. Run \`vercel switch\` or verify the project ID.`,
};
}
if (/not_linked/.test(topCode)) {
return {
usable: false,
blocker: 'not_linked',
detail: `${top[1]}/${entries.length} metric queries returned NOT_LINKED. Link the app directory first: \`vercel link --yes --project <project-name-or-id> --cwd <project-dir>\`; add \`--team <team-id-or-slug>\` when the team is known.`,
};
}
return {
usable: false,
blocker: 'all_failed_other',
detail: `Every metric query failed; top error code was \`${topCode}\` (${top?.[1]}/${entries.length}).`,
};
}
// Some queries succeeded; zero rows across the board = "no traffic in window",
// NOT an Observability Plus billing issue.
const totalRows = successes.reduce((s, m) => s + (Array.isArray(m.rows) ? m.rows.length : 0), 0);
if (totalRows === 0) {
return {
usable: true,
blocker: 'no_traffic',
detail: 'Observability Plus queries succeeded but every metric returned 0 rows. Either the project has no traffic in the 14-day window, or Observability Plus retention is limited (free tier = 1 day on Pro).',
};
}
return { usable: true, blocker: null, detail: 'Observability Plus is usable; queries returned data.' };
}
// Run main() only as a CLI; the test suite imports diagnoseObservabilityPlus directly.
import { fileURLToPath } from 'node:url';
import { realpathSync } from 'node:fs';
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main().catch((err) => {
console.error('[collect-signals] FAILED:', redactSensitiveText(err.message));
process.exit(1);
});
}
@@ -0,0 +1,296 @@
#!/usr/bin/env node
// Collect raw sub-agent outputs into the recommendations.json array consumed by
// verify-and-regen. Sub-agent hosts often wrap JSON in prose or markdown fences,
// so extraction is permissive while candidateRef coverage stays strict.
import { readFile, readdir, stat, writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve, basename } from 'node:path';
const log = (...a) => console.error('[collect-sub-agent-outputs]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.inputs.length === 0 && !args.manifestPath) {
console.error('usage: node scripts/collect-sub-agent-outputs.mjs [--manifest briefs/manifest.json] <output-file-or-dir...> [--out recommendations.json] [--strict]');
process.exit(1);
}
const manifest = args.manifestPath
? JSON.parse(await readFile(args.manifestPath, 'utf-8'))
: null;
const expected = manifest ? readExpectedBriefs(manifest) : [];
const preResolvedRecords = manifest ? readPreResolvedRecords(manifest) : [];
const files = args.inputs.length > 0 ? await collectInputFiles(args.inputs) : [];
const collected = [];
const summary = {
files: files.length,
kept: 0,
abstained: 0,
parseFailed: 0,
nonObject: 0,
missingCandidateRef: 0,
};
const errors = [];
for (const file of files) {
const raw = await readFile(file, 'utf-8');
const extracted = extractJsonValue(raw);
if (!extracted.ok) {
summary.parseFailed++;
const msg = `${file}: ${extracted.reason}`;
if (args.strict) errors.push(msg);
else log(`warn: ${msg}`);
continue;
}
const records = normalizeOutput(extracted.value);
if (records.length === 0) {
summary.nonObject++;
const msg = `${file}: JSON did not contain a recommendation or abstention object`;
if (args.strict) errors.push(msg);
else log(`warn: ${msg}`);
continue;
}
for (const record of records) {
const candidateRef = record.candidateRef ?? inferCandidateRefFromFile(file, expected, records.length);
if (!candidateRef) {
summary.missingCandidateRef++;
errors.push(`${file}: output is missing candidateRef`);
continue;
}
collected.push({
sourcePath: file,
record: record.candidateRef ? record : { ...record, candidateRef },
});
}
}
let ordered = collected;
if (expected.length > 0) {
const byRef = new Map();
for (const item of collected) {
const ref = item.record.candidateRef;
if (!expected.some((b) => b.candidateRef === ref)) {
errors.push(`${item.sourcePath}: unknown candidateRef ${ref}`);
continue;
}
if (byRef.has(ref)) {
errors.push(`${item.sourcePath}: duplicate output for candidateRef ${ref}`);
continue;
}
byRef.set(ref, item);
}
const missing = expected.filter((b) => !byRef.has(b.candidateRef));
for (const b of missing) errors.push(`missing output for candidateRef ${b.candidateRef}`);
ordered = expected.map((b) => byRef.get(b.candidateRef)).filter(Boolean);
} else {
ordered = collected.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
}
const records = [...preResolvedRecords, ...ordered.map((item) => item.record)];
summary.kept = records.filter((r) => r?.abstain !== true).length;
summary.abstained = records.filter((r) => r?.abstain === true).length;
if (errors.length > 0) {
for (const e of errors) log(`error: ${e}`);
process.exit(2);
}
if (records.length === 0) {
log('error: no recommendation or abstention records collected');
process.exit(2);
}
const serialized = JSON.stringify(records, null, 2) + '\n';
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, serialized, 'utf-8');
log(`wrote ${serialized.length}B → ${args.outPath}`);
} else {
process.stdout.write(serialized);
}
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)`);
}
function parseArgs(argv) {
const out = { inputs: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--manifest') out.manifestPath = resolve(argv[++i]);
else if (a.startsWith('--manifest=')) out.manifestPath = resolve(a.slice('--manifest='.length));
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--strict') out.strict = true;
else out.inputs.push(resolve(a));
}
return out;
}
async function collectInputFiles(paths) {
const out = [];
for (const p of paths) {
const s = await stat(p);
if (s.isDirectory()) out.push(...await walkDir(p));
else if (s.isFile()) out.push(p);
}
return out.sort((a, b) => a.localeCompare(b));
}
async function walkDir(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const out = [];
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
if (e.name.startsWith('.')) continue;
const p = resolve(dir, e.name);
if (e.isDirectory()) out.push(...await walkDir(p));
else if (e.isFile()) out.push(p);
}
return out;
}
function readExpectedBriefs(manifest) {
if (!manifest || typeof manifest !== 'object' || !Array.isArray(manifest.briefs)) {
throw new TypeError('manifest must contain a briefs array');
}
return manifest.briefs.map((b, i) => {
if (!b?.candidateRef) throw new TypeError(`manifest.briefs[${i}].candidateRef is required`);
return {
group: b.group ?? null,
index: b.index ?? i,
candidateRef: b.candidateRef,
};
});
}
function readPreResolvedRecords(manifest) {
if (!manifest || !Array.isArray(manifest.preResolvedRecords)) return [];
return manifest.preResolvedRecords.map((r, i) => {
if (!isRecordObject(r)) {
throw new TypeError(`manifest.preResolvedRecords[${i}] must be a recommendation or no-recommendation record`);
}
if (!r.candidateRef) {
throw new TypeError(`manifest.preResolvedRecords[${i}].candidateRef is required`);
}
return r;
});
}
function extractJsonValue(raw) {
for (const block of extractFenceBlocks(raw)) {
const parsed = tryParseJson(block);
if (parsed.ok) return parsed;
}
const full = tryParseJson(raw);
if (full.ok) return full;
for (const span of findBalancedJsonSpans(raw)) {
const parsed = tryParseJson(span);
if (parsed.ok) return parsed;
}
return { ok: false, reason: 'no valid JSON object or array found' };
}
function extractFenceBlocks(raw) {
const out = [];
const re = /```(?:json|JSON)?\s*\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(raw)) !== null) out.push(m[1].trim());
return out;
}
function tryParseJson(raw) {
try {
return { ok: true, value: JSON.parse(raw.trim()) };
} catch (err) {
return { ok: false, reason: err.message };
}
}
function findBalancedJsonSpans(raw) {
const spans = [];
for (let i = 0; i < raw.length; i++) {
const ch = raw[i];
if (ch !== '{' && ch !== '[') continue;
const closeFor = ch === '{' ? '}' : ']';
const stack = [closeFor];
let inString = false;
let escape = false;
for (let j = i + 1; j < raw.length; j++) {
const c = raw[j];
if (inString) {
if (escape) escape = false;
else if (c === '\\') escape = true;
else if (c === '"') inString = false;
continue;
}
if (c === '"') {
inString = true;
continue;
}
if (c === '{') stack.push('}');
else if (c === '[') stack.push(']');
else if (c === '}' || c === ']') {
if (stack.at(-1) !== c) break;
stack.pop();
if (stack.length === 0) {
spans.push(raw.slice(i, j + 1));
i = j;
break;
}
}
}
}
return spans;
}
function normalizeOutput(value) {
const unwrapped = unwrapEnvelope(value);
if (Array.isArray(unwrapped)) return unwrapped.filter(isRecordObject);
if (isRecordObject(unwrapped)) return [unwrapped];
if (unwrapped && typeof unwrapped === 'object') {
if (isRecordObject(unwrapped.recommendation)) return [unwrapped.recommendation];
if (Array.isArray(unwrapped.recommendations)) return unwrapped.recommendations.filter(isRecordObject);
}
return [];
}
function unwrapEnvelope(value) {
let current = value;
for (let depth = 0; depth < 2; depth++) {
if (!current || typeof current !== 'object' || Array.isArray(current)) return current;
if (Array.isArray(current.recommendations) || current.recommendation) return current;
const keys = Object.keys(current);
const envelopeKey = ['data', 'result', 'insights'].find((k) => keys.length === 1 && k in current);
if (!envelopeKey) return current;
current = current[envelopeKey];
}
return current;
}
function isRecordObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
if (value.abstain === true) return true;
return ['what', 'why', 'fix', 'bucket', 'affectedFiles', 'citations'].some((k) => k in value);
}
function inferCandidateRefFromFile(file, expected, recordCount) {
if (recordCount !== 1 || expected.length === 0) return null;
if (expected.length === 1) return expected[0].candidateRef;
const name = basename(file);
const matches = expected.filter((b) => {
if (!b.group && b.index == null) return false;
const group = escapeRegExp(String(b.group ?? ''));
const index = escapeRegExp(String(b.index));
return new RegExp(`(?:^|[^A-Za-z0-9])${group}[-_.]?${index}(?:[^A-Za-z0-9]|$)`).test(name);
});
return matches.length === 1 ? matches[0].candidateRef : null;
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
main().catch((err) => {
console.error('[collect-sub-agent-outputs] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
@@ -0,0 +1,319 @@
#!/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 <merged.json> <gate.json> [--cwd <project-dir>]');
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 <project-dir> 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 <dir-linked-to-the-collected-project>.');
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 <dir-linked-to-the-collected-project>, 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 <team>`, or re-link with `vercel link --yes --project <project> --team <team-slug>`.');
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 <dir-linked-to-the-collected-project>, 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);
});
@@ -0,0 +1,166 @@
#!/usr/bin/env node
// Pure-JS deterministic gate. Reads merged signals.json, emits
// {toLaunch, platform, gated}. Same input → byte-identical output (modulo
// appliedAt). Sort keys are stable and explicit — never change without
// a co-located golden-output test update.
import { readFile } from 'node:fs/promises';
import { gates, DEFAULT_MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs';
import { applyAuthDisqualifier } from '../lib/auth-route.mjs';
import { dedupeCandidates } from '../lib/route-normalize.mjs';
import { validateCandidates } from '../lib/gates/contract.mjs';
import { applyHardGates } from '../lib/gates/hard-gates.mjs';
import { selectLaunchCandidates } from '../lib/gates/select-candidates.mjs';
import { routePathMatchScore } from '../lib/investigation-brief.mjs';
const SCHEMA_VERSION = '1.1';
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.signalsPath) {
console.error('usage: node scripts/gate-investigations.mjs <signals.json> [--max-candidates N|all]');
console.error(' VERCEL_OPTIMIZE_MAX_CANDIDATES env var supported (same values)');
process.exit(1);
}
const budget = resolveBudget(args);
const signals = JSON.parse(await readFile(args.signalsPath, 'utf-8'));
const allSeeds = gates.flatMap((g) => {
try {
return g.gate(signals) ?? [];
} catch (err) {
console.error(`[gate-investigations] gate ${g.metadata?.id} threw: ${err.message}`);
return [];
}
});
const validSeeds = validateCandidates(allSeeds, { source: 'gate-output' });
const annotated = validSeeds.map(applyAuthDisqualifier);
const sorted = annotated.slice().sort(stableCompare);
// Next.js 16 segment-tree metric paths surface the same source file under
// many encoded labels (city variants, _tree/_index siblings, base64 flag
// prefixes). Without dedup the budget gets shredded ~4-10x per page.
const { deduped, dropped } = dedupeCandidates(sorted);
const displayAnnotated = deduped.map((candidate) => attachDisplayRoute(candidate, signals));
const hardGateResult = applyHardGates(displayAnnotated, signals);
const gateable = hardGateResult.allowed;
// Account-scope candidates don't compete with code-scope for the budget.
const codeScoped = gateable.filter((c) => !c.disqualified && c.scope !== 'account');
const platformScoped = gateable.filter((c) => !c.disqualified && c.scope === 'account');
const selection = selectLaunchCandidates(codeScoped, budget, {
diversify: args.budgetSource === 'default',
});
const toLaunch = selection.selected;
const skippedByBudget = selection.skipped;
const budgetLabel = budget === Infinity ? 'unlimited (all)' : String(budget);
const gated = [
...gateable
.filter((c) => c.disqualified)
.map((c) => ({ ...c, gatedReason: c.disqualifyReason ?? 'disqualified' })),
...hardGateResult.gated,
...skippedByBudget.map((c) => ({
...c,
gatedReason: `skippedByBudget (max-candidates=${budgetLabel}; raise with --max-candidates N or =all)`,
})),
...dropped.map((d) => ({
...d.candidate,
gatedReason: `coveredBy (${d.mergedInto}) — ${d.reason}`,
})),
];
process.stdout.write(JSON.stringify({
schemaVersion: SCHEMA_VERSION,
gateVersion: GATE_VERSION,
appliedAt: new Date().toISOString(),
budget: {
maxCandidates: budget === Infinity ? 'all' : budget,
source: args.budgetSource,
selection: selection.selectionMode,
},
toLaunch,
platform: platformScoped,
gated,
gateMetadata: gates.map((g) => ({
id: g.metadata?.id,
threshold: g.metadata?.threshold,
billingDimension: g.metadata?.billingDimension,
sourceCitation: g.metadata?.sourceCitation,
})),
}, null, 2) + '\n');
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--max-candidates') out.maxCandidatesArg = argv[++i];
else if (a.startsWith('--max-candidates=')) out.maxCandidatesArg = a.slice('--max-candidates='.length);
else out.positional.push(a);
}
out.signalsPath = out.positional[0];
return out;
}
function resolveBudget(args) {
const raw = args.maxCandidatesArg ?? process.env.VERCEL_OPTIMIZE_MAX_CANDIDATES;
if (raw == null || raw === '') {
args.budgetSource = 'default';
return DEFAULT_MAX_CODE_CANDIDATES;
}
const trimmed = String(raw).trim().toLowerCase();
if (trimmed === 'all' || trimmed === 'unlimited' || trimmed === '-1') {
args.budgetSource = args.maxCandidatesArg != null ? 'flag' : 'env';
return Infinity;
}
const n = Number(trimmed);
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
console.error(`[gate-investigations] bad budget value '${raw}'; expected positive integer or 'all'`);
process.exit(2);
}
args.budgetSource = args.maxCandidatesArg != null ? 'flag' : 'env';
return n;
}
// Total ordering: priority desc, kind asc, route asc. Underpins byte-identical output.
function stableCompare(a, b) {
const pa = a.priority ?? 0;
const pb = b.priority ?? 0;
if (pa !== pb) return pb - pa;
const ka = String(a.kind ?? '');
const kb = String(b.kind ?? '');
if (ka !== kb) return ka.localeCompare(kb);
const ra = String(a.route ?? a.hostname ?? '');
const rb = String(b.route ?? b.hostname ?? '');
return ra.localeCompare(rb);
}
function attachDisplayRoute(candidate, signals) {
if (!candidate || candidate.scope !== 'route' || typeof candidate.route !== 'string') return candidate;
if (!candidate.route.includes('[*]')) return candidate;
const routes = (signals.codebase?.routes ?? [])
.map((route) => route?.routePath)
.filter((routePath) => typeof routePath === 'string' && routePath.length > 0);
if (routes.length === 0) return candidate;
let bestRoute = null;
let bestScore = 0;
for (const routePath of routes) {
const score = routePathMatchScore(routePath, candidate.route);
if (score > bestScore) {
bestRoute = routePath;
bestScore = score;
}
}
if (!bestRoute || bestScore <= 0 || bestRoute === candidate.route) return candidate;
return { ...candidate, displayRoute: bestRoute };
}
main().catch((err) => {
console.error('[gate-investigations] FAILED:', err.message);
process.exit(1);
});
@@ -0,0 +1,192 @@
#!/usr/bin/env node
// Deterministically combines Vercel metric collection with the local codebase
// scan. Keeps the merged artifact shape stable: collect-signals output at the
// top level, scan-codebase output under `codebase`.
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
import { realpathSync } from 'node:fs';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { routePathMatchScore } from '../lib/investigation-brief.mjs';
import { canonicalizeRoute } from '../lib/route-normalize.mjs';
const log = (...args) => console.error('[merge-signals]', ...args);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.signalsPath || !args.codebasePath) {
console.error('usage: node scripts/merge-signals.mjs <signals.json> <codebase.json> [--out merged.json] [--force]');
process.exit(1);
}
const [signals, codebase] = await Promise.all([
readJson(args.signalsPath, 'signals'),
readJson(args.codebasePath, 'codebase scan'),
]);
const merged = mergeSignals(signals, codebase);
const body = JSON.stringify(merged, null, 2) + '\n';
if (args.outPath) {
await writeOutput(args.outPath, body, { force: args.force });
log(`wrote ${args.outPath}`);
} else {
process.stdout.write(body);
}
}
export function mergeSignals(signals, codebase) {
assertObject(signals, 'signals');
assertObject(codebase, 'codebase scan');
if (!signals.schemaVersion) {
throw new Error('signals.json is missing schemaVersion; pass collect-signals output as the first file.');
}
if (!Array.isArray(codebase.routes) || !Array.isArray(codebase.findings) || !codebase.stack) {
throw new Error('codebase.json must be scan-codebase output with stack, routes[], and findings[].');
}
return {
...signals,
codebase: annotateCodebaseScan(signals, codebase),
};
}
export function annotateCodebaseScan(signals, codebase) {
const index = buildRouteMetricIndex(signals);
return {
...codebase,
findings: (codebase.findings ?? []).map((finding) => annotateFinding(finding, index)),
};
}
function annotateFinding(finding, index) {
if (!finding || typeof finding !== 'object') return finding;
if (finding.trafficIndependent) return finding;
if (!finding.route) return { ...finding, o11ySignal: 'NO-ROUTE-MAPPING' };
const summary = bestRouteSummary(finding.route, index);
if (!summary || !hasTraffic(summary)) return { ...finding, o11ySignal: 'COLD-PATH' };
return { ...finding, o11ySignal: formatRouteSignal(summary) };
}
function buildRouteMetricIndex(signals) {
const out = new Map();
const ensure = (route) => {
const canonical = canonicalizeRoute(route);
const existing = out.get(canonical) ?? { route: canonical };
out.set(canonical, existing);
return existing;
};
for (const row of rows(signals, 'fnStatusByRoute')) {
if (!row.route) continue;
const summary = ensure(row.route);
summary.functionRuns = (summary.functionRuns ?? 0) + numeric(row.value);
}
for (const row of rows(signals, 'fnDurationP95ByRoute')) {
if (!row.route) continue;
ensure(row.route).p95Ms = numeric(row.value);
}
for (const row of rows(signals, 'requestsByRouteCache')) {
if (!row.route) continue;
const summary = ensure(row.route);
const count = numeric(row.value);
summary.requests = (summary.requests ?? 0) + count;
if (String(row.cache_result).toUpperCase() === 'HIT') {
summary.cacheHits = (summary.cacheHits ?? 0) + count;
}
}
return out;
}
function rows(signals, metricId) {
const rows = signals?.metrics?.[metricId]?.rows;
return Array.isArray(rows) ? rows : [];
}
function numeric(value) {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
function bestRouteSummary(route, index) {
const canonical = canonicalizeRoute(route);
const exact = index.get(canonical);
if (exact) return exact;
let best = null;
for (const summary of index.values()) {
const score = routePathMatchScore(canonical, summary.route);
if (score <= 0) continue;
if (!best || score > best.score) best = { score, summary };
}
return best?.summary ?? null;
}
function hasTraffic(summary) {
return (summary.functionRuns ?? 0) > 0 || (summary.requests ?? 0) > 0;
}
function formatRouteSignal(summary) {
const parts = [];
if ((summary.functionRuns ?? 0) > 0) parts.push(`inv=${Math.round(summary.functionRuns)}`);
else if ((summary.requests ?? 0) > 0) parts.push(`requests=${Math.round(summary.requests)}`);
if ((summary.p95Ms ?? 0) > 0) parts.push(`p95=${Math.round(summary.p95Ms)}ms`);
if ((summary.requests ?? 0) > 0 && summary.cacheHits != null) {
const hitRate = Math.round((summary.cacheHits / summary.requests) * 100);
parts.push(`cache=${hitRate}%`);
}
return parts.join(',') || 'COLD-PATH';
}
function parseArgs(argv) {
const out = { positional: [], force: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--out') out.outPath = argv[++i];
else if (a.startsWith('--out=')) out.outPath = a.slice('--out='.length);
else if (a === '--force') out.force = true;
else out.positional.push(a);
}
out.signalsPath = out.positional[0];
out.codebasePath = out.positional[1];
return out;
}
async function readJson(path, label) {
try {
return JSON.parse(await readFile(path, 'utf-8'));
} catch (err) {
throw new Error(`Could not read ${label} JSON at ${path}: ${err.message}`);
}
}
function assertObject(value, label) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be a JSON object.`);
}
}
async function writeOutput(path, body, { force }) {
if (!force && await exists(path)) {
throw new Error(`output file already exists: ${path}. Use a fresh run directory or pass --force to overwrite.`);
}
await mkdir(dirname(path), { recursive: true });
await writeFile(path, body);
}
async function exists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main().catch((err) => {
console.error('[merge-signals] FAILED:', err.message);
process.exit(1);
});
}
@@ -0,0 +1,231 @@
#!/usr/bin/env node
// Emits the ENTIRE prompt a sub-agent sees for one candidate (candidate +
// deep-dive evidence + filtered citations + playbook + protocol + output
// schema). --list emits a manifest the orchestrator uses to decide fan-out
// vs serial. Brief → stdout, status → stderr.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import {
buildBrief,
inferPlaybook,
inferFrameworkPlaybook,
resolveFiles,
citationSubset,
} from '../lib/investigation-brief.mjs';
import { supportTopicSubset } from '../lib/support-topics.mjs';
import { candidateRefFor } from '../lib/reconcile-candidates.mjs';
import { formatCandidateLabel } from '../lib/display-labels.mjs';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const PLAYBOOKS_DIR = join(HERE, '..', 'references', 'playbooks');
const log = (...a) => console.error('[prepare-brief]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.mergedPath || !args.investigationPath) {
console.error('usage: node scripts/prepare-investigation-brief.mjs <merged.json> <investigation.json> [--index N] [--group toLaunch|platform] [--out FILE]');
console.error(' or: node scripts/prepare-investigation-brief.mjs <merged.json> <investigation.json> --list');
process.exit(1);
}
const [merged, investigation] = await Promise.all([
readFile(args.mergedPath, 'utf-8').then(JSON.parse),
readFile(args.investigationPath, 'utf-8').then(JSON.parse),
]);
if (args.list) {
const manifest = buildManifest(merged, investigation);
process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
return;
}
const group = args.group ?? 'toLaunch';
const index = args.index ?? 0;
const pool = Array.isArray(investigation[group]) ? investigation[group] : [];
if (index < 0 || index >= pool.length) {
console.error(`[prepare-brief] FATAL: ${group}[${index}] out of range (${group} has ${pool.length} entries)`);
process.exit(2);
}
let candidate = pool[index];
// Scan output may live at merged.codebase (older shape) or merged.signals.codebase
// (current shape, after the jq merge nests it under signals). Resolve either.
const codebase = pickCodebase(merged);
const signals = {
...merged,
codebase,
};
const files = resolveFiles(candidate, signals);
candidate = {
...candidate,
candidateRef: candidate.candidateRef ?? candidateRefFor(candidate, files),
};
const playbookId = inferPlaybook(signals);
const playbookBody = playbookId ? await tryReadPlaybook(playbookId) : null;
const frameworkPlaybookId = inferFrameworkPlaybook(signals);
const frameworkPlaybookBody = frameworkPlaybookId ? await tryReadPlaybook(frameworkPlaybookId) : null;
const stack = signals.stack ?? signals.codebase?.stack ?? {};
const framework = stack.framework ?? 'unknown';
const version = stack.frameworkVersion ?? 'unknown';
const citations = await citationSubset(candidate.kind, framework, version);
const supportTopics = await supportTopicSubset({
candidate,
signals,
framework,
version,
profile: playbookId,
frameworkPlaybookId,
});
const brief = buildBrief({
candidate,
candidateIndex: index,
candidateGroup: group,
files,
signals,
citations,
playbookId,
playbookBody,
frameworkPlaybookId,
frameworkPlaybookBody,
supportTopics,
generatedAt: args.deterministic ? null : new Date().toISOString(),
});
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeBriefFile(args.outPath, brief, { force: args.force });
log(`wrote ${brief.length}B → ${args.outPath}`);
} else {
process.stdout.write(brief + '\n');
}
}
function buildManifest(merged, investigation) {
const out = [];
const groups = ['toLaunch', 'platform'];
for (const group of groups) {
const pool = Array.isArray(investigation[group]) ? investigation[group] : [];
pool.forEach((c, i) => {
const files = resolveFiles(c, { ...merged, codebase: pickCodebase(merged) });
const candidateRef = c.candidateRef ?? candidateRefFor(c, files);
out.push({
group,
index: i,
kind: c.kind,
route: c.route ?? c.hostname ?? null,
scope: c.scope ?? null,
priority: c.priority ?? null,
confidence: c.confidence ?? null,
o11ySignal: c.o11ySignal ?? null,
files,
candidateRef,
label: formatCandidateLabel({ ...c, files }),
});
});
}
return {
schemaVersion: '1.0',
totalBriefs: out.length,
toLaunchCount: out.filter((b) => b.group === 'toLaunch').length,
platformCount: out.filter((b) => b.group === 'platform').length,
preResolvedRecords: Array.isArray(investigation.preResolvedRecords)
? investigation.preResolvedRecords
: [],
fanoutPlan: buildFanoutPlan(out),
briefs: out,
};
}
function buildFanoutPlan(briefs) {
const groups = new Map();
for (const brief of briefs) {
const key = candidateFamilyKey(brief);
const existing = groups.get(key) ?? {
familyKey: key,
label: brief.label,
kind: brief.kind,
primaryBrief: { group: brief.group, index: brief.index, candidateRef: brief.candidateRef },
relatedBriefs: [],
};
if (existing.primaryBrief.candidateRef !== brief.candidateRef) {
existing.relatedBriefs.push({ group: brief.group, index: brief.index, candidateRef: brief.candidateRef });
}
groups.set(key, existing);
}
return {
totalFamilies: groups.size,
families: [...groups.values()].map((g) => ({
...g,
totalBriefs: 1 + g.relatedBriefs.length,
})),
};
}
function candidateFamilyKey(brief) {
const file = Array.isArray(brief.files) && brief.files.length > 0 ? brief.files[0] : null;
const target = file ?? brief.route ?? brief.scope ?? '<account>';
return `${brief.kind ?? 'unknown'}:${target}`;
}
// Prefer merged.codebase, fall back to merged.signals.codebase, then empty.
// Also accepts a fully-shaped scan doc directly (used in tests).
function pickCodebase(merged) {
if (!merged || typeof merged !== 'object') return {};
if (merged.codebase && typeof merged.codebase === 'object' && (merged.codebase.routes || merged.codebase.findings)) {
return merged.codebase;
}
if (merged.signals?.codebase && typeof merged.signals.codebase === 'object') {
return merged.signals.codebase;
}
return {};
}
async function tryReadPlaybook(id) {
try {
return await readFile(join(PLAYBOOKS_DIR, `${id}.md`), 'utf-8');
} catch {
return null;
}
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--index') out.index = Number(argv[++i]);
else if (a.startsWith('--index=')) out.index = Number(a.slice('--index='.length));
else if (a === '--group') out.group = argv[++i];
else if (a.startsWith('--group=')) out.group = a.slice('--group='.length);
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--list') out.list = true;
else if (a === '--deterministic') out.deterministic = true;
else if (a === '--force') out.force = true;
else out.positional.push(a);
}
out.mergedPath = out.positional[0];
out.investigationPath = out.positional[1];
return out;
}
async function writeBriefFile(outPath, brief, { force = false } = {}) {
try {
await writeFile(outPath, brief + '\n', { encoding: 'utf-8', flag: force ? 'w' : 'wx' });
} catch (err) {
if (err?.code === 'EEXIST') {
throw new Error(`output file already exists: ${outPath}. Use a fresh run directory or pass --force to overwrite.`);
}
throw err;
}
}
main().catch((err) => {
console.error('[prepare-brief] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
@@ -0,0 +1,62 @@
#!/usr/bin/env node
// Deterministic reconciliation between deep-dive and investigator fan-out.
// Reads investigation-evidence.json, removes candidates whose follow-up metric
// evidence already disproves/reframes the gate hypothesis, and emits the same
// shape with preResolvedRecords for the final report.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { reconcileInvestigation } from '../lib/reconcile-candidates.mjs';
const log = (...a) => console.error('[reconcile-candidates]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.investigationPath) {
console.error('usage: node scripts/reconcile-candidates.mjs <investigation-evidence.json> [--gate gate.json] [--out reconciled-investigation.json]');
process.exit(1);
}
const [investigation, gate] = await Promise.all([
readFile(args.investigationPath, 'utf-8').then(JSON.parse),
args.gatePath ? readFile(args.gatePath, 'utf-8').then(JSON.parse) : null,
]);
const reconciled = reconcileInvestigation(investigation, { gate });
const serialized = JSON.stringify({
...reconciled,
reconciledAt: args.noTimestamp ? null : new Date().toISOString(),
}, null, 2) + '\n';
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, serialized, 'utf-8');
log(`wrote ${serialized.length}B -> ${args.outPath}`);
} else {
process.stdout.write(serialized);
}
const dropped = reconciled.reconciliation?.droppedBeforeInvestigation ?? 0;
if (dropped > 0) log(`dropped ${dropped} candidate(s) before investigation`);
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--gate') out.gatePath = resolve(argv[++i]);
else if (a.startsWith('--gate=')) out.gatePath = resolve(a.slice('--gate='.length));
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--no-timestamp') out.noTimestamp = true;
else out.positional.push(a);
}
out.investigationPath = out.positional[0] ? resolve(out.positional[0]) : null;
return out;
}
main().catch((err) => {
console.error('[reconcile-candidates] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
@@ -0,0 +1,437 @@
#!/usr/bin/env node
// Final pipeline step. Emits customer-facing markdown from
// recommendations.json + gate.json + signals.json.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { buildFinalReportMessage, renderReport } from '../lib/render-report.mjs';
import { dedupeRecommendations } from '../lib/dedup-recs.mjs';
import { canonicalizeRoute } from '../lib/route-normalize.mjs';
import { hasUnsupportedCacheLifeCdnText, splitCustomerSafeObservations } from '../lib/observation-safety.mjs';
const log = (...a) => console.error('[render-report]', ...a);
const HARD_REGEN_TRIGGERS = new Set([
'project_config_contradiction',
'cache_vary_safety',
'semantic_safety',
]);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.recsPath || !args.gatePath || !args.signalsPath) {
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]');
process.exit(1);
}
const [recsRaw, gateRaw, signalsRaw] = await Promise.all([
readFile(args.recsPath, 'utf-8').then(JSON.parse),
readFile(args.gatePath, 'utf-8').then(JSON.parse),
readFile(args.signalsPath, 'utf-8').then(JSON.parse),
]);
// Accept either a raw rec array OR the verify-and-regen wrapper
// {recsGraded, qualityDropped, ...}. Stale-rec defense: when verify-and-regen
// flagged a hard-safety issue but the orchestrator skipped re-spawn, the
// original rec is still in recsGraded. Filter it here so it can't ship; it
// surfaces in "Investigated, no change recommended" instead.
const hardRegenRefs = new Set(
Array.isArray(recsRaw.regenPlan)
? recsRaw.regenPlan
.filter((p) => HARD_REGEN_TRIGGERS.has(p.regenTrigger))
.map((p) => p.candidateRef)
.filter(Boolean)
: []
);
const activeCandidates = [
...(Array.isArray(gateRaw.toLaunch) ? gateRaw.toLaunch : []),
...(Array.isArray(gateRaw.platform) ? gateRaw.platform : []),
];
const enforceCurrentGate = !Array.isArray(recsRaw) && activeCandidates.length > 0;
const staleRecommendationDrops = [];
const wrapperRecommendations = Array.isArray(recsRaw.renderableRecommendations)
? recsRaw.renderableRecommendations
: (recsRaw.recsGraded ?? []);
const needsReviewDrops = [];
const candidateRecommendations = Array.isArray(recsRaw)
? recsRaw.filter((r) => r?.abstain !== true)
: wrapperRecommendations
.filter((r, i) => (r.quality?.overall ?? 0) >= 0.55)
.filter((r) => !hardRegenRefs.has(r.candidateRef));
const recommendationsRaw = candidateRecommendations
.filter((r) => {
if (r?.abstain === true || r?.needsReview !== true) return true;
needsReviewDrops.push({
candidateRef: r.candidateRef ?? null,
reason: 'This recommendation needs a manual safety review before it is ready to apply.',
});
return false;
})
.filter((r) => {
if (!enforceCurrentGate) return true;
if (recommendationMatchesActiveCandidate(r, activeCandidates)) return true;
staleRecommendationDrops.push({
candidateRef: r.candidateRef ?? null,
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.',
});
return false;
});
const recommendations = dedupeRecommendations(recommendationsRaw);
const readyTargets = new Set(
recommendations
.map((r) => candidateTarget(r?.candidateRef))
.filter(Boolean)
);
const droppedContradictions = !Array.isArray(recsRaw)
? (recsRaw.recsGraded ?? [])
.map((r, i) => ({ r, i }))
.filter(({ r }) => hardRegenRefs.has(r.candidateRef))
.map(({ r, i }) => ({
candidateRef: r.candidateRef ?? null,
reason: publicHardRegenReason(recsRaw.regenPlan?.find((p) => p.index === i || p.candidateRef === r.candidateRef)),
}))
: [];
const gated = Array.isArray(gateRaw.gated) ? gateRaw.gated : [];
// No-change findings are first-class investigation outputs ("the hypothesis didn't hold").
// Contradiction-dropped recs ride alongside them so customers see WHY a rec
// was held back instead of it silently disappearing.
const baseAbstentions = Array.isArray(recsRaw)
? recsRaw.filter((r) => r?.abstain === true).map((r) => ({
candidateRef: r.candidateRef ?? null,
reason: publicNoChangeReason(r.reason ?? '(no reason recorded)'),
}))
: (recsRaw.abstentions ?? []).map((r) => ({
...r,
reason: publicNoChangeReason(r.reason ?? '(no reason recorded)'),
}));
const publicBaseAbstentions = baseAbstentions.filter((r) => !readyTargets.has(candidateTarget(r?.candidateRef)));
// Observations: no-change findings carrying a structured non-perf finding
// (deployment regression, error storm, etc.).
const flattenedObservations = Array.isArray(recsRaw)
? flattenObservations(recsRaw.filter((r) => r?.abstain === true))
: flattenObservations([
...(Array.isArray(recsRaw.observations) ? recsRaw.observations : []),
...(Array.isArray(recsRaw.abstentions) ? recsRaw.abstentions : []),
]);
const { observations: safeObservations, heldBackObservations } = splitCustomerSafeObservations(flattenedObservations, baseAbstentions, signalsRaw);
const observations = suppressReadyCoveredObservations(safeObservations, recommendations);
const abstentions = [
...publicBaseAbstentions,
...droppedContradictions,
...staleRecommendationDrops,
...needsReviewDrops,
...(Array.isArray(recsRaw.withheldRecommendations) ? recsRaw.withheldRecommendations.map((d) => ({
candidateRef: d.candidateRef ?? null,
reason: publicWithheldReason(d),
needsEvidence: true,
})) : []),
...(Array.isArray(recsRaw.sanitizerDropped) ? recsRaw.sanitizerDropped.map((d) => ({
candidateRef: d.candidateRef ?? null,
reason: `This needs a closer review before it is safe to apply: ${d.reason ?? 'review required'}.`,
needsEvidence: true,
})) : []),
...(Array.isArray(recsRaw.heldBackObservations) ? recsRaw.heldBackObservations.map((d) => ({
...d,
needsEvidence: true,
})) : []),
...heldBackObservations,
];
// Full catalog lets the renderer recover o11ySignal + aliasRoutes that recs
// didn't propagate, and canonicalize segment-tree candidateRefs.
const allCandidates = [
...activeCandidates,
...gated,
];
const md = renderReport({
recommendations,
gated,
abstentions,
observations,
signals: signalsRaw,
candidates: allCandidates,
opts: {
projectName: args.projectName,
generatedAt: args.noTimestamp ? null : new Date().toISOString(),
heldBackCount: (Number.isInteger(recsRaw.summary?.withheldRecommendations)
? recsRaw.summary.withheldRecommendations
: (Array.isArray(recsRaw.regenPlan) ? recsRaw.regenPlan.length : 0) +
(Array.isArray(recsRaw.qualityDropped) ? recsRaw.qualityDropped.length : 0)) +
(Array.isArray(recsRaw.sanitizerDropped) ? recsRaw.sanitizerDropped.length : 0) +
(Array.isArray(recsRaw.heldBackObservations) ? recsRaw.heldBackObservations.length : 0) +
heldBackObservations.length,
noChangeCount: Number.isInteger(recsRaw.summary?.abstentions)
? Math.min(recsRaw.summary.abstentions, publicBaseAbstentions.length)
: publicBaseAbstentions.length,
},
});
if (args.debugOutPath) {
const debugArtifact = buildDebugArtifact({
recsRaw,
recommendationsRaw,
recommendations,
gateRaw,
abstentions,
observations,
heldBackObservations,
staleRecommendationDrops,
droppedContradictions,
});
const serializedDebug = JSON.stringify(debugArtifact, null, 2) + '\n';
await mkdir(dirname(args.debugOutPath), { recursive: true });
await writeFile(args.debugOutPath, serializedDebug, 'utf-8');
log(`wrote debug ${serializedDebug.length}B → ${args.debugOutPath}`);
}
if (args.messageOutPath) {
const messageArtifact = buildFinalReportMessage({
reportPath: args.outPath ?? '(stdout)',
markdown: md,
recommendations,
signals: signalsRaw,
});
const serializedMessage = JSON.stringify(messageArtifact, null, 2) + '\n';
await mkdir(dirname(args.messageOutPath), { recursive: true });
await writeFile(args.messageOutPath, serializedMessage, 'utf-8');
log(`wrote final message ${serializedMessage.length}B → ${args.messageOutPath}`);
}
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, md + '\n', 'utf-8');
log(`wrote ${md.length}B → ${args.outPath}`);
} else {
process.stdout.write(md + '\n');
}
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--project') out.projectName = argv[++i];
else if (a.startsWith('--project=')) out.projectName = a.slice('--project='.length);
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--message-out') out.messageOutPath = resolve(argv[++i]);
else if (a.startsWith('--message-out=')) out.messageOutPath = resolve(a.slice('--message-out='.length));
else if (a === '--no-timestamp') out.noTimestamp = true;
else if (a === '--debug-out') out.debugOutPath = resolve(argv[++i]);
else if (a.startsWith('--debug-out=')) out.debugOutPath = resolve(a.slice('--debug-out='.length));
else if (a === '--debug') {
console.error('[render-report] --debug no longer writes internal details into customer markdown; use --debug-out FILE');
}
else out.positional.push(a);
}
out.recsPath = out.positional[0];
out.gatePath = out.positional[1];
out.signalsPath = out.positional[2];
return out;
}
function publicWithheldReason(record) {
switch (record?.reason) {
case 'needs_review':
return 'Automated checks added a safety caveat, so this run kept the recommendation out of the ready-to-apply list.';
case 'quality_floor':
return 'The recommendation did not meet the evidence bar for this report.';
case 'project_config_contradiction':
case 'cache_vary_safety':
case 'semantic_safety':
return publicHardRegenReason({ regenTrigger: record.reason });
default:
return 'This recommendation needs stronger evidence before it is safe to apply.';
}
}
function publicHardRegenReason(plan) {
switch (plan?.regenTrigger) {
case 'project_config_contradiction':
return 'The recommendation tried to turn on a project setting that is already enabled. Re-run the investigation with refreshed project-config evidence.';
case 'cache_vary_safety':
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.';
case 'semantic_safety':
return 'This recommendation needs stronger framework evidence before it is safe to apply. Re-run the investigation with that evidence in scope.';
default:
return 'This recommendation needs stronger evidence before it is safe to apply. Re-run the investigation with those checks in scope.';
}
}
function recommendationMatchesActiveCandidate(rec, candidates) {
const ref = parseCandidateRef(rec?.candidateRef);
if (!ref) return true;
return candidates.some((candidate) => candidateMatchesRef(candidate, ref));
}
function parseCandidateRef(ref) {
if (typeof ref !== 'string' || ref.length === 0) return null;
const [kind, ...targetParts] = ref.split(':');
if (!kind) return null;
return { kind, target: targetParts.join(':') };
}
function candidateMatchesRef(candidate, ref) {
if (!candidate || candidate.kind !== ref.kind) return false;
if (candidate.scope === 'account' || ref.target === '<account>') return true;
const candidateTarget = candidate.route ?? candidate.hostname ?? candidate.file ?? candidate.target ?? null;
if (!candidateTarget || !ref.target) return false;
const a = String(candidateTarget);
const b = String(ref.target);
return a === b || canonicalizeRoute(a) === canonicalizeRoute(b);
}
function suppressReadyCoveredObservations(observations, recommendations = []) {
if (!Array.isArray(observations) || observations.length === 0) return [];
const readyFamiliesByTarget = new Map();
for (const rec of recommendations) {
const parsed = parseCandidateRef(rec?.candidateRef);
const target = candidateTarget(rec?.candidateRef);
const family = candidateFamily(parsed?.kind);
if (!target || !family) continue;
const set = readyFamiliesByTarget.get(target) ?? new Set();
set.add(family);
readyFamiliesByTarget.set(target, set);
}
return observations.filter((observation) => {
const parsed = parseCandidateRef(observation?.candidateRef);
const target = candidateTarget(observation?.candidateRef);
const family = candidateFamily(parsed?.kind);
if (!target || !family) return true;
return !readyFamiliesByTarget.get(target)?.has(family);
});
}
function candidateFamily(kind) {
switch (kind) {
case 'uncached_route':
case 'cache_header_gap':
case 'missing_cache_headers':
case 'max_age_without_s_maxage':
return 'cache';
case 'slow_route':
case 'cold_start':
case 'external_api_slow':
case 'cwv_poor':
return 'performance';
case 'route_errors':
return 'reliability';
case 'isr_overrevalidation':
return 'isr';
case 'middleware_heavy':
return 'middleware';
case 'build_minutes_fanout':
return 'build';
default:
return kind || null;
}
}
function candidateTarget(ref) {
if (typeof ref !== 'string') return null;
const idx = ref.indexOf(':');
if (idx === -1) return null;
return ref.slice(idx + 1);
}
function publicNoChangeReason(reason) {
if (hasUnsupportedCacheLifeCdnText(reason)) {
return 'This candidate overlapped a cache-lifetime draft that did not meet the framework evidence bar. No supported change shipped from this run.';
}
return reason;
}
function buildDebugArtifact({
recsRaw,
recommendationsRaw,
recommendations,
gateRaw,
abstentions = [],
observations = [],
heldBackObservations = [],
staleRecommendationDrops = [],
droppedContradictions = [],
}) {
const wrapper = Array.isArray(recsRaw) ? null : recsRaw;
const sourceRecords = Array.isArray(recsRaw)
? recsRaw
: (recsRaw.recsGraded ?? []);
const summary = wrapper?.summary
? {
...wrapper.summary,
rawRecommendationCount: recommendationsRaw.length,
renderedRecommendationCount: recommendations.length,
}
: null;
return {
schemaVersion: '1.0',
summary,
regenPlan: wrapper?.regenPlan ?? [],
qualityDropped: wrapper?.qualityDropped ?? [],
withheldRecommendations: wrapper?.withheldRecommendations ?? [],
abstentions,
observations,
heldBackObservations,
staleRecommendationDrops,
droppedContradictions,
sanitizerDropped: wrapper?.sanitizerDropped ?? [],
renderedRecommendationCount: recommendations.length,
rawRecommendationCount: recommendationsRaw.length,
gateBudget: gateRaw?.budget ?? null,
recommendations: sourceRecords
.filter((record) => record && record.abstain !== true)
.map((record) => ({
candidateRef: record.candidateRef ?? null,
what: record.what ?? null,
verification: record.verification ?? null,
quality: record.quality ?? null,
passRate: record.passRate ?? record.verification?.passRate ?? null,
avgQuality: record.avgQuality ?? null,
needsReview: record.needsReview === true,
sanitizerTrail: Array.isArray(record.sanitizerTrail) ? record.sanitizerTrail : [],
})),
};
}
function flattenObservations(records) {
const out = [];
for (const record of records) {
if (!record || typeof record !== 'object') continue;
if (record.observation && typeof record.observation === 'object') {
out.push({
candidateRef: record.candidateRef ?? null,
summary: coerceOptionalString(record.observation.summary),
evidence: record.observation.evidence ?? null,
suggestedAction: record.observation.suggestedAction ?? null,
kind: record.observation.kind ?? 'other',
});
continue;
}
if ('summary' in record || 'evidence' in record || 'suggestedAction' in record || 'kind' in record) {
out.push({
candidateRef: record.candidateRef ?? null,
summary: coerceOptionalString(record.summary),
evidence: record.evidence ?? null,
suggestedAction: record.suggestedAction ?? null,
kind: record.kind ?? 'other',
});
}
}
return out;
}
function coerceOptionalString(value) {
return value == null ? value : String(value);
}
main().catch((err) => {
console.error('[render-report] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
@@ -0,0 +1,313 @@
#!/usr/bin/env node
// Walks the repo, runs every scanner in lib/scanners/, emits findings + routes
// + stack as JSON. Output is merged into signals.codebase.*. New scanners drop
// into lib/scanners/ + the barrel; this file is closed for modification.
import { readdir, readFile } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { scanners } from '../lib/scanners/index.mjs';
import { detectStack } from '../lib/vercel.mjs';
import {
detectMonorepoRoot,
listWorkspacePackages,
buildResolver,
resolveWorkspaceImports,
} from '../lib/workspace-resolver.mjs';
const SCHEMA_VERSION = '1.0';
const SKIP_DIRS = new Set(['node_modules', '.next', '.vercel', 'dist', 'build', '.git', 'coverage', '.turbo', '__tests__', 'cypress']);
const SKIP_FILE_PATTERNS = [/\.test\./, /\.spec\./, /\.d\.ts$/];
async function main() {
const rootDir = process.argv[2] || process.cwd();
process.stderr.write(`[scan-codebase] scanning ${rootDir}\n`);
const [stack, files, routes] = await Promise.all([
detectStack(rootDir),
collectFiles(rootDir),
enumerateRoutes(rootDir),
]);
// In a monorepo, route files often re-export from workspace packages. Without
// resolving those, sub-agents abstain because the workspace path is outside
// their read scope.
const monorepoRoot = await detectMonorepoRoot(rootDir);
let workspacePackages = [];
let resolver = () => null;
if (monorepoRoot) {
workspacePackages = await listWorkspacePackages(monorepoRoot);
resolver = buildResolver(workspacePackages);
process.stderr.write(`[scan-codebase] monorepo root: ${monorepoRoot} (${workspacePackages.length} workspace packages)\n`);
}
await enrichRoutesWithWorkspaceImports(routes, rootDir, resolver, monorepoRoot);
process.stderr.write(`[scan-codebase] ${files.length} files, ${routes.length} routes, ${scanners.length} scanners\n`);
const findings = [];
for (const scanner of scanners) {
try {
const applicable = filterApplicable(files, scanner.metadata);
// Scanners may be sync or async (large-static-asset does fs.stat walks).
const found = await scanner.scan({ files: applicable, rootDir, routes, stack });
for (const f of (found ?? [])) {
findings.push({
...f,
route: mapFileToRoute(f.file, routes),
});
}
} catch (err) {
process.stderr.write(`[scan-codebase] scanner ${scanner.metadata?.id} threw: ${err.message}\n`);
}
}
findings.sort((a, b) =>
a.file.localeCompare(b.file)
|| (a.line ?? 0) - (b.line ?? 0)
|| a.pattern.localeCompare(b.pattern)
);
process.stdout.write(JSON.stringify({
schemaVersion: SCHEMA_VERSION,
scannedAt: new Date().toISOString(),
rootDir,
monorepoRoot: monorepoRoot ?? null,
workspacePackages: workspacePackages.map((p) => ({ name: p.name, dir: relative(monorepoRoot ?? rootDir, p.dir) })),
stack,
routes,
findings,
scannerMetadata: scanners.map((s) => ({
id: s.metadata.id,
title: s.metadata.title,
severity: s.metadata.severity,
billingDimension: s.metadata.billingDimension,
trafficIndependent: s.metadata.trafficIndependent,
})),
}, null, 2) + '\n');
process.stderr.write(`[scan-codebase] ${findings.length} finding(s)\n`);
}
// Record workspace-package imports per route so the brief allowlists them and
// sub-agents can investigate the real source rather than abstaining on a thin
// re-export shell. Capped to keep the brief focused (source order ≈ import order,
// so the primary view component usually leads).
const WORKSPACE_IMPORT_LIMIT_PER_ROUTE = 12;
async function enrichRoutesWithWorkspaceImports(routes, scanRootDir, resolver, monorepoRoot) {
if (!monorepoRoot) return;
for (const r of routes) {
if (!r?.file) continue;
const abs = join(scanRootDir, r.file);
const resolved = await resolveWorkspaceImports(abs, resolver, {
pureBarrelDepth: 3,
suffixFanoutDepth: 2,
perSpecifierCap: 3,
});
if (resolved.length === 0) continue;
// Paths must be relative to the monorepo root so they align between signals + verifier.
r.workspaceImports = resolved
.slice(0, WORKSPACE_IMPORT_LIMIT_PER_ROUTE)
.map((abs) => relative(monorepoRoot, abs));
}
}
async function collectFiles(root) {
const entries = await readdir(root, { recursive: true, withFileTypes: true });
const out = [];
for (const e of entries) {
if (!e.isFile()) continue;
const segments = (e.parentPath ?? e.path ?? root).split('/');
if (segments.some((s) => SKIP_DIRS.has(s))) continue;
if (SKIP_FILE_PATTERNS.some((re) => re.test(e.name))) continue;
if (!/\.(tsx?|jsx?|mjs|cjs|html|svelte|astro|vue|json)$/.test(e.name)) continue;
const full = join(e.parentPath ?? e.path ?? root, e.name);
try {
const content = await readFile(full, 'utf-8');
if (content.length > 500_000) continue;
out.push({ path: relative(root, full), content });
} catch {}
}
return out;
}
function filterApplicable(files, meta) {
const incl = meta.includeGlobs ?? ['**/*'];
return files.filter((f) => incl.some((g) => globMatch(g, f.path)));
}
// Tiny glob → regex. Supports **, *, and {a,b} alternation.
function globMatch(pattern, path) {
const re = new RegExp(
'^' +
pattern
.replace(/[.+^$()|[\]\\]/g, '\\$&')
.replace(/\{([^}]+)\}/g, (_, inner) => '(' + inner.split(',').join('|') + ')')
.replace(/\*\*/g, '__GLOBSTAR__')
.replace(/\*/g, '[^/]*')
.replace(/__GLOBSTAR__/g, '.*')
+ '$'
);
return re.test(path);
}
async function enumerateRoutes(root) {
const entries = await readdir(root, { recursive: true, withFileTypes: true });
const routes = [];
for (const e of entries) {
if (!e.isFile()) continue;
const segments = (e.parentPath ?? e.path ?? root).split('/');
if (segments.some((s) => SKIP_DIRS.has(s))) continue;
const full = join(e.parentPath ?? e.path ?? root, e.name);
const rel = relative(root, full);
// App Router: route groups ((name)), parallel routes (@slot), private folders
// (_name), and the top-level page.tsx (no path segment) all need explicit handling.
let m = rel.match(/^(?:src\/)?app\/(.*)\/(page|route|layout)\.(tsx?|jsx?)$/);
if (!m) {
const top = rel.match(/^(?:src\/)?app\/(page|route|layout)\.(tsx?|jsx?)$/);
if (top) {
routes.push({
routePath: '/',
file: rel,
type: routeEntryType(top[1]),
});
continue;
}
}
if (m) {
const stripped = m[1]
.split('/')
.filter((seg) => !/^\([^)]+\)$/.test(seg) && !/^@/.test(seg) && !/^_/.test(seg))
.join('/')
.replace(/^\/+|\/+$/g, '');
const routePath = stripped === '' ? '/' : `/${stripped}`;
routes.push({
routePath,
file: rel,
type: routeEntryType(m[2]),
});
continue;
}
// Astro endpoint filenames commonly include the response extension
// (`feed.xml.ts`, `robots.txt.ts`). Handle these before the generic
// `src/pages` rule, which otherwise treats them as page components.
m = rel.match(/^src\/pages\/(.*\.(?:xml|json|txt|rss|atom|svg|png|jpg|jpeg|webp))\.(tsx?|jsx?|mjs|cjs)$/);
if (m) {
const name = normalizeRouteFileStem(m[1]);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: 'route',
});
continue;
}
m = rel.match(/^(?:src\/)?pages\/(.*)\.(tsx?|jsx?)$/);
if (m) {
const name = m[1].replace(/\/index$/, '').replace(/^index$/, '');
const isApi = /^api\//.test(name);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: isApi ? 'route' : 'page',
});
continue;
}
// Nuxt 3/4 pages. Dynamic segments use the same bracket shape as metrics
// (`[id]`, `[...slug]`), so keep them intact for route matching.
m = rel.match(/^(?:app\/)?pages\/(.*)\.vue$/);
if (m) {
const name = normalizeRouteFileStem(m[1]);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: 'page',
});
continue;
}
// Nuxt server routes: server/api/foo.get.ts -> /api/foo,
// server/routes/rss.xml.ts -> /rss.xml.
m = rel.match(/^server\/(api|routes)\/(.*)\.(tsx?|jsx?|mjs|cjs)$/);
if (m) {
const base = m[1] === 'api' ? 'api/' : '';
const name = normalizeRouteFileStem(`${base}${m[2]}`);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: 'route',
});
continue;
}
// Astro pages and endpoints. This is limited framework support, but route
// mapping still improves reports when Vercel metrics use user-facing paths.
m = rel.match(/^src\/pages\/(.*)\.(astro|tsx?|jsx?|mjs|cjs)$/);
if (m) {
const name = normalizeRouteFileStem(m[1]);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: m[2] === 'astro' ? 'page' : 'route',
});
continue;
}
// SvelteKit: +page.svelte = page, +page.server.{ts,js} pairs with it (treat
// as page), +server.{ts,js} = API route, +layout.* = ancestor layout context.
// Route groups (auth) stripped like Next; dynamic segments [slug]/[...rest]/[[opt]] preserved.
m = rel.match(/^src\/routes\/(.*)\/\+(page\.svelte|page\.server\.(?:ts|js)|server\.(?:ts|js)|layout\.svelte|layout\.server\.(?:ts|js))$/);
if (m || /^src\/routes\/\+(page\.svelte|page\.server\.(?:ts|js)|server\.(?:ts|js)|layout\.svelte|layout\.server\.(?:ts|js))$/.test(rel)) {
const fileTypeMatch = rel.match(/\+(page\.svelte|page\.server\.(?:ts|js)|server\.(?:ts|js)|layout\.svelte|layout\.server\.(?:ts|js))$/);
const fileType = fileTypeMatch?.[1] ?? '';
const segs = (m?.[1] ?? '').split('/').filter(Boolean)
.filter((seg) => !/^\([^)]+\)$/.test(seg));
const routePath = segs.length === 0 ? '/' : '/' + segs.join('/');
const type = fileType.startsWith('server') ? 'route' : fileType.startsWith('layout') ? 'layout' : 'page';
// When +page.svelte AND +page.server.ts both exist, +page.svelte wins ownership.
const existing = type === 'layout' ? null : routes.find((r) => r.routePath === routePath && r.type !== 'layout');
if (existing) {
if (fileType === 'page.svelte' && existing.type === 'page') {
existing.file = rel;
}
continue;
}
routes.push({ routePath, file: rel, type });
continue;
}
}
return routes.sort((a, b) =>
a.routePath.localeCompare(b.routePath)
|| routeTypeOrder(a.type) - routeTypeOrder(b.type)
|| a.file.localeCompare(b.file)
);
}
function routeEntryType(name) {
return name === 'route' ? 'route' : name === 'layout' ? 'layout' : 'page';
}
function normalizeRouteFileStem(stem) {
return String(stem ?? '')
.replace(/\/index$/, '')
.replace(/^index$/, '')
.replace(/\.(?:get|post|put|patch|delete|options|head)$/, '')
.replace(/^\/+|\/+$/g, '');
}
function routeTypeOrder(type) {
return type === 'page' ? 0 : type === 'route' ? 1 : type === 'layout' ? 2 : 3;
}
function mapFileToRoute(filePath, routes) {
const r = routes.find((rt) => rt.file === filePath);
return r?.routePath ?? null;
}
main().catch((err) => {
process.stderr.write(`[scan-codebase] FAILED: ${err.message}\n`);
process.exit(1);
});
@@ -0,0 +1,346 @@
#!/usr/bin/env node
// Verify → grade → emit regenPlan. Does NOT spawn sub-agents — the
// orchestrator reads regenPlan, re-spawns one sub-agent per targeted candidate
// with topFailures injected, then re-runs this script. Thresholds are tuned
// below (REGEN_*, QUALITY_FLOOR) — read those constants for the live values.
import { readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { mkdir } from 'node:fs/promises';
import { verifyClaim } from '../lib/verify-claim.mjs';
import { extractClaims, summarizeClaimResults } from '../lib/extract-claims.mjs';
import { gradeRecommendation, applyQualityFloor } from '../lib/grade-recommendation.mjs';
import { deriveProjectFacts } from '../lib/project-facts.mjs';
import { resolveRepoRoot } from '../lib/repo-root.mjs';
import { applySanitizers } from '../lib/sanitizers/index.mjs';
const SCHEMA_VERSION = '1.0';
const REGEN_PASS_RATE_THRESHOLD = 0.8;
// 1/1 failed is as broken as 1/5; below 2 claims is below the noise floor.
const REGEN_MIN_CLAIMS = 2;
// The Poor/Fair grade boundary — Poor recs erode trust faster than recall helps.
const QUALITY_FLOOR = 0.55;
const log = (...a) => console.error('[verify-and-regen]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.recsPath) {
console.error('usage: node scripts/verify-and-regen.mjs <recommendations.json> [--signals merged.json] [--repo-root DIR] [--out FILE]');
process.exit(1);
}
const recs = JSON.parse(await readFile(args.recsPath, 'utf-8'));
if (!Array.isArray(recs)) {
console.error('[verify-and-regen] FATAL: recommendations.json must be an array of rec objects');
process.exit(2);
}
let framework, version, cacheComponents, knownFindings = [], projectFacts = [], signals = null;
if (args.signalsPath) {
signals = JSON.parse(await readFile(args.signalsPath, 'utf-8'));
const stack = signals.stack ?? signals.codebase?.stack ?? {};
framework = stack.framework;
version = stack.frameworkVersion;
cacheComponents = stack.cacheComponents;
knownFindings = (signals.codebase?.findings ?? signals.findings ?? [])
.filter((f) => f.file && (f.line != null))
.map((f) => ({ file: f.file, line: f.line }));
projectFacts = deriveProjectFacts(signals);
if (projectFacts.length > 0) {
log(`project facts in play: ${projectFacts.map((f) => f.id).join(', ')}`);
}
}
// Repo-root priority: (1) signals.project.rootDirectory from Vercel API
// (authoritative — returns "apps/<name>" so cwd can be back-mapped without
// filesystem probing), (2) supplied --repo-root, (3) walk-up from cwd.
const rootResult = await resolveRepoRoot(recs, args.repoRoot, process.cwd(), signals);
const repoRoot = rootResult.root;
if (rootResult.source === 'api') {
log(`repo-root from Vercel API: '${repoRoot}' (rootDirectory='${rootResult.apiOffset}')`);
} else if (rootResult.source === 'auto-detected') {
log(`repo-root auto-detected: '${repoRoot}' (probe: ${rootResult.probe})`);
} else if (rootResult.source === 'corrected') {
log(`repo-root auto-corrected: '${args.repoRoot}' → '${repoRoot}' (sub-agent paths resolve there)`);
}
log(`verifying ${recs.length} rec(s) — framework=${framework ?? '?'}@${version ?? '?'} repoRoot=${repoRoot}`);
// knownFindings MUST combine scanner findings + sub-agent's verified
// findingRefs — scanner-only grounding would miss every metric-gate rec.
// Abstentions are first-class outputs ({abstain:true, candidateRef, reason})
// and MUST NOT be graded; the abstention IS the answer.
const recsGraded = [];
const abstentions = [];
const observations = [];
const sanitizerDropped = [];
for (let i = 0; i < recs.length; i++) {
const rec = recs[i];
if (rec?.abstain === true) {
abstentions.push({
index: i,
candidateRef: rec.candidateRef ?? null,
reason: rec.reason ?? '(no reason recorded)',
});
// Observation: real non-perf signal worth surfacing (regression, error storm).
if (rec.observation && typeof rec.observation === 'object' && rec.observation.summary) {
observations.push({
index: i,
candidateRef: rec.candidateRef ?? null,
summary: String(rec.observation.summary),
evidence: rec.observation.evidence ?? null,
suggestedAction: rec.observation.suggestedAction ?? null,
kind: rec.observation.kind ?? 'other',
});
}
continue;
}
const baseClaimCtx = {
framework,
version,
repoRoot,
projectFacts,
projectRootDirectory: signals?.project?.rootDirectory ?? null,
cacheComponents,
signals,
};
const initialClaims = extractClaims(rec, baseClaimCtx);
const initialVerifyResults = await Promise.all(initialClaims.map((c) => verifyClaim(c)));
const initialClaimsWithResults = initialVerifyResults.map((r, j) => ({
...r,
type: initialClaims[j]?.type,
claimType: initialClaims[j]?.type,
claim: initialClaims[j],
}));
const sanitizerResult = await applySanitizers(rec, {
framework,
version,
signals,
verifyResults: initialClaimsWithResults,
});
if (!sanitizerResult.kept) {
sanitizerDropped.push({
index: i,
candidateRef: rec.candidateRef ?? null,
what: rec.what ?? null,
reason: sanitizerResult.dropReason ?? 'automated-check',
});
continue;
}
const sanitizedRec = sanitizerResult.rec;
const claims = extractClaims(sanitizedRec, baseClaimCtx);
const verifyResults = await Promise.all(claims.map((c) => verifyClaim(c)));
const verification = summarizeClaimResults(verifyResults);
// A findingRef whose file_exists claim verified counts as grounding evidence.
const verifiedRefs = [];
for (let j = 0; j < claims.length; j++) {
const c = claims[j];
const r = verifyResults[j];
if (r?.disposition !== 'verified') continue;
if (c.sourceField === 'findingRefs' && c.type === 'file_exists') {
const ref = (rec.findingRefs ?? []).find((x) => String(x).startsWith(c.file + ':'));
if (ref) {
const m = String(ref).match(/^(.+?):(\d+)$/);
if (m) verifiedRefs.push({ file: m[1], line: Number(m[2]) });
}
}
}
const recKnownFindings = [...knownFindings, ...verifiedRefs];
const quality = gradeRecommendation(sanitizedRec, { knownFindings: recKnownFindings });
recsGraded.push({
index: i,
rec: { ...sanitizedRec, verification, verifyResults, quality },
claims,
verifyResults,
verification,
quality,
});
}
// Project-config contradictions are a HARD trigger: a "turn on Fluid" rec on
// a project where Fluid is already on passes 8/9 claims but is the wrong rec.
// passRate alone won't catch this.
const regenPlan = [];
for (const g of recsGraded) {
const { passRate, verifiable } = g.verification;
const claimsWithResults = g.verifyResults.map((r, j) => ({ ...r, claim: g.claims[j] }));
const contradictions = claimsWithResults.filter(
(r) => r.disposition === 'failed' && r.claim?.type === 'does_not_contradict_project_config'
);
const triggeredByPassRate = verifiable >= REGEN_MIN_CLAIMS && passRate < REGEN_PASS_RATE_THRESHOLD;
const cacheSafetyFailures = claimsWithResults.filter(
(r) => r.disposition === 'failed' && (
r.claim?.type === 'cache_vary_matches_dynamic_inputs' ||
r.claim?.type === 'cache_vary_cardinality_safe'
)
);
const semanticSafetyFailures = claimsWithResults.filter(
(r) => r.disposition === 'failed' && (
r.claim?.type === 'next_cached_not_found_causal_support' ||
r.claim?.type === 'next_stable_cache_api_for_version' ||
r.claim?.type === 'next_runtime_cache_api_for_version' ||
r.claim?.type === 'next_cache_life_single_execution' ||
r.claim?.type === 'next_cache_lifetime_freshness_supported' ||
r.claim?.type === 'next_cache_components_route_chain_file' ||
r.claim?.type === 'next_cache_life_cdn_header_semantics' ||
r.claim?.type === 'image_response_headers_citation' ||
r.claim?.type === 'next_image_priority_api_for_version' ||
r.claim?.type === 'next_cache_components_route_segment_config' ||
r.claim?.type === 'next_route_revalidate_static_prereq' ||
r.claim?.type === 'next_cache_tag_invalidation_supported' ||
r.claim?.type === 'cache_rec_not_error_dominated_or_acknowledged' ||
r.claim?.type === 'cache_control_header_syntax' ||
r.claim?.type === 'cache_control_headers_citation' ||
r.claim?.type === 'cache_404_long_ttl_safety' ||
r.claim?.type === 'route_error_not_found_status_and_scope' ||
r.claim?.type === 'immutable_dynamic_route_safety' ||
r.claim?.type === 'auth_guard_parallelization_safety' ||
r.claim?.type === 'parallelization_impact_not_overclaimed' ||
r.claim?.type === 'parallelization_not_cpu_bound_work' ||
r.claim?.type === 'runtime_error_cause_supported' ||
r.claim?.type === 'vercel_ignore_command_project_state'
)
);
const triggeredByContradiction = contradictions.length > 0;
const triggeredByCacheSafety = cacheSafetyFailures.length > 0;
const triggeredBySemanticSafety = semanticSafetyFailures.length > 0;
if (!triggeredByPassRate && !triggeredByContradiction && !triggeredByCacheSafety && !triggeredBySemanticSafety) continue;
const failures = claimsWithResults
.filter((r) => r.disposition === 'failed')
.slice(0, 5);
regenPlan.push({
index: g.index,
candidateRef: g.rec.candidateRef ?? null,
what: g.rec.what ?? null,
verifiableClaimCount: verifiable,
passRate,
regenTrigger: triggeredByContradiction
? 'project_config_contradiction'
: triggeredByCacheSafety
? 'cache_vary_safety'
: triggeredBySemanticSafety
? 'semantic_safety'
: 'pass_rate_below_threshold',
topFailures: failures.map((f) => ({
claimType: f.claim?.type,
field: f.claim?.sourceField,
url: f.claim?.url,
file: f.claim?.file,
pattern: f.claim?.pattern,
reason: f.reason,
})),
regenBriefHint: triggeredByContradiction
? 'Sub-agent recommended toggling on a project setting that is already enabled. Re-spawn with the project-config Strengths block highlighted; the rec must drop the contradictory step and keep only the actionable parts.'
: triggeredByCacheSafety
? 'Sub-agent recommended CDN caching with unsafe or missing Vary behavior. Re-spawn with the cache safety failure highlighted; the rec must use a low-cardinality Vary header that matches the dynamic inputs, or abstain.'
: triggeredBySemanticSafety
? 'Sub-agent made a framework-semantic claim that failed deterministic checks. Re-spawn with the failure highlighted; the rec must either add version-correct code/citations/runtime evidence or abstain.'
: 'Re-spawn the sub-agent with this rec\'s topFailures injected as feedback. Re-emit the rec only if regenPassRate >= originalPassRate AND citation count not gutted.',
});
}
const qualityCheck = applyQualityFloor(recsGraded.map((g) => g.rec), QUALITY_FLOOR);
const hardRegenIndexes = new Set(regenPlan.map((p) => p.index));
const qualityDroppedIndexes = new Set(
qualityCheck.dropped
.map((d) => recsGraded.findIndex((g) => g.rec === d.rec))
.filter((i) => i >= 0)
);
const needsReviewIndexes = new Set(
recsGraded
.filter((g) => g.rec.needsReview === true)
.map((g) => g.index)
);
const verifiedRecommendations = recsGraded
.filter((g) => !hardRegenIndexes.has(g.index) && !qualityDroppedIndexes.has(g.index) && !needsReviewIndexes.has(g.index))
.map((g) => g.rec);
const withheldRecommendations = recsGraded
.filter((g) => hardRegenIndexes.has(g.index) || qualityDroppedIndexes.has(g.index) || needsReviewIndexes.has(g.index))
.map((g) => ({
index: g.index,
candidateRef: g.rec.candidateRef ?? null,
what: g.rec.what ?? null,
reason: hardRegenIndexes.has(g.index)
? (regenPlan.find((p) => p.index === g.index)?.regenTrigger ?? 'verification')
: qualityDroppedIndexes.has(g.index)
? 'quality_floor'
: 'needs_review',
}));
const summary = {
totalRecs: recs.length,
abstentions: abstentions.length,
observations: observations.length,
sanitizerDropped: sanitizerDropped.length,
needsRegen: regenPlan.length,
qualityDropped: qualityCheck.dropped.length,
needsReview: needsReviewIndexes.size,
verifiedRecommendations: verifiedRecommendations.length,
withheldRecommendations: withheldRecommendations.length,
averagePassRate: recsGraded.length > 0
? round4(recsGraded.reduce((s, g) => s + g.verification.passRate, 0) / recsGraded.length)
: null,
averageQuality: recsGraded.length > 0
? round4(recsGraded.reduce((s, g) => s + g.quality.overall, 0) / recsGraded.length)
: null,
};
const output = {
schemaVersion: SCHEMA_VERSION,
summary,
recsGraded: recsGraded.map((g) => g.rec),
verifiedRecommendations,
renderableRecommendations: verifiedRecommendations,
withheldRecommendations,
abstentions,
observations,
sanitizerDropped,
regenPlan,
qualityDropped: qualityCheck.dropped.map((d) => ({
index: recsGraded.findIndex((g) => g.rec === d.rec),
candidateRef: d.rec.candidateRef ?? null,
quality: d.rec.quality,
reason: d.reason,
})),
};
const serialized = JSON.stringify(output, null, 2) + '\n';
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, serialized, 'utf-8');
log(`wrote ${serialized.length}B → ${args.outPath}`);
} else {
process.stdout.write(serialized);
}
log(`done: ${summary.totalRecs} records checked; ${summary.verifiedRecommendations} ready, ${summary.withheldRecommendations} held back, ${summary.abstentions} found no supported change, ${summary.sanitizerDropped} dropped by safety checks`);
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--signals') out.signalsPath = argv[++i];
else if (a.startsWith('--signals=')) out.signalsPath = a.slice('--signals='.length);
else if (a === '--repo-root') out.repoRoot = argv[++i];
else if (a.startsWith('--repo-root=')) out.repoRoot = a.slice('--repo-root='.length);
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else out.positional.push(a);
}
out.recsPath = out.positional[0];
return out;
}
function round4(n) { return Math.round(n * 10000) / 10000; }
main().catch((err) => {
console.error('[verify-and-regen] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
@@ -0,0 +1,19 @@
#!/usr/bin/env node
// CLI shell around lib/verify-claim.mjs. argv[2] = JSON claim, stdout = result.
// Claim `type` enum: pattern_count | pattern_exists | pattern_absent | file_exists |
// code_snippet | repo_count | citation_in_library | citation_applies_to_version.
import { verifyClaim } from '../lib/verify-claim.mjs';
const SCHEMA_VERSION = '1.0';
async function main() {
const claim = JSON.parse(process.argv[2] || '{}');
const result = await verifyClaim(claim);
process.stdout.write(JSON.stringify({ schemaVersion: SCHEMA_VERSION, ...result }, null, 2) + '\n');
}
main().catch((err) => {
process.stderr.write(`[verify-finding] FAILED: ${err.message}\n`);
process.exit(1);
});