fix: baseline invalidation registry contracts
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"snapshotDigest": "53468eada148a7afc821b0885b8a53bf47978bfd17adf26b424054beedcce608",
|
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
|
||||||
"owner": "frontend-platform",
|
"owner": "frontend-platform",
|
||||||
"reason": "Make the reference feature route access policy explicitly session-required",
|
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
|
||||||
"approvedAt": "2026-08-01T07:18:30.000Z"
|
"approvedAt": "2026-08-01T15:15:45.537Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"registryId": "FE-REG-ROUTE",
|
"registryId": "FE-REG-ROUTE",
|
||||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 9,
|
"rowCount": 10,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
@@ -121,6 +121,19 @@
|
|||||||
"searchSchema": null,
|
"searchSchema": null,
|
||||||
"title": "인증 연동"
|
"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": {
|
"EXAMPLES_STATES": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"chunkId": "route-examples-states",
|
"chunkId": "route-examples-states",
|
||||||
@@ -218,7 +231,7 @@
|
|||||||
"registryId": "FE-REG-ROUTE-RUNTIME",
|
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 9,
|
"rowCount": 10,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
@@ -275,6 +288,12 @@
|
|||||||
"routeId": "EXAMPLES_AUTH",
|
"routeId": "EXAMPLES_AUTH",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
|
"EXAMPLES_PLATFORM": {
|
||||||
|
"moduleId": "platform-overview-page",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "EXAMPLES_PLATFORM",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
"EXAMPLES_STATES": {
|
"EXAMPLES_STATES": {
|
||||||
"moduleId": "state-gallery-page",
|
"moduleId": "state-gallery-page",
|
||||||
"paramsCodec": "none",
|
"paramsCodec": "none",
|
||||||
@@ -408,41 +427,101 @@
|
|||||||
"rows": {
|
"rows": {
|
||||||
"CREATE_REFERENCE_RESOURCE": {
|
"CREATE_REFERENCE_RESOURCE": {
|
||||||
"auth": "external-session",
|
"auth": "external-session",
|
||||||
|
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
|
||||||
|
"contractVersion": 2,
|
||||||
|
"csrfProfileId": "NO_CSRF_BEARER",
|
||||||
"idempotency": "keyed",
|
"idempotency": "keyed",
|
||||||
|
"idempotencyKeyPolicy": "REQUIRED",
|
||||||
|
"mapperId": "ReferenceResourceMapper",
|
||||||
|
"maxEncodedSearchBytes": 0,
|
||||||
|
"maxResponseBytes": 32768,
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"operationId": "CREATE_REFERENCE_RESOURCE",
|
"operationId": "CREATE_REFERENCE_RESOURCE",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"path": "/api/reference-resources",
|
"path": "/api/reference-resources",
|
||||||
|
"pathParameterNames": [],
|
||||||
|
"pathSchema": "NoRequest",
|
||||||
|
"protocol": "REST",
|
||||||
|
"providerId": "PRIMARY_API",
|
||||||
|
"replayPolicy": "KEYED_COMMAND",
|
||||||
"requestSchema": "CreateReferenceResourceCommand",
|
"requestSchema": "CreateReferenceResourceCommand",
|
||||||
"requestSource": "body",
|
"requestSource": "body",
|
||||||
|
"responseMediaTypes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
"responseSchema": "ReferenceResourcePayload",
|
"responseSchema": "ReferenceResourcePayload",
|
||||||
"retry": "runtime",
|
"retry": "runtime",
|
||||||
|
"semantics": "COMMAND",
|
||||||
|
"successStatuses": [
|
||||||
|
200,
|
||||||
|
201
|
||||||
|
],
|
||||||
"timeoutMs": null
|
"timeoutMs": null
|
||||||
},
|
},
|
||||||
"GET_REFERENCE_RESOURCE": {
|
"GET_REFERENCE_RESOURCE": {
|
||||||
"auth": "external-session",
|
"auth": "external-session",
|
||||||
|
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
|
||||||
|
"contractVersion": 2,
|
||||||
|
"csrfProfileId": "NO_CSRF_BEARER",
|
||||||
"idempotency": "safe",
|
"idempotency": "safe",
|
||||||
|
"idempotencyKeyPolicy": "NONE",
|
||||||
|
"mapperId": "ReferenceResourceMapper",
|
||||||
|
"maxEncodedSearchBytes": 0,
|
||||||
|
"maxResponseBytes": 32768,
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"operationId": "GET_REFERENCE_RESOURCE",
|
"operationId": "GET_REFERENCE_RESOURCE",
|
||||||
"owner": "feature-frontend-form-page-platform",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"path": "/api/reference-resources/{resourceId}",
|
"path": "/api/reference-resources/{resourceId}",
|
||||||
|
"pathParameterNames": [
|
||||||
|
"resourceId"
|
||||||
|
],
|
||||||
|
"pathSchema": "ReferenceResourceParams",
|
||||||
|
"protocol": "REST",
|
||||||
|
"providerId": "PRIMARY_API",
|
||||||
|
"replayPolicy": "SAFE",
|
||||||
"requestSchema": "NoRequest",
|
"requestSchema": "NoRequest",
|
||||||
"requestSource": "none",
|
"requestSource": "none",
|
||||||
|
"responseMediaTypes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
"responseSchema": "ReferenceResourcePayload",
|
"responseSchema": "ReferenceResourcePayload",
|
||||||
"retry": "runtime",
|
"retry": "runtime",
|
||||||
|
"semantics": "QUERY",
|
||||||
|
"successStatuses": [
|
||||||
|
200
|
||||||
|
],
|
||||||
"timeoutMs": null
|
"timeoutMs": null
|
||||||
},
|
},
|
||||||
"LIST_REFERENCE_RESOURCES": {
|
"LIST_REFERENCE_RESOURCES": {
|
||||||
"auth": "external-session",
|
"auth": "external-session",
|
||||||
|
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
|
||||||
|
"contractVersion": 2,
|
||||||
|
"csrfProfileId": "NO_CSRF_BEARER",
|
||||||
"idempotency": "safe",
|
"idempotency": "safe",
|
||||||
|
"idempotencyKeyPolicy": "NONE",
|
||||||
|
"mapperId": "ReferenceResourceListMapper",
|
||||||
|
"maxEncodedSearchBytes": 4096,
|
||||||
|
"maxResponseBytes": 262144,
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"operationId": "LIST_REFERENCE_RESOURCES",
|
"operationId": "LIST_REFERENCE_RESOURCES",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"path": "/api/reference-resources",
|
"path": "/api/reference-resources",
|
||||||
|
"pathParameterNames": [],
|
||||||
|
"pathSchema": "NoRequest",
|
||||||
|
"protocol": "REST",
|
||||||
|
"providerId": "PRIMARY_API",
|
||||||
|
"replayPolicy": "SAFE",
|
||||||
"requestSchema": "ReferenceResourceListQuery",
|
"requestSchema": "ReferenceResourceListQuery",
|
||||||
"requestSource": "search",
|
"requestSource": "search",
|
||||||
|
"responseMediaTypes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
"responseSchema": "ReferenceResourceListPayload",
|
"responseSchema": "ReferenceResourceListPayload",
|
||||||
"retry": "runtime",
|
"retry": "runtime",
|
||||||
|
"semantics": "QUERY",
|
||||||
|
"successStatuses": [
|
||||||
|
200
|
||||||
|
],
|
||||||
"timeoutMs": null
|
"timeoutMs": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,9 +570,12 @@
|
|||||||
"rows": {
|
"rows": {
|
||||||
"CreateReferenceResourceCommand": {
|
"CreateReferenceResourceCommand": {
|
||||||
"boundary": "api-request",
|
"boundary": "api-request",
|
||||||
|
"direction": "REQUEST",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"runtime": "zod",
|
"runtime": "zod",
|
||||||
"schemaId": "CreateReferenceResourceCommand"
|
"schemaId": "CreateReferenceResourceCommand",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
},
|
},
|
||||||
"none": {
|
"none": {
|
||||||
"boundary": "route-params",
|
"boundary": "route-params",
|
||||||
@@ -503,9 +585,12 @@
|
|||||||
},
|
},
|
||||||
"NoRequest": {
|
"NoRequest": {
|
||||||
"boundary": "api-request",
|
"boundary": "api-request",
|
||||||
|
"direction": "REQUEST",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"runtime": "zod",
|
"runtime": "zod",
|
||||||
"schemaId": "NoRequest"
|
"schemaId": "NoRequest",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
},
|
},
|
||||||
"NotFoundSplat": {
|
"NotFoundSplat": {
|
||||||
"boundary": "route-params",
|
"boundary": "route-params",
|
||||||
@@ -515,27 +600,39 @@
|
|||||||
},
|
},
|
||||||
"ReferenceResourceListPayload": {
|
"ReferenceResourceListPayload": {
|
||||||
"boundary": "api-response",
|
"boundary": "api-response",
|
||||||
|
"direction": "RESPONSE",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"runtime": "zod",
|
"runtime": "zod",
|
||||||
"schemaId": "ReferenceResourceListPayload"
|
"schemaId": "ReferenceResourceListPayload",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
},
|
},
|
||||||
"ReferenceResourceListQuery": {
|
"ReferenceResourceListQuery": {
|
||||||
"boundary": "route-search-api-request",
|
"boundary": "route-search-api-request",
|
||||||
|
"direction": "REQUEST",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"runtime": "zod",
|
"runtime": "zod",
|
||||||
"schemaId": "ReferenceResourceListQuery"
|
"schemaId": "ReferenceResourceListQuery",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
},
|
},
|
||||||
"ReferenceResourceParams": {
|
"ReferenceResourceParams": {
|
||||||
"boundary": "route-params",
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"runtime": "zod",
|
"runtime": "zod",
|
||||||
"schemaId": "ReferenceResourceParams"
|
"schemaId": "ReferenceResourceParams",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
},
|
},
|
||||||
"ReferenceResourcePayload": {
|
"ReferenceResourcePayload": {
|
||||||
"boundary": "api-response",
|
"boundary": "api-response",
|
||||||
|
"direction": "RESPONSE",
|
||||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||||
"runtime": "zod",
|
"runtime": "zod",
|
||||||
"schemaId": "ReferenceResourcePayload"
|
"schemaId": "ReferenceResourcePayload",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -585,12 +682,6 @@
|
|||||||
"phase": "runtime",
|
"phase": "runtime",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"API_CONTRACT_VERSION": {
|
|
||||||
"classification": "public",
|
|
||||||
"defaultValue": null,
|
|
||||||
"phase": "runtime",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"APP_ENV": {
|
"APP_ENV": {
|
||||||
"classification": "public",
|
"classification": "public",
|
||||||
"defaultValue": null,
|
"defaultValue": null,
|
||||||
@@ -603,6 +694,12 @@
|
|||||||
"phase": "runtime",
|
"phase": "runtime",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
"CAPABILITY_OVERRIDES": {
|
||||||
|
"classification": "public",
|
||||||
|
"defaultValue": null,
|
||||||
|
"phase": "runtime",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
"CONFIG_SCHEMA_VERSION": {
|
"CONFIG_SCHEMA_VERSION": {
|
||||||
"classification": "public",
|
"classification": "public",
|
||||||
"defaultValue": null,
|
"defaultValue": null,
|
||||||
@@ -795,7 +892,7 @@
|
|||||||
"registryId": "FE-REG-ERROR",
|
"registryId": "FE-REG-ERROR",
|
||||||
"owner": "feature-frontend-error-classification-boundary-contract",
|
"owner": "feature-frontend-error-classification-boundary-contract",
|
||||||
"source": "src/contracts/errors.ts",
|
"source": "src/contracts/errors.ts",
|
||||||
"rowCount": 31,
|
"rowCount": 38,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"kind",
|
"kind",
|
||||||
@@ -1041,6 +1138,24 @@
|
|||||||
"telemetryEvent": "release.mismatch.detected",
|
"telemetryEvent": "release.mismatch.detected",
|
||||||
"userMessageKey": "error.deploy_mismatch"
|
"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": {
|
"ENVELOPE_MISMATCH": {
|
||||||
"action": "contact-support",
|
"action": "contact-support",
|
||||||
"defaultRetryable": false,
|
"defaultRetryable": false,
|
||||||
@@ -1077,6 +1192,24 @@
|
|||||||
"telemetryEvent": "api.request.failed",
|
"telemetryEvent": "api.request.failed",
|
||||||
"userMessageKey": "error.forbidden"
|
"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": {
|
"MALFORMED_JSON": {
|
||||||
"action": "contact-support",
|
"action": "contact-support",
|
||||||
"defaultRetryable": false,
|
"defaultRetryable": false,
|
||||||
@@ -1095,6 +1228,24 @@
|
|||||||
"telemetryEvent": "api.request.failed",
|
"telemetryEvent": "api.request.failed",
|
||||||
"userMessageKey": "error.malformed_json"
|
"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": {
|
"NETWORK_UNREACHABLE": {
|
||||||
"action": "retry",
|
"action": "retry",
|
||||||
"defaultRetryable": true,
|
"defaultRetryable": true,
|
||||||
@@ -1131,6 +1282,24 @@
|
|||||||
"telemetryEvent": "api.request.failed",
|
"telemetryEvent": "api.request.failed",
|
||||||
"userMessageKey": "error.not_found"
|
"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": {
|
"QUERY_CACHE_FAILURE": {
|
||||||
"action": "retry",
|
"action": "retry",
|
||||||
"defaultRetryable": false,
|
"defaultRetryable": false,
|
||||||
@@ -1257,6 +1426,42 @@
|
|||||||
"telemetryEvent": "api.request.failed",
|
"telemetryEvent": "api.request.failed",
|
||||||
"userMessageKey": "error.request_timeout"
|
"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": {
|
"SCHEMA_MISMATCH": {
|
||||||
"action": "contact-support",
|
"action": "contact-support",
|
||||||
"defaultRetryable": false,
|
"defaultRetryable": false,
|
||||||
@@ -1275,6 +1480,24 @@
|
|||||||
"telemetryEvent": "api.request.failed",
|
"telemetryEvent": "api.request.failed",
|
||||||
"userMessageKey": "error.schema_mismatch"
|
"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": {
|
"SERVER_FAILURE": {
|
||||||
"action": "retry",
|
"action": "retry",
|
||||||
"defaultRetryable": true,
|
"defaultRetryable": true,
|
||||||
@@ -1404,68 +1627,116 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"registryId": "FE-REG-QUERY",
|
"registryId": "FE-REG-QUERY-INVALIDATION",
|
||||||
"owner": "feature-frontend-server-state-caching-contract",
|
"owner": "feature-frontend-server-state-caching-contract",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 1,
|
"rowCount": 1,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"namespace",
|
"topicId",
|
||||||
"serialization",
|
"namespace.namespaceId",
|
||||||
"identity",
|
"namespace.namespaceVersion"
|
||||||
"invalidation",
|
|
||||||
"invalidationTopic",
|
|
||||||
"crossContext",
|
|
||||||
"version",
|
|
||||||
"persistence"
|
|
||||||
],
|
],
|
||||||
"fieldTypes": {
|
"fieldTypes": {
|
||||||
"namespace": "array",
|
"topicId": "string",
|
||||||
"serialization": "string",
|
"namespace.namespaceId": "string",
|
||||||
"identity": "string",
|
"namespace.namespaceVersion": "integer"
|
||||||
"invalidation": "string",
|
|
||||||
"invalidationTopic": "string",
|
|
||||||
"crossContext": "string",
|
|
||||||
"version": "integer",
|
|
||||||
"persistence": "string"
|
|
||||||
},
|
|
||||||
"uniqueFields": [
|
|
||||||
"namespace",
|
|
||||||
"invalidationTopic"
|
|
||||||
],
|
|
||||||
"allowedValues": {
|
|
||||||
"crossContext": [
|
|
||||||
"invalidate-only"
|
|
||||||
],
|
|
||||||
"persistence": [
|
|
||||||
"disabled"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
"uniqueFields": [],
|
||||||
|
"allowedValues": {},
|
||||||
"references": [],
|
"references": [],
|
||||||
"keyField": null,
|
"keyField": null,
|
||||||
"breakingFields": [
|
"breakingFields": [
|
||||||
"namespace",
|
"topics",
|
||||||
"serialization",
|
"namespaces",
|
||||||
"identity",
|
"edges"
|
||||||
"invalidationTopic",
|
],
|
||||||
"crossContext",
|
"rowsPath": "edges",
|
||||||
"version",
|
"rowKeyFields": [
|
||||||
"persistence"
|
"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": {
|
"rows": {
|
||||||
"REFERENCE_RESOURCE": {
|
"[\"qinv.01k10f7m3w9p6r2c8v5n4x\"]": {
|
||||||
"crossContext": "invalidate-only",
|
"topicId": "qinv.01k10f7m3w9p6r2c8v5n4x",
|
||||||
"identity": "no-pii-token-or-raw-url",
|
"topicVersion": 1
|
||||||
"invalidation": "reference resource namespace after successful mutation",
|
|
||||||
"invalidationTopic": "qinv.01k10f7m3w9p6r2c8v5n4x",
|
|
||||||
"namespace": [
|
|
||||||
"reference-resource",
|
|
||||||
1
|
|
||||||
],
|
|
||||||
"persistence": "disabled",
|
|
||||||
"serialization": "canonical-object-order",
|
|
||||||
"version": 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1655,7 +1926,7 @@
|
|||||||
"registryId": "FE-REG-RELEASE",
|
"registryId": "FE-REG-RELEASE",
|
||||||
"owner": "feature-frontend-release-cache-rollback-contract",
|
"owner": "feature-frontend-release-cache-rollback-contract",
|
||||||
"source": "src/contracts/release-tokens.ts",
|
"source": "src/contracts/release-tokens.ts",
|
||||||
"rowCount": 8,
|
"rowCount": 9,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"token",
|
"token",
|
||||||
@@ -1681,7 +1952,7 @@
|
|||||||
},
|
},
|
||||||
"rows": {
|
"rows": {
|
||||||
"apiContractVersion": {
|
"apiContractVersion": {
|
||||||
"compatibilityRole": "schema compatibility",
|
"compatibilityRole": "legacy V1 scalar; superseded by contractSetDigest",
|
||||||
"source": "frontend/backend agreement",
|
"source": "frontend/backend agreement",
|
||||||
"token": "apiContractVersion"
|
"token": "apiContractVersion"
|
||||||
},
|
},
|
||||||
@@ -1715,6 +1986,11 @@
|
|||||||
"source": "runtime config schema",
|
"source": "runtime config schema",
|
||||||
"token": "configSchemaVersion"
|
"token": "configSchemaVersion"
|
||||||
},
|
},
|
||||||
|
"contractSetDigest": {
|
||||||
|
"compatibilityRole": "release coherence for multi-package contracts",
|
||||||
|
"source": "compiled external contract package set",
|
||||||
|
"token": "contractSetDigest"
|
||||||
|
},
|
||||||
"releaseId": {
|
"releaseId": {
|
||||||
"compatibilityRole": "rollback target",
|
"compatibilityRole": "rollback target",
|
||||||
"source": "deploy system",
|
"source": "deploy system",
|
||||||
|
|||||||
@@ -407,17 +407,25 @@
|
|||||||
"namespace.namespaceVersion"
|
"namespace.namespaceVersion"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
"snapshotProjection": {
|
||||||
|
"singletonRowKey": "invalidation-graph",
|
||||||
|
"canonicalArrayKeyFields": {
|
||||||
|
"topics": ["$value"],
|
||||||
|
"namespaces": ["namespaceId", "namespaceVersion"],
|
||||||
|
"edges": [
|
||||||
|
"topicId",
|
||||||
|
"namespace.namespaceId",
|
||||||
|
"namespace.namespaceVersion"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"consumers": [
|
"consumers": [
|
||||||
{
|
{
|
||||||
"path": "src/bootstrap/runtime-adapters.ts",
|
"path": "src/bootstrap/runtime-adapters.ts",
|
||||||
"token": "indexInvalidationRegistry(INVALIDATION_REGISTRY)"
|
"token": "indexInvalidationRegistry(INVALIDATION_REGISTRY)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"breakingFields": [
|
"breakingFields": ["topics", "namespaces", "edges"]
|
||||||
"topicId",
|
|
||||||
"namespace.namespaceId",
|
|
||||||
"namespace.namespaceVersion"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"registryId": "FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
|
"registryId": "FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
|
||||||
|
|||||||
+117
-2
@@ -25,6 +25,10 @@ type RegistryReference = Readonly<{
|
|||||||
targetField: string;
|
targetField: string;
|
||||||
}>;
|
}>;
|
||||||
type RegistryConsumer = Readonly<{ path: string; token: string }>;
|
type RegistryConsumer = Readonly<{ path: string; token: string }>;
|
||||||
|
type RegistrySnapshotProjection = Readonly<{
|
||||||
|
singletonRowKey: string;
|
||||||
|
canonicalArrayKeyFields: Readonly<Record<string, readonly string[]>>;
|
||||||
|
}>;
|
||||||
type RegistrySpecification = Readonly<{
|
type RegistrySpecification = Readonly<{
|
||||||
registryId: string;
|
registryId: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -38,6 +42,7 @@ type RegistrySpecification = Readonly<{
|
|||||||
keyField?: string;
|
keyField?: string;
|
||||||
uniqueFields?: readonly string[];
|
uniqueFields?: readonly string[];
|
||||||
uniqueFieldSets?: readonly (readonly string[])[];
|
uniqueFieldSets?: readonly (readonly string[])[];
|
||||||
|
snapshotProjection?: RegistrySnapshotProjection;
|
||||||
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
|
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
|
||||||
references?: readonly RegistryReference[];
|
references?: readonly RegistryReference[];
|
||||||
breakingFields?: readonly string[];
|
breakingFields?: readonly string[];
|
||||||
@@ -215,6 +220,111 @@ function projectRegistryRows(
|
|||||||
return projected;
|
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[]> {
|
async function filesBelow(directory: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const entries = await readdir(directory, { withFileTypes: true });
|
const entries = await readdir(directory, { withFileTypes: true });
|
||||||
@@ -367,14 +477,19 @@ for (const specification of governance.registries) {
|
|||||||
...(specification.uniqueFieldSets
|
...(specification.uniqueFieldSets
|
||||||
? { uniqueFieldSets: specification.uniqueFieldSets }
|
? { uniqueFieldSets: specification.uniqueFieldSets }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(specification.snapshotProjection
|
||||||
|
? { snapshotProjection: specification.snapshotProjection }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
|
const snapshotRows = projectSnapshotRows(specification, rows, registryRows);
|
||||||
|
if (!snapshotRows) continue;
|
||||||
snapshots.push({
|
snapshots.push({
|
||||||
registryId: specification.registryId,
|
registryId: specification.registryId,
|
||||||
owner: specification.owner,
|
owner: specification.owner,
|
||||||
source: sourcePath ?? specification.path,
|
source: sourcePath ?? specification.path,
|
||||||
rowCount: Object.keys(registryRows).length,
|
rowCount: Object.keys(snapshotRows).length,
|
||||||
contract,
|
contract,
|
||||||
rows: canonicalizeRegistryValue(registryRows),
|
rows: canonicalizeRegistryValue(snapshotRows),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-5
@@ -30,11 +30,19 @@
|
|||||||
"namespace.namespaceVersion"
|
"namespace.namespaceVersion"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"breakingFields": [
|
"snapshotProjection": {
|
||||||
"topicId",
|
"singletonRowKey": "invalidation-graph",
|
||||||
"namespace.namespaceId",
|
"canonicalArrayKeyFields": {
|
||||||
"namespace.namespaceVersion"
|
"topics": ["$value"],
|
||||||
]
|
"namespaces": ["namespaceId", "namespaceVersion"],
|
||||||
|
"edges": [
|
||||||
|
"topicId",
|
||||||
|
"namespace.namespaceId",
|
||||||
|
"namespace.namespaceVersion"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"breakingFields": ["topics", "namespaces", "edges"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"registryId": "FIXTURE-INVALIDATION-VERSIONS",
|
"registryId": "FIXTURE-INVALIDATION-VERSIONS",
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import { tmpdir } from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
diffRegistrySnapshots,
|
||||||
|
validateBreakingEvidence,
|
||||||
|
verifyRegistryBaselineApproval,
|
||||||
|
} from "../../scripts/lib/registry-compatibility.ts";
|
||||||
|
|
||||||
type RegistryDefinition = Readonly<{
|
type RegistryDefinition = Readonly<{
|
||||||
registryId: string;
|
registryId: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -15,6 +21,10 @@ type RegistryDefinition = Readonly<{
|
|||||||
rowKeyFields?: string[];
|
rowKeyFields?: string[];
|
||||||
uniqueFields?: string[];
|
uniqueFields?: string[];
|
||||||
uniqueFieldSets?: string[][];
|
uniqueFieldSets?: string[][];
|
||||||
|
snapshotProjection?: {
|
||||||
|
singletonRowKey: string;
|
||||||
|
canonicalArrayKeyFields: Record<string, string[]>;
|
||||||
|
};
|
||||||
consumers?: Array<{ path: string; token: string }>;
|
consumers?: Array<{ path: string; token: string }>;
|
||||||
breakingFields?: string[];
|
breakingFields?: string[];
|
||||||
}>;
|
}>;
|
||||||
@@ -65,17 +75,25 @@ describe("registry governance manifest", () => {
|
|||||||
"namespace.namespaceVersion",
|
"namespace.namespaceVersion",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
snapshotProjection: {
|
||||||
|
singletonRowKey: "invalidation-graph",
|
||||||
|
canonicalArrayKeyFields: {
|
||||||
|
topics: ["$value"],
|
||||||
|
namespaces: ["namespaceId", "namespaceVersion"],
|
||||||
|
edges: [
|
||||||
|
"topicId",
|
||||||
|
"namespace.namespaceId",
|
||||||
|
"namespace.namespaceVersion",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
consumers: [
|
consumers: [
|
||||||
{
|
{
|
||||||
path: "src/bootstrap/runtime-adapters.ts",
|
path: "src/bootstrap/runtime-adapters.ts",
|
||||||
token: "indexInvalidationRegistry(INVALIDATION_REGISTRY)",
|
token: "indexInvalidationRegistry(INVALIDATION_REGISTRY)",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakingFields: [
|
breakingFields: ["topics", "namespaces", "edges"],
|
||||||
"topicId",
|
|
||||||
"namespace.namespaceId",
|
|
||||||
"namespace.namespaceVersion",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(byId.get("FE-REG-QUERY-INVALIDATION-TOPIC-VERSION")).toMatchObject({
|
expect(byId.get("FE-REG-QUERY-INVALIDATION-TOPIC-VERSION")).toMatchObject({
|
||||||
path: "src/features/installed-feature-contracts.ts",
|
path: "src/features/installed-feature-contracts.ts",
|
||||||
@@ -113,18 +131,37 @@ describe("registry governance manifest", () => {
|
|||||||
|
|
||||||
expect(result.status, result.stderr).toBe(0);
|
expect(result.status, result.stderr).toBe(0);
|
||||||
const report = JSON.parse(await readFile(artifact, "utf8")) as {
|
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(
|
expect(
|
||||||
report.registries.find(
|
report.registries.find(
|
||||||
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
|
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
|
||||||
)?.rows,
|
)?.rows,
|
||||||
).toHaveProperty('["qinv.fixture.changed","orders",1]');
|
).toEqual({
|
||||||
expect(
|
"invalidation-graph": {
|
||||||
report.registries.find(
|
topics: ["orders", "qinv.fixture.changed"],
|
||||||
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
|
namespaces: [
|
||||||
)?.rows,
|
{ namespaceId: "orders", namespaceVersion: 1 },
|
||||||
).toHaveProperty('["orders","qinv.fixture.changed",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(
|
expect(
|
||||||
report.registries.find(
|
report.registries.find(
|
||||||
(registry) => registry.registryId === "FIXTURE-INVALIDATION-VERSIONS",
|
(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 () => {
|
it("rejects duplicate composite invalidation edges after projection", async () => {
|
||||||
const directory = await mkdtemp(
|
const directory = await mkdtemp(
|
||||||
path.join(tmpdir(), "registry-governance-duplicate-"),
|
path.join(tmpdir(), "registry-governance-duplicate-"),
|
||||||
|
|||||||
Reference in New Issue
Block a user