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
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"snapshotDigest": "53468eada148a7afc821b0885b8a53bf47978bfd17adf26b424054beedcce608",
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
"owner": "frontend-platform",
"reason": "Make the reference feature route access policy explicitly session-required",
"approvedAt": "2026-08-01T07:18:30.000Z"
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
"approvedAt": "2026-08-01T15:15:45.537Z"
}
+342 -66
View File
@@ -5,7 +5,7 @@
"registryId": "FE-REG-ROUTE",
"owner": "feature-frontend-routing-release-recovery-runtime",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 9,
"rowCount": 10,
"contract": {
"requiredFields": [
"routeId",
@@ -121,6 +121,19 @@
"searchSchema": null,
"title": "인증 연동"
},
"EXAMPLES_PLATFORM": {
"access": "public",
"chunkId": "route-examples-platform",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "플랫폼 구성",
"navigationOrder": 15,
"paramsSchema": null,
"path": "/examples/platform",
"routeId": "EXAMPLES_PLATFORM",
"searchSchema": null,
"title": "플랫폼 구성"
},
"EXAMPLES_STATES": {
"access": "public",
"chunkId": "route-examples-states",
@@ -218,7 +231,7 @@
"registryId": "FE-REG-ROUTE-RUNTIME",
"owner": "feature-frontend-routing-release-recovery-runtime",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 9,
"rowCount": 10,
"contract": {
"requiredFields": [
"routeId",
@@ -275,6 +288,12 @@
"routeId": "EXAMPLES_AUTH",
"searchCodec": "none"
},
"EXAMPLES_PLATFORM": {
"moduleId": "platform-overview-page",
"paramsCodec": "none",
"routeId": "EXAMPLES_PLATFORM",
"searchCodec": "none"
},
"EXAMPLES_STATES": {
"moduleId": "state-gallery-page",
"paramsCodec": "none",
@@ -408,41 +427,101 @@
"rows": {
"CREATE_REFERENCE_RESOURCE": {
"auth": "external-session",
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
"contractVersion": 2,
"csrfProfileId": "NO_CSRF_BEARER",
"idempotency": "keyed",
"idempotencyKeyPolicy": "REQUIRED",
"mapperId": "ReferenceResourceMapper",
"maxEncodedSearchBytes": 0,
"maxResponseBytes": 32768,
"method": "POST",
"operationId": "CREATE_REFERENCE_RESOURCE",
"owner": "feature-frontend-reference-feature-vertical-slice",
"path": "/api/reference-resources",
"pathParameterNames": [],
"pathSchema": "NoRequest",
"protocol": "REST",
"providerId": "PRIMARY_API",
"replayPolicy": "KEYED_COMMAND",
"requestSchema": "CreateReferenceResourceCommand",
"requestSource": "body",
"responseMediaTypes": [
"application/json"
],
"responseSchema": "ReferenceResourcePayload",
"retry": "runtime",
"semantics": "COMMAND",
"successStatuses": [
200,
201
],
"timeoutMs": null
},
"GET_REFERENCE_RESOURCE": {
"auth": "external-session",
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
"contractVersion": 2,
"csrfProfileId": "NO_CSRF_BEARER",
"idempotency": "safe",
"idempotencyKeyPolicy": "NONE",
"mapperId": "ReferenceResourceMapper",
"maxEncodedSearchBytes": 0,
"maxResponseBytes": 32768,
"method": "GET",
"operationId": "GET_REFERENCE_RESOURCE",
"owner": "feature-frontend-form-page-platform",
"owner": "feature-frontend-reference-feature-vertical-slice",
"path": "/api/reference-resources/{resourceId}",
"pathParameterNames": [
"resourceId"
],
"pathSchema": "ReferenceResourceParams",
"protocol": "REST",
"providerId": "PRIMARY_API",
"replayPolicy": "SAFE",
"requestSchema": "NoRequest",
"requestSource": "none",
"responseMediaTypes": [
"application/json"
],
"responseSchema": "ReferenceResourcePayload",
"retry": "runtime",
"semantics": "QUERY",
"successStatuses": [
200
],
"timeoutMs": null
},
"LIST_REFERENCE_RESOURCES": {
"auth": "external-session",
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
"contractVersion": 2,
"csrfProfileId": "NO_CSRF_BEARER",
"idempotency": "safe",
"idempotencyKeyPolicy": "NONE",
"mapperId": "ReferenceResourceListMapper",
"maxEncodedSearchBytes": 4096,
"maxResponseBytes": 262144,
"method": "GET",
"operationId": "LIST_REFERENCE_RESOURCES",
"owner": "feature-frontend-reference-feature-vertical-slice",
"path": "/api/reference-resources",
"pathParameterNames": [],
"pathSchema": "NoRequest",
"protocol": "REST",
"providerId": "PRIMARY_API",
"replayPolicy": "SAFE",
"requestSchema": "ReferenceResourceListQuery",
"requestSource": "search",
"responseMediaTypes": [
"application/json"
],
"responseSchema": "ReferenceResourceListPayload",
"retry": "runtime",
"semantics": "QUERY",
"successStatuses": [
200
],
"timeoutMs": null
}
}
@@ -491,9 +570,12 @@
"rows": {
"CreateReferenceResourceCommand": {
"boundary": "api-request",
"direction": "REQUEST",
"owner": "feature-frontend-reference-feature-vertical-slice",
"runtime": "zod",
"schemaId": "CreateReferenceResourceCommand"
"schemaId": "CreateReferenceResourceCommand",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"none": {
"boundary": "route-params",
@@ -503,9 +585,12 @@
},
"NoRequest": {
"boundary": "api-request",
"direction": "REQUEST",
"owner": "feature-frontend-reference-feature-vertical-slice",
"runtime": "zod",
"schemaId": "NoRequest"
"schemaId": "NoRequest",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"NotFoundSplat": {
"boundary": "route-params",
@@ -515,27 +600,39 @@
},
"ReferenceResourceListPayload": {
"boundary": "api-response",
"direction": "RESPONSE",
"owner": "feature-frontend-reference-feature-vertical-slice",
"runtime": "zod",
"schemaId": "ReferenceResourceListPayload"
"schemaId": "ReferenceResourceListPayload",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"ReferenceResourceListQuery": {
"boundary": "route-search-api-request",
"direction": "REQUEST",
"owner": "feature-frontend-reference-feature-vertical-slice",
"runtime": "zod",
"schemaId": "ReferenceResourceListQuery"
"schemaId": "ReferenceResourceListQuery",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"ReferenceResourceParams": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-frontend-reference-feature-vertical-slice",
"runtime": "zod",
"schemaId": "ReferenceResourceParams"
"schemaId": "ReferenceResourceParams",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"ReferenceResourcePayload": {
"boundary": "api-response",
"direction": "RESPONSE",
"owner": "feature-frontend-reference-feature-vertical-slice",
"runtime": "zod",
"schemaId": "ReferenceResourcePayload"
"schemaId": "ReferenceResourcePayload",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
}
}
},
@@ -585,12 +682,6 @@
"phase": "runtime",
"required": true
},
"API_CONTRACT_VERSION": {
"classification": "public",
"defaultValue": null,
"phase": "runtime",
"required": true
},
"APP_ENV": {
"classification": "public",
"defaultValue": null,
@@ -603,6 +694,12 @@
"phase": "runtime",
"required": true
},
"CAPABILITY_OVERRIDES": {
"classification": "public",
"defaultValue": null,
"phase": "runtime",
"required": false
},
"CONFIG_SCHEMA_VERSION": {
"classification": "public",
"defaultValue": null,
@@ -795,7 +892,7 @@
"registryId": "FE-REG-ERROR",
"owner": "feature-frontend-error-classification-boundary-contract",
"source": "src/contracts/errors.ts",
"rowCount": 31,
"rowCount": 38,
"contract": {
"requiredFields": [
"kind",
@@ -1041,6 +1138,24 @@
"telemetryEvent": "release.mismatch.detected",
"userMessageKey": "error.deploy_mismatch"
},
"DUPLICATE_IN_FLIGHT": {
"action": "none",
"defaultRetryable": false,
"kind": "DUPLICATE_IN_FLIGHT",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "info",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.duplicate_in_flight"
},
"ENVELOPE_MISMATCH": {
"action": "contact-support",
"defaultRetryable": false,
@@ -1077,6 +1192,24 @@
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.forbidden"
},
"IDENTITY_INTERN_LIMIT_EXCEEDED": {
"action": "retry",
"defaultRetryable": false,
"kind": "IDENTITY_INTERN_LIMIT_EXCEEDED",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "warning",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.identity_intern_limit_exceeded"
},
"MALFORMED_JSON": {
"action": "contact-support",
"defaultRetryable": false,
@@ -1095,6 +1228,24 @@
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.malformed_json"
},
"MAPPING_CONTRACT_VIOLATION": {
"action": "contact-support",
"defaultRetryable": false,
"kind": "MAPPING_CONTRACT_VIOLATION",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "error",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.mapping_contract_violation"
},
"NETWORK_UNREACHABLE": {
"action": "retry",
"defaultRetryable": true,
@@ -1131,6 +1282,24 @@
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.not_found"
},
"PAGINATION_CONTRACT_VIOLATION": {
"action": "contact-support",
"defaultRetryable": false,
"kind": "PAGINATION_CONTRACT_VIOLATION",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "error",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.pagination_contract_violation"
},
"QUERY_CACHE_FAILURE": {
"action": "retry",
"defaultRetryable": false,
@@ -1257,6 +1426,42 @@
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.request_timeout"
},
"RESPONSE_BODY_LIMIT": {
"action": "contact-support",
"defaultRetryable": false,
"kind": "RESPONSE_BODY_LIMIT",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "error",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.response_body_limit"
},
"RESULT_LIMIT_EXCEEDED": {
"action": "contact-support",
"defaultRetryable": false,
"kind": "RESULT_LIMIT_EXCEEDED",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "error",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.result_limit_exceeded"
},
"SCHEMA_MISMATCH": {
"action": "contact-support",
"defaultRetryable": false,
@@ -1275,6 +1480,24 @@
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.schema_mismatch"
},
"SCOPE_GENERATION_CHANGED": {
"action": "none",
"defaultRetryable": false,
"kind": "SCOPE_GENERATION_CHANGED",
"redaction": [
"authorization",
"body",
"cause",
"headers",
"query",
"stack",
"storageValue",
"url"
],
"severity": "info",
"telemetryEvent": "api.request.failed",
"userMessageKey": "error.scope_generation_changed"
},
"SERVER_FAILURE": {
"action": "retry",
"defaultRetryable": true,
@@ -1404,68 +1627,116 @@
}
},
{
"registryId": "FE-REG-QUERY",
"registryId": "FE-REG-QUERY-INVALIDATION",
"owner": "feature-frontend-server-state-caching-contract",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 1,
"contract": {
"requiredFields": [
"namespace",
"serialization",
"identity",
"invalidation",
"invalidationTopic",
"crossContext",
"version",
"persistence"
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
],
"fieldTypes": {
"namespace": "array",
"serialization": "string",
"identity": "string",
"invalidation": "string",
"invalidationTopic": "string",
"crossContext": "string",
"version": "integer",
"persistence": "string"
},
"uniqueFields": [
"namespace",
"invalidationTopic"
],
"allowedValues": {
"crossContext": [
"invalidate-only"
],
"persistence": [
"disabled"
]
"topicId": "string",
"namespace.namespaceId": "string",
"namespace.namespaceVersion": "integer"
},
"uniqueFields": [],
"allowedValues": {},
"references": [],
"keyField": null,
"breakingFields": [
"namespace",
"serialization",
"identity",
"invalidationTopic",
"crossContext",
"version",
"persistence"
"topics",
"namespaces",
"edges"
],
"rowsPath": "edges",
"rowKeyFields": [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
],
"uniqueFieldSets": [
[
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
]
],
"snapshotProjection": {
"singletonRowKey": "invalidation-graph",
"canonicalArrayKeyFields": {
"topics": [
"$value"
],
"namespaces": [
"namespaceId",
"namespaceVersion"
],
"edges": [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
]
}
}
},
"rows": {
"invalidation-graph": {
"edges": [
{
"namespace": {
"namespaceId": "reference-resource",
"namespaceVersion": 1
},
"topicId": "qinv.01k10f7m3w9p6r2c8v5n4x"
}
],
"namespaces": [
{
"namespaceId": "reference-resource",
"namespaceVersion": 1
}
],
"topics": [
"qinv.01k10f7m3w9p6r2c8v5n4x"
]
}
}
},
{
"registryId": "FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
"owner": "feature-frontend-server-state-caching-contract",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 1,
"contract": {
"requiredFields": [
"topicId",
"topicVersion"
],
"fieldTypes": {
"topicId": "string",
"topicVersion": "integer"
},
"uniqueFields": [
"topicId"
],
"allowedValues": {},
"references": [],
"keyField": null,
"breakingFields": [
"topicId",
"topicVersion"
],
"rowKeyFields": [
"topicId"
]
},
"rows": {
"REFERENCE_RESOURCE": {
"crossContext": "invalidate-only",
"identity": "no-pii-token-or-raw-url",
"invalidation": "reference resource namespace after successful mutation",
"invalidationTopic": "qinv.01k10f7m3w9p6r2c8v5n4x",
"namespace": [
"reference-resource",
1
],
"persistence": "disabled",
"serialization": "canonical-object-order",
"version": 1
"[\"qinv.01k10f7m3w9p6r2c8v5n4x\"]": {
"topicId": "qinv.01k10f7m3w9p6r2c8v5n4x",
"topicVersion": 1
}
}
},
@@ -1655,7 +1926,7 @@
"registryId": "FE-REG-RELEASE",
"owner": "feature-frontend-release-cache-rollback-contract",
"source": "src/contracts/release-tokens.ts",
"rowCount": 8,
"rowCount": 9,
"contract": {
"requiredFields": [
"token",
@@ -1681,7 +1952,7 @@
},
"rows": {
"apiContractVersion": {
"compatibilityRole": "schema compatibility",
"compatibilityRole": "legacy V1 scalar; superseded by contractSetDigest",
"source": "frontend/backend agreement",
"token": "apiContractVersion"
},
@@ -1715,6 +1986,11 @@
"source": "runtime config schema",
"token": "configSchemaVersion"
},
"contractSetDigest": {
"compatibilityRole": "release coherence for multi-package contracts",
"source": "compiled external contract package set",
"token": "contractSetDigest"
},
"releaseId": {
"compatibilityRole": "rollback target",
"source": "deploy system",
+13 -5
View File
@@ -407,17 +407,25 @@
"namespace.namespaceVersion"
]
],
"snapshotProjection": {
"singletonRowKey": "invalidation-graph",
"canonicalArrayKeyFields": {
"topics": ["$value"],
"namespaces": ["namespaceId", "namespaceVersion"],
"edges": [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
]
}
},
"consumers": [
{
"path": "src/bootstrap/runtime-adapters.ts",
"token": "indexInvalidationRegistry(INVALIDATION_REGISTRY)"
}
],
"breakingFields": [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
]
"breakingFields": ["topics", "namespaces", "edges"]
},
{
"registryId": "FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
+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),
});
}
+13 -5
View File
@@ -30,11 +30,19 @@
"namespace.namespaceVersion"
]
],
"breakingFields": [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
]
"snapshotProjection": {
"singletonRowKey": "invalidation-graph",
"canonicalArrayKeyFields": {
"topics": ["$value"],
"namespaces": ["namespaceId", "namespaceVersion"],
"edges": [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion"
]
}
},
"breakingFields": ["topics", "namespaces", "edges"]
},
{
"registryId": "FIXTURE-INVALIDATION-VERSIONS",
+218 -12
View File
@@ -4,6 +4,12 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
diffRegistrySnapshots,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "../../scripts/lib/registry-compatibility.ts";
type RegistryDefinition = Readonly<{
registryId: string;
owner: string;
@@ -15,6 +21,10 @@ type RegistryDefinition = Readonly<{
rowKeyFields?: string[];
uniqueFields?: string[];
uniqueFieldSets?: string[][];
snapshotProjection?: {
singletonRowKey: string;
canonicalArrayKeyFields: Record<string, string[]>;
};
consumers?: Array<{ path: string; token: string }>;
breakingFields?: string[];
}>;
@@ -65,17 +75,25 @@ describe("registry governance manifest", () => {
"namespace.namespaceVersion",
],
],
snapshotProjection: {
singletonRowKey: "invalidation-graph",
canonicalArrayKeyFields: {
topics: ["$value"],
namespaces: ["namespaceId", "namespaceVersion"],
edges: [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion",
],
},
},
consumers: [
{
path: "src/bootstrap/runtime-adapters.ts",
token: "indexInvalidationRegistry(INVALIDATION_REGISTRY)",
},
],
breakingFields: [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion",
],
breakingFields: ["topics", "namespaces", "edges"],
});
expect(byId.get("FE-REG-QUERY-INVALIDATION-TOPIC-VERSION")).toMatchObject({
path: "src/features/installed-feature-contracts.ts",
@@ -113,18 +131,37 @@ describe("registry governance manifest", () => {
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(await readFile(artifact, "utf8")) as {
registries: Array<{ registryId: string; rows: Record<string, unknown> }>;
registries: Array<{
registryId: string;
rows: Record<string, Record<string, unknown>>;
}>;
};
expect(
report.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
)?.rows,
).toHaveProperty('["qinv.fixture.changed","orders",1]');
expect(
report.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
)?.rows,
).toHaveProperty('["orders","qinv.fixture.changed",1]');
).toEqual({
"invalidation-graph": {
topics: ["orders", "qinv.fixture.changed"],
namespaces: [
{ namespaceId: "orders", namespaceVersion: 1 },
{ namespaceId: "qinv.fixture.changed", namespaceVersion: 1 },
],
edges: [
{
topicId: "orders",
namespace: {
namespaceId: "qinv.fixture.changed",
namespaceVersion: 1,
},
},
{
topicId: "qinv.fixture.changed",
namespace: { namespaceId: "orders", namespaceVersion: 1 },
},
],
},
});
expect(
report.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-VERSIONS",
@@ -135,6 +172,175 @@ describe("registry governance manifest", () => {
}
});
it("requires explicit evidence for post-baseline graph and version drift", async () => {
const directory = await mkdtemp(
path.join(tmpdir(), "registry-governance-tamper-"),
);
const artifact = path.join(directory, "registries.json");
try {
const result = spawnSync(
process.execPath,
[
"scripts/check-registries.ts",
"--governance",
"tests/fixtures/registry/invalidation/governance.json",
"--artifact",
artifact,
"--no-baseline",
],
{ encoding: "utf8" },
);
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(await readFile(artifact, "utf8")) as {
registries: Array<{
registryId: string;
contract: Record<string, unknown>;
rows: Record<string, Record<string, unknown>>;
}>;
};
const baseline = { schemaVersion: 2, registries: report.registries };
expect(
baseline.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
)?.rows["invalidation-graph"],
).toBeDefined();
const tamperCases = [
{
name: "topic",
registryId: "FIXTURE-INVALIDATION-EDGES",
field: "topics",
apply(snapshot: typeof baseline) {
const graph = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows["invalidation-graph"];
(graph?.topics as string[])[0] = "tampered-topic";
},
},
{
name: "namespace",
registryId: "FIXTURE-INVALIDATION-EDGES",
field: "namespaces",
apply(snapshot: typeof baseline) {
const graph = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows["invalidation-graph"];
const namespaces = graph?.namespaces as Array<{
namespaceId: string;
}>;
namespaces[0]!.namespaceId = "tampered-namespace";
},
},
{
name: "edge",
registryId: "FIXTURE-INVALIDATION-EDGES",
field: "edges",
apply(snapshot: typeof baseline) {
const graph = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows["invalidation-graph"];
const edges = graph?.edges as Array<{ topicId: string }>;
edges[0]!.topicId = "tampered-edge";
},
},
{
name: "version",
registryId: "FIXTURE-INVALIDATION-VERSIONS",
field: "topicVersion",
apply(snapshot: typeof baseline) {
const versions = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows;
const firstVersion = versions?.[Object.keys(versions)[0] ?? ""];
if (!firstVersion) throw new Error("version fixture row missing");
firstVersion.topicVersion = 2;
},
},
];
for (const tamperCase of tamperCases) {
const current = structuredClone(baseline);
tamperCase.apply(current);
const diff = diffRegistrySnapshots(baseline, current);
expect(diff.impact, tamperCase.name).toBe("breaking");
expect(
diff.changes.some((change) => change.kind === "registry-added"),
tamperCase.name,
).toBe(false);
const breakingChanges = diff.changes.filter(
(change) => change.impact === "breaking",
);
expect(breakingChanges, tamperCase.name).toEqual(
expect.arrayContaining([
expect.objectContaining({
registryId: tamperCase.registryId,
field: tamperCase.field,
}),
]),
);
expect(
validateBreakingEvidence(diff, { changes: [] }).passed,
tamperCase.name,
).toBe(false);
expect(
validateBreakingEvidence(diff, {
changes: breakingChanges.map((change) => ({
changeId: change.changeId,
versionBump: "2",
migration: `migrate ${tamperCase.name}`,
compatibilityWindow: "one release",
rollback: `restore ${tamperCase.name} baseline`,
owner: "fixture-owner",
})),
}).passed,
tamperCase.name,
).toBe(true);
}
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it("pins the full invalidation graph and versions in the approved repository baseline", async () => {
const baseline = JSON.parse(
await readFile("config/contracts/registry-baseline.json", "utf8"),
) as {
schemaVersion: number;
registries: Array<{
registryId: string;
rows: Record<string, Record<string, unknown>>;
}>;
};
const approval = JSON.parse(
await readFile(
"config/contracts/registry-baseline.approval.json",
"utf8",
),
) as Record<string, unknown>;
expect(verifyRegistryBaselineApproval(baseline, approval).passed).toBe(true);
expect(baseline.registries).toHaveLength(11);
const graph = baseline.registries.find(
(registry) => registry.registryId === "FE-REG-QUERY-INVALIDATION",
);
const versions = baseline.registries.find(
(registry) =>
registry.registryId ===
"FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
);
expect(graph?.rows["invalidation-graph"]).toMatchObject({
topics: expect.any(Array),
namespaces: expect.any(Array),
edges: expect.any(Array),
});
expect(versions?.rows).toBeDefined();
expect(approval).toMatchObject({
owner: "frontend-platform",
reason:
"Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
});
});
it("rejects duplicate composite invalidation edges after projection", async () => {
const directory = await mkdtemp(
path.join(tmpdir(), "registry-governance-duplicate-"),