fix: harden invalidation registry governance
This commit is contained in:
+122
-16
@@ -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") ||
|
||||
|
||||
@@ -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")],
|
||||
|
||||
Reference in New Issue
Block a user