fix: harden invalidation registry governance

This commit is contained in:
DongHyeonka
2026-08-01 23:59:11 +09:00
parent 73a50426d6
commit 0eb23875cb
15 changed files with 906 additions and 147 deletions
+122 -16
View File
@@ -31,10 +31,13 @@ type RegistrySpecification = Readonly<{
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[])[];
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
references?: readonly RegistryReference[];
breakingFields?: readonly string[];
@@ -145,6 +148,73 @@ function matchesDeclaredType(value: unknown, declaration: string): boolean {
);
}
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;
}
async function filesBelow(directory: string): Promise<string[]> {
try {
const entries = await readdir(directory, { withFileTypes: true });
@@ -180,12 +250,10 @@ for (const specification of governance.registries) {
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`);
const registryRows = projectRegistryRows(specification, rows);
if (!registryRows) {
continue;
}
const registryRows = rows as RegistryRows;
rowsByRegistry.set(specification.registryId, registryRows);
sourcesByRegistry.set(
specification.registryId,
@@ -198,7 +266,7 @@ for (const specification of governance.registries) {
continue;
}
for (const field of specification.requiredFields) {
if (!(field in row)) {
if (!lookupField(row, field).found) {
failures.push(`${specification.registryId}.${rowName} missing ${field}`);
}
}
@@ -206,17 +274,20 @@ for (const specification of governance.registries) {
specification.fieldTypes ?? {},
)) {
if (
field in row &&
!matchesDeclaredType(row[field], String(declaredType))
lookupField(row, field).found &&
!matchesDeclaredType(
lookupField(row, field).value,
String(declaredType),
)
) {
failures.push(
`${specification.registryId}.${rowName}.${field} expected ${declaredType}, received ${runtimeType(row[field])}`,
`${specification.registryId}.${rowName}.${field} expected ${declaredType}, received ${runtimeType(lookupField(row, field).value)}`,
);
}
}
if (
specification.keyField &&
row[specification.keyField] !== rowName
lookupField(row, specification.keyField).value !== rowName
) {
failures.push(
`${specification.registryId}.${rowName}.${specification.keyField} must match its registry key`,
@@ -228,8 +299,9 @@ for (const specification of governance.registries) {
const values = new Map<string, string>();
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const value = row[field];
if (value === undefined) 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(
@@ -241,16 +313,38 @@ for (const specification of governance.registries) {
}
}
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, row[field]))
!allowed.some((value) =>
Object.is(value, lookupField(row, field).value),
)
) {
failures.push(
`${specification.registryId}.${rowName}.${field} has unknown value ${String(row[field])}`,
`${specification.registryId}.${rowName}.${field} has unknown value ${String(lookupField(row, field).value)}`,
);
}
}
@@ -264,6 +358,15 @@ for (const specification of governance.registries) {
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 }
: {}),
});
snapshots.push({
registryId: specification.registryId,
@@ -289,12 +392,12 @@ for (const specification of governance.registries) {
const targetValues = new Set(
Object.values(targetRows)
.filter((row) => row && typeof row === "object" && !Array.isArray(row))
.map((row) => row[reference.targetField])
.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 = row[reference.field];
const value = lookupField(row, reference.field).value;
if (
value !== undefined &&
value !== null &&
@@ -346,7 +449,10 @@ for (const specification of governance.registries) {
) {
continue;
}
const identity = row[specification.consumerIdentityField];
const identity = lookupField(
row,
specification.consumerIdentityField,
).value;
if (
(typeof identity !== "string" &&
typeof identity !== "number") ||
+21 -3
View File
@@ -1,5 +1,6 @@
import { spawnSync } from "node:child_process";
import {
access,
cp,
mkdir,
readFile,
@@ -14,6 +15,11 @@ const fixtureRoot = path.resolve(".tmp/reference-feature-removal");
const pnpmCli = requireEnvironment("npm_execpath");
const featureSource = "src/features/reference-feature";
const featureTests = "tests/features/reference-feature";
const commonTestPaths = [
"tests/unit/external-contract-runtime.test.ts",
"tests/unit/http-execution-v3.test.ts",
"tests/unit/runtime-adapters.test.ts",
];
const featureOwnedPaths = [
featureSource,
featureTests,
@@ -22,9 +28,6 @@ const featureOwnedPaths = [
"tests/mocks",
"tests/fixtures/typecheck/invalid-feature-input.ts",
"tests/fixtures/typecheck/invalid-reference-operation.ts",
"tests/unit/external-contract-runtime.test.ts",
"tests/unit/http-execution-v3.test.ts",
"tests/unit/runtime-adapters.test.ts",
];
const copyTargets = [
"src",
@@ -314,6 +317,21 @@ for (const root of ["src", "tests"]) {
}
const checks: Array<[string, boolean]> = [
[
"common-test-evidence",
(
await Promise.all(
commonTestPaths.map(async (testPath) => {
try {
await access(path.join(fixtureRoot, testPath));
return true;
} catch {
return false;
}
}),
)
).every(Boolean),
],
["typecheck", runPnpm("check:types")],
["architecture", runPnpm("check:architecture")],
["registry-structure", runPnpm("check:registries:structure")],