fix: baseline invalidation registry contracts

This commit is contained in:
DongHyeonka
2026-08-02 00:28:17 +09:00
parent 0eb23875cb
commit 53d181fbe4
6 changed files with 706 additions and 93 deletions
+117 -2
View File
@@ -25,6 +25,10 @@ type RegistryReference = Readonly<{
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;
@@ -38,6 +42,7 @@ type RegistrySpecification = Readonly<{
keyField?: string;
uniqueFields?: readonly string[];
uniqueFieldSets?: readonly (readonly string[])[];
snapshotProjection?: RegistrySnapshotProjection;
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
references?: readonly RegistryReference[];
breakingFields?: readonly string[];
@@ -215,6 +220,111 @@ function projectRegistryRows(
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 });
@@ -367,14 +477,19 @@ for (const specification of governance.registries) {
...(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(registryRows).length,
rowCount: Object.keys(snapshotRows).length,
contract,
rows: canonicalizeRegistryValue(registryRows),
rows: canonicalizeRegistryValue(snapshotRows),
});
}