import { EMISSION_CALL, FINDING_EMISSION, REPORT_SUFFIX, RULE_DIR, SKIP_PREFIX, STEP_DIR, } from "../constants/report.constants.ts"; import { containsInCode, declaresProperty } from "../predicates/source.predicate.ts"; import { existsSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs"; import { fieldOf, tryParse } from "../readers/json.reader.ts"; import { GENERATED_DIR } from "../constants/path.constants.ts"; import { identityOf } from "../registries/rule.registry.ts"; import { isObject } from "../predicates/schema.predicate.ts"; import { moduleSource } from "../analyzers/graph.analyzer.ts"; import { resolve } from "node:path"; const SOURCE_EXTENSION = ".ts"; const QUOTE = '"'; const COMPOSITE_SEPARATOR = "/"; const COMPOSITE_KEYED = "composite-keyed"; interface ParsedReport { readonly entry: string; readonly path: string; readonly value: object; } const reportIdOf = function reportIdOf(entry: string): string { return entry.slice(0, entry.length - REPORT_SUFFIX.length); }; const reportEntries = function reportEntries(repoRoot: string): string[] { const dir = resolve(repoRoot, GENERATED_DIR); return existsSync(dir) ? readdirSync(dir).filter((entry) => entry.endsWith(REPORT_SUFFIX)) : []; }; const reportsIn = function reportsIn(repoRoot: string): ParsedReport[] { const dir = resolve(repoRoot, GENERATED_DIR); return reportEntries(repoRoot).flatMap((entry) => { const path = resolve(dir, entry); const value = tryParse(readFileSync(path, "utf8"))?.value; return typeof value === "object" && value !== null ? [{ entry, path, value }] : []; }); }; const textOf = function textOf(value: unknown): string { return typeof value === "string" ? value : ""; }; const countOf = function countOf(value: unknown): number { return typeof value === "number" ? value : 0; }; export const isConstructShaped = function isConstructShaped(id: string): boolean { for (const char of id) { if (char < "a" || char > "z") { return false; } } return id.length > 0; }; export const reportMissing = function reportMissing(repoRoot: string, id: string): boolean { return !existsSync(resolve(repoRoot, GENERATED_DIR, `${id}${REPORT_SUFFIX}`)); }; export const REPORT_IDENTITY_FIELDS = ["scope", "authoritative", "verdict"]; export const missingReportIdentity = function missingReportIdentity(repoRoot: string, id: string): string[] { const target = resolve(repoRoot, GENERATED_DIR, `${id}${REPORT_SUFFIX}`); if (!existsSync(target)) { return []; } const value = tryParse(readFileSync(target, "utf8"))?.value; if (typeof value !== "object" || value === null) { return [...REPORT_IDENTITY_FIELDS]; } return REPORT_IDENTITY_FIELDS.filter((field) => fieldOf(value, field) === undefined); }; const quotedArgument = function quotedArgument(source: string, at: number): string | null { const comma = source.indexOf(",", at); let open = comma === -1 ? source.length + 1 : comma + 1; while (source.charAt(open) === " ") { open += 1; } if (source.charAt(open) !== QUOTE) { return null; } const close = source.indexOf(QUOTE, open + 1); const value = close === -1 ? source.slice(open + 1) : source.slice(open + 1, close); return value.length > 0 ? value : null; }; export const emittedIds = function emittedIds(source: string): string[] { const out = new Set(); let at = source.indexOf(EMISSION_CALL); while (at !== -1) { const id = quotedArgument(source, at + EMISSION_CALL.length); if (id !== null) { out.add(id); } at = source.indexOf(EMISSION_CALL, at + 1); } return [...out]; }; const idsDeclaredIn = function idsDeclaredIn(repoRoot: string, relativeDir: string): string[] { const dir = resolve(repoRoot, relativeDir); if (!existsSync(dir)) { return []; } return readdirSync(dir) .filter((entry) => entry.endsWith(SOURCE_EXTENSION)) .flatMap((entry) => [identityOf(entry), ...emittedIds(readFileSync(resolve(dir, entry), "utf8"))]); }; export const stepEmittedIds = function stepEmittedIds(repoRoot: string): string[] { return [...new Set(idsDeclaredIn(repoRoot, STEP_DIR))]; }; export const claimedReportIds = function claimedReportIds(repoRoot: string): Set { return new Set([...stepEmittedIds(repoRoot), ...idsDeclaredIn(repoRoot, RULE_DIR)]); }; export const orphanReports = function orphanReports(repoRoot: string, claimed: ReadonlySet): string[] { return reportsIn(repoRoot) .filter((report) => !claimed.has(reportIdOf(report.entry))) .filter((report) => fieldOf(report.value, "rule") !== undefined && fieldOf(report.value, "stage") !== undefined) .map((report) => report.entry); }; export const deleteReport = function deleteReport(repoRoot: string, name: string): void { rmSync(resolve(repoRoot, GENERATED_DIR, name), { force: true }); }; export interface ScopeGap { readonly report: string; readonly handed: number; readonly reached: number; readonly named: number; } const namedSkips = function namedSkips(derivations: Readonly>): number { return Object.entries(derivations) .filter(([key]) => key.startsWith(SKIP_PREFIX)) .flatMap(([, value]): unknown[] => (Array.isArray(value) ? value : [])).length; }; const scopeGap = function scopeGap(report: ParsedReport): ScopeGap[] { const derivations = fieldOf(report.value, "derivations"); const reachedList = isObject(derivations) ? derivations["reached"] : undefined; if (!isObject(derivations) || reachedList === undefined) { return []; } const handed = countOf(fieldOf(report.value, "scanned")); if (!Array.isArray(reachedList)) { return [{ handed, named: 0, reached: -1, report: report.entry }]; } const reached = reachedList.length; const named = namedSkips(derivations); return handed <= reached || handed - reached === named ? [] : [{ handed, named, reached, report: report.entry }]; }; export const unaccountedScopeGaps = function unaccountedScopeGaps(repoRoot: string): ScopeGap[] { return reportsIn(repoRoot).flatMap(scopeGap); }; export interface UnevaluableScope { readonly report: string; readonly handed: number; } const isVerdictBearing = function isVerdictBearing(value: object): boolean { return typeof fieldOf(value, "verdict") === "string" && typeof fieldOf(value, "scanned") === "number"; }; const publishesAPopulation = function publishesAPopulation(derivations: unknown): boolean { return isObject(derivations) && Object.values(derivations).some((value) => Array.isArray(value)); }; export const unevaluableScopes = function unevaluableScopes(repoRoot: string): UnevaluableScope[] { return reportsIn(repoRoot) .filter((report) => isVerdictBearing(report.value)) .flatMap((report) => { const handed = countOf(fieldOf(report.value, "scanned")); const published = publishesAPopulation(fieldOf(report.value, "derivations")); return handed === 0 || published ? [] : [{ handed, report: report.entry }]; }); }; export const selfAuditedReports = function selfAuditedReports(repoRoot: string, auditor: string): string[] { return reportEntries(repoRoot).filter((entry) => reportIdOf(entry) === auditor); }; export interface ReportStanding { readonly report: string; readonly staleSurfaces: readonly string[]; } const stampOf = function stampOf(absolute: string): number { return existsSync(absolute) ? statSync(absolute).mtimeMs : 0; }; const reachedSurfaces = function reachedSurfaces(value: object): string[] { const derivations = fieldOf(value, "derivations"); if (!isObject(derivations)) { return []; } return Object.values(derivations) .flatMap((held): unknown[] => (Array.isArray(held) ? held : [])) .filter((member): member is string => typeof member === "string" && member.includes(COMPOSITE_SEPARATOR)); }; const contradiction = function contradiction( name: string, value: unknown, derivations: Readonly>, ): string[] { const subject = isObject(value) ? value["subject"] : undefined; if (!isObject(value) || value["property"] !== COMPOSITE_KEYED || typeof subject !== "string") { return []; } const held = derivations[subject]; const flat = Array.isArray(held) ? held.filter((member: unknown): member is string => typeof member === "string" && !member.includes(COMPOSITE_SEPARATOR)) : []; return flat.length === 0 ? [] : [ `${name} declares ${subject} composite-keyed and that key carries ${String(flat.length)} member(s) with no separator: ${flat.join(", ")}`, ]; }; export const contradictedContracts = function contradictedContracts(derivations: Record): string[] { return Object.entries(derivations).flatMap(([name, value]) => contradiction(name, value, derivations)); }; export interface UnrepairableLocus { readonly report: string; readonly rule: string; readonly target: string; readonly reason: "frozenTarget" | "permanentSpan"; } interface UnrepairableCandidate { readonly key: string; readonly locus: UnrepairableLocus; } interface RepairJudges { readonly frozen: (target: string) => boolean; readonly permanentSpan: (path: string, locus: string) => boolean; } const reasonOf = function reasonOf( target: string, reported: string, locus: string, judges: RepairJudges, ): UnrepairableLocus["reason"] | null { if (judges.frozen(target)) { return "frozenTarget"; } return judges.permanentSpan(reported, locus) ? "permanentSpan" : null; }; const candidateOf = function candidateOf(entry: string, finding: unknown, judges: RepairJudges): UnrepairableCandidate[] { const remediation = isObject(finding) ? finding["remediation"] : undefined; const target = isObject(remediation) ? remediation["target"] : undefined; if (!isObject(finding) || typeof target !== "string") { return []; } const locus = textOf(finding["locus"]); const reason = reasonOf(target, textOf(finding["path"]), locus, judges); const rule = textOf(finding["rule"]); return reason === null ? [] : [{ key: `${entry}|${rule}|${target}|${locus}`, locus: { reason, report: entry, rule, target } }]; }; export const unrepairableLoci = function unrepairableLoci( repoRoot: string, frozen: (target: string) => boolean, permanentSpan: (path: string, locus: string) => boolean, ): UnrepairableLocus[] { const judges: RepairJudges = { frozen, permanentSpan }; const candidates = reportsIn(repoRoot).flatMap((report) => { const held = fieldOf(report.value, "findings"); return Array.isArray(held) ? held.flatMap((finding: unknown) => candidateOf(report.entry, finding, judges)) : []; }); const seen = new Set(); return candidates .filter((candidate) => { const fresh = !seen.has(candidate.key); seen.add(candidate.key); return fresh; }) .map((candidate) => candidate.locus); }; export const withdrawnStandings = function withdrawnStandings(repoRoot: string): ReportStanding[] { return reportsIn(repoRoot).flatMap((report) => { const written = stampOf(report.path); const stale = reachedSurfaces(report.value) .filter((surface) => stampOf(resolve(repoRoot, surface)) > written) .toSorted((left, right) => left.localeCompare(right, "en")); return stale.length === 0 ? [] : [{ report: report.entry, staleSurfaces: stale }]; }); }; export const missingFindingFields = function missingFindingFields( path: string, read: (path: string) => string, known: ReadonlySet, required: readonly string[], ): string[] { const reachable = moduleSource(path, read, known); return required.filter((field) => !declaresProperty(reachable, field)); }; export interface ShapeGap { readonly path: string; readonly field: string; } export const findingShapeGaps = function findingShapeGaps( paths: readonly string[], read: (path: string) => string, required: readonly string[], ): ShapeGap[] { const known = new Set(paths); return paths .filter((path) => path.endsWith(SOURCE_EXTENSION) && containsInCode(read(path), FINDING_EMISSION)) .flatMap((path) => missingFindingFields(path, read, known, required).map((field) => ({ field, path }))); };