fix: baseline invalidation registry contracts
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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-"),
|
||||
|
||||
Reference in New Issue
Block a user