Files
clean-architecture-frontend…/scripts/check-registries.ts
T

687 lines
21 KiB
TypeScript

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.ts";
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
type RegistryRow = Record<string, unknown>;
type RegistryRows = Record<string, RegistryRow>;
type RegistryReference = Readonly<{
registryId: string;
field: string;
targetField: string;
}>;
type RegistryConsumer = Readonly<{ path: string; token: string }>;
type RegistrySnapshotProjection = Readonly<{
singletonRowKey: string;
canonicalArrayKeyFields: Readonly<Record<string, readonly string[]>>;
}>;
type RegistrySpecification = Readonly<{
registryId: string;
owner: string;
path: string;
exportName: string;
declaredRows?: unknown;
rowsPath?: string;
rowKeyFields?: readonly string[];
requiredFields: readonly string[];
fieldTypes?: Readonly<Record<string, string>>;
keyField?: string;
uniqueFields?: readonly string[];
uniqueFieldSets?: readonly (readonly string[])[];
snapshotProjection?: RegistrySnapshotProjection;
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
references?: readonly RegistryReference[];
breakingFields?: readonly string[];
consumers?: readonly RegistryConsumer[];
consumerIdentityField?: string;
consumerDirectories?: readonly string[];
orphanExemptRows?: readonly string[];
}>;
type RegistryGovernance = Readonly<{
registries: readonly RegistrySpecification[];
sourceDirectories?: readonly string[];
}>;
type RegistrySnapshot = Record<string, unknown>;
type CompatibilitySummary = Readonly<{
impact: string;
changes: readonly unknown[];
}>;
function argumentValue(
name: string,
fallback: string | undefined,
): string | undefined {
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 =
argumentValue("--governance", defaultGovernancePath) ??
defaultGovernancePath;
const artifactPath =
argumentValue("--artifact", "artifacts/quality/registries.json") ??
"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"),
) as RegistryGovernance;
const failures: string[] = [];
const owners = new Map<string, string>();
const snapshots: RegistrySnapshot[] = [];
const rowsByRegistry = new Map<string, RegistryRows>();
const sourcesByRegistry = new Map<string, string>();
const registryExtensions = [".ts", ".tsx", ".mts", ".cts"];
async function resolveRegistrySource(
declaredPath: string,
): Promise<string | null> {
const extension = path.extname(declaredPath);
const basePath = extension
? declaredPath.slice(0, -extension.length)
: declaredPath;
const candidates: string[] = [];
for (const candidateExtension of registryExtensions) {
const candidate = `${basePath}${candidateExtension}`;
try {
await access(candidate);
candidates.push(candidate);
} catch {
// Continue through the supported TypeScript source extensions.
}
}
if (candidates.length > 1) {
failures.push(
`ambiguous registry source ${declaredPath}: ${candidates.join(", ")}`,
);
return null;
}
return candidates[0] ?? null;
}
function runtimeType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
if (Number.isInteger(value)) return "integer";
return typeof value;
}
function matchesDeclaredType(value: unknown, declaration: string): boolean {
const actual = runtimeType(value);
return declaration
.split("|")
.some(
(candidate) =>
candidate === actual ||
(candidate === "number" && actual === "integer"),
);
}
type FieldLookup = Readonly<{
found: boolean;
value: unknown;
}>;
function lookupField(value: unknown, fieldPath: string): FieldLookup {
let current = value;
for (const segment of fieldPath.split(".")) {
if (
!current ||
typeof current !== "object" ||
Array.isArray(current) ||
!Object.hasOwn(current, segment)
) {
return Object.freeze({ found: false, value: undefined });
}
current = (current as Record<string, unknown>)[segment];
}
return Object.freeze({ found: true, value: current });
}
function projectRegistryRows(
specification: RegistrySpecification,
exportedValue: unknown,
): RegistryRows | null {
const selected = specification.rowsPath
? lookupField(exportedValue, specification.rowsPath)
: Object.freeze({ found: true, value: exportedValue });
if (!selected.found) {
failures.push(
`${specification.registryId} is missing rows path ${specification.rowsPath}`,
);
return null;
}
if (!Array.isArray(selected.value)) {
if (
!selected.value ||
typeof selected.value !== "object"
) {
failures.push(`${specification.registryId} is not an object registry`);
return null;
}
return selected.value as RegistryRows;
}
const projected: RegistryRows = {};
const keyOccurrences = new Map<string, number>();
for (const [index, row] of selected.value.entries()) {
if (!row || typeof row !== "object" || Array.isArray(row)) {
projected[String(index)] = row as RegistryRow;
continue;
}
const keyValues = (specification.rowKeyFields ?? []).map(
(field) => lookupField(row, field).value,
);
const baseKey =
keyValues.length > 0
? JSON.stringify(keyValues.map(canonicalizeRegistryValue))
: String(index);
const occurrence = keyOccurrences.get(baseKey) ?? 0;
keyOccurrences.set(baseKey, occurrence + 1);
projected[occurrence === 0 ? baseKey : `${baseKey}#${occurrence + 1}`] =
row as RegistryRow;
}
return projected;
}
function snapshotKeyField(
value: unknown,
fieldPath: string,
): FieldLookup {
return fieldPath === "$value"
? Object.freeze({ found: true, value })
: lookupField(value, fieldPath);
}
function replaceProjectedField(
row: RegistryRow,
fieldPath: string,
value: unknown,
): boolean {
const segments = fieldPath.split(".");
const finalSegment = segments.pop();
if (!finalSegment) return false;
let current = row;
for (const segment of segments) {
const next = current[segment];
if (!next || typeof next !== "object" || Array.isArray(next)) {
return false;
}
current = next as RegistryRow;
}
current[finalSegment] = value;
return true;
}
function projectSnapshotRows(
specification: RegistrySpecification,
exportedValue: unknown,
validatedRows: RegistryRows,
): RegistryRows | null {
const projection = specification.snapshotProjection;
if (!projection) return validatedRows;
if (
!exportedValue ||
typeof exportedValue !== "object" ||
Array.isArray(exportedValue)
) {
failures.push(
`${specification.registryId} snapshot projection requires an object export`,
);
return null;
}
const canonicalRow = canonicalizeRegistryValue(exportedValue) as RegistryRow;
for (const [fieldPath, keyFields] of Object.entries(
projection.canonicalArrayKeyFields,
)) {
const selected = lookupField(exportedValue, fieldPath);
if (!selected.found || !Array.isArray(selected.value)) {
failures.push(
`${specification.registryId} snapshot field ${fieldPath} must be an array`,
);
return null;
}
const keyedItems: Array<{ identity: string; value: unknown }> = [];
const identities = new Set<string>();
for (const [index, item] of selected.value.entries()) {
const keyValues = keyFields.map((field) => snapshotKeyField(item, field));
const missingIndex = keyValues.findIndex((field) => !field.found);
if (missingIndex >= 0) {
failures.push(
`${specification.registryId} snapshot field ${fieldPath}[${index}] is missing key ${keyFields[missingIndex]}`,
);
return null;
}
const identity = JSON.stringify(
keyValues.map((field) => canonicalizeRegistryValue(field.value)),
);
if (identities.has(identity)) {
failures.push(
`${specification.registryId} snapshot field ${fieldPath} duplicates key ${identity}`,
);
return null;
}
identities.add(identity);
keyedItems.push({
identity,
value: canonicalizeRegistryValue(item),
});
}
keyedItems.sort((left, right) =>
left.identity.localeCompare(right.identity),
);
if (
!replaceProjectedField(
canonicalRow,
fieldPath,
keyedItems.map((item) => item.value),
)
) {
failures.push(
`${specification.registryId} snapshot field ${fieldPath} cannot be projected`,
);
return null;
}
}
return Object.freeze({
[projection.singletonRowKey]: canonicalRow,
});
}
async function filesBelow(directory: string): Promise<string[]> {
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) =>
/\.(?:ts|tsx|mts|cts)$/.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 registryModule = (await import(
`${pathToFileURL(path.resolve(sourcePath)).href}?registry-check=${Date.now()}`
)) as Record<string, unknown>;
rows = registryModule[specification.exportName];
} catch {
if (!rows) failures.push(`missing registry source ${specification.path}`);
}
const registryRows = projectRegistryRows(specification, rows);
if (!registryRows) {
continue;
}
rowsByRegistry.set(specification.registryId, registryRows);
sourcesByRegistry.set(
specification.registryId,
sourcePath ?? specification.path,
);
for (const [rowName, row] of Object.entries(registryRows)) {
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 (!lookupField(row, field).found) {
failures.push(`${specification.registryId}.${rowName} missing ${field}`);
}
}
for (const [field, declaredType] of Object.entries(
specification.fieldTypes ?? {},
)) {
if (
lookupField(row, field).found &&
!matchesDeclaredType(
lookupField(row, field).value,
String(declaredType),
)
) {
failures.push(
`${specification.registryId}.${rowName}.${field} expected ${declaredType}, received ${runtimeType(lookupField(row, field).value)}`,
);
}
}
if (
specification.keyField &&
lookupField(row, specification.keyField).value !== rowName
) {
failures.push(
`${specification.registryId}.${rowName}.${specification.keyField} must match its registry key`,
);
}
}
for (const field of specification.uniqueFields ?? []) {
const values = new Map<string, string>();
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const selected = lookupField(row, field);
if (!selected.found) continue;
const value = selected.value;
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 fields of specification.uniqueFieldSets ?? []) {
const values = new Map<string, string>();
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const selected = fields.map((field) => lookupField(row, field));
if (selected.some((field) => !field.found)) continue;
const identity = JSON.stringify(
selected.map((field) => canonicalizeRegistryValue(field.value)),
);
const previous = values.get(identity);
if (previous) {
failures.push(
`${specification.registryId}.${rowName} duplicates ${fields.join("+")}=${identity} from ${previous}`,
);
} else {
values.set(identity, rowName);
}
}
}
for (const [field, allowed] of Object.entries(
specification.allowedValues ?? {},
)) {
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
if (
!allowed.some((value) =>
Object.is(value, lookupField(row, field).value),
)
) {
failures.push(
`${specification.registryId}.${rowName}.${field} has unknown value ${String(lookupField(row, field).value)}`,
);
}
}
}
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 ?? [],
...(specification.rowsPath
? { rowsPath: specification.rowsPath }
: {}),
...(specification.rowKeyFields
? { rowKeyFields: specification.rowKeyFields }
: {}),
...(specification.uniqueFieldSets
? { uniqueFieldSets: specification.uniqueFieldSets }
: {}),
...(specification.snapshotProjection
? { snapshotProjection: specification.snapshotProjection }
: {}),
});
const snapshotRows = projectSnapshotRows(specification, rows, registryRows);
if (!snapshotRows) continue;
snapshots.push({
registryId: specification.registryId,
owner: specification.owner,
source: sourcePath ?? specification.path,
rowCount: Object.keys(snapshotRows).length,
contract,
rows: canonicalizeRegistryValue(snapshotRows),
});
}
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) => lookupField(row, reference.targetField).value)
.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 = lookupField(row, reference.field).value;
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 = lookupField(
row,
specification.consumerIdentityField,
).value;
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 = canonicalizeRegistryValue({
schemaVersion: 2,
registries: snapshots,
}) as Readonly<Record<string, unknown>>;
let baselineDigest: string | null = null;
const currentDigest = registrySnapshotDigest(currentSnapshot);
let compatibility: CompatibilitySummary = {
impact: "not-evaluated",
changes: [],
};
if (baselinePath && approvalPath && evidencePath) {
try {
const baseline = JSON.parse(
await readFile(baselinePath, "utf8"),
) as Record<string, unknown>;
const approval = JSON.parse(
await readFile(approvalPath, "utf8"),
) as Record<string, unknown>;
const approvalResult = verifyRegistryBaselineApproval(baseline, approval);
baselineDigest = approvalResult.actualDigest;
if (!approvalResult.passed) {
failures.push(
`registry baseline approval digest mismatch: approved=${approvalResult.approvedDigest} actual=${approvalResult.actualDigest}`,
);
}
const registryDiff = diffRegistrySnapshots(baseline, currentSnapshot);
compatibility = registryDiff;
const evidence = JSON.parse(
await readFile(evidencePath, "utf8"),
) as Record<string, unknown>;
const evidenceResult = validateBreakingEvidence(registryDiff, 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,
};
if (usesRepositoryBaseline && failures.length === 0) {
try {
assertMatchesJsonSchema(
JSON.parse(
await readFile(
"schemas/artifacts/registry-snapshot.schema.json",
"utf8",
),
),
report,
"registry snapshot",
);
} catch {
failures.push("registry snapshot JSON Schema mismatch");
}
}
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`,
);