import { type RuleContext, STAGES, type Stage, type StageResult } from "../types/rule.types.ts"; import { existsSync, statSync } from "node:fs"; import { readSource, toPosix } from "../iterators/file.iterator.ts"; import type { Finding } from "../types/segment.types.ts"; import { cleanStage } from "../steps/source.step.ts"; import { discoverRules } from "../registries/rule.registry.ts"; import { gateStage } from "../steps/gate.step.ts"; import { loadTaxonomy } from "../resolvers/taxonomy.resolver.ts"; import { qualityStage } from "../steps/quality.step.ts"; import { resolve } from "node:path"; import { resolveScope } from "../resolvers/scope.resolver.ts"; import { snapshotStage } from "../steps/snapshot.step.ts"; import { typecheckStage } from "../steps/typecheck.step.ts"; import { writeRuleReport } from "../reporters/rule.reporter.ts"; export interface RunOptions { readonly repoRoot: string; readonly ruleId: string | null; readonly stage: Stage | null; readonly scope: string | null; readonly bypass: readonly string[]; readonly fix: boolean; } export type { StageResult }; export interface RunResult { readonly findings: readonly Finding[]; readonly stages: readonly StageResult[]; readonly scanned: number; readonly registered: number; readonly bypassedAny: boolean; readonly scope: string; readonly authoritative: boolean; readonly moved: readonly string[]; readonly movedByThisRun: readonly string[]; readonly written: readonly string[]; readonly escaped: readonly string[]; readonly unfulfilled: readonly string[]; readonly incomparable: readonly string[]; readonly unresolvedScope?: string; } const WHOLE_SCOPE = "whole"; const withinDeclaredScope = function withinDeclaredScope( path: string, scope: string, handed: readonly string[], ): boolean { if (scope === WHOLE_SCOPE) { return true; } return handed.includes(path); }; export const runPipeline = async function runPipeline(options: RunOptions): Promise { const narrowings: string[] = []; if (options.scope !== null) { narrowings.push(`path=${options.scope}`); } if (options.ruleId !== null) { narrowings.push(`rule=${options.ruleId}`); } if (options.stage !== null) { narrowings.push(`stage=${options.stage}`); } if (options.bypass.length > 0) { narrowings.push(`bypassed=${options.bypass.join("+")}`); } const scope = narrowings.length === 0 ? WHOLE_SCOPE : narrowings.join(" ยท "); const authoritative = narrowings.length === 0; const taxonomy = loadTaxonomy(); const registry = await discoverRules(options.repoRoot); const findings: Finding[] = [...registry.findings]; const written: string[] = []; const stages: StageResult[] = []; const incomparable: string[] = []; const { byJurisdiction } = resolveScope(options.repoRoot, taxonomy, options.scope); const paths = byJurisdiction.all; if (options.scope !== null && paths.length === 0) { return { authoritative, bypassedAny: false, escaped: [], findings: [], incomparable: [], moved: [], movedByThisRun: [], registered: registry.rules.length, scanned: 0, scope, stages: [], unfulfilled: [], unresolvedScope: options.scope, written: [], }; } const cache = new Map(); const stamps = new Map(); const stampOf = (absolute: string): number => { if (!existsSync(absolute)) { return 0; } return statSync(absolute).mtimeMs; }; const read = (path: string): string => { const hit = cache.get(path); if (hit !== undefined) { return hit; } const absolute = resolve(options.repoRoot, path); const source = readSource(absolute); cache.set(path, source); stamps.set(path, stampOf(absolute)); return source; }; for (const path of paths) { if (stamps.has(path)) { continue; } stamps.set(path, stampOf(resolve(options.repoRoot, path))); } let bypassedAny = false; const stepOptions = { authoritative, bypass: options.bypass, fix: options.fix, repoRoot: options.repoRoot, scanned: paths.length, scope, }; const whole = options.ruleId === null && options.stage === null; const steps = whole ? [cleanStage, typecheckStage, qualityStage, gateStage] : [cleanStage]; for (const step of steps) { const outcome = step(stepOptions); stages.push(outcome.stage); for (const finding of outcome.findings) { findings.push(finding); } if (outcome.stage.bypassed) { bypassedAny = true; } } const snapshot = snapshotStage(stepOptions, paths); stages.push(snapshot.stage); for (const finding of snapshot.findings) { findings.push(finding); } if (snapshot.stage.bypassed) { bypassedAny = true; } for (const stage of STAGES) { if (options.stage !== null && options.stage !== stage) { continue; } for (const registered of registry.rules) { const { declaration } = registered; if (declaration.stage !== stage) { continue; } if (options.ruleId !== null && options.ruleId !== registered.id) { continue; } if (options.bypass.includes(registered.id) || options.bypass.includes(stage)) { bypassedAny = true; stages.push({ bypassed: true, findings: 0, healed: 0, invariant: declaration.invariant, rule: registered.id, stage, }); continue; } const jurisdiction = byJurisdiction[declaration.jurisdiction]; const scoped = declaration.extensions.length === 0 ? jurisdiction : jurisdiction.filter((path) => { for (const ext of declaration.extensions) { if (path.length > ext.length && path.endsWith(ext)) { return true; } } return false; }); const declared = declaration.reads ?? []; const extra: string[] = []; for (const wanted of declared) { if (scoped.includes(wanted)) { continue; } if (!existsSync(resolve(options.repoRoot, wanted))) { continue; } extra.push(wanted); } const handed = extra.length === 0 ? scoped : [...scoped, ...extra]; if (declaration.wholeScopeOnly === true && !authoritative) { incomparable.push(registered.id); stages.push({ bypassed: true, findings: 0, healed: 0, invariant: declaration.invariant, rule: registered.id, stage: declaration.stage, }); continue; } const context: RuleContext = { exists: (path: string) => existsSync(resolve(options.repoRoot, path)), id: registered.id, paths: handed, read, repoRoot: options.repoRoot, taxonomy, }; const started = process.hrtime.bigint(); const result = declaration.check(context, options.fix); const elapsed = Number((process.hrtime.bigint() - started) / 1000n) / 1000; writeRuleReport(options.repoRoot, registered.id, { authoritative, derivations: result.derivations, findings: result.findings, healed: result.healed, invariant: declaration.invariant, mechanism: toPosix(options.repoRoot, registered.path), rule: registered.id, scanned: handed.length, scope, stage: declaration.stage, verdict: result.findings.length === 0 ? "pass" : "fail", }); for (const path of result.healed) { written.push(path); } for (const finding of result.findings) { findings.push(finding); } stages.push({ bypassed: false, elapsed, findings: result.findings.length, healed: result.healed.length, invariant: declaration.invariant, rule: registered.id, stage, }); } } const moved: string[] = []; const movedByThisRun: string[] = []; for (const [path, stamp] of stamps) { if (stampOf(resolve(options.repoRoot, path)) === stamp) { continue; } if (written.includes(path)) { movedByThisRun.push(path); continue; } moved.push(path); } const escaped = written.filter((path) => !withinDeclaredScope(path, scope, paths)); const unfulfilled = written.filter((path) => stamps.has(path) && !movedByThisRun.includes(path)); return { authoritative, bypassedAny, escaped, findings, incomparable, moved, movedByThisRun, registered: registry.rules.length, scanned: paths.length, scope, stages, unfulfilled, written, }; };