87 lines
3.1 KiB
JavaScript
87 lines
3.1 KiB
JavaScript
// Auto-detect repo-root for claim verification. Priority: Vercel API rootDirectory > --repo-root > walk-up.
|
|
// In a monorepo the sub-agent emits paths like `apps/<app>/src/...` and the verifier needs the prefix root, not the app dir.
|
|
|
|
import { access } from 'node:fs/promises';
|
|
import { join, dirname, resolve, normalize } from 'node:path';
|
|
|
|
// Prefer affectedFiles[0] over findingRefs — findingRefs often share the same file.
|
|
export function pickProbeFile(recs) {
|
|
for (const r of (recs ?? [])) {
|
|
if (r?.abstain) continue;
|
|
const af = Array.isArray(r?.affectedFiles) ? r.affectedFiles[0] : null;
|
|
if (typeof af === 'string' && af.length > 0) return af;
|
|
const ref = Array.isArray(r?.findingRefs) ? r.findingRefs[0] : null;
|
|
if (typeof ref === 'string' && ref.length > 0) {
|
|
const m = ref.match(/^(.+?):\d+$/);
|
|
if (m) return m[1];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function fileResolvesAt(root, file) {
|
|
try {
|
|
await access(join(root, file));
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function detectRepoRoot(probeFile, startDir, maxDepth = 10) {
|
|
let dir = resolve(startDir);
|
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
if (await fileResolvesAt(dir, probeFile)) return dir;
|
|
const parent = dirname(dir);
|
|
if (parent === dir) return null;
|
|
dir = parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// rootDirectory "apps/fixture-site" + cwd .../monorepo/apps/fixture-site → repo root .../monorepo.
|
|
export function deriveRootFromSignals(signals, cwd = process.cwd()) {
|
|
const dir = signals?.project?.rootDirectory;
|
|
if (!dir || typeof dir !== 'string') return null;
|
|
const offset = normalize(dir).replace(/^\.\/?/, '').replace(/\/$/, '');
|
|
if (!offset) return null;
|
|
const cwdAbs = resolve(cwd);
|
|
// Match `<root>/<offset>` OR `<root>/<offset>/<more>` — orchestrator may run from a subdir.
|
|
const parts = cwdAbs.split('/');
|
|
const offsetParts = offset.split('/');
|
|
for (let start = parts.length - offsetParts.length; start >= 0; start--) {
|
|
const slice = parts.slice(start, start + offsetParts.length).join('/');
|
|
if (slice === offset) {
|
|
const root = parts.slice(0, start).join('/');
|
|
return root || '/';
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function resolveRepoRoot(recs, suppliedRoot, cwd = process.cwd(), signals = null) {
|
|
if (signals) {
|
|
const apiRoot = deriveRootFromSignals(signals, cwd);
|
|
if (apiRoot) {
|
|
return { root: apiRoot, source: 'api', probe: null, apiOffset: signals?.project?.rootDirectory ?? null };
|
|
}
|
|
}
|
|
|
|
const probe = pickProbeFile(recs);
|
|
if (!probe) {
|
|
return { root: suppliedRoot ?? '.', source: suppliedRoot ? 'supplied' : 'default', probe: null };
|
|
}
|
|
if (suppliedRoot && await fileResolvesAt(suppliedRoot, probe)) {
|
|
return { root: suppliedRoot, source: 'supplied', probe };
|
|
}
|
|
const detected = await detectRepoRoot(probe, suppliedRoot ?? cwd);
|
|
if (detected) {
|
|
return {
|
|
root: detected,
|
|
source: suppliedRoot ? 'corrected' : 'auto-detected',
|
|
probe,
|
|
};
|
|
}
|
|
return { root: suppliedRoot ?? '.', source: suppliedRoot ? 'supplied' : 'default', probe };
|
|
}
|