import { access, mkdir, readFile, readdir, writeFile, } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { canonicalizeRegistryValue, diffRegistrySnapshots, registrySnapshotDigest, validateBreakingEvidence, verifyRegistryBaselineApproval, } from "./lib/registry-compatibility.mjs"; /** @param {string} name @param {string | undefined} fallback */ function argumentValue(name, fallback) { const index = process.argv.indexOf(name); return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback; } const defaultGovernancePath = "config/contracts/registry-governance.json"; const governancePath = /** @type {string} */ ( argumentValue("--governance", defaultGovernancePath) ); const artifactPath = /** @type {string} */ ( argumentValue("--artifact", "artifacts/quality/registries.json") ); const usesRepositoryBaseline = governancePath === defaultGovernancePath && !process.argv.includes("--no-baseline"); const baselinePath = argumentValue( "--baseline", usesRepositoryBaseline ? "config/contracts/registry-baseline.json" : undefined, ); const approvalPath = argumentValue( "--approval", usesRepositoryBaseline ? "config/contracts/registry-baseline.approval.json" : undefined, ); const evidencePath = argumentValue( "--compatibility-evidence", usesRepositoryBaseline ? "config/contracts/registry-change-evidence.json" : undefined, ); const governance = JSON.parse(await readFile(governancePath, "utf8")); const failures = []; const owners = new Map(); const snapshots = []; const rowsByRegistry = new Map(); const sourcesByRegistry = new Map(); const registryExtensions = [".js", ".jsx", ".mjs", ".ts", ".tsx", ".mts"]; /** @param {string} declaredPath */ async function resolveRegistrySource(declaredPath) { const extension = path.extname(declaredPath); const basePath = extension ? declaredPath.slice(0, -extension.length) : declaredPath; const candidates = []; for (const candidateExtension of registryExtensions) { const candidate = `${basePath}${candidateExtension}`; try { await access(candidate); candidates.push(candidate); } catch { // A TypeScript migration may replace the declared extension. } } if (candidates.length > 1) { failures.push( `ambiguous registry source ${declaredPath}: ${candidates.join(", ")}`, ); return null; } return candidates[0] ?? null; } /** @param {unknown} value */ function runtimeType(value) { if (value === null) return "null"; if (Array.isArray(value)) return "array"; if (Number.isInteger(value)) return "integer"; return typeof value; } /** @param {unknown} value @param {string} declaration */ function matchesDeclaredType(value, declaration) { const actual = runtimeType(value); return declaration .split("|") .some( (candidate) => candidate === actual || (candidate === "number" && actual === "integer"), ); } /** @param {string} directory @returns {Promise} */ async function filesBelow(directory) { try { const entries = await readdir(directory, { withFileTypes: true }); const groups = await Promise.all( entries.map((entry) => { const target = path.join(directory, entry.name); return entry.isDirectory() ? filesBelow(target) : [target]; }), ); return groups.flat().filter((file) => /\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(file), ); } catch { return []; } } for (const specification of governance.registries) { if (owners.has(specification.registryId)) { failures.push(`duplicate owner for ${specification.registryId}`); } owners.set(specification.registryId, specification.owner); let rows = specification.declaredRows; const sourcePath = await resolveRegistrySource(specification.path); try { if (!sourcePath) throw new Error("missing registry source"); const module = await import( `${pathToFileURL(path.resolve(sourcePath)).href}?registry-check=${Date.now()}` ); rows = module[specification.exportName]; } catch { if (!rows) failures.push(`missing registry source ${specification.path}`); } if (!rows || typeof rows !== "object" || Array.isArray(rows)) { failures.push(`${specification.registryId} is not an object registry`); continue; } rowsByRegistry.set(specification.registryId, rows); sourcesByRegistry.set( specification.registryId, sourcePath ?? specification.path, ); for (const [rowName, row] of Object.entries(rows)) { if (!row || typeof row !== "object" || Array.isArray(row)) { failures.push(`${specification.registryId}.${rowName} is not an object`); continue; } for (const field of specification.requiredFields) { if (!(field in row)) { failures.push(`${specification.registryId}.${rowName} missing ${field}`); } } for (const [field, declaredType] of Object.entries( specification.fieldTypes ?? {}, )) { if ( field in row && !matchesDeclaredType(row[field], String(declaredType)) ) { failures.push( `${specification.registryId}.${rowName}.${field} expected ${declaredType}, received ${runtimeType(row[field])}`, ); } } if ( specification.keyField && row[specification.keyField] !== rowName ) { failures.push( `${specification.registryId}.${rowName}.${specification.keyField} must match its registry key`, ); } } for (const field of specification.uniqueFields ?? []) { const values = new Map(); for (const [rowName, row] of Object.entries(rows)) { if (!row || typeof row !== "object" || Array.isArray(row)) continue; const value = row[field]; if (value === undefined) continue; const identity = JSON.stringify(canonicalizeRegistryValue(value)); if (values.has(identity)) { failures.push( `${specification.registryId}.${rowName} duplicates ${field}=${String(value)} from ${values.get(identity)}`, ); } else { values.set(identity, rowName); } } } for (const [field, allowed] of Object.entries( specification.allowedValues ?? {}, )) { for (const [rowName, row] of Object.entries(rows)) { if (!row || typeof row !== "object" || Array.isArray(row)) continue; if ( !allowed.some( /** @param {unknown} value */ (value) => Object.is(value, row[field]), ) ) { failures.push( `${specification.registryId}.${rowName}.${field} has unknown value ${String(row[field])}`, ); } } } const contract = Object.freeze({ requiredFields: specification.requiredFields, fieldTypes: specification.fieldTypes ?? {}, uniqueFields: specification.uniqueFields ?? [], allowedValues: specification.allowedValues ?? {}, references: specification.references ?? [], keyField: specification.keyField ?? null, breakingFields: specification.breakingFields ?? [], }); snapshots.push({ registryId: specification.registryId, owner: specification.owner, source: sourcePath ?? specification.path, rowCount: Object.keys(rows).length, contract, rows: canonicalizeRegistryValue(rows), }); } for (const specification of governance.registries) { const rows = rowsByRegistry.get(specification.registryId); if (!rows) continue; for (const reference of specification.references ?? []) { const targetRows = rowsByRegistry.get(reference.registryId); if (!targetRows) { failures.push( `${specification.registryId} references unknown registry ${reference.registryId}`, ); continue; } const targetValues = new Set( Object.values(targetRows) .filter((row) => row && typeof row === "object" && !Array.isArray(row)) .map((row) => row[reference.targetField]) .filter((value) => value !== undefined && value !== null), ); for (const [rowName, row] of Object.entries(rows)) { if (!row || typeof row !== "object" || Array.isArray(row)) continue; const value = row[reference.field]; if ( value !== undefined && value !== null && !targetValues.has(value) ) { failures.push( `${specification.registryId}.${rowName}.${reference.field} references unknown ${reference.registryId}.${reference.targetField}=${String(value)}`, ); } } } for (const consumer of specification.consumers ?? []) { try { const source = await readFile(consumer.path, "utf8"); if (!source.includes(consumer.token)) { failures.push( `${specification.registryId} consumer ${consumer.path} is missing ${consumer.token}`, ); } } catch { failures.push( `${specification.registryId} consumer source is missing: ${consumer.path}`, ); } } if (specification.consumerIdentityField) { const consumerFiles = ( await Promise.all( (specification.consumerDirectories ?? []).map(filesBelow), ) ).flat(); const sourcePath = sourcesByRegistry.get(specification.registryId); const consumerText = ( await Promise.all( consumerFiles .filter((file) => file !== sourcePath) .map((file) => readFile(file, "utf8")), ) ).join("\n"); const exemptions = new Set(specification.orphanExemptRows ?? []); for (const [rowName, row] of Object.entries(rows)) { if ( !row || typeof row !== "object" || Array.isArray(row) || exemptions.has(rowName) ) { continue; } const identity = row[specification.consumerIdentityField]; if ( (typeof identity !== "string" && typeof identity !== "number") || !consumerText.includes(String(identity)) ) { failures.push( `${specification.registryId}.${rowName} has no executable consumer for ${specification.consumerIdentityField}=${String(identity)}`, ); } } } } const sourceFiles = governance.sourceDirectories ?? [ "src/application", "src/presentation", "src/domain", ]; const adHocPatterns = [ { name: "direct fetch", expression: /\bfetch\s*\(/ }, { name: "direct localStorage", expression: /\blocalStorage\.(?:get|set|remove)Item/, }, { name: "direct import.meta.env", expression: /\bimport\.meta\.env\./ }, { name: "raw API path", expression: /["']\/api\// }, ]; for (const sourceDirectory of sourceFiles) { for (const file of await filesBelow(sourceDirectory)) { const content = await readFile(file, "utf8"); for (const pattern of adHocPatterns) { if (pattern.expression.test(content)) { failures.push(`ad-hoc ${pattern.name} in ${file}`); } } } } const currentSnapshot = /** @type {Readonly>} */ ( canonicalizeRegistryValue({ schemaVersion: 2, registries: snapshots, }) ); let baselineDigest = null; let currentDigest = registrySnapshotDigest(currentSnapshot); let compatibility = /** @type {{impact: string, changes: readonly Record[]}} */ ({ impact: "not-evaluated", changes: [], }); if (baselinePath && approvalPath && evidencePath) { try { const baseline = JSON.parse(await readFile(baselinePath, "utf8")); const approval = JSON.parse(await readFile(approvalPath, "utf8")); const approvalResult = verifyRegistryBaselineApproval(baseline, approval); baselineDigest = approvalResult.actualDigest; if (!approvalResult.passed) { failures.push( `registry baseline approval digest mismatch: approved=${approvalResult.approvedDigest} actual=${approvalResult.actualDigest}`, ); } compatibility = diffRegistrySnapshots(baseline, currentSnapshot); const evidence = JSON.parse(await readFile(evidencePath, "utf8")); const evidenceResult = validateBreakingEvidence(compatibility, evidence); failures.push(...evidenceResult.failures); } catch (error) { failures.push( `registry compatibility evidence unavailable: ${ error instanceof Error ? error.name : "unknown" }`, ); } } const report = { schemaVersion: 2, generatedAt: new Date().toISOString(), baselineDigest, currentDigest, compatibility, failures, registries: snapshots, }; await mkdir(path.dirname(artifactPath), { recursive: true }); await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`); if (failures.length > 0) { process.stderr.write(`Registry governance failed:\n${failures.join("\n")}\n`); process.exit(1); } process.stdout.write( `Registry governance: ${snapshots.length} registries PASS; compatibility=${compatibility.impact}\n`, );