diff --git a/.gitignore b/.gitignore index 087872e..d9f9671 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ artifacts/**/*.xml artifacts/**/*.txt artifacts/**/*.sarif artifacts/tests/e2e/ +artifacts/storybook/ +artifacts/tests/storybook/ +artifacts/tests/visual/ !artifacts/**/.gitkeep diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..3566608 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,15 @@ +import type { StorybookConfig } from "@storybook/react-vite"; + +const config: StorybookConfig = { + stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: ["@storybook/addon-a11y"], + framework: { + name: "@storybook/react-vite", + options: {}, + }, + core: { + disableTelemetry: true, + }, +}; + +export default config; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..9c11377 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,88 @@ +import type { Preview } from "@storybook/react-vite"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { MemoryRouter } from "react-router-dom"; + +import { createAnonymousSessionAdapter } from "../src/adapters/auth/external-session-adapter.js"; +import { createQueryClient } from "../src/adapters/query-cache/tanstack-query-cache.js"; +import { createApplication } from "../src/application/create-application.js"; +import { LocaleProvider } from "../src/presentation/i18n/index.js"; +import { ApplicationProvider } from "../src/presentation/providers/application-provider.js"; +import { SessionProvider } from "../src/presentation/providers/session-provider.jsx"; +import { ThemeProvider } from "../src/presentation/providers/theme-provider.jsx"; +import "../src/presentation/styles/theme.css"; + +const preferences = new Map(); +const application = createApplication({ + session: createAnonymousSessionAdapter(), + preferences: { + read: (name) => ({ ok: true, value: preferences.get(name) }), + write: (name, value) => { + preferences.set(name, structuredClone(value)); + return { ok: true }; + }, + remove: (name) => { + preferences.delete(name); + return { ok: true }; + }, + }, + diagnostics: { record() {} }, + telemetry: { emit() {} }, + releaseInfo: { + getCurrent: async () => ({ + buildId: "storybook-build", + releaseId: "storybook-release", + configSchemaVersion: "1", + apiContractVersion: "1", + assetManifestHash: "storybook-assets", + routeChunks: {}, + }), + refresh: async () => ({ + buildId: "storybook-build", + releaseId: "storybook-release", + configSchemaVersion: "1", + apiContractVersion: "1", + assetManifestHash: "storybook-assets", + routeChunks: {}, + }), + }, + navigation: { reload() {} }, +}); +const queryClient = createQueryClient(); + +const preview: Preview = { + decorators: [ + (Story) => ( + + + + + + +
+
+ +
+ + + + + + + ), + ], + parameters: { + a11y: { + test: "error", + }, + controls: { + expanded: true, + }, + options: { + storySort: { + order: ["Platform"], + }, + }, + }, +}; + +export default preview; diff --git a/config/ci/gates.json b/config/ci/gates.json index cd5b07a..4664a75 100644 --- a/config/ci/gates.json +++ b/config/ci/gates.json @@ -100,9 +100,19 @@ }, "FE-GATE-005": { "name": "unit", - "steps": [{ "script": "test:unit", "expect": "pass" }], + "steps": [ + { "script": "test:unit", "expect": "pass" }, + { "script": "test:coverage", "expect": "pass" }, + { "script": "check:coverage:fixture", "expect": "fail" } + ], "logPath": "artifacts/quality/gates/FE-GATE-005.txt", - "evidence": ["artifacts/tests/unit.xml"], + "evidence": [ + "artifacts/tests/unit.xml", + "artifacts/tests/coverage.xml", + "artifacts/tests/coverage/coverage-summary.json", + "artifacts/quality/risk-coverage.json", + "artifacts/quality/risk-coverage-fixture.json" + ], "retentionClass": "merge-cycle" }, "FE-GATE-006": { @@ -127,9 +137,24 @@ }, "FE-GATE-008": { "name": "e2e", - "steps": [{ "script": "test:e2e", "expect": "pass" }], + "steps": [ + { "script": "test:e2e", "expect": "pass" }, + { "script": "test:storybook", "expect": "pass" }, + { "script": "test:visual", "expect": "pass" }, + { "script": "check:test-evidence", "expect": "pass" }, + { "script": "check:test-evidence:fixture", "expect": "fail" } + ], "logPath": "artifacts/quality/gates/FE-GATE-008.txt", - "evidence": ["artifacts/tests/e2e/report/index.html"], + "evidence": [ + "artifacts/tests/e2e/report/index.html", + "artifacts/tests/e2e/results.xml", + "artifacts/tests/storybook/report/index.html", + "artifacts/tests/storybook/results.xml", + "artifacts/tests/visual/report/index.html", + "artifacts/tests/visual/results.xml", + "artifacts/quality/test-evidence.json", + "artifacts/quality/test-evidence-fixture.json" + ], "retentionClass": "merge-cycle" }, "FE-GATE-009": { @@ -162,6 +187,11 @@ { "script": "check:diagnostics", "expect": "pass" }, { "script": "check:diagnostics:fixture", "expect": "fail" }, { "script": "check:registries", "expect": "pass" }, + { + "script": "check:registries:compatibility-fixtures", + "expect": "pass" + }, + { "script": "check:registries:baseline-fixture", "expect": "fail" }, { "script": "check:registries:fixture", "expect": "fail" }, { "script": "check:routes:fixture", "expect": "fail" } ], @@ -175,6 +205,8 @@ "artifacts/quality/diagnostics.json", "artifacts/quality/diagnostics-fixture.json", "artifacts/quality/registries.json", + "artifacts/quality/registry-compatibility-fixtures.json", + "artifacts/quality/registry-baseline-fixture.json", "artifacts/quality/registry-fixture.json", "artifacts/quality/route-registry-fixture.json" ], @@ -182,11 +214,15 @@ }, "FE-GATE-011": { "name": "build", - "steps": [{ "script": "build", "expect": "pass" }], + "steps": [ + { "script": "build", "expect": "pass" }, + { "script": "build:storybook", "expect": "pass" } + ], "logPath": "artifacts/quality/gates/FE-GATE-011.txt", "evidence": [ "artifacts/release/build-manifest.json", - "artifacts/release/runtime-config.schema.json" + "artifacts/release/runtime-config.schema.json", + "artifacts/storybook/static/index.html" ], "retentionClass": "release-coherence" }, diff --git a/config/contracts/registry-baseline.approval.json b/config/contracts/registry-baseline.approval.json new file mode 100644 index 0000000..aea11d9 --- /dev/null +++ b/config/contracts/registry-baseline.approval.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 1, + "snapshotDigest": "e8448e46bc65326e9b2eb23cdce0870242faedb7a354942389c4a95b0e392d90", + "owner": "frontend-platform", + "reason": "RP-10 initial approved executable registry baseline", + "approvedAt": "2026-07-26T07:53:45.969Z" +} diff --git a/config/contracts/registry-baseline.json b/config/contracts/registry-baseline.json new file mode 100644 index 0000000..c185c8f --- /dev/null +++ b/config/contracts/registry-baseline.json @@ -0,0 +1,1701 @@ +{ + "schemaVersion": 2, + "registries": [ + { + "registryId": "FE-REG-ROUTE", + "owner": "feature-frontend-routing-release-recovery-runtime", + "source": "src/features/installed-feature-contracts.js", + "rowCount": 9, + "contract": { + "requiredFields": [ + "routeId", + "path", + "paramsSchema", + "searchSchema", + "access", + "loadingSurface", + "errorSurface", + "chunkId", + "title", + "navigationLabel", + "navigationOrder" + ], + "fieldTypes": { + "routeId": "string", + "path": "string", + "paramsSchema": "string|null", + "searchSchema": "string|null", + "access": "string", + "loadingSurface": "string", + "errorSurface": "string", + "chunkId": "string", + "title": "string", + "navigationLabel": "string|null", + "navigationOrder": "integer|null" + }, + "uniqueFields": [ + "routeId", + "path", + "chunkId" + ], + "allowedValues": { + "access": [ + "public", + "session-required", + "integration-defined" + ], + "paramsSchema": [ + null, + "NotFoundSplat", + "ReferenceResourceParams" + ], + "searchSchema": [ + null, + "ReferenceResourceListQuery" + ], + "loadingSurface": [ + "app-shell", + "example-page", + "reference-resource-list", + "reference-resource-detail", + "reference-resource-form", + "reference-resource-status", + "none" + ], + "errorSurface": [ + "route-boundary", + "feature-boundary", + "not-found" + ] + }, + "references": [ + { + "field": "routeId", + "registryId": "FE-REG-ROUTE-RUNTIME", + "targetField": "routeId" + }, + { + "field": "paramsSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + }, + { + "field": "searchSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + } + ], + "keyField": "routeId", + "breakingFields": [ + "routeId", + "path", + "paramsSchema", + "searchSchema", + "access", + "chunkId" + ] + }, + "rows": { + "APP_HOME": { + "access": "public", + "chunkId": "route-home", + "errorSurface": "route-boundary", + "loadingSurface": "app-shell", + "navigationLabel": "시작", + "navigationOrder": 10, + "paramsSchema": null, + "path": "/", + "routeId": "APP_HOME", + "searchSchema": null, + "title": "시작" + }, + "EXAMPLES_AUTH": { + "access": "public", + "chunkId": "route-examples-auth", + "errorSurface": "route-boundary", + "loadingSurface": "example-page", + "navigationLabel": "인증 연동", + "navigationOrder": 40, + "paramsSchema": null, + "path": "/examples/auth", + "routeId": "EXAMPLES_AUTH", + "searchSchema": null, + "title": "인증 연동" + }, + "EXAMPLES_STATES": { + "access": "public", + "chunkId": "route-examples-states", + "errorSurface": "route-boundary", + "loadingSurface": "example-page", + "navigationLabel": "화면 상태", + "navigationOrder": 30, + "paramsSchema": null, + "path": "/examples/states", + "routeId": "EXAMPLES_STATES", + "searchSchema": null, + "title": "화면 상태" + }, + "EXAMPLES_UI": { + "access": "public", + "chunkId": "route-examples-ui", + "errorSurface": "route-boundary", + "loadingSurface": "example-page", + "navigationLabel": "UI 구성요소", + "navigationOrder": 20, + "paramsSchema": null, + "path": "/examples/ui", + "routeId": "EXAMPLES_UI", + "searchSchema": null, + "title": "UI 구성요소" + }, + "NOT_FOUND": { + "access": "public", + "chunkId": "route-not-found", + "errorSurface": "not-found", + "loadingSurface": "none", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "NotFoundSplat", + "path": "*", + "routeId": "NOT_FOUND", + "searchSchema": null, + "title": "페이지를 찾을 수 없음" + }, + "REFERENCE_RESOURCE_DETAIL": { + "access": "integration-defined", + "chunkId": "route-reference-resource-detail", + "errorSurface": "feature-boundary", + "loadingSurface": "reference-resource-detail", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "ReferenceResourceParams", + "path": "/examples/reference-resources/:resourceId", + "routeId": "REFERENCE_RESOURCE_DETAIL", + "searchSchema": null, + "title": "Reference detail" + }, + "REFERENCE_RESOURCE_FORM": { + "access": "integration-defined", + "chunkId": "route-reference-resource-form", + "errorSurface": "feature-boundary", + "loadingSurface": "reference-resource-form", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": null, + "path": "/examples/reference-resources/new", + "routeId": "REFERENCE_RESOURCE_FORM", + "searchSchema": null, + "title": "Reference form" + }, + "REFERENCE_RESOURCE_LIST": { + "access": "integration-defined", + "chunkId": "route-reference-resources", + "errorSurface": "feature-boundary", + "loadingSurface": "reference-resource-list", + "navigationLabel": "Reference feature", + "navigationOrder": 50, + "paramsSchema": null, + "path": "/examples/reference-resources", + "routeId": "REFERENCE_RESOURCE_LIST", + "searchSchema": "ReferenceResourceListQuery", + "title": "Reference feature" + }, + "REFERENCE_RESOURCE_STATUS": { + "access": "integration-defined", + "chunkId": "route-reference-resource-status", + "errorSurface": "feature-boundary", + "loadingSurface": "reference-resource-status", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": null, + "path": "/examples/reference-resources/status", + "routeId": "REFERENCE_RESOURCE_STATUS", + "searchSchema": null, + "title": "Reference status" + } + } + }, + { + "registryId": "FE-REG-ROUTE-RUNTIME", + "owner": "feature-frontend-routing-release-recovery-runtime", + "source": "src/features/installed-feature-contracts.js", + "rowCount": 9, + "contract": { + "requiredFields": [ + "routeId", + "moduleId", + "paramsCodec", + "searchCodec" + ], + "fieldTypes": { + "routeId": "string", + "moduleId": "string", + "paramsCodec": "string", + "searchCodec": "string" + }, + "uniqueFields": [ + "routeId", + "moduleId" + ], + "allowedValues": {}, + "references": [ + { + "field": "routeId", + "registryId": "FE-REG-ROUTE", + "targetField": "routeId" + }, + { + "field": "paramsCodec", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + }, + { + "field": "searchCodec", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + } + ], + "keyField": "routeId", + "breakingFields": [ + "routeId", + "moduleId", + "paramsCodec", + "searchCodec" + ] + }, + "rows": { + "APP_HOME": { + "moduleId": "home-page", + "paramsCodec": "none", + "routeId": "APP_HOME", + "searchCodec": "none" + }, + "EXAMPLES_AUTH": { + "moduleId": "auth-example-page", + "paramsCodec": "none", + "routeId": "EXAMPLES_AUTH", + "searchCodec": "none" + }, + "EXAMPLES_STATES": { + "moduleId": "state-gallery-page", + "paramsCodec": "none", + "routeId": "EXAMPLES_STATES", + "searchCodec": "none" + }, + "EXAMPLES_UI": { + "moduleId": "ui-gallery-page", + "paramsCodec": "none", + "routeId": "EXAMPLES_UI", + "searchCodec": "none" + }, + "NOT_FOUND": { + "moduleId": "not-found-page", + "paramsCodec": "NotFoundSplat", + "routeId": "NOT_FOUND", + "searchCodec": "none" + }, + "REFERENCE_RESOURCE_DETAIL": { + "moduleId": "reference-resource-detail-page", + "paramsCodec": "ReferenceResourceParams", + "routeId": "REFERENCE_RESOURCE_DETAIL", + "searchCodec": "none" + }, + "REFERENCE_RESOURCE_FORM": { + "moduleId": "reference-resource-form-page", + "paramsCodec": "none", + "routeId": "REFERENCE_RESOURCE_FORM", + "searchCodec": "none" + }, + "REFERENCE_RESOURCE_LIST": { + "moduleId": "reference-resource-page", + "paramsCodec": "none", + "routeId": "REFERENCE_RESOURCE_LIST", + "searchCodec": "ReferenceResourceListQuery" + }, + "REFERENCE_RESOURCE_STATUS": { + "moduleId": "reference-resource-status-page", + "paramsCodec": "none", + "routeId": "REFERENCE_RESOURCE_STATUS", + "searchCodec": "none" + } + } + }, + { + "registryId": "FE-REG-API", + "owner": "feature-frontend-api-client-response-envelope-contract", + "source": "src/features/installed-feature-contracts.js", + "rowCount": 3, + "contract": { + "requiredFields": [ + "method", + "path", + "operationId", + "auth", + "timeoutMs", + "idempotency", + "retry", + "requestSource", + "requestSchema", + "responseSchema", + "owner" + ], + "fieldTypes": { + "method": "string", + "path": "string", + "operationId": "string", + "auth": "string", + "timeoutMs": "integer|null", + "idempotency": "string", + "retry": "string", + "requestSource": "string", + "requestSchema": "string", + "responseSchema": "string", + "owner": "string" + }, + "uniqueFields": [ + "operationId" + ], + "allowedValues": { + "method": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "auth": [ + "none", + "external-session" + ], + "idempotency": [ + "safe", + "keyed", + "none" + ], + "retry": [ + "runtime", + "never" + ], + "requestSource": [ + "none", + "search", + "body" + ] + }, + "references": [ + { + "field": "requestSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + }, + { + "field": "responseSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + } + ], + "keyField": "operationId", + "breakingFields": [ + "method", + "path", + "operationId", + "auth", + "idempotency", + "requestSource", + "requestSchema", + "responseSchema" + ] + }, + "rows": { + "CREATE_REFERENCE_RESOURCE": { + "auth": "external-session", + "idempotency": "keyed", + "method": "POST", + "operationId": "CREATE_REFERENCE_RESOURCE", + "owner": "feature-frontend-reference-feature-vertical-slice", + "path": "/api/reference-resources", + "requestSchema": "CreateReferenceResourceCommand", + "requestSource": "body", + "responseSchema": "ReferenceResourcePayload", + "retry": "runtime", + "timeoutMs": null + }, + "GET_REFERENCE_RESOURCE": { + "auth": "external-session", + "idempotency": "safe", + "method": "GET", + "operationId": "GET_REFERENCE_RESOURCE", + "owner": "feature-frontend-form-page-platform", + "path": "/api/reference-resources/{resourceId}", + "requestSchema": "NoRequest", + "requestSource": "none", + "responseSchema": "ReferenceResourcePayload", + "retry": "runtime", + "timeoutMs": null + }, + "LIST_REFERENCE_RESOURCES": { + "auth": "external-session", + "idempotency": "safe", + "method": "GET", + "operationId": "LIST_REFERENCE_RESOURCES", + "owner": "feature-frontend-reference-feature-vertical-slice", + "path": "/api/reference-resources", + "requestSchema": "ReferenceResourceListQuery", + "requestSource": "search", + "responseSchema": "ReferenceResourceListPayload", + "retry": "runtime", + "timeoutMs": null + } + } + }, + { + "registryId": "FE-REG-SCHEMA", + "owner": "feature-frontend-contract-schema-registry", + "source": "src/features/installed-feature-contracts.js", + "rowCount": 8, + "contract": { + "requiredFields": [ + "schemaId", + "boundary", + "owner", + "runtime" + ], + "fieldTypes": { + "schemaId": "string", + "boundary": "string", + "owner": "string", + "runtime": "string" + }, + "uniqueFields": [ + "schemaId" + ], + "allowedValues": { + "boundary": [ + "route-params", + "route-search", + "route-search-api-request", + "api-request", + "api-response" + ], + "runtime": [ + "zod" + ] + }, + "references": [], + "keyField": "schemaId", + "breakingFields": [ + "schemaId", + "boundary", + "runtime" + ] + }, + "rows": { + "CreateReferenceResourceCommand": { + "boundary": "api-request", + "owner": "feature-frontend-reference-feature-vertical-slice", + "runtime": "zod", + "schemaId": "CreateReferenceResourceCommand" + }, + "none": { + "boundary": "route-params", + "owner": "feature-frontend-routing-release-recovery-runtime", + "runtime": "zod", + "schemaId": "none" + }, + "NoRequest": { + "boundary": "api-request", + "owner": "feature-frontend-reference-feature-vertical-slice", + "runtime": "zod", + "schemaId": "NoRequest" + }, + "NotFoundSplat": { + "boundary": "route-params", + "owner": "feature-frontend-routing-release-recovery-runtime", + "runtime": "zod", + "schemaId": "NotFoundSplat" + }, + "ReferenceResourceListPayload": { + "boundary": "api-response", + "owner": "feature-frontend-reference-feature-vertical-slice", + "runtime": "zod", + "schemaId": "ReferenceResourceListPayload" + }, + "ReferenceResourceListQuery": { + "boundary": "route-search-api-request", + "owner": "feature-frontend-reference-feature-vertical-slice", + "runtime": "zod", + "schemaId": "ReferenceResourceListQuery" + }, + "ReferenceResourceParams": { + "boundary": "route-params", + "owner": "feature-frontend-reference-feature-vertical-slice", + "runtime": "zod", + "schemaId": "ReferenceResourceParams" + }, + "ReferenceResourcePayload": { + "boundary": "api-response", + "owner": "feature-frontend-reference-feature-vertical-slice", + "runtime": "zod", + "schemaId": "ReferenceResourcePayload" + } + } + }, + { + "registryId": "FE-REG-ENV", + "owner": "feature-frontend-env-runtime-config-contract", + "source": "src/contracts/env.js", + "rowCount": 14, + "contract": { + "requiredFields": [ + "phase", + "classification", + "required", + "defaultValue" + ], + "fieldTypes": { + "phase": "string", + "classification": "string", + "required": "boolean", + "defaultValue": "string|integer|boolean|null" + }, + "uniqueFields": [], + "allowedValues": { + "phase": [ + "build", + "runtime" + ], + "classification": [ + "public", + "public-sensitive", + "public-metadata", + "compile-time" + ] + }, + "references": [], + "keyField": null, + "breakingFields": [ + "phase", + "classification", + "required" + ] + }, + "rows": { + "API_BASE_URL": { + "classification": "public-sensitive", + "defaultValue": null, + "phase": "runtime", + "required": true + }, + "API_CONTRACT_VERSION": { + "classification": "public", + "defaultValue": null, + "phase": "runtime", + "required": true + }, + "APP_ENV": { + "classification": "public", + "defaultValue": null, + "phase": "runtime", + "required": true + }, + "AUTH_MODE": { + "classification": "public", + "defaultValue": "external", + "phase": "runtime", + "required": true + }, + "CONFIG_SCHEMA_VERSION": { + "classification": "public", + "defaultValue": null, + "phase": "runtime", + "required": true + }, + "MAX_RETRY_ATTEMPTS": { + "classification": "public", + "defaultValue": 2, + "phase": "runtime", + "required": false + }, + "RELEASE_MANIFEST_URL": { + "classification": "public", + "defaultValue": "/release-manifest.json", + "phase": "runtime", + "required": true + }, + "REQUEST_TIMEOUT_MS": { + "classification": "public", + "defaultValue": 10000, + "phase": "runtime", + "required": false + }, + "TELEMETRY_ENABLED": { + "classification": "public", + "defaultValue": false, + "phase": "runtime", + "required": true + }, + "TELEMETRY_ENDPOINT": { + "classification": "public-sensitive", + "defaultValue": null, + "phase": "runtime", + "required": false + }, + "VITE_BUILD_ID": { + "classification": "public-metadata", + "defaultValue": null, + "phase": "build", + "required": true + }, + "VITE_COMMIT_SHA": { + "classification": "public-metadata", + "defaultValue": "local", + "phase": "build", + "required": false + }, + "VITE_ROUTER_BASE_PATH": { + "classification": "compile-time", + "defaultValue": "/", + "phase": "build", + "required": true + }, + "VITE_RUNTIME_CONFIG_URL": { + "classification": "compile-time", + "defaultValue": "/config.json", + "phase": "build", + "required": true + } + } + }, + { + "registryId": "FE-REG-STORAGE", + "owner": "feature-frontend-storage-registry-contract", + "source": "src/contracts/storage-keys.js", + "rowCount": 4, + "contract": { + "requiredFields": [ + "logicalName", + "physicalKey", + "backend", + "classification", + "schemaVersion", + "ttl", + "migration", + "quotaFallback" + ], + "fieldTypes": { + "logicalName": "string", + "physicalKey": "string", + "backend": "string", + "classification": "string", + "schemaVersion": "integer", + "ttl": "integer|string|null", + "migration": "string|function", + "quotaFallback": "string" + }, + "uniqueFields": [ + "logicalName", + "physicalKey" + ], + "allowedValues": { + "backend": [ + "memory", + "sessionStorage", + "localStorage", + "indexedDB", + "disabled", + "forbidden" + ], + "classification": [ + "public-preference", + "opaque-cache", + "sensitive-forbidden" + ], + "quotaFallback": [ + "memory", + "no-persist", + "feature-disable" + ] + }, + "references": [], + "keyField": "logicalName", + "breakingFields": [ + "logicalName", + "physicalKey", + "backend", + "classification", + "schemaVersion", + "migration" + ] + }, + "rows": { + "AUTH_TOKEN": { + "backend": "forbidden", + "classification": "sensitive-forbidden", + "logicalName": "AUTH_TOKEN", + "migration": "discard", + "name": "auth-token", + "physicalKey": "ca-frontend:auth:v1:auth-token", + "quotaFallback": "feature-disable", + "schemaVersion": 1, + "scope": "auth", + "ttl": null + }, + "CHUNK_RELOAD_GUARD": { + "backend": "sessionStorage", + "classification": "opaque-cache", + "logicalName": "CHUNK_RELOAD_GUARD", + "migration": "discard", + "name": "chunk-reload-guard", + "physicalKey": "ca-frontend:release:v1:chunk-reload-guard", + "quotaFallback": "no-persist", + "schemaVersion": 1, + "scope": "release", + "ttl": "session" + }, + "COLOR_SCHEME": { + "backend": "localStorage", + "classification": "public-preference", + "logicalName": "COLOR_SCHEME", + "migration": "discard", + "name": "color-scheme", + "physicalKey": "ca-frontend:preference:v1:color-scheme", + "quotaFallback": "memory", + "schemaVersion": 1, + "scope": "preference", + "ttl": null + }, + "QUERY_PERSISTENCE": { + "backend": "disabled", + "classification": "sensitive-forbidden", + "logicalName": "QUERY_PERSISTENCE", + "migration": "discard", + "name": "query-persistence", + "physicalKey": "ca-frontend:cache:v1:query-persistence", + "quotaFallback": "feature-disable", + "schemaVersion": 1, + "scope": "cache", + "ttl": null + } + } + }, + { + "registryId": "FE-REG-ERROR", + "owner": "feature-frontend-error-classification-boundary-contract", + "source": "src/contracts/errors.js", + "rowCount": 31, + "contract": { + "requiredFields": [ + "kind", + "defaultRetryable", + "severity", + "userMessageKey", + "action", + "telemetryEvent", + "redaction" + ], + "fieldTypes": { + "kind": "string", + "defaultRetryable": "boolean", + "severity": "string", + "userMessageKey": "string", + "action": "string", + "telemetryEvent": "string", + "redaction": "array" + }, + "uniqueFields": [ + "kind" + ], + "allowedValues": { + "severity": [ + "info", + "warning", + "error" + ], + "action": [ + "retry", + "reauth", + "navigate", + "reload-once", + "contact-support", + "none" + ] + }, + "references": [], + "keyField": "kind", + "breakingFields": [ + "kind", + "userMessageKey", + "action", + "telemetryEvent" + ] + }, + "rows": { + "API_CONTRACT_MISMATCH": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "API_CONTRACT_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "app.boot.failed", + "userMessageKey": "error.api_contract_mismatch" + }, + "ASSET_MISMATCH": { + "action": "reload-once", + "defaultRetryable": false, + "kind": "ASSET_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "release.mismatch.detected", + "userMessageKey": "error.asset_mismatch" + }, + "AUTH_INTEGRATION_FAILURE": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "AUTH_INTEGRATION_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.auth_integration_failure" + }, + "AUTH_REQUIRED": { + "action": "reauth", + "defaultRetryable": false, + "kind": "AUTH_REQUIRED", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "info", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.auth_required" + }, + "BOOT_CONFIG_FAILURE": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "BOOT_CONFIG_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "app.boot.failed", + "userMessageKey": "error.boot_config_failure" + }, + "BUILD_MISMATCH": { + "action": "reload-once", + "defaultRetryable": false, + "kind": "BUILD_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "release.mismatch.detected", + "userMessageKey": "error.build_mismatch" + }, + "CHUNK_LOAD_FAILURE": { + "action": "reload-once", + "defaultRetryable": false, + "kind": "CHUNK_LOAD_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "release.mismatch.detected", + "userMessageKey": "error.chunk_load_failure" + }, + "CONFIG_MISMATCH": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "CONFIG_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "app.boot.failed", + "userMessageKey": "error.config_mismatch" + }, + "CONFLICT": { + "action": "retry", + "defaultRetryable": false, + "kind": "CONFLICT", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.conflict" + }, + "CONTENT_TYPE_MISMATCH": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "CONTENT_TYPE_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.content_type_mismatch" + }, + "DEPLOY_MISMATCH": { + "action": "reload-once", + "defaultRetryable": false, + "kind": "DEPLOY_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "release.mismatch.detected", + "userMessageKey": "error.deploy_mismatch" + }, + "ENVELOPE_MISMATCH": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "ENVELOPE_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.envelope_mismatch" + }, + "FORBIDDEN": { + "action": "navigate", + "defaultRetryable": false, + "kind": "FORBIDDEN", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.forbidden" + }, + "MALFORMED_JSON": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "MALFORMED_JSON", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.malformed_json" + }, + "NETWORK_UNREACHABLE": { + "action": "retry", + "defaultRetryable": true, + "kind": "NETWORK_UNREACHABLE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.network_unreachable" + }, + "NOT_FOUND": { + "action": "navigate", + "defaultRetryable": false, + "kind": "NOT_FOUND", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "info", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.not_found" + }, + "QUERY_CACHE_FAILURE": { + "action": "retry", + "defaultRetryable": false, + "kind": "QUERY_CACHE_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "query.cache.failed", + "userMessageKey": "error.query_cache_failure" + }, + "RATE_LIMITED": { + "action": "retry", + "defaultRetryable": true, + "kind": "RATE_LIMITED", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.rate_limited" + }, + "RELEASE_MANIFEST_FAILURE": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "RELEASE_MANIFEST_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "app.boot.failed", + "userMessageKey": "error.release_manifest_failure" + }, + "RELEASE_MISMATCH": { + "action": "reload-once", + "defaultRetryable": false, + "kind": "RELEASE_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "release.mismatch.detected", + "userMessageKey": "error.release_mismatch" + }, + "RENDER_FAILURE": { + "action": "reload-once", + "defaultRetryable": false, + "kind": "RENDER_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "ui.render.failed", + "userMessageKey": "error.render_failure" + }, + "REQUEST_ABORTED": { + "action": "none", + "defaultRetryable": false, + "kind": "REQUEST_ABORTED", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "info", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.request_aborted" + }, + "REQUEST_TIMEOUT": { + "action": "retry", + "defaultRetryable": true, + "kind": "REQUEST_TIMEOUT", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.request_timeout" + }, + "SCHEMA_MISMATCH": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "SCHEMA_MISMATCH", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.schema_mismatch" + }, + "SERVER_FAILURE": { + "action": "retry", + "defaultRetryable": true, + "kind": "SERVER_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.server_failure" + }, + "STORAGE_QUOTA_EXCEEDED": { + "action": "none", + "defaultRetryable": false, + "kind": "STORAGE_QUOTA_EXCEEDED", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "storage.operation.failed", + "userMessageKey": "error.storage_quota_exceeded" + }, + "STORAGE_UNAVAILABLE": { + "action": "none", + "defaultRetryable": false, + "kind": "STORAGE_UNAVAILABLE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "storage.operation.failed", + "userMessageKey": "error.storage_unavailable" + }, + "TELEMETRY_FAILURE": { + "action": "none", + "defaultRetryable": false, + "kind": "TELEMETRY_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "info", + "telemetryEvent": "telemetry.delivery.dropped", + "userMessageKey": "error.telemetry_failure" + }, + "UNKNOWN_CLIENT_FAILURE": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "UNKNOWN_CLIENT_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "warning", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.unknown_client_failure" + }, + "UNKNOWN_FAILURE": { + "action": "contact-support", + "defaultRetryable": false, + "kind": "UNKNOWN_FAILURE", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "error", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.unknown_failure" + }, + "VALIDATION_REJECTED": { + "action": "none", + "defaultRetryable": false, + "kind": "VALIDATION_REJECTED", + "redaction": [ + "authorization", + "body", + "cause", + "headers", + "query", + "stack", + "storageValue", + "url" + ], + "severity": "info", + "telemetryEvent": "api.request.failed", + "userMessageKey": "error.validation_rejected" + } + } + }, + { + "registryId": "FE-REG-QUERY", + "owner": "feature-frontend-server-state-caching-contract", + "source": "src/features/installed-feature-contracts.js", + "rowCount": 1, + "contract": { + "requiredFields": [ + "namespace", + "serialization", + "identity", + "invalidation", + "version", + "persistence" + ], + "fieldTypes": { + "namespace": "array", + "serialization": "string", + "identity": "string", + "invalidation": "string", + "version": "integer", + "persistence": "string" + }, + "uniqueFields": [ + "namespace" + ], + "allowedValues": { + "persistence": [ + "disabled" + ] + }, + "references": [], + "keyField": null, + "breakingFields": [ + "namespace", + "serialization", + "identity", + "version", + "persistence" + ] + }, + "rows": { + "REFERENCE_RESOURCE": { + "identity": "no-pii-token-or-raw-url", + "invalidation": "reference resource namespace after successful mutation", + "namespace": [ + "reference-resource", + 1 + ], + "persistence": "disabled", + "serialization": "canonical-object-order", + "version": 1 + } + } + }, + { + "registryId": "FE-REG-TELEMETRY", + "owner": "feature-frontend-diagnostics-telemetry-runtime", + "source": "src/contracts/telemetry.js", + "rowCount": 5, + "contract": { + "requiredFields": [ + "eventName", + "trigger", + "requiredAttributes", + "optionalAttributes", + "forbiddenAttributes", + "sampling", + "delivery" + ], + "fieldTypes": { + "eventName": "string", + "trigger": "string", + "requiredAttributes": "array", + "optionalAttributes": "array", + "forbiddenAttributes": "array", + "sampling": "string", + "delivery": "string" + }, + "uniqueFields": [ + "eventName" + ], + "allowedValues": { + "delivery": [ + "best-effort" + ] + }, + "references": [], + "keyField": "eventName", + "breakingFields": [ + "eventName", + "requiredAttributes", + "forbiddenAttributes", + "delivery" + ] + }, + "rows": { + "api.request.failed": { + "delivery": "best-effort", + "eventName": "api.request.failed", + "forbiddenAttributes": [ + "access_token", + "authorization_header", + "cookie", + "email", + "query_string", + "raw_url", + "raw_user_id", + "refresh_token", + "request_body", + "response_body", + "stack_in_user_message", + "storage_value", + "user_name" + ], + "optionalAttributes": [ + "duration_bucket", + "operation_id" + ], + "requiredAttributes": [ + "attempt_count_bucket", + "error_kind", + "http_status_group", + "route_id" + ], + "sampling": "all", + "trigger": "terminal API failure" + }, + "app.boot.failed": { + "delivery": "best-effort", + "eventName": "app.boot.failed", + "forbiddenAttributes": [ + "access_token", + "authorization_header", + "cookie", + "email", + "query_string", + "raw_url", + "raw_user_id", + "refresh_token", + "request_body", + "response_body", + "stack_in_user_message", + "storage_value", + "user_name" + ], + "optionalAttributes": [], + "requiredAttributes": [ + "build_id", + "config_schema_version", + "error_kind" + ], + "sampling": "all", + "trigger": "boot validation failure" + }, + "release.mismatch.detected": { + "delivery": "best-effort", + "eventName": "release.mismatch.detected", + "forbiddenAttributes": [ + "access_token", + "authorization_header", + "cookie", + "email", + "query_string", + "raw_url", + "raw_user_id", + "refresh_token", + "request_body", + "response_body", + "stack_in_user_message", + "storage_value", + "user_name" + ], + "optionalAttributes": [], + "requiredAttributes": [ + "active_release_id", + "build_id", + "mismatch_kind" + ], + "sampling": "all", + "trigger": "release tuple mismatch" + }, + "telemetry.delivery.dropped": { + "delivery": "best-effort", + "eventName": "telemetry.delivery.dropped", + "forbiddenAttributes": [ + "access_token", + "authorization_header", + "cookie", + "email", + "query_string", + "raw_url", + "raw_user_id", + "refresh_token", + "request_body", + "response_body", + "stack_in_user_message", + "storage_value", + "user_name" + ], + "optionalAttributes": [], + "requiredAttributes": [ + "queue_size_bucket", + "reason" + ], + "sampling": "internal-counter", + "trigger": "queue or sink failure" + }, + "ui.render.failed": { + "delivery": "best-effort", + "eventName": "ui.render.failed", + "forbiddenAttributes": [ + "access_token", + "authorization_header", + "cookie", + "email", + "query_string", + "raw_url", + "raw_user_id", + "refresh_token", + "request_body", + "response_body", + "stack_in_user_message", + "storage_value", + "user_name" + ], + "optionalAttributes": [], + "requiredAttributes": [ + "build_id", + "component_boundary", + "route_id" + ], + "sampling": "all", + "trigger": "React boundary catch" + } + } + }, + { + "registryId": "FE-REG-RELEASE", + "owner": "feature-frontend-release-cache-rollback-contract", + "source": "src/contracts/release-tokens.js", + "rowCount": 8, + "contract": { + "requiredFields": [ + "token", + "source", + "compatibilityRole" + ], + "fieldTypes": { + "token": "string", + "source": "string", + "compatibilityRole": "string" + }, + "uniqueFields": [ + "token" + ], + "allowedValues": {}, + "references": [], + "keyField": "token", + "breakingFields": [ + "token", + "source", + "compatibilityRole" + ] + }, + "rows": { + "apiContractVersion": { + "compatibilityRole": "schema compatibility", + "source": "frontend/backend agreement", + "token": "apiContractVersion" + }, + "appVersion": { + "compatibilityRole": "human release label", + "source": "manifest", + "token": "appVersion" + }, + "assetManifestHash": { + "compatibilityRole": "chunk integrity and mismatch detection", + "source": "build output", + "token": "assetManifestHash" + }, + "buildId": { + "compatibilityRole": "asset and HTML coherence", + "source": "CI build", + "token": "buildId" + }, + "builtAt": { + "compatibilityRole": "diagnostics only; never cache identity", + "source": "CI", + "token": "builtAt" + }, + "commitSha": { + "compatibilityRole": "source traceability", + "source": "VCS", + "token": "commitSha" + }, + "configSchemaVersion": { + "compatibilityRole": "boot compatibility", + "source": "runtime config schema", + "token": "configSchemaVersion" + }, + "releaseId": { + "compatibilityRole": "rollback target", + "source": "deploy system", + "token": "releaseId" + } + } + } + ] +} diff --git a/config/contracts/registry-change-evidence.json b/config/contracts/registry-change-evidence.json new file mode 100644 index 0000000..62d2c3c --- /dev/null +++ b/config/contracts/registry-change-evidence.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": 1, + "changes": [] +} diff --git a/config/contracts/registry-governance.json b/config/contracts/registry-governance.json index 22990e2..52d7c14 100644 --- a/config/contracts/registry-governance.json +++ b/config/contracts/registry-governance.json @@ -1,13 +1,46 @@ { - "schemaVersion": 1, + "schemaVersion": 2, + "sourceDirectories": [ + "src/application", + "src/presentation", + "src/domain" + ], "registries": [ { "registryId": "FE-REG-ROUTE", "path": "src/features/installed-feature-contracts.js", "exportName": "ROUTE_REGISTRY", - "owner": "feature-routing-navigation-guard-contract", + "owner": "feature-frontend-routing-release-recovery-runtime", + "keyField": "routeId", + "requiredFields": [ + "routeId", + "path", + "paramsSchema", + "searchSchema", + "access", + "loadingSurface", + "errorSurface", + "chunkId", + "title", + "navigationLabel", + "navigationOrder" + ], + "fieldTypes": { + "routeId": "string", + "path": "string", + "paramsSchema": "string|null", + "searchSchema": "string|null", + "access": "string", + "loadingSurface": "string", + "errorSurface": "string", + "chunkId": "string", + "title": "string", + "navigationLabel": "string|null", + "navigationOrder": "integer|null" + }, "uniqueFields": ["routeId", "path", "chunkId"], "allowedValues": { + "access": ["public", "session-required", "integration-defined"], "paramsSchema": [null, "NotFoundSplat", "ReferenceResourceParams"], "searchSchema": [null, "ReferenceResourceListQuery"], "loadingSurface": [ @@ -23,17 +56,6 @@ "route-boundary", "feature-boundary", "not-found" - ], - "chunkId": [ - "route-home", - "route-examples-ui", - "route-examples-states", - "route-examples-auth", - "route-reference-resources", - "route-reference-resource-detail", - "route-reference-resource-form", - "route-reference-resource-status", - "route-not-found" ] }, "references": [ @@ -41,16 +63,30 @@ "field": "routeId", "registryId": "FE-REG-ROUTE-RUNTIME", "targetField": "routeId" + }, + { + "field": "paramsSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + }, + { + "field": "searchSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" } ], - "requiredFields": [ + "consumers": [ + { + "path": "src/presentation/routes/app-router.tsx", + "token": "ROUTE_REGISTRY" + } + ], + "breakingFields": [ "routeId", "path", "paramsSchema", "searchSchema", "access", - "loadingSurface", - "errorSurface", "chunkId" ] }, @@ -59,41 +95,56 @@ "path": "src/features/installed-feature-contracts.js", "exportName": "ROUTE_RUNTIME_CONTRACT", "owner": "feature-frontend-routing-release-recovery-runtime", + "keyField": "routeId", "requiredFields": [ "routeId", "moduleId", "paramsCodec", "searchCodec" ], - "uniqueFields": ["routeId", "moduleId"], - "allowedValues": { - "moduleId": [ - "home-page", - "ui-gallery-page", - "state-gallery-page", - "auth-example-page", - "reference-resource-page", - "reference-resource-detail-page", - "reference-resource-form-page", - "reference-resource-status-page", - "not-found-page" - ], - "paramsCodec": ["none", "NotFoundSplat", "ReferenceResourceParams"], - "searchCodec": ["none", "ReferenceResourceListQuery"] + "fieldTypes": { + "routeId": "string", + "moduleId": "string", + "paramsCodec": "string", + "searchCodec": "string" }, + "uniqueFields": ["routeId", "moduleId"], "references": [ { "field": "routeId", "registryId": "FE-REG-ROUTE", "targetField": "routeId" + }, + { + "field": "paramsCodec", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + }, + { + "field": "searchCodec", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" } + ], + "consumers": [ + { + "path": "src/presentation/routes/route-codecs.ts", + "token": "ROUTE_RUNTIME_CONTRACT" + } + ], + "breakingFields": [ + "routeId", + "moduleId", + "paramsCodec", + "searchCodec" ] }, { "registryId": "FE-REG-API", "path": "src/features/installed-feature-contracts.js", "exportName": "API_OPERATIONS", - "owner": "feature-api-client-response-envelope-contract", + "owner": "feature-frontend-api-client-response-envelope-contract", + "keyField": "operationId", "requiredFields": [ "method", "path", @@ -106,20 +157,127 @@ "requestSchema", "responseSchema", "owner" + ], + "fieldTypes": { + "method": "string", + "path": "string", + "operationId": "string", + "auth": "string", + "timeoutMs": "integer|null", + "idempotency": "string", + "retry": "string", + "requestSource": "string", + "requestSchema": "string", + "responseSchema": "string", + "owner": "string" + }, + "uniqueFields": ["operationId"], + "allowedValues": { + "method": ["GET", "POST", "PUT", "PATCH", "DELETE"], + "auth": ["none", "external-session"], + "idempotency": ["safe", "keyed", "none"], + "retry": ["runtime", "never"], + "requestSource": ["none", "search", "body"] + }, + "references": [ + { + "field": "requestSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + }, + { + "field": "responseSchema", + "registryId": "FE-REG-SCHEMA", + "targetField": "schemaId" + } + ], + "consumerIdentityField": "operationId", + "consumerDirectories": [ + "src/features/reference-feature/adapters", + "src/features/reference-feature/application" + ], + "breakingFields": [ + "method", + "path", + "operationId", + "auth", + "idempotency", + "requestSource", + "requestSchema", + "responseSchema" ] }, + { + "registryId": "FE-REG-SCHEMA", + "path": "src/features/installed-feature-contracts.js", + "exportName": "SCHEMA_REGISTRY", + "owner": "feature-frontend-contract-schema-registry", + "keyField": "schemaId", + "requiredFields": ["schemaId", "boundary", "owner", "runtime"], + "fieldTypes": { + "schemaId": "string", + "boundary": "string", + "owner": "string", + "runtime": "string" + }, + "uniqueFields": ["schemaId"], + "allowedValues": { + "boundary": [ + "route-params", + "route-search", + "route-search-api-request", + "api-request", + "api-response" + ], + "runtime": ["zod"] + }, + "consumerIdentityField": "schemaId", + "consumerDirectories": [ + "src/presentation/routes", + "src/features/reference-feature/presentation", + "src/features/reference-feature/contracts" + ], + "breakingFields": ["schemaId", "boundary", "runtime"] + }, { "registryId": "FE-REG-ENV", "path": "src/contracts/env.js", "exportName": "ENV_REGISTRY", "owner": "feature-frontend-env-runtime-config-contract", - "requiredFields": ["phase", "classification", "required", "defaultValue"] + "requiredFields": ["phase", "classification", "required", "defaultValue"], + "fieldTypes": { + "phase": "string", + "classification": "string", + "required": "boolean", + "defaultValue": "string|integer|boolean|null" + }, + "allowedValues": { + "phase": ["build", "runtime"], + "classification": [ + "public", + "public-sensitive", + "public-metadata", + "compile-time" + ] + }, + "consumers": [ + { + "path": "src/bootstrap/runtime-config-schema.js", + "token": "APP_ENV" + }, + { + "path": "src/contracts/env.js", + "token": "getBuildConfig" + } + ], + "breakingFields": ["phase", "classification", "required"] }, { "registryId": "FE-REG-STORAGE", "path": "src/contracts/storage-keys.js", "exportName": "STORAGE_REGISTRY", "owner": "feature-frontend-storage-registry-contract", + "keyField": "logicalName", "requiredFields": [ "logicalName", "physicalKey", @@ -129,6 +287,44 @@ "ttl", "migration", "quotaFallback" + ], + "fieldTypes": { + "logicalName": "string", + "physicalKey": "string", + "backend": "string", + "classification": "string", + "schemaVersion": "integer", + "ttl": "integer|string|null", + "migration": "string|function", + "quotaFallback": "string" + }, + "uniqueFields": ["logicalName", "physicalKey"], + "allowedValues": { + "backend": [ + "memory", + "sessionStorage", + "localStorage", + "indexedDB", + "disabled", + "forbidden" + ], + "classification": [ + "public-preference", + "opaque-cache", + "sensitive-forbidden" + ], + "quotaFallback": ["memory", "no-persist", "feature-disable"] + }, + "consumerIdentityField": "logicalName", + "consumerDirectories": ["src", "tests"], + "orphanExemptRows": ["QUERY_PERSISTENCE", "AUTH_TOKEN"], + "breakingFields": [ + "logicalName", + "physicalKey", + "backend", + "classification", + "schemaVersion", + "migration" ] }, { @@ -136,6 +332,7 @@ "path": "src/contracts/errors.js", "exportName": "ERROR_REGISTRY", "owner": "feature-frontend-error-classification-boundary-contract", + "keyField": "kind", "requiredFields": [ "kind", "defaultRetryable", @@ -144,13 +341,41 @@ "action", "telemetryEvent", "redaction" - ] + ], + "fieldTypes": { + "kind": "string", + "defaultRetryable": "boolean", + "severity": "string", + "userMessageKey": "string", + "action": "string", + "telemetryEvent": "string", + "redaction": "array" + }, + "uniqueFields": ["kind"], + "allowedValues": { + "severity": ["info", "warning", "error"], + "action": [ + "retry", + "reauth", + "navigate", + "reload-once", + "contact-support", + "none" + ] + }, + "consumers": [ + { + "path": "src/adapters/http/client.js", + "token": "failure(" + } + ], + "breakingFields": ["kind", "userMessageKey", "action", "telemetryEvent"] }, { "registryId": "FE-REG-QUERY", "path": "src/features/installed-feature-contracts.js", "exportName": "QUERY_REGISTRY", - "owner": "feature-server-state-caching-contract", + "owner": "feature-frontend-server-state-caching-contract", "requiredFields": [ "namespace", "serialization", @@ -158,13 +383,39 @@ "invalidation", "version", "persistence" + ], + "fieldTypes": { + "namespace": "array", + "serialization": "string", + "identity": "string", + "invalidation": "string", + "version": "integer", + "persistence": "string" + }, + "uniqueFields": ["namespace"], + "allowedValues": { + "persistence": ["disabled"] + }, + "consumers": [ + { + "path": "src/features/reference-feature/contracts/reference-feature-contract.js", + "token": "referenceQueryKeys" + } + ], + "breakingFields": [ + "namespace", + "serialization", + "identity", + "version", + "persistence" ] }, { "registryId": "FE-REG-TELEMETRY", "path": "src/contracts/telemetry.js", "exportName": "TELEMETRY_REGISTRY", - "owner": "feature-frontend-observability-logging-trace-contract", + "owner": "feature-frontend-diagnostics-telemetry-runtime", + "keyField": "eventName", "requiredFields": [ "eventName", "trigger", @@ -173,6 +424,31 @@ "forbiddenAttributes", "sampling", "delivery" + ], + "fieldTypes": { + "eventName": "string", + "trigger": "string", + "requiredAttributes": "array", + "optionalAttributes": "array", + "forbiddenAttributes": "array", + "sampling": "string", + "delivery": "string" + }, + "uniqueFields": ["eventName"], + "allowedValues": { + "delivery": ["best-effort"] + }, + "consumers": [ + { + "path": "scripts/check-diagnostics.mjs", + "token": "TELEMETRY_REGISTRY" + } + ], + "breakingFields": [ + "eventName", + "requiredAttributes", + "forbiddenAttributes", + "delivery" ] }, { @@ -180,11 +456,21 @@ "path": "src/contracts/release-tokens.js", "exportName": "RELEASE_TOKEN_REGISTRY", "owner": "feature-frontend-release-cache-rollback-contract", - "requiredFields": ["token", "source", "compatibilityRole"] + "keyField": "token", + "requiredFields": ["token", "source", "compatibilityRole"], + "fieldTypes": { + "token": "string", + "source": "string", + "compatibilityRole": "string" + }, + "uniqueFields": ["token"], + "consumers": [ + { + "path": "src/bootstrap/load-release-manifest.js", + "token": "assetManifestHash" + } + ], + "breakingFields": ["token", "source", "compatibilityRole"] } - ], - "compatibilityImpact": { - "allowed": ["none", "additive", "behavior-change", "breaking"], - "current": "behavior-change" - } + ] } diff --git a/config/testing/risk-coverage.json b/config/testing/risk-coverage.json new file mode 100644 index 0000000..27445cf --- /dev/null +++ b/config/testing/risk-coverage.json @@ -0,0 +1,92 @@ +{ + "schemaVersion": 1, + "summary": { + "lines": 80, + "statements": 78, + "functions": 85, + "branches": 68 + }, + "criticalModules": [ + { + "path": "src/adapters/http/retry-policy.js", + "minimum": { + "lines": 80, + "statements": 78, + "functions": 95, + "branches": 78 + } + }, + { + "path": "src/adapters/storage/browser-storage-adapter.js", + "minimum": { + "lines": 60, + "statements": 60, + "functions": 70, + "branches": 60 + } + }, + { + "path": "src/adapters/telemetry/best-effort-telemetry.js", + "minimum": { + "lines": 85, + "statements": 85, + "functions": 70, + "branches": 75 + } + }, + { + "path": "src/application/policies/compatibility.js", + "minimum": { + "lines": 95, + "statements": 95, + "functions": 95, + "branches": 75 + } + }, + { + "path": "src/application/policies/performance-budgets.js", + "minimum": { + "lines": 80, + "statements": 80, + "functions": 80, + "branches": 40 + } + }, + { + "path": "src/application/policies/promotion-readiness.js", + "minimum": { + "lines": 95, + "statements": 95, + "functions": 95, + "branches": 95 + } + }, + { + "path": "src/application/use-cases/decide-chunk-recovery.js", + "minimum": { + "lines": 90, + "statements": 90, + "functions": 95, + "branches": 85 + } + }, + { + "path": "src/contracts/diagnostics.ts", + "minimum": { + "lines": 68, + "statements": 68, + "functions": 95, + "branches": 58 + } + }, + { + "path": "scripts/lib/registry-compatibility.mjs", + "minimum": { + "lines": 80, + "statements": 80, + "functions": 85, + "branches": 60 + } + } + ] +} diff --git a/docs/architecture/decisions/VD-08-storybook-and-visual-evidence.md b/docs/architecture/decisions/VD-08-storybook-and-visual-evidence.md new file mode 100644 index 0000000..49a30d1 --- /dev/null +++ b/docs/architecture/decisions/VD-08-storybook-and-visual-evidence.md @@ -0,0 +1,71 @@ +# VD-08: 개발용 Storybook과 로컬 시각 회귀 증적 + +- 상태: Accepted +- 결정일: 2026-07-26 +- 적용 브랜치: `feature-frontend-test-registry-evidence-hardening` +- 재검토: 제품이 cloud visual review, 다중 OS baseline 또는 별도 디자인 시스템 + 배포를 요구할 때 + +## 배경 + +`/examples/ui`와 `/examples/states`는 실제 application composition 안에서 공통 +UI와 상태 표면을 보여 주지만, primitive를 격리해 interaction과 접근성을 검증하는 +workshop은 아니었다. 실패 시 screenshot도 디버깅 증거일 뿐 의도된 UI 기준선과 +현재 렌더의 차이를 차단하지 못했다. + +외부 visual review 서비스, 별도 Storybook 배포와 브랜드별 baseline은 아직 +선정되지 않았다. 이 결정을 기다리며 UI 회귀 검증을 비워 두거나 production +application bundle에 workshop runtime을 포함하는 것 모두 적절하지 않다. + +## 결정 + +1. Storybook은 development dependency와 별도 static artifact로만 사용한다. + production entry와 application `dist`에는 Storybook runtime, story 또는 + 테스트 selector를 포함하지 않는다. +2. story는 public design-system entry를 소비하고 실제 locale, theme, session, + router와 query provider 계약으로 렌더한다. production component를 복제한 + story 전용 구현을 만들지 않는다. +3. interaction과 story-level axe는 Playwright가 정적 Storybook을 대상으로 + 실행한다. unexpected console, page error와 request failure는 테스트 실패다. +4. 시각 회귀는 production `build` 후 `preview`를 대상으로 pinned Chromium, + locale, color scheme과 viewport에서 `toHaveScreenshot()`으로 실행한다. +5. 최초 기준선은 wide shell, compact pseudo-locale drawer, dark design-system + gallery, loading/empty/error/access 상태 표면을 포함한다. +6. animation과 caret만 결정적으로 비활성화한다. `html`, `body`, `main` 또는 + application 전체를 mask해 false PASS를 만드는 설정은 gate가 거절한다. +7. snapshot 갱신은 `test:visual:update`라는 명시적 명령으로 분리하고 PNG diff를 + review한다. 일반 `test:visual`은 승인 기준선을 변경하지 않는다. +8. local visual threshold는 작은 rasterization 차이만 허용하며 실제 layout, + copy, theme 또는 상태 변화가 숨겨지도록 확대하지 않는다. +9. `/examples/*`는 production composition smoke로 유지하고 Storybook story의 + 대체물로 취급하지 않는다. 반대로 Storybook만 통과해 application shell + integration을 완료 처리하지 않는다. +10. cloud service가 선정되지 않아도 repository-local workshop, interaction, + a11y와 visual baseline gate는 완전하게 실행 가능해야 한다. + +## 실행과 증적 + +- workshop config: `.storybook/main.ts`, `.storybook/preview.tsx` +- story: `src/presentation/design-system/design-system.stories.tsx` +- interaction/a11y: `tests/storybook/workshop.spec.ts` +- visual: `tests/visual/platform.visual.spec.ts` +- baseline: `tests/visual/__snapshots__/` +- production E2E: `playwright.config.js` +- local dev E2E: `playwright.dev.config.js` +- evidence policy: `scripts/check-test-evidence.mjs` + +CI는 JUnit, HTML report, failure trace/screenshot, visual baseline 존재 여부와 +금지된 full-screen mask/무소유 skip fixture를 함께 검사한다. + +## 한계와 재검토 조건 + +로컬 기준선은 실제 iOS/Android 기기, 여러 운영체제의 font rasterization, +디자인 승인 workflow와 다중 브랜드를 증명하지 않는다. 이를 요구하면 동일 +public component와 story를 입력으로 사용하는 외부 review adapter를 추가하되, +provider 결과가 없을 때 임의 PASS로 대체하지 않는다. + +## Rollback + +Storybook dependency/config, workshop test와 visual config/baseline은 production +runtime 변경 없이 독립적으로 제거할 수 있다. rollback 후에도 `/examples/*`, +component behavior, automated accessibility와 built-dist E2E는 유지한다. diff --git a/docs/architecture/frontend-platform-capability-review.md b/docs/architecture/frontend-platform-capability-review.md index 8669872..c9ff0a1 100644 --- a/docs/architecture/frontend-platform-capability-review.md +++ b/docs/architecture/frontend-platform-capability-review.md @@ -12,7 +12,7 @@ - 기본 번들에 포함할 역량과 필요할 때 설치할 확장 역량을 구분한다. - 특정 벤더를 채택하더라도 제품 코드가 벤더 API에 직접 결합되지 않는지 확인한다. -최초 검토 기준은 `develop`의 `cb195f8`이며, RP-01~RP-08 구현 결과를 이 문서에 +최초 검토 기준은 `develop`의 `cb195f8`이며, RP-01~RP-10 구현 결과를 이 문서에 누적 반영했다. 이후 구현으로 경로나 세부 내용이 달라질 수 있으므로, 각 항목은 문서의 경로뿐 아니라 해당 테스트와 아키텍처 게이트로 계속 검증해야 한다. @@ -39,15 +39,16 @@ recovery 계약, 제거 가능한 reference 수직 슬라이스, form/page, desi i18n 실행 경계와 diagnostics/telemetry production wiring은 구현됐다. 현재 선행 해결 대상은 다음과 같다. -1. registry evidence와 실제 compatibility diff -2. 공급망과 optional adapter recipe 심화 게이트 +1. 공급망의 transitive inventory/license/vulnerability/SBOM/provenance 검증 +2. optional adapter의 opt-in 경계와 제거 가능한 recipe 따라서 현재 상태를 “프론트 공통부가 모두 구현됐다”고 표현하면 범위가 과장된다. 더 정확한 표현은 다음과 같다. -> 운영·안전 계약과 범용 앱 셸은 갖춰졌지만, 기능 개발자가 사용하는 application -> API, 서버 상태, 폼, 라우팅, 페이지 패턴의 표준 수직 경로는 아직 보강이 -> 필요하다. +> application API, 서버 상태, 폼, 라우팅, 페이지, 디자인 시스템과 테스트 증적의 +> 표준 수직 경로는 갖춰졌다. 현재 남은 저장소 내부 범위는 공급망 검증과 +> opt-in adapter recipe이며 실제 hosting·IdP·운영 provider는 프로젝트 통합 +> 범위다. ## 3. 판정 기준 @@ -77,17 +78,17 @@ i18n 실행 경계와 diagnostics/telemetry production wiring은 구현됐다. | 앱 셸·반응형 | 준비됨 | native modal Drawer, compact/desktop layout, Escape/link dismiss/focus restore, pseudo reflow와 RTL direction | compact browser matrix 유지 | | 페이지 템플릿 | 준비됨 | Standard/Collection/Detail/Form/Status와 public design-system entry | feature별 slot 조합 유지 | | 디자인 토큰 | 준비됨 | primitive/semantic/component CSS, 48-token 자동 계약, dark/forced-colors/reduced-motion | 제품 brand token은 외부 프로젝트에서 확장 | -| 공통 UI | 준비됨 | action/form/feedback/overlay/navigation primitive와 pattern, compatibility export | Storybook/visual은 RP-10 | +| 공통 UI | 준비됨 | action/form/feedback/overlay/navigation primitive와 pattern, compatibility export | public story와 visual state matrix 유지 | | 아이콘 | 준비됨 | Lucide static vendor facade와 semantic icon/IconButton 접근성 계약 | 의미 icon 추가 시 bundle/접근성 기준 적용 | | 폼 | 준비됨 | Zod 기반 local facade, error summary/focus, 422 allowlist, dirty/pending/conflict 정책 | 복합 form 요구가 생기면 VD-04 조건으로 vendor adapter 평가 | | 국제화 | 준비됨 | 137-key typed catalog, locale provider, Intl formatter, safe fallback/alias, pseudo·RTL gate | 실제 locale·번역 승인은 프로젝트에서 연결 | | logging/diagnostics | 준비됨 | 별도 `DiagnosticsPort`, 8-event registry, allowlist, bounded/no-op adapter와 production producer | 실제 프로젝트의 remote sink는 port 뒤에서 선택 | | telemetry | 준비됨/프로젝트 선택 | 5-event registry, bounded queue, redaction/value policy, boot·HTTP·render·release·drop producer | analytics/RUM/error vendor와 consent는 프로젝트에서 선택 | | 비동기 상태 불변식 | 준비됨 | 배타적 typed overlay, stale latch, 실제 retry/conflict action | reference 화면에서 전체 상태 전시 | -| 단위·통합·E2E | 준비됨 | Vitest, RTL, MSW, Playwright 3엔진 | TS 테스트 검사, 실제 bootstrap 통합, 위험 시나리오 보강 | -| UI 회귀 검증 | 미제공 | axe/reflow는 있으나 visual baseline 없음 | Storybook 또는 동급 workshop과 시각 회귀 | -| 샘플 제거 | 준비됨 | feature/catalog/test 제거 후 type/architecture/registry/test/home/build 8단계 검증 | 새 contribution도 같은 제거 gate에 포함 | -| registry·compatibility 집행 | 부분 준비 | registry와 gate는 있으나 실제 before/after 및 orphan 검사가 제한적 | type/reference/orphan/diff/migration을 자동 검증 | +| 단위·통합·E2E | 준비됨 | source/test strict typecheck, shared MSW 19개 scenario, 실제 bootstrap, built-dist 3엔진·compact E2E | 제품별 critical flow를 같은 catalog/gate에 추가 | +| UI 회귀 검증 | 준비됨 | dev-only Storybook interaction/axe와 pinned Chromium visual baseline 4종 | cloud review와 다중 OS/device는 프로젝트 선택 | +| 샘플 제거 | 준비됨 | feature/catalog/test 제거 후 type/architecture/registry/test/home/build 9단계 검증 | 새 contribution도 같은 제거 gate에 포함 | +| registry·compatibility 집행 | 준비됨 | 10개 registry type/reference/consumer/orphan, 승인 digest와 actual semantic diff, breaking evidence | public 계약 변경 시 baseline review 유지 | | 공급망 검사 | 부분 준비 | lockfile·문서·gate는 있으나 실제 transitive 취약점/license/SBOM 깊이가 부족 | pinned scanner와 policy exception/증적 연결 | | realtime·offline·file 등 | 프로젝트 선택 | 현재 없음 | port/adapter recipe와 선택 기준 제공 | diff --git a/docs/architecture/frontend-platform-implementation-roadmap.md b/docs/architecture/frontend-platform-implementation-roadmap.md index bae45af..6a6c979 100644 --- a/docs/architecture/frontend-platform-implementation-roadmap.md +++ b/docs/architecture/frontend-platform-implementation-roadmap.md @@ -808,6 +808,35 @@ RP-10은 직전 승인 registry snapshot과 test evidence다. flaky visual/brows infrastructure commit은 product behavior와 분리한다. 장기 skip으로 PASS하지 않고 owner와 만료 시한이 있는 quarantine만 허용한다. +**구현 증거 (2026-07-26)** + +- VD-08에서 dev-only Storybook static workshop과 production build를 대상으로 한 + local Playwright visual baseline을 채택하고 cloud review는 선택 사항으로 + 분리했다. +- 10개 registry의 required field, runtime type, enum, unique, cross-reference, + consumer와 orphan을 검사하고 승인 snapshot digest와 현재 snapshot의 actual + semantic diff를 계산한다. 행·field·type·path뿐 아니라 registry 검증 계약 + 변경도 breaking evidence 대상이다. +- ordering-only 변경은 `none`, row addition은 `additive`, 일반 값 변경은 + `behavior-change`, 제거/type/path/contract 변경은 `breaking`으로 계산한다. + breaking에는 version, migration, compatibility window, rollback과 owner를 + 요구하며 digest 변조와 누락 fixture가 실제로 실패한다. +- API operation별 19개 shared MSW scenario catalog와 strict unhandled-request + server를 제공하고 reference vertical integration이 공통 envelope/handler를 + 사용한다. +- 기본 Playwright는 `build` + `preview`의 실제 `dist`를 Chromium, Firefox, + WebKit에서 검사하고 별도 compact project를 제공한다. 개발 피드백용 Vite + profile은 `playwright.dev.config.js`로 분리했다. +- Storybook public primitive story, interaction과 axe test, wide/compact/ + pseudo/dark/state surface의 pinned Chromium visual baseline 4종을 CI evidence로 + 연결했다. +- V8 coverage와 9개 high-risk module을 대상으로 40개 scoped threshold를 + 적용하고 threshold 미달 fixture를 차단한다. +- deterministic clock/random/scheduler/storage, unexpected console/page error/ + request failure 정책, 무소유 skip과 full-screen mask 금지 gate를 제공한다. +- JUnit, HTML report, trace/screenshot, coverage, registry와 fixture artifact를 + 기존 26개 blocking gate taxonomy에 연결했다. + ### 11. `feature-frontend-supply-chain-verification` **목표** diff --git a/docs/testing/frontend-platform-testing-strategy.md b/docs/testing/frontend-platform-testing-strategy.md index 1250d77..22787e0 100644 --- a/docs/testing/frontend-platform-testing-strategy.md +++ b/docs/testing/frontend-platform-testing-strategy.md @@ -43,7 +43,12 @@ - `test:unit` - `test:component` - `test:integration` +- `test:coverage` - `test:e2e` +- `test:e2e:dev` +- `build:storybook` +- `test:storybook` +- `test:visual` - `test:a11y` - `review:a11y-manual` - `test:sample-removal` @@ -67,7 +72,7 @@ field/documentation 단계를 구성한다. - 320px reflow와 mobile navigation을 E2E로 확인한다. - release build의 bundle과 lab performance budget이 별도 gate다. -### 2.3 확인된 공백 +### 2.3 RP-10에서 닫힌 공백과 남은 외부 범위 #### 테스트 TypeScript typecheck 기반 @@ -76,20 +81,12 @@ field/documentation 단계를 구성한다. 검사하되 실패를 의도한 `tests/fixtures`는 별도 negative command가 소유한다. Vitest의 변환 성공을 TypeScript typecheck의 대체물로 취급하지 않는다. -#### 실제 bootstrap integration test가 없다 +#### 실제 bootstrap integration -`tests/component/bootstrap-shell.test.jsx`는 production bootstrap을 import하지 -않고 테스트 내부의 ``만 렌더링한다. E2E는 실제 entry를 통과하지만, -다음 실패를 작은 통합 테스트에서 식별하기 어렵다. - -- runtime config fetch 실패 -- config/manifest mismatch -- adapter composition 실패 -- provider 순서 또는 누락 -- external auth owner 유무 -- product tree를 마운트하기 전 fail-closed -- boot error shell의 safe metadata -- StrictMode와 unmount cleanup +runtime config와 release manifest를 검증한 composition root에서 실제 provider +순서와 application input을 연결하는 component/integration test를 제공한다. +production Playwright profile은 source fixture가 아니라 `build` + `preview`의 +실제 entry와 hashed route chunk를 사용한다. #### TanStack Query의 React integration test 기반 @@ -100,53 +97,44 @@ optimistic commit/rollback, conflict 해제와 namespace invalidation이 실제 QueryClient 위에서 실행된다. HTTP 자동 retry가 소유자이므로 이 adapter의 query/mutation vendor retry는 꺼져 있다. -#### Form 테스트가 단일 TextField 흐름에 머문다 +#### Form과 route 위험 -현재 component/E2E는 label, description, error association과 빈 값 submit을 -검사한다. error summary, 첫 오류 focus, async validation race, 422 field error, -double submit, dirty navigation, mutation conflict는 없다. +form component/reference feature test가 error summary, 첫 오류 focus, Zod +transform, 422 allowlist, double submit, dirty navigation, optimistic rollback과 +conflict를 검증한다. route registry/runtime 양방향 참조, codec, lazy module, +location reset, scroll restoration, blocker와 bounded chunk recovery도 +unit/component/built artifact 검증에 연결됐다. -#### Route registry와 실행 tree가 별도로 테스트된다 +#### Storybook과 시각 회귀 -registry snapshot과 일부 navigation/access policy test는 있으나 다음 계약을 -강제하지 않는다. +public design-system primitive를 실제 platform provider로 렌더하는 dev-only +Storybook, interaction/axe test와 production build를 대상으로 한 +`toHaveScreenshot()` baseline 4종을 제공한다. cloud review, 다중 OS font +rasterization과 실제 device farm은 프로젝트가 요구할 때 연결한다. -- 모든 route ID에 lazy runtime module이 존재하는가 -- params/search가 실제 codec으로 검증되는가 -- deep link와 basename refresh가 동작하는가 -- chunk load failure가 1회 reload/support surface로 연결되는가 -- route error boundary가 location 변경 시 reset되는가 -- scroll restoration과 form navigation blocker가 동작하는가 +#### Shared MSW와 결정성 -#### Storybook과 시각 회귀가 없다 +operation별 success/empty/slow/network/timeout/content/envelope/schema/auth/ +403/404/409/422/429/retry/terminal을 포함한 19개 scenario catalog와 strict +unhandled-request server를 공유한다. clock/random/scheduler/storage helper와 +browser console/page/network failure 정책으로 비결정적 우회를 차단한다. -`/examples/ui`는 통합 gallery지만 component별 모든 state를 격리하지 않는다. -Storybook story, interaction story, story-level axe, `toHaveScreenshot()` baseline이 -없다. `screenshot: "only-on-failure"`는 디버깅 증거이며 시각 회귀 테스트가 아니다. +#### Built-dist와 compact E2E -#### MSW scenario가 공유되지 않는다 +기본 `test:e2e`는 CI에서 기존 server를 재사용하지 않고 `build` + `preview`를 +Chromium, Firefox, WebKit과 compact project로 실행한다. 빠른 Vite 개발 profile은 +`test:e2e:dev`로 분리한다. -integration file마다 `setupServer`, handler, response body를 다시 정의한다. -Node integration, Storybook browser, feature component test, E2E mock service가 같은 -시나리오 이름과 contract fixture를 공유하지 않는다. +#### 위험 기반 coverage -#### E2E가 개발 서버를 대상으로 한다 +V8 text/JSON/LCOV를 생성하고 전체 기준과 retry/storage/telemetry/compatibility/ +performance/promotion/chunk/diagnostics/registry compatibility 9개 high-risk +module에 40개 scoped threshold를 적용한다. critical module 누락 또는 threshold +미달 fixture는 merge gate를 실패시킨다. -현재 Playwright web server는 `pnpm dev`다. route behavior 확인에는 유효하지만 -다음 release 위험은 production build/preview에서만 확인할 수 있다. - -- hashed lazy chunk -- source 변환과 tree shaking -- base path -- deep-link fallback -- build-time environment -- release manifest와 runtime config 조합 -- minified code의 chunk failure - -#### Coverage가 실행·차단되지 않는다 - -`vitest.config.js`에는 reporter만 선언되어 있고 coverage provider, script, -threshold, diff policy가 없다. +남은 범위는 실제 device/browser farm, cloud visual approval, 외부 인증·telemetry +provider와 production field data다. 이 증거가 없을 때 저장소 내부 test를 +`PRODUCTION_READY`의 대체물로 사용하지 않는다. ## 3. 위험 기반 테스트 계층 @@ -1228,8 +1216,9 @@ corepack pnpm build corepack pnpm check:bundle ``` -TypeScript test, Storybook, coverage, visual, built-dist 명령이 도입되면 위 목록과 -CI registry에 추가한다. +TypeScript test, Storybook, coverage, visual과 built-dist 명령은 +`config/ci/gates.json`의 blocking step과 JUnit/HTML/trace/fixture evidence에 +연결되어 있다. ## 18. Feature Definition of Done diff --git a/eslint.config.js b/eslint.config.js index 0226cf8..bf1aca3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -293,6 +293,12 @@ export default [ }, }, }, + { + files: [`tests/support/browser/**/*.${sourceExtensions}`], + rules: { + "react-hooks/rules-of-hooks": "off", + }, + }, { files: [ `tests/fixtures/architecture/forbidden/**/*.${sourceExtensions}`, diff --git a/package.json b/package.json index 9844fcb..e5c758a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build": "vite build && node scripts/generate-build-manifest.mjs", "build:release": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security", "preview": "vite preview", - "lint": "eslint src scripts tests vite.config.js vitest.config.js playwright.config.js --max-warnings=0", + "lint": "eslint src scripts tests .storybook vite.config.js vitest.config.js playwright*.config.js --max-warnings=0", "check:architecture": "node scripts/check-architecture.mjs", "check:design-system": "node scripts/check-design-system.mjs", "check:design-system:fixture": "node scripts/check-design-system.mjs --fixture", @@ -42,16 +42,29 @@ "test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml", "test:integration": "vitest run tests/integration --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/integration.xml", "test:e2e": "playwright test", + "test:e2e:dev": "playwright test --config playwright.dev.config.js", + "storybook": "storybook dev -p 6006", + "build:storybook": "storybook build -o artifacts/storybook/static", + "test:storybook": "playwright test --config playwright.storybook.config.js", + "test:visual": "playwright test --config playwright.visual.config.js", + "test:visual:update": "playwright test --config playwright.visual.config.js --update-snapshots", + "check:test-evidence": "node scripts/check-test-evidence.mjs", + "check:test-evidence:fixture": "node scripts/check-test-evidence.mjs --source-root tests/fixtures/test-evidence/forbidden --artifact artifacts/quality/test-evidence-fixture.json", "test:a11y": "playwright test --grep @a11y && node scripts/write-a11y-report.mjs", "review:a11y-manual": "node scripts/verify-a11y-manual.mjs", "test:sample-removal": "node scripts/test-sample-removal.mjs", "test:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests", + "test:coverage": "vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.mjs", + "check:coverage:fixture": "node scripts/check-risk-coverage.mjs --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json", "test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature", "verify:lockfile": "corepack pnpm install --frozen-lockfile", "generate:supply-chain": "node scripts/generate-supply-chain.mjs", "scan:security": "node scripts/security-scan.mjs", "check:browser-security": "node scripts/check-browser-security.mjs", "check:registries": "node scripts/check-registries.mjs", + "check:registries:structure": "node scripts/check-registries.mjs --no-baseline", + "check:registries:compatibility-fixtures": "node scripts/check-registry-compatibility-fixtures.mjs", + "check:registries:baseline-fixture": "node scripts/check-registries.mjs --approval tests/fixtures/registry/compatibility/tampered-approval.json --artifact artifacts/quality/registry-baseline-fixture.json", "check:registries:fixture": "node scripts/check-registries.mjs --governance tests/fixtures/registry/forbidden/governance.json --artifact artifacts/quality/registry-fixture.json", "check:routes:fixture": "node scripts/check-registries.mjs --governance tests/fixtures/registry/routes/governance.json --artifact artifacts/quality/route-registry-fixture.json", "verify:compatibility": "node scripts/check-compatibility.mjs", @@ -82,6 +95,8 @@ "@babel/plugin-syntax-typescript": "8.0.3", "@eslint/js": "10.0.1", "@playwright/test": "1.62.0", + "@storybook/addon-a11y": "10.5.4", + "@storybook/react-vite": "10.5.4", "@tailwindcss/vite": "4.3.3", "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", @@ -90,12 +105,14 @@ "@types/react": "19.2.8", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.4", + "@vitest/coverage-v8": "4.1.10", "dependency-cruiser": "18.1.0", "eslint": "10.8.0", "eslint-plugin-react-hooks": "7.1.1", "globals": "17.7.0", "jsdom": "29.1.1", "msw": "2.15.0", + "storybook": "10.5.4", "tailwindcss": "4.3.3", "typescript": "7.0.2", "vite": "8.1.5", diff --git a/playwright.config.js b/playwright.config.js index b87fd86..0be5b51 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -6,20 +6,44 @@ export default defineConfig({ reporter: [ ["list"], ["html", { outputFolder: "./artifacts/tests/e2e/report", open: "never" }], + ["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }], ], use: { - baseURL: "http://127.0.0.1:5173", + baseURL: "http://127.0.0.1:4173", trace: "retain-on-failure", screenshot: "only-on-failure", }, webServer: { - command: "corepack pnpm dev --host 127.0.0.1", - url: "http://127.0.0.1:5173", - reuseExistingServer: !process.env.CI, + command: + "corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4173", + url: "http://127.0.0.1:4173", + reuseExistingServer: false, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, - { name: "webkit", use: { ...devices["Desktop Safari"] } }, + { + name: "chromium", + testIgnore: "**/compact-smoke.spec.js", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: "**/compact-smoke.spec.js", + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: "**/compact-smoke.spec.js", + use: { ...devices["Desktop Safari"] }, + }, + { + name: "chromium-compact", + testMatch: "**/compact-smoke.spec.js", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 390, height: 844 }, + hasTouch: true, + isMobile: true, + }, + }, ], }); diff --git a/playwright.dev.config.js b/playwright.dev.config.js new file mode 100644 index 0000000..ddb8fb7 --- /dev/null +++ b/playwright.dev.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from "@playwright/test"; + +import releaseConfig from "./playwright.config.js"; + +export default defineConfig({ + ...releaseConfig, + use: { + ...releaseConfig.use, + baseURL: "http://127.0.0.1:5173", + }, + webServer: { + command: "corepack pnpm dev --host 127.0.0.1", + url: "http://127.0.0.1:5173", + reuseExistingServer: true, + }, +}); diff --git a/playwright.storybook.config.js b/playwright.storybook.config.js new file mode 100644 index 0000000..62d2ace --- /dev/null +++ b/playwright.storybook.config.js @@ -0,0 +1,34 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/storybook", + outputDir: "./artifacts/tests/storybook/results", + reporter: [ + ["list"], + [ + "html", + { + outputFolder: "./artifacts/tests/storybook/report", + open: "never", + }, + ], + [ + "junit", + { outputFile: "./artifacts/tests/storybook/results.xml" }, + ], + ], + use: { + baseURL: "http://127.0.0.1:6006", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + webServer: { + command: + "corepack pnpm build:storybook && node scripts/serve-static.mjs artifacts/storybook/static 6006", + url: "http://127.0.0.1:6006", + reuseExistingServer: false, + }, + projects: [ + { name: "chromium-workshop", use: { ...devices["Desktop Chrome"] } }, + ], +}); diff --git a/playwright.visual.config.js b/playwright.visual.config.js new file mode 100644 index 0000000..f66f996 --- /dev/null +++ b/playwright.visual.config.js @@ -0,0 +1,37 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/visual", + snapshotDir: "./tests/visual/__snapshots__", + outputDir: "./artifacts/tests/visual/results", + reporter: [ + ["list"], + [ + "html", + { outputFolder: "./artifacts/tests/visual/report", open: "never" }, + ], + ["junit", { outputFile: "./artifacts/tests/visual/results.xml" }], + ], + expect: { + toHaveScreenshot: { + animations: "disabled", + caret: "hide", + maxDiffPixelRatio: 0.002, + scale: "css", + }, + }, + use: { + ...devices["Desktop Chrome"], + baseURL: "http://127.0.0.1:4174", + colorScheme: "light", + locale: "en-US", + trace: "retain-on-failure", + }, + webServer: { + command: + "corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4174", + url: "http://127.0.0.1:4174", + reuseExistingServer: false, + }, + projects: [{ name: "chromium-visual" }], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c23938d..ddd157b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,9 +48,15 @@ importers: '@playwright/test': specifier: 1.62.0 version: 1.62.0 + '@storybook/addon-a11y': + specifier: 10.5.4 + version: 10.5.4(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8)) + '@storybook/react-vite': + specifier: 10.5.4 + version: 10.5.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(supports-color@7.2.0)(typescript@7.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) '@tailwindcss/vite': specifier: 4.3.3 - version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) '@testing-library/jest-dom': specifier: 7.0.0 version: 7.0.0(@testing-library/dom@10.4.1) @@ -71,7 +77,10 @@ importers: version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-react': specifier: 6.0.4 - version: 6.0.4(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + version: 6.0.4(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@vitest/coverage-v8': + specifier: 4.1.10 + version: 4.1.10(vitest@4.1.10) dependency-cruiser: specifier: 18.1.0 version: 18.1.0 @@ -90,6 +99,9 @@ importers: msw: specifier: 2.15.0 version: 2.15.0(@types/node@24.13.3)(typescript@7.0.2) + storybook: + specifier: 10.5.4 + version: 10.5.4(@types/react@19.2.8)(react@19.2.8) tailwindcss: specifier: 4.3.3 version: 4.3.3 @@ -98,10 +110,10 @@ importers: version: 7.0.2 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.13.3)(jsdom@29.1.1)(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) packages: @@ -281,6 +293,10 @@ packages: resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} engines: {node: ^22.18.0 || >=24.11.0} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -324,12 +340,183 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -433,6 +620,15 @@ packages: '@types/node': optional: true + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -471,9 +667,242 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] + '@playwright/test@1.62.0': resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} engines: {node: '>=20'} @@ -577,9 +1006,98 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@storybook/addon-a11y@10.5.4': + resolution: {integrity: sha512-UWdZtB7Dh1GjqxTPOrisUNDhDGF5pKGzZdzW9DSSHMBcAOx2dsTmXGzLSFprAz4LTT0OHCtKhxNqKK0JZJ0Y8g==} + peerDependencies: + storybook: ^10.5.4 + + '@storybook/builder-vite@10.5.4': + resolution: {integrity: sha512-eXdgow+brSzZrG3CnG18YwxZwEYNAZIh2G5qKwNoZf0uk2ZCPgLRQwtVAcd7BiiEDGnXUHm9pycAS+UiF4F4Mg==} + peerDependencies: + storybook: ^10.5.4 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/csf-plugin@10.5.4': + resolution: {integrity: sha512-DSp5Z/eZlRnKq0KrKLJE6uoYf/Ysc+FP0Z5DVTGnOrie+z3tC0lNi9I4RB++EXkJeUDS9/4dxvJZVSWjhLlXxw==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.5.4 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.1.0': + resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/react-dom-shim@10.5.4': + resolution: {integrity: sha512-YdlppEOReg8MvTECRNuf79gu2zL83JqKDHIR/65eS0M6y+ue9pkpfjYo7hZVIcyOcRd9npBDXMdt2kC92bCuaA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.4 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@storybook/react-vite@10.5.4': + resolution: {integrity: sha512-iw7EAA98n30vpf+ZSy2Ll4Ne7oyZ/lS6W22u5Sp5UOI2oAkYuklxIWjRIKGqpY0BHP+GqaGYtUGMZQnBqgul2g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.4 + typescript: '>= 4.9.x' + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/react@10.5.4': + resolution: {integrity: sha512-tOxfVgbYcaVsArN8XTDkJfdsnsnHh1LxjRHVpJ/N+VEkz4FveK/XH3jOLV0YqgrG8yXza7+CteDP4FfPVQY/mw==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.4 + typescript: '>= 4.9.x' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + typescript: + optional: true + '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -686,6 +1204,13 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + '@testing-library/jest-dom@7.0.0': resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} engines: {node: '>=22', npm: '>=6', yarn: '>=1'} @@ -719,12 +1244,27 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -751,6 +1291,9 @@ packages: '@types/react@19.2.8': resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -890,6 +1433,18 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.10': resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} @@ -904,6 +1459,9 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.10': resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} @@ -913,12 +1471,21 @@ packages: '@vitest/snapshot@4.1.10': resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.10': resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + acorn-jsx-walk@2.0.0: resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==} @@ -966,6 +1533,13 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + axe-core@4.12.1: resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} @@ -991,9 +1565,17 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1002,6 +1584,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1058,9 +1644,25 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dependency-cruiser@18.1.0: resolution: {integrity: sha512-pbsH0gQ15BxXi97SCuMeVgsh8VzYpziGIVaMdnALbVu+TYjSZrW9/nOvsPU+NjHLmJSF9R1UijjoACq6Ne/ybQ==} engines: {node: ^22||^24||>=26} @@ -1074,6 +1676,10 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -1105,6 +1711,11 @@ packages: es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1145,6 +1756,11 @@ packages: resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -1157,6 +1773,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1235,6 +1854,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -1271,6 +1894,9 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1302,6 +1928,11 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1314,6 +1945,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-installed-globally@1.0.0: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} engines: {node: '>=18'} @@ -1328,9 +1964,25 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1369,6 +2021,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1532,6 +2187,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -1551,6 +2209,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} @@ -1565,6 +2230,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1598,6 +2267,10 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1605,6 +2278,13 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1627,12 +2307,20 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1670,6 +2358,15 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: @@ -1699,6 +2396,10 @@ packages: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + recast@0.23.12: + resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} + engines: {node: '>= 4'} + rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} @@ -1732,6 +2433,10 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + safe-regex@2.1.1: resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} @@ -1779,6 +2484,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1789,6 +2498,21 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + storybook@10.5.4: + resolution: {integrity: sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==} + hasBin: true + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 || ^0.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + prettier: + optional: true + vite-plus: + optional: true + strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -1808,6 +2532,10 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1830,6 +2558,9 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1841,10 +2572,18 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tldts-core@7.4.9: resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} @@ -1860,6 +2599,10 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + tsconfig-paths-webpack-plugin@4.2.0: resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} engines: {node: '>=10.13.0'} @@ -1891,6 +2634,10 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -1903,6 +2650,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite@8.1.5: resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2000,6 +2752,9 @@ packages: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@5.0.0: resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} engines: {node: '>=20'} @@ -2026,6 +2781,22 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -2295,6 +3066,8 @@ snapshots: '@babel/helper-string-parser': 8.0.0 '@babel/helper-validator-identifier': 8.0.4 + '@bcoe/v8-coverage@1.0.2': {} + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 @@ -2329,16 +3102,121 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': dependencies: eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) @@ -2418,6 +3296,14 @@ snapshots: optionalDependencies: '@types/node': 24.13.3 + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@7.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@7.0.2) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + optionalDependencies: + typescript: 7.0.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2453,6 +3339,20 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {} @@ -2464,8 +3364,135 @@ snapshots: '@open-draft/until@2.1.0': {} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.139.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + '@playwright/test@1.62.0': dependencies: playwright: 1.62.0 @@ -2521,8 +3548,95 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/pluginutils@5.4.0': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + '@standard-schema/spec@1.1.0': {} + '@storybook/addon-a11y@10.5.4(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))': + dependencies: + '@storybook/global': 5.0.0 + axe-core: 4.12.1 + storybook: 10.5.4(@types/react@19.2.8)(react@19.2.8) + + '@storybook/builder-vite@10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@storybook/csf-plugin': 10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + storybook: 10.5.4(@types/react@19.2.8)(react@19.2.8) + ts-dedent: 2.3.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + storybook: 10.5.4(@types/react@19.2.8)(react@19.2.8) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.1 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + '@storybook/global@5.0.0': {} + + '@storybook/icons@2.1.0(react@19.2.8)': + dependencies: + react: 19.2.8 + + '@storybook/react-dom-shim@10.5.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))': + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + storybook: 10.5.4(@types/react@19.2.8)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@storybook/react-vite@10.5.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(supports-color@7.2.0)(typescript@7.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@7.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@rollup/pluginutils': 5.4.0 + '@storybook/builder-vite': 10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@storybook/react': 10.5.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(supports-color@7.2.0)(typescript@7.0.2) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 19.2.8 + react-docgen: 8.0.3(supports-color@7.2.0) + react-dom: 19.2.8(react@19.2.8) + resolve: 1.22.12 + storybook: 10.5.4(@types/react@19.2.8)(react@19.2.8) + tsconfig-paths: 4.2.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - webpack + + '@storybook/react@10.5.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8))(supports-color@7.2.0)(typescript@7.0.2)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.5.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.8)(react@19.2.8)) + react: 19.2.8 + react-docgen: 8.0.3(supports-color@7.2.0) + react-docgen-typescript: 2.4.0(typescript@7.0.2) + react-dom: 19.2.8(react@19.2.8) + storybook: 10.5.4(@types/react@19.2.8)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + typescript: 7.0.2 + transitivePeerDependencies: + - supports-color + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2584,12 +3698,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) '@tanstack/query-core@5.101.4': {} @@ -2609,6 +3723,16 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.1)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': dependencies: '@adobe/css-tools': 4.5.0 @@ -2640,6 +3764,27 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2647,6 +3792,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -2669,6 +3816,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/resolve@1.20.6': {} + '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 24.13.3 @@ -2735,10 +3884,32 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 '@vitest/expect@4.1.10': dependencies: @@ -2749,14 +3920,18 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.15.0(@types/node@24.13.3)(typescript@7.0.2) - vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 '@vitest/pretty-format@4.1.10': dependencies: @@ -2774,14 +3949,26 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@4.1.10': {} + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@webcontainer/env@1.1.1': {} + acorn-jsx-walk@2.0.0: {} acorn-jsx@5.3.2(acorn@8.17.0): @@ -2821,6 +4008,16 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + axe-core@4.12.1: {} balanced-match@4.0.4: {} @@ -2843,8 +4040,20 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + caniuse-lite@1.0.30001806: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} chalk@4.1.2: @@ -2852,6 +4061,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + cli-width@4.1.0: {} cliui@8.0.1: @@ -2902,8 +4113,19 @@ snapshots: decimal.js@10.6.0: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + dependency-cruiser@18.1.0: dependencies: acorn: 8.17.0 @@ -2929,6 +4151,10 @@ snapshots: detect-libc@2.1.2: {} + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -2950,6 +4176,35 @@ snapshots: es-module-lexer@2.3.1: {} + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -3019,6 +4274,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -3029,6 +4286,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -3089,6 +4348,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -3122,6 +4387,8 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + html-escaper@2.0.2: {} + ignore@5.3.2: {} ignore@7.0.6: {} @@ -3140,6 +4407,8 @@ snapshots: dependencies: hasown: 2.0.4 + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3148,6 +4417,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-installed-globally@1.0.0: dependencies: global-directory: 4.0.1 @@ -3159,8 +4432,25 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jiti@2.7.0: {} js-tokens@10.0.0: {} @@ -3203,6 +4493,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3316,6 +4608,8 @@ snapshots: dependencies: p-locate: 5.0.0 + loupe@3.2.1: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -3332,6 +4626,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + mdn-data@2.27.1: {} min-indent@1.0.1: {} @@ -3342,6 +4646,8 @@ snapshots: minimist@1.2.8: {} + minipass@7.1.3: {} + ms@2.1.3: {} msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2): @@ -3379,6 +4685,13 @@ snapshots: obug@2.1.4: {} + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3390,6 +4703,53 @@ snapshots: outvariant@1.4.3: {} + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -3408,10 +4768,17 @@ snapshots: path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.5: {} @@ -3445,6 +4812,25 @@ snapshots: punycode@2.3.1: {} + react-docgen-typescript@2.4.0(typescript@7.0.2): + dependencies: + typescript: 7.0.2 + + react-docgen@8.0.3(supports-color@7.2.0): + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/types': 7.29.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 @@ -3468,6 +4854,14 @@ snapshots: react@19.2.8: {} + recast@0.23.12: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + rechoir@0.8.0: dependencies: resolve: 1.22.12 @@ -3513,6 +4907,8 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 + run-applescript@7.1.0: {} + safe-regex@2.1.1: dependencies: regexp-tree: 0.1.27 @@ -3545,12 +4941,40 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + stackback@0.0.2: {} statuses@2.0.2: {} std-env@4.2.0: {} + storybook@10.5.4(@types/react@19.2.8)(react@19.2.8): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@19.2.8) + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 6.10.0(@testing-library/dom@10.4.1) + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.28.1 + jsonc-parser: 3.3.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.24.2 + recast: 0.23.12 + semver: 7.8.5 + use-sync-external-store: 1.6.0(react@19.2.8) + ws: 8.21.1 + optionalDependencies: + '@types/react': 19.2.8 + transitivePeerDependencies: + - bufferutil + - react + - utf-8-validate + strict-event-emitter@0.5.1: {} string-width@4.2.3: @@ -3569,6 +4993,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-indent@4.1.1: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3583,6 +5009,8 @@ snapshots: tapable@2.3.3: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -3592,8 +5020,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinyrainbow@2.0.0: {} + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: {} + tldts-core@7.4.9: {} tldts@7.4.9: @@ -3608,6 +5040,8 @@ snapshots: dependencies: punycode: 2.3.1 + ts-dedent@2.3.0: {} + tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 @@ -3621,8 +5055,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: @@ -3659,6 +5092,13 @@ snapshots: undici@7.28.0: {} + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.17.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + until-async@3.0.2: {} update-browserslist-db@1.2.3(browserslist@4.28.7): @@ -3671,7 +5111,11 @@ snapshots: dependencies: punycode: 2.3.1 - vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0): + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -3680,13 +5124,14 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.3 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.10(@types/node@24.13.3)(jsdom@29.1.1)(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)): + vitest@4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -3703,10 +5148,11 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) jsdom: 29.1.1 transitivePeerDependencies: - msw @@ -3719,6 +5165,8 @@ snapshots: webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@5.0.0: {} whatwg-url@16.0.1: @@ -3746,6 +5194,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + ws@8.21.1: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4c5d8b7..49bbf51 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ allowBuilds: + esbuild: true msw: true minimumReleaseAgeExclude: - '@playwright/test@1.62.0' diff --git a/schemas/artifacts/registry-snapshot.schema.json b/schemas/artifacts/registry-snapshot.schema.json index 8a948f8..e62f38f 100644 --- a/schemas/artifacts/registry-snapshot.schema.json +++ b/schemas/artifacts/registry-snapshot.schema.json @@ -4,24 +4,49 @@ "required": [ "schemaVersion", "generatedAt", - "compatibilityImpact", + "baselineDigest", + "currentDigest", + "compatibility", "failures", "registries" ], "properties": { - "schemaVersion": { "const": 1 }, + "schemaVersion": { "const": 2 }, "generatedAt": { "type": "string", "format": "date-time" }, - "compatibilityImpact": { - "enum": ["none", "additive", "behavior-change", "breaking"] + "baselineDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "currentDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "compatibility": { + "type": "object", + "required": ["impact", "changes"], + "properties": { + "impact": { + "enum": ["none", "additive", "behavior-change", "breaking"] + }, + "changes": { "type": "array" } + }, + "additionalProperties": false }, "failures": { "type": "array", "maxItems": 0 }, "registries": { "type": "array", - "minItems": 8, - "maxItems": 8, + "minItems": 10, + "maxItems": 10, "items": { "type": "object", - "required": ["registryId", "owner", "source", "rowCount", "rows"] + "required": [ + "registryId", + "owner", + "source", + "rowCount", + "contract", + "rows" + ] } } }, diff --git a/scripts/check-registries.mjs b/scripts/check-registries.mjs index 7586c6d..f545d6c 100644 --- a/scripts/check-registries.mjs +++ b/scripts/check-registries.mjs @@ -1,28 +1,65 @@ -import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { + access, + mkdir, + readFile, + readdir, + writeFile, +} from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; -/** @param {string} name @param {string} fallback */ +import { + canonicalizeRegistryValue, + diffRegistrySnapshots, + registrySnapshotDigest, + validateBreakingEvidence, + verifyRegistryBaselineApproval, +} from "./lib/registry-compatibility.mjs"; + +/** @param {string} name @param {string | undefined} fallback */ function argumentValue(name, fallback) { const index = process.argv.indexOf(name); - return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback; + return index >= 0 && process.argv[index + 1] + ? process.argv[index + 1] + : fallback; } -const governancePath = argumentValue( - "--governance", - "config/contracts/registry-governance.json", +const defaultGovernancePath = "config/contracts/registry-governance.json"; +const governancePath = + /** @type {string} */ ( + argumentValue("--governance", defaultGovernancePath) + ); +const artifactPath = + /** @type {string} */ ( + argumentValue("--artifact", "artifacts/quality/registries.json") + ); +const usesRepositoryBaseline = + governancePath === defaultGovernancePath && + !process.argv.includes("--no-baseline"); +const baselinePath = argumentValue( + "--baseline", + usesRepositoryBaseline + ? "config/contracts/registry-baseline.json" + : undefined, ); -const artifactPath = argumentValue( - "--artifact", - "artifacts/quality/registries.json", +const approvalPath = argumentValue( + "--approval", + usesRepositoryBaseline + ? "config/contracts/registry-baseline.approval.json" + : undefined, ); -const governance = JSON.parse( - await readFile(governancePath, "utf8"), +const evidencePath = argumentValue( + "--compatibility-evidence", + usesRepositoryBaseline + ? "config/contracts/registry-change-evidence.json" + : undefined, ); +const governance = JSON.parse(await readFile(governancePath, "utf8")); const failures = []; const owners = new Map(); const snapshots = []; const rowsByRegistry = new Map(); +const sourcesByRegistry = new Map(); const registryExtensions = [".js", ".jsx", ".mjs", ".ts", ".tsx", ".mts"]; /** @param {string} declaredPath */ @@ -38,7 +75,7 @@ async function resolveRegistrySource(declaredPath) { await access(candidate); candidates.push(candidate); } catch { - // A migration may legitimately replace the declared extension. + // A TypeScript migration may replace the declared extension. } } if (candidates.length > 1) { @@ -50,6 +87,44 @@ async function resolveRegistrySource(declaredPath) { return candidates[0] ?? null; } +/** @param {unknown} value */ +function runtimeType(value) { + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + if (Number.isInteger(value)) return "integer"; + return typeof value; +} + +/** @param {unknown} value @param {string} declaration */ +function matchesDeclaredType(value, declaration) { + const actual = runtimeType(value); + return declaration + .split("|") + .some( + (candidate) => + candidate === actual || + (candidate === "number" && actual === "integer"), + ); +} + +/** @param {string} directory @returns {Promise} */ +async function filesBelow(directory) { + try { + const entries = await readdir(directory, { withFileTypes: true }); + const groups = await Promise.all( + entries.map((entry) => { + const target = path.join(directory, entry.name); + return entry.isDirectory() ? filesBelow(target) : [target]; + }), + ); + return groups.flat().filter((file) => + /\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(file), + ); + } catch { + return []; + } +} + for (const specification of governance.registries) { if (owners.has(specification.registryId)) { failures.push(`duplicate owner for ${specification.registryId}`); @@ -74,6 +149,10 @@ for (const specification of governance.registries) { } rowsByRegistry.set(specification.registryId, rows); + sourcesByRegistry.set( + specification.registryId, + sourcePath ?? specification.path, + ); for (const [rowName, row] of Object.entries(rows)) { if (!row || typeof row !== "object" || Array.isArray(row)) { @@ -85,6 +164,26 @@ for (const specification of governance.registries) { failures.push(`${specification.registryId}.${rowName} missing ${field}`); } } + for (const [field, declaredType] of Object.entries( + specification.fieldTypes ?? {}, + )) { + if ( + field in row && + !matchesDeclaredType(row[field], String(declaredType)) + ) { + failures.push( + `${specification.registryId}.${rowName}.${field} expected ${declaredType}, received ${runtimeType(row[field])}`, + ); + } + } + if ( + specification.keyField && + row[specification.keyField] !== rowName + ) { + failures.push( + `${specification.registryId}.${rowName}.${specification.keyField} must match its registry key`, + ); + } } for (const field of specification.uniqueFields ?? []) { @@ -93,12 +192,13 @@ for (const specification of governance.registries) { if (!row || typeof row !== "object" || Array.isArray(row)) continue; const value = row[field]; if (value === undefined) continue; - if (values.has(value)) { + const identity = JSON.stringify(canonicalizeRegistryValue(value)); + if (values.has(identity)) { failures.push( - `${specification.registryId}.${rowName} duplicates ${field}=${String(value)} from ${values.get(value)}`, + `${specification.registryId}.${rowName} duplicates ${field}=${String(value)} from ${values.get(identity)}`, ); } else { - values.set(value, rowName); + values.set(identity, rowName); } } } @@ -121,12 +221,22 @@ for (const specification of governance.registries) { } } + const contract = Object.freeze({ + requiredFields: specification.requiredFields, + fieldTypes: specification.fieldTypes ?? {}, + uniqueFields: specification.uniqueFields ?? [], + allowedValues: specification.allowedValues ?? {}, + references: specification.references ?? [], + keyField: specification.keyField ?? null, + breakingFields: specification.breakingFields ?? [], + }); snapshots.push({ registryId: specification.registryId, owner: specification.owner, source: sourcePath ?? specification.path, rowCount: Object.keys(rows).length, - rows, + contract, + rows: canonicalizeRegistryValue(rows), }); } @@ -145,18 +255,74 @@ for (const specification of governance.registries) { Object.values(targetRows) .filter((row) => row && typeof row === "object" && !Array.isArray(row)) .map((row) => row[reference.targetField]) - .filter((value) => value !== undefined), + .filter((value) => value !== undefined && value !== null), ); for (const [rowName, row] of Object.entries(rows)) { if (!row || typeof row !== "object" || Array.isArray(row)) continue; const value = row[reference.field]; - if (value !== undefined && !targetValues.has(value)) { + if ( + value !== undefined && + value !== null && + !targetValues.has(value) + ) { failures.push( `${specification.registryId}.${rowName}.${reference.field} references unknown ${reference.registryId}.${reference.targetField}=${String(value)}`, ); } } } + + for (const consumer of specification.consumers ?? []) { + try { + const source = await readFile(consumer.path, "utf8"); + if (!source.includes(consumer.token)) { + failures.push( + `${specification.registryId} consumer ${consumer.path} is missing ${consumer.token}`, + ); + } + } catch { + failures.push( + `${specification.registryId} consumer source is missing: ${consumer.path}`, + ); + } + } + + if (specification.consumerIdentityField) { + const consumerFiles = ( + await Promise.all( + (specification.consumerDirectories ?? []).map(filesBelow), + ) + ).flat(); + const sourcePath = sourcesByRegistry.get(specification.registryId); + const consumerText = ( + await Promise.all( + consumerFiles + .filter((file) => file !== sourcePath) + .map((file) => readFile(file, "utf8")), + ) + ).join("\n"); + const exemptions = new Set(specification.orphanExemptRows ?? []); + for (const [rowName, row] of Object.entries(rows)) { + if ( + !row || + typeof row !== "object" || + Array.isArray(row) || + exemptions.has(rowName) + ) { + continue; + } + const identity = row[specification.consumerIdentityField]; + if ( + (typeof identity !== "string" && + typeof identity !== "number") || + !consumerText.includes(String(identity)) + ) { + failures.push( + `${specification.registryId}.${rowName} has no executable consumer for ${specification.consumerIdentityField}=${String(identity)}`, + ); + } + } + } } const sourceFiles = governance.sourceDirectories ?? [ @@ -166,59 +332,80 @@ const sourceFiles = governance.sourceDirectories ?? [ ]; const adHocPatterns = [ { name: "direct fetch", expression: /\bfetch\s*\(/ }, - { name: "direct localStorage", expression: /\blocalStorage\.(?:get|set|remove)Item/ }, + { + name: "direct localStorage", + expression: /\blocalStorage\.(?:get|set|remove)Item/, + }, { name: "direct import.meta.env", expression: /\bimport\.meta\.env\./ }, { name: "raw API path", expression: /["']\/api\// }, ]; -/** @param {string} directory */ -async function scanDirectory(directory) { - try { - await access(directory); - } catch { - return; - } - const entries = await import("node:fs/promises").then(({ readdir }) => - readdir(directory, { withFileTypes: true }), - ); - for (const entry of entries) { - const target = path.join(directory, entry.name); - if (entry.isDirectory()) { - await scanDirectory(target); - continue; - } - if (!/\.(js|jsx|mjs|ts|tsx|mts)$/.test(entry.name)) continue; - const content = await readFile(target, "utf8"); +for (const sourceDirectory of sourceFiles) { + for (const file of await filesBelow(sourceDirectory)) { + const content = await readFile(file, "utf8"); for (const pattern of adHocPatterns) { if (pattern.expression.test(content)) { - failures.push(`ad-hoc ${pattern.name} in ${target}`); + failures.push(`ad-hoc ${pattern.name} in ${file}`); } } } } -for (const sourceDirectory of sourceFiles) { - await scanDirectory(sourceDirectory); +const currentSnapshot = + /** @type {Readonly>} */ ( + canonicalizeRegistryValue({ + schemaVersion: 2, + registries: snapshots, + }) + ); +let baselineDigest = null; +let currentDigest = registrySnapshotDigest(currentSnapshot); +let compatibility = + /** @type {{impact: string, changes: readonly Record[]}} */ ({ + impact: "not-evaluated", + changes: [], + }); + +if (baselinePath && approvalPath && evidencePath) { + try { + const baseline = JSON.parse(await readFile(baselinePath, "utf8")); + const approval = JSON.parse(await readFile(approvalPath, "utf8")); + const approvalResult = verifyRegistryBaselineApproval(baseline, approval); + baselineDigest = approvalResult.actualDigest; + if (!approvalResult.passed) { + failures.push( + `registry baseline approval digest mismatch: approved=${approvalResult.approvedDigest} actual=${approvalResult.actualDigest}`, + ); + } + compatibility = diffRegistrySnapshots(baseline, currentSnapshot); + const evidence = JSON.parse(await readFile(evidencePath, "utf8")); + const evidenceResult = validateBreakingEvidence(compatibility, evidence); + failures.push(...evidenceResult.failures); + } catch (error) { + failures.push( + `registry compatibility evidence unavailable: ${ + error instanceof Error ? error.name : "unknown" + }`, + ); + } } -await mkdir("artifacts/quality", { recursive: true }); -await writeFile( - artifactPath, - `${JSON.stringify( - { - schemaVersion: 1, - generatedAt: new Date().toISOString(), - compatibilityImpact: governance.compatibilityImpact.current, - failures, - registries: snapshots, - }, - null, - 2, - )}\n`, -); +const report = { + schemaVersion: 2, + generatedAt: new Date().toISOString(), + baselineDigest, + currentDigest, + compatibility, + failures, + registries: snapshots, +}; +await mkdir(path.dirname(artifactPath), { recursive: true }); +await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`); if (failures.length > 0) { process.stderr.write(`Registry governance failed:\n${failures.join("\n")}\n`); process.exit(1); } -process.stdout.write(`Registry governance: ${snapshots.length} registries PASS\n`); +process.stdout.write( + `Registry governance: ${snapshots.length} registries PASS; compatibility=${compatibility.impact}\n`, +); diff --git a/scripts/check-registry-compatibility-fixtures.mjs b/scripts/check-registry-compatibility-fixtures.mjs new file mode 100644 index 0000000..b1c2a4d --- /dev/null +++ b/scripts/check-registry-compatibility-fixtures.mjs @@ -0,0 +1,63 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; + +import { + diffRegistrySnapshots, + validateBreakingEvidence, + verifyRegistryBaselineApproval, +} from "./lib/registry-compatibility.mjs"; + +const fixtures = JSON.parse( + await readFile( + "tests/fixtures/registry/compatibility/semantic-diff.json", + "utf8", + ), +); +const results = []; +for (const fixture of fixtures.cases) { + const actual = diffRegistrySnapshots(fixture.before, fixture.after); + results.push({ + id: fixture.id, + expected: fixture.expected, + actual: actual.impact, + passed: actual.impact === fixture.expected, + }); +} + +const breaking = diffRegistrySnapshots( + fixtures.breakingEvidence.before, + fixtures.breakingEvidence.after, +); +const missingEvidence = validateBreakingEvidence(breaking, { + schemaVersion: 1, + changes: [], +}); +results.push({ + id: "breaking-evidence-required", + expected: false, + actual: missingEvidence.passed, + passed: !missingEvidence.passed, +}); + +const tamperedApproval = verifyRegistryBaselineApproval( + fixtures.tamperedApproval.snapshot, + fixtures.tamperedApproval.approval, +); +results.push({ + id: "tampered-baseline-digest", + expected: false, + actual: tamperedApproval.passed, + passed: !tamperedApproval.passed, +}); + +await mkdir("artifacts/quality", { recursive: true }); +await writeFile( + "artifacts/quality/registry-compatibility-fixtures.json", + `${JSON.stringify({ schemaVersion: 1, results }, null, 2)}\n`, +); +if (results.some((result) => !result.passed)) { + process.stderr.write("Registry compatibility fixture failed.\n"); + process.exit(1); +} +process.stdout.write( + `Registry compatibility fixtures: ${results.length} PASS\n`, +); diff --git a/scripts/check-risk-coverage.mjs b/scripts/check-risk-coverage.mjs new file mode 100644 index 0000000..83c4f78 --- /dev/null +++ b/scripts/check-risk-coverage.mjs @@ -0,0 +1,88 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +/** @param {string} name @param {string} fallback */ +function argumentValue(name, fallback) { + const index = process.argv.indexOf(name); + return index >= 0 && process.argv[index + 1] + ? process.argv[index + 1] + : fallback; +} + +const policyPath = argumentValue( + "--policy", + "config/testing/risk-coverage.json", +); +const summaryPath = argumentValue( + "--summary", + "artifacts/tests/coverage/coverage-summary.json", +); +const artifactPath = argumentValue( + "--artifact", + "artifacts/quality/risk-coverage.json", +); +const policy = JSON.parse(await readFile(policyPath, "utf8")); +const summary = JSON.parse(await readFile(summaryPath, "utf8")); +const failures = []; +/** @type {Array<{ + * scope: string, + * metric: string, + * threshold: number, + * received: number | undefined, + * passed: boolean + * }>} */ +const results = []; + +/** + * @param {string} scope + * @param {Record} actual + * @param {Record} minimum + */ +function evaluate(scope, actual, minimum) { + for (const [metric, threshold] of Object.entries(minimum)) { + const received = actual?.[metric]?.pct; + const passed = + typeof received === "number" && + Number.isFinite(received) && + received >= threshold; + results.push({ scope, metric, threshold, received, passed }); + if (!passed) { + failures.push( + `${scope}.${metric} expected >= ${threshold}, received ${String(received)}`, + ); + } + } +} + +evaluate("total", summary.total, policy.summary); +for (const modulePolicy of policy.criticalModules) { + const key = Object.keys(summary).find( + (candidate) => + candidate !== "total" && + candidate.replaceAll("\\", "/").endsWith(`/${modulePolicy.path}`), + ); + if (!key) { + failures.push(`critical module missing from coverage: ${modulePolicy.path}`); + continue; + } + evaluate(modulePolicy.path, summary[key], modulePolicy.minimum); +} + +const artifact = { + schemaVersion: 1, + policy: policyPath, + summary: summaryPath, + status: failures.length === 0 ? "PASS" : "FAIL", + results, + failures, +}; +await mkdir(path.dirname(artifactPath), { recursive: true }); +await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`); + +if (failures.length > 0) { + process.stderr.write(`Risk coverage failed:\n- ${failures.join("\n- ")}\n`); + process.exit(1); +} +process.stdout.write( + `Risk coverage: PASS (${results.length} scoped thresholds)\n`, +); diff --git a/scripts/check-test-evidence.mjs b/scripts/check-test-evidence.mjs new file mode 100644 index 0000000..c22a94c --- /dev/null +++ b/scripts/check-test-evidence.mjs @@ -0,0 +1,167 @@ +import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; + +/** @param {string} name @param {string} fallback */ +function argumentValue(name, fallback) { + const index = process.argv.indexOf(name); + return index >= 0 && process.argv[index + 1] + ? process.argv[index + 1] + : fallback; +} + +const sourceRoot = argumentValue("--source-root", "tests"); +const artifactPath = argumentValue( + "--artifact", + "artifacts/quality/test-evidence.json", +); +const fixtureMode = sourceRoot !== "tests"; +const failures = []; +const facts = { + scannedFiles: 0, + visualBaselines: 0, + sharedScenarios: 0, +}; + +/** @param {string} target @returns {Promise} */ +async function filesBelow(target) { + try { + const metadata = await stat(target); + if (metadata.isFile()) return [target]; + const entries = await readdir(target, { withFileTypes: true }); + const groups = await Promise.all( + entries.map((entry) => filesBelow(path.join(target, entry.name))), + ); + return groups.flat(); + } catch { + return []; + } +} + +const sourceFiles = (await filesBelow(sourceRoot)).filter( + (file) => fixtureMode || !file.split(path.sep).includes("fixtures"), +); +for (const file of sourceFiles) { + if (!/\.(?:js|jsx|mjs|ts|tsx|fixture|txt)$/.test(file)) continue; + const source = await readFile(file, "utf8"); + facts.scannedFiles += 1; + const skipPattern = + /\b(?:test|it|describe)(?:\.describe)?\.(?:skip|fixme)\s*\(/g; + if (skipPattern.test(source)) { + const quarantine = + /quarantine\(owner=[^)]+,\s*defect=[^)]+,\s*expires=\d{4}-\d{2}-\d{2}\)/; + if (!quarantine.test(source)) { + failures.push(`${file}: skip/fixme lacks owned expiring quarantine`); + } + } + const wholeUiMask = + /\bmask\s*:\s*\[[\s\S]{0,240}(?:locator|getByRole)\s*\(\s*["'](?:html|body|main|application|document)["']/i; + if (wholeUiMask.test(source)) { + failures.push(`${file}: screenshot mask may not cover the whole UI`); + } +} + +if (!fixtureMode) { + const e2eConfig = await readFile("playwright.config.js", "utf8"); + for (const token of [ + "pnpm build", + "pnpm preview", + "reuseExistingServer: false", + '"junit"', + 'trace: "retain-on-failure"', + '"chromium-compact"', + '"firefox"', + '"webkit"', + ]) { + if (!e2eConfig.includes(token)) { + failures.push(`playwright.config.js missing release evidence token ${token}`); + } + } + + const e2eFiles = (await filesBelow("tests/e2e")).filter((file) => + /\.spec\.(?:js|ts)$/.test(file), + ); + for (const file of e2eFiles) { + const source = await readFile(file, "utf8"); + if (!source.includes("support/browser/strict-browser-test")) { + failures.push(`${file}: bypasses strict browser fixture`); + } + } + + const scenarioCatalog = await readFile( + "tests/mocks/scenarios/catalog.ts", + "utf8", + ); + const scenarioIdBlock = + scenarioCatalog.match( + /HTTP_SCENARIO_IDS\s*=\s*Object\.freeze\(\[([\s\S]*?)\]\s*as const\)/, + )?.[1] ?? ""; + facts.sharedScenarios = (scenarioIdBlock.match(/"[^"]+"/g) ?? []).length; + if (facts.sharedScenarios < 19) { + failures.push("shared MSW catalog must retain all 19 failure scenarios"); + } + const handler = await readFile( + "tests/mocks/handlers/reference-resources.ts", + "utf8", + ); + if ( + !handler.includes("assertOperationScenario") || + !handler.includes("../scenarios/catalog.js") + ) { + failures.push("MSW handler bypasses shared scenario catalog"); + } + + const baselineFiles = (await filesBelow("tests/visual/__snapshots__")).filter( + (file) => file.endsWith(".png"), + ); + facts.visualBaselines = baselineFiles.length; + if (facts.visualBaselines < 4) { + failures.push("visual baseline requires at least four risk surfaces"); + } + for (const required of [ + "playwright.storybook.config.js", + "playwright.visual.config.js", + "tests/storybook/workshop.spec.ts", + "artifacts/tests/storybook/results.xml", + "artifacts/tests/visual/results.xml", + ]) { + if ((await filesBelow(required)).length === 0) { + failures.push(`test evidence missing ${required}`); + } + } + + const requiredBuiltFiles = [ + "dist/index.html", + "dist/config.json", + "dist/release-manifest.json", + "dist/runtime-config.schema.json", + "dist/.vite/manifest.json", + ]; + for (const required of requiredBuiltFiles) { + if ((await filesBelow(required)).length === 0) { + failures.push(`built-dist contract missing ${required}`); + } + } + const sourceMaps = (await filesBelow("dist")).filter((file) => + file.endsWith(".map"), + ); + if (sourceMaps.length > 0) { + failures.push(`production dist contains source maps: ${sourceMaps.join(", ")}`); + } +} + +const report = { + schemaVersion: 1, + sourceRoot, + status: failures.length === 0 ? "PASS" : "FAIL", + facts, + failures, +}; +await mkdir(path.dirname(artifactPath), { recursive: true }); +await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`); +if (failures.length > 0) { + process.stderr.write(`Test evidence failed:\n- ${failures.join("\n- ")}\n`); + process.exit(1); +} +process.stdout.write( + `Test evidence: PASS (${facts.scannedFiles} files, ${facts.visualBaselines} baselines, ${facts.sharedScenarios} scenarios)\n`, +); diff --git a/scripts/lib/registry-compatibility.mjs b/scripts/lib/registry-compatibility.mjs new file mode 100644 index 0000000..d42dc20 --- /dev/null +++ b/scripts/lib/registry-compatibility.mjs @@ -0,0 +1,321 @@ +import { createHash } from "node:crypto"; + +export const COMPATIBILITY_IMPACTS = Object.freeze([ + "none", + "additive", + "behavior-change", + "breaking", +]); + +const impactRank = new Map( + COMPATIBILITY_IMPACTS.map((impact, index) => [impact, index]), +); + +/** @param {unknown} value @returns {unknown} */ +export function canonicalizeRegistryValue(value) { + if (Array.isArray(value)) { + const projected = + /** @type {unknown[]} */ (value.map(canonicalizeRegistryValue)); + return projected.every( + (item) => + item === null || + ["string", "number", "boolean"].includes(typeof item), + ) + ? projected.sort((left, right) => + JSON.stringify(left).localeCompare(JSON.stringify(right)), + ) + : projected; + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, canonicalizeRegistryValue(item)]), + ); + } + return value; +} + +/** @param {unknown} value @returns {string} */ +export function canonicalRegistryJson(value) { + return JSON.stringify(canonicalizeRegistryValue(value)) ?? "undefined"; +} + +/** @param {unknown} snapshot */ +export function registrySnapshotDigest(snapshot) { + return createHash("sha256") + .update(canonicalRegistryJson(snapshot)) + .digest("hex"); +} + +/** @param {string} current @param {string} candidate */ +function strongestImpact(current, candidate) { + return (impactRank.get(candidate) ?? 0) > (impactRank.get(current) ?? 0) + ? candidate + : current; +} + +/** @param {unknown} value */ +function valueType(value) { + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + return typeof value; +} + +/** + * @param {string} registryId + * @param {string} rowName + * @param {string} field + * @param {string} kind + */ +function changeId(registryId, rowName, field, kind) { + return `${registryId}:${rowName}:${field}:${kind}`; +} + +/** + * Calculates a semantic diff. Object key and primitive-array ordering is + * canonicalized before comparison and therefore cannot create a false change. + * + * @param {Readonly>} before + * @param {Readonly>} after + */ +export function diffRegistrySnapshots(before, after) { + const changes = /** @type {Array>} */ ([]); + let impact = "none"; + const beforeRegistries = + /** @type {Map>} */ (new Map( + /** @type {Array>} */ (before.registries ?? []).map( + (registry) => [String(registry.registryId), registry], + ), + )); + const afterRegistries = + /** @type {Map>} */ (new Map( + /** @type {Array>} */ (after.registries ?? []).map( + (registry) => [String(registry.registryId), registry], + ), + )); + const registryIds = new Set([ + ...beforeRegistries.keys(), + ...afterRegistries.keys(), + ]); + + for (const registryId of [...registryIds].sort()) { + const previous = beforeRegistries.get(registryId); + const current = afterRegistries.get(registryId); + if (!previous || !current) { + const changeImpact = previous ? "breaking" : "additive"; + impact = strongestImpact(impact, changeImpact); + changes.push({ + changeId: changeId(registryId, "*", "*", previous ? "removed" : "added"), + registryId, + rowName: "*", + field: "*", + kind: previous ? "registry-removed" : "registry-added", + impact: changeImpact, + }); + continue; + } + + const previousContract = + /** @type {Record} */ (previous.contract ?? {}); + const currentContract = + /** @type {Record} */ (current.contract ?? {}); + const contractFields = new Set([ + ...Object.keys(previousContract), + ...Object.keys(currentContract), + ]); + for (const field of [...contractFields].sort()) { + const beforeHas = Object.hasOwn(previousContract, field); + const afterHas = Object.hasOwn(currentContract, field); + const beforeValue = previousContract[field]; + const afterValue = currentContract[field]; + if ( + beforeHas && + afterHas && + canonicalRegistryJson(beforeValue) === + canonicalRegistryJson(afterValue) + ) { + continue; + } + const kind = !beforeHas + ? "contract-field-added" + : !afterHas + ? "contract-field-removed" + : "contract-field-changed"; + impact = strongestImpact(impact, "breaking"); + changes.push({ + changeId: changeId(registryId, "$contract", field, kind), + registryId, + rowName: "$contract", + field, + kind, + impact: "breaking", + before: canonicalizeRegistryValue(beforeValue), + after: canonicalizeRegistryValue(afterValue), + }); + } + + const breakingFields = new Set( + /** @type {string[]} */ ( + currentContract.breakingFields ?? [] + ), + ); + const beforeRows = + /** @type {Record>} */ ( + previous.rows ?? {} + ); + const afterRows = + /** @type {Record>} */ (current.rows ?? {}); + const rowNames = new Set([ + ...Object.keys(beforeRows), + ...Object.keys(afterRows), + ]); + for (const rowName of [...rowNames].sort()) { + const beforeRow = beforeRows[rowName]; + const afterRow = afterRows[rowName]; + if (!beforeRow || !afterRow) { + const changeImpact = beforeRow ? "breaking" : "additive"; + impact = strongestImpact(impact, changeImpact); + changes.push({ + changeId: changeId( + registryId, + rowName, + "*", + beforeRow ? "removed" : "added", + ), + registryId, + rowName, + field: "*", + kind: beforeRow ? "row-removed" : "row-added", + impact: changeImpact, + }); + continue; + } + + const fields = new Set([ + ...Object.keys(beforeRow), + ...Object.keys(afterRow), + ]); + for (const field of [...fields].sort()) { + const beforeHas = Object.hasOwn(beforeRow, field); + const afterHas = Object.hasOwn(afterRow, field); + const beforeValue = beforeRow[field]; + const afterValue = afterRow[field]; + if ( + beforeHas && + afterHas && + canonicalRegistryJson(beforeValue) === + canonicalRegistryJson(afterValue) + ) { + continue; + } + let kind; + let changeImpact; + if (!beforeHas) { + kind = "field-added"; + changeImpact = "additive"; + } else if (!afterHas) { + kind = "field-removed"; + changeImpact = "breaking"; + } else if (valueType(beforeValue) !== valueType(afterValue)) { + kind = "field-type-changed"; + changeImpact = "breaking"; + } else if ( + Array.isArray(beforeValue) && + Array.isArray(afterValue) && + beforeValue.some( + (item) => + !afterValue.some( + (candidate) => + canonicalRegistryJson(candidate) === + canonicalRegistryJson(item), + ), + ) + ) { + kind = "allowed-value-removed"; + changeImpact = "breaking"; + } else { + kind = "field-changed"; + changeImpact = breakingFields.has(field) + ? "breaking" + : "behavior-change"; + } + impact = strongestImpact(impact, changeImpact); + changes.push({ + changeId: changeId(registryId, rowName, field, kind), + registryId, + rowName, + field, + kind, + impact: changeImpact, + before: canonicalizeRegistryValue(beforeValue), + after: canonicalizeRegistryValue(afterValue), + }); + } + } + } + + return Object.freeze({ + impact, + changes: Object.freeze(changes), + }); +} + +/** + * @param {Readonly>} snapshot + * @param {Readonly>} approval + */ +export function verifyRegistryBaselineApproval(snapshot, approval) { + const actualDigest = registrySnapshotDigest(snapshot); + const approvedDigest = approval.snapshotDigest; + return Object.freeze({ + passed: + approval.schemaVersion === 1 && + typeof approval.owner === "string" && + approval.owner.length > 0 && + typeof approval.approvedAt === "string" && + approvedDigest === actualDigest, + actualDigest, + approvedDigest: + typeof approvedDigest === "string" ? approvedDigest : "missing", + }); +} + +/** + * @param {ReturnType} diff + * @param {Readonly>} evidenceFile + */ +export function validateBreakingEvidence(diff, evidenceFile) { + const evidence = new Map( + /** @type {Array>} */ ( + evidenceFile.changes ?? [] + ).map((entry) => [entry.changeId, entry]), + ); + const failures = []; + for (const change of diff.changes.filter( + (entry) => entry.impact === "breaking", + )) { + const entry = evidence.get(change.changeId); + if (!entry) { + failures.push(`breaking change missing evidence: ${change.changeId}`); + continue; + } + for (const field of [ + "versionBump", + "migration", + "compatibilityWindow", + "rollback", + "owner", + ]) { + if (typeof entry[field] !== "string" || entry[field].trim().length === 0) { + failures.push( + `breaking change ${change.changeId} missing non-empty ${field}`, + ); + } + } + } + return Object.freeze({ + passed: failures.length === 0, + failures: Object.freeze(failures), + }); +} diff --git a/scripts/serve-static.mjs b/scripts/serve-static.mjs new file mode 100644 index 0000000..34a0c67 --- /dev/null +++ b/scripts/serve-static.mjs @@ -0,0 +1,47 @@ +import { createReadStream } from "node:fs"; +import { access, stat } from "node:fs/promises"; +import { createServer } from "node:http"; +import path from "node:path"; + +const root = path.resolve(process.argv[2] ?? "artifacts/storybook/static"); +const port = Number(process.argv[3] ?? 6006); +const contentTypes = /** @type {Readonly>} */ ({ + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", +}); + +await access(root); +const server = createServer(async (request, response) => { + try { + const url = new URL(request.url ?? "/", `http://127.0.0.1:${port}`); + const decoded = decodeURIComponent(url.pathname); + const requested = path.resolve(root, `.${decoded}`); + if (requested !== root && !requested.startsWith(`${root}${path.sep}`)) { + response.writeHead(403).end(); + return; + } + const details = await stat(requested).catch(() => null); + const file = details?.isDirectory() + ? path.join(requested, "index.html") + : requested; + await access(file); + response.writeHead(200, { + "Content-Type": + contentTypes[path.extname(file)] ?? "application/octet-stream", + "Cache-Control": "no-store", + }); + createReadStream(file).pipe(response); + } catch { + response.writeHead(404).end(); + } +}); +server.listen(port, "127.0.0.1", () => { + process.stdout.write(`Static evidence server: ${root} on ${port}\n`); +}); +for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, () => server.close(() => process.exit(0))); +} diff --git a/scripts/test-sample-removal.mjs b/scripts/test-sample-removal.mjs index 647df44..4899823 100644 --- a/scripts/test-sample-removal.mjs +++ b/scripts/test-sample-removal.mjs @@ -18,6 +18,7 @@ const featureOwnedPaths = [ featureSource, featureTests, "tests/e2e/reference-form.spec.js", + "tests/mocks", ]; const copyTargets = [ "src", @@ -41,6 +42,7 @@ const copyTargets = [ const emptyContracts = `import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.js"; import { PLATFORM_ROUTE_REGISTRY } from "../contracts/routes.js"; +import { PLATFORM_SCHEMA_REGISTRY } from "../contracts/schema-registry.js"; export const INSTALLED_FEATURE_CONTRACTS = /** @type {readonly unknown[]} */ (Object.freeze([])); @@ -48,6 +50,7 @@ export const ROUTE_REGISTRY = PLATFORM_ROUTE_REGISTRY; export const ROUTE_RUNTIME_CONTRACT = PLATFORM_ROUTE_RUNTIME_CONTRACT; export const API_OPERATIONS = Object.freeze({}); export const QUERY_REGISTRY = Object.freeze({}); +export const SCHEMA_REGISTRY = PLATFORM_SCHEMA_REGISTRY; export const NAVIGATION_ROUTES = Object.freeze( Object.values(ROUTE_REGISTRY) .filter((definition) => definition.navigationOrder !== null) @@ -145,6 +148,39 @@ await writeFile( path.join(fixtureRoot, "src/features/installed-feature-messages.js"), emptyMessages, ); +const governanceFile = path.join( + fixtureRoot, + "config/contracts/registry-governance.json", +); +const removalGovernance = JSON.parse(await readFile(governanceFile, "utf8")); +removalGovernance.registries = removalGovernance.registries.map( + /** @param {Record} registry */ + (registry) => ({ + ...registry, + ...(Array.isArray(registry.consumers) + ? { + consumers: registry.consumers.filter( + /** @param {{path?: string}} consumer */ + (consumer) => + !consumer.path?.includes("features/reference-feature"), + ), + } + : {}), + ...(Array.isArray(registry.consumerDirectories) + ? { + consumerDirectories: registry.consumerDirectories.filter( + /** @param {string} directory */ + (directory) => + !directory.includes("features/reference-feature"), + ), + } + : {}), + }), +); +await writeFile( + governanceFile, + `${JSON.stringify(removalGovernance, null, 2)}\n`, +); /** @type {string[]} */ const residue = []; @@ -165,7 +201,7 @@ for (const root of ["src", "tests"]) { const checks = [ ["typecheck", runPnpm("check:types")], ["architecture", runPnpm("check:architecture")], - ["registry", runPnpm("check:registries")], + ["registry-structure", runPnpm("check:registries:structure")], ["unit-integration", runPnpm("test:all")], [ "home-smoke", diff --git a/scripts/update-registry-baseline.mjs b/scripts/update-registry-baseline.mjs new file mode 100644 index 0000000..6bf9a3c --- /dev/null +++ b/scripts/update-registry-baseline.mjs @@ -0,0 +1,41 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +import { registrySnapshotDigest } from "./lib/registry-compatibility.mjs"; + +const inputPath = + process.argv[2] ?? "artifacts/quality/registry-current-snapshot.json"; +const outputPath = + process.argv[3] ?? "config/contracts/registry-baseline.json"; +const owner = process.env.REGISTRY_BASELINE_OWNER; +const reason = process.env.REGISTRY_BASELINE_REASON; + +if (!owner || !reason) { + process.stderr.write( + "REGISTRY_BASELINE_OWNER and REGISTRY_BASELINE_REASON are required.\n", + ); + process.exit(1); +} + +const input = JSON.parse(await readFile(inputPath, "utf8")); +const snapshot = input.registries + ? { schemaVersion: 2, registries: input.registries } + : input; +const digest = registrySnapshotDigest(snapshot); +await mkdir(path.dirname(outputPath), { recursive: true }); +await writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`); +await writeFile( + "config/contracts/registry-baseline.approval.json", + `${JSON.stringify( + { + schemaVersion: 1, + snapshotDigest: digest, + owner, + reason, + approvedAt: new Date().toISOString(), + }, + null, + 2, + )}\n`, +); +process.stdout.write(`Registry baseline updated: ${digest}\n`); diff --git a/src/bootstrap/main.jsx b/src/bootstrap/main.jsx index 3454208..2a57e4c 100644 --- a/src/bootstrap/main.jsx +++ b/src/bootstrap/main.jsx @@ -20,6 +20,8 @@ const root = createRoot(rootElement); async function boot() { try { const composition = await createRuntimeComposition(); + document.documentElement.dataset.buildId = composition.release.buildId; + document.documentElement.dataset.releaseId = composition.release.releaseId; initializeColorScheme(composition.application.preferences); root.render(); } catch (error) { diff --git a/src/contracts/schema-registry.js b/src/contracts/schema-registry.js new file mode 100644 index 0000000..459f7b0 --- /dev/null +++ b/src/contracts/schema-registry.js @@ -0,0 +1,27 @@ +/** + * @typedef {{ + * schemaId: string, + * boundary: "route-params" | "route-search" | + * "route-search-api-request" | "api-request" | "api-response", + * owner: string, + * runtime: "zod" + * }} SchemaDefinition + */ + +export const PLATFORM_SCHEMA_REGISTRY = + /** @type {Readonly>>} */ ( + Object.freeze({ + none: Object.freeze({ + schemaId: "none", + boundary: "route-params", + owner: "feature-frontend-routing-release-recovery-runtime", + runtime: "zod", + }), + NotFoundSplat: Object.freeze({ + schemaId: "NotFoundSplat", + boundary: "route-params", + owner: "feature-frontend-routing-release-recovery-runtime", + runtime: "zod", + }), + }) + ); diff --git a/src/features/installed-feature-contracts.js b/src/features/installed-feature-contracts.js index d21258a..5d59893 100644 --- a/src/features/installed-feature-contracts.js +++ b/src/features/installed-feature-contracts.js @@ -1,5 +1,6 @@ import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.js"; import { PLATFORM_ROUTE_REGISTRY } from "../contracts/routes.js"; +import { PLATFORM_SCHEMA_REGISTRY } from "../contracts/schema-registry.js"; import { REFERENCE_FEATURE_CONTRACT } from "./reference-feature/contracts/reference-feature-contract.js"; export const INSTALLED_FEATURE_CONTRACTS = Object.freeze([ @@ -20,6 +21,10 @@ export const API_OPERATIONS = Object.freeze({ export const QUERY_REGISTRY = Object.freeze({ ...REFERENCE_FEATURE_CONTRACT.queryRegistry, }); +export const SCHEMA_REGISTRY = Object.freeze({ + ...PLATFORM_SCHEMA_REGISTRY, + ...REFERENCE_FEATURE_CONTRACT.schemas, +}); export const NAVIGATION_ROUTES = Object.freeze( Object.values(ROUTE_REGISTRY) diff --git a/src/features/reference-feature/contracts/reference-feature-contract.js b/src/features/reference-feature/contracts/reference-feature-contract.js index 86f4d07..c808f34 100644 --- a/src/features/reference-feature/contracts/reference-feature-contract.js +++ b/src/features/reference-feature/contracts/reference-feature-contract.js @@ -14,6 +14,44 @@ export const referenceQueryKeys = Object.freeze({ export const REFERENCE_FEATURE_CONTRACT = Object.freeze({ featureId: REFERENCE_FEATURE_ID, + schemas: Object.freeze({ + ReferenceResourceParams: Object.freeze({ + schemaId: "ReferenceResourceParams", + boundary: "route-params", + owner: "feature-frontend-reference-feature-vertical-slice", + runtime: "zod", + }), + ReferenceResourceListQuery: Object.freeze({ + schemaId: "ReferenceResourceListQuery", + boundary: "route-search-api-request", + owner: "feature-frontend-reference-feature-vertical-slice", + runtime: "zod", + }), + CreateReferenceResourceCommand: Object.freeze({ + schemaId: "CreateReferenceResourceCommand", + boundary: "api-request", + owner: "feature-frontend-reference-feature-vertical-slice", + runtime: "zod", + }), + NoRequest: Object.freeze({ + schemaId: "NoRequest", + boundary: "api-request", + owner: "feature-frontend-reference-feature-vertical-slice", + runtime: "zod", + }), + ReferenceResourceListPayload: Object.freeze({ + schemaId: "ReferenceResourceListPayload", + boundary: "api-response", + owner: "feature-frontend-reference-feature-vertical-slice", + runtime: "zod", + }), + ReferenceResourcePayload: Object.freeze({ + schemaId: "ReferenceResourcePayload", + boundary: "api-response", + owner: "feature-frontend-reference-feature-vertical-slice", + runtime: "zod", + }), + }), routes: Object.freeze({ REFERENCE_RESOURCE_LIST: Object.freeze({ routeId: "REFERENCE_RESOURCE_LIST", diff --git a/src/features/reference-feature/contracts/reference-schemas.ts b/src/features/reference-feature/contracts/reference-schemas.ts index 0c6031b..9cd7654 100644 --- a/src/features/reference-feature/contracts/reference-schemas.ts +++ b/src/features/reference-feature/contracts/reference-schemas.ts @@ -39,6 +39,7 @@ const payloadSchemas = { const requestSchemas = { ReferenceResourceListQuery: referenceResourceListQuerySchema, + NoRequest: z.object({}).strict(), CreateReferenceResourceCommand: z .object({ name: z.string().trim().min(1).max(120), diff --git a/src/presentation/design-system/design-system.stories.tsx b/src/presentation/design-system/design-system.stories.tsx new file mode 100644 index 0000000..f379e37 --- /dev/null +++ b/src/presentation/design-system/design-system.stories.tsx @@ -0,0 +1,128 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { useState } from "react"; + +import { + Alert, + Badge, + Button, + Card, + Dialog, + Menu, + ProgressBar, + Skeleton, + Tabs, + TextArea, + TextField, +} from "./index.js"; + +const meta = { + title: "Platform/Design System", + component: Button, + tags: ["autodocs"], + parameters: { + layout: "padded", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primitives: Story = { + render: () => ( +
+ +
+ + + + +
+
+ Neutral + Success + Warning + Danger +
+
+ + +