📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user