Compare commits

...
97 changed files with 15057 additions and 371 deletions
+3
View File
@@ -10,4 +10,7 @@ artifacts/**/*.xml
artifacts/**/*.txt
artifacts/**/*.sarif
artifacts/tests/e2e/
artifacts/storybook/
artifacts/tests/storybook/
artifacts/tests/visual/
!artifacts/**/.gitkeep
+15
View File
@@ -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;
+88
View File
@@ -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<string, unknown>();
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) => (
<ApplicationProvider application={application}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<LocaleProvider>
<ThemeProvider>
<SessionProvider>
<div id="portal-root" />
<main className="ui-page" style={{ padding: "1rem" }}>
<Story />
</main>
</SessionProvider>
</ThemeProvider>
</LocaleProvider>
</MemoryRouter>
</QueryClientProvider>
</ApplicationProvider>
),
],
parameters: {
a11y: {
test: "error",
},
controls: {
expanded: true,
},
options: {
storySort: {
order: ["Platform"],
},
},
},
};
export default preview;
+72 -11
View File
@@ -58,7 +58,10 @@
"gates": {
"FE-GATE-001": {
"name": "manifest-lockfile",
"steps": [{ "script": "verify:lockfile", "expect": "pass" }],
"steps": [
{ "script": "verify:lockfile", "expect": "pass" },
{ "script": "check:frozen-lockfile:fixture", "expect": "pass" }
],
"logPath": "artifacts/quality/install.txt",
"evidence": ["artifacts/quality/install.txt"],
"retentionClass": "merge-cycle"
@@ -100,9 +103,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 +140,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 +190,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 +208,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 +217,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"
},
@@ -203,14 +242,32 @@
"FE-GATE-013": {
"name": "security",
"steps": [
{ "script": "verify:reproducible-build", "expect": "pass" },
{ "script": "build:release", "expect": "pass" },
{ "script": "verify:supply-chain", "expect": "pass" },
{ "script": "check:supply-chain:fixtures", "expect": "pass" },
{
"script": "check:supply-chain:provider-fixtures",
"expect": "pass"
},
{ "script": "scan:security:fixture", "expect": "fail" },
{ "script": "check:browser-security", "expect": "pass" }
],
"logPath": "artifacts/quality/gates/FE-GATE-013.txt",
"evidence": [
"artifacts/security/scan.sarif",
"artifacts/security/scan-fixture.sarif",
"artifacts/release/dependency-inventory.json",
"artifacts/security/dependency-diff.json"
"artifacts/release/sbom.cdx.json",
"artifacts/release/provenance.json",
"artifacts/release/reproducible-build.json",
"artifacts/security/dependency-diff.json",
"artifacts/security/license-report.json",
"artifacts/security/vulnerability-report.json",
"artifacts/security/supply-chain-verification.json",
"artifacts/security/supply-chain-coherence.json",
"artifacts/security/supply-chain-fixtures.json",
"artifacts/security/supply-chain-provider-fixtures.json"
],
"retentionClass": "release-coherence"
},
@@ -224,11 +281,15 @@
"FE-GATE-015": {
"name": "release-coherence",
"steps": [
{ "script": "build", "expect": "pass" },
{ "script": "verify:release", "expect": "pass" }
{ "script": "build:release", "expect": "pass" },
{ "script": "verify:release", "expect": "pass" },
{ "script": "verify:supply-chain:promotion", "expect": "pass" }
],
"logPath": "artifacts/quality/gates/FE-GATE-015.txt",
"evidence": ["artifacts/release/verification.json"],
"evidence": [
"artifacts/release/verification.json",
"artifacts/security/promotion-verification.json"
],
"retentionClass": "release-coherence"
},
"FE-GATE-016": {
@@ -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"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"changes": []
}
+328 -42
View File
@@ -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"
}
]
}
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"snapshotDigest": "ce4fa9b7944f27553067228bd6c9e73e7dc05875c283255b50d7eb3ad2923f6d",
"owner": "frontend-platform",
"reason": "RP-11-initial-transitive-inventory",
"approvedAt": "2026-07-26T08:27:17.874Z"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"changes": []
}
+24
View File
@@ -0,0 +1,24 @@
{
"schemaVersion": 1,
"allowedLicenses": [
"(MIT OR CC0-1.0)",
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BlueOak-1.0.0",
"CC-BY-4.0",
"CC0-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0"
],
"deniedLicensePatterns": [
"(^|\\s)AGPL",
"(^|\\s)GPL",
"SSPL",
"BUSL"
],
"unknownLicensePolicy": "allow-only-unmaterialized-platform-optional"
}
+30
View File
@@ -0,0 +1,30 @@
{
"schemaVersion": 1,
"trackedRoots": [
"src",
"scripts",
"tests",
"config",
"public",
"schemas",
".storybook",
"package.json",
"pnpm-lock.yaml",
"vite.config.js",
"vitest.config.js",
"playwright.config.js"
],
"generatedRoots": ["dist", "artifacts/release"],
"excludedPaths": [
"tests/fixtures/security/secret-detection/forbidden"
],
"allowlist": [
{
"path": "tests/fixtures/security/secret-detection/allowed/test-credentials.ts",
"ruleId": "assigned-secret",
"owner": "frontend-platform",
"reason": "Synthetic credential verifies the scoped test-only allowlist.",
"expiresAt": "2027-07-26T00:00:00.000Z"
}
]
}
@@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"exceptions": []
}
@@ -0,0 +1,8 @@
{
"schemaVersion": 1,
"providerMode": "external-file",
"inputEnvironment": "VULNERABILITY_REPORT_PATH",
"blockAtSeverity": "high",
"allowedSeverities": ["unknown", "low", "moderate", "high", "critical"],
"missingProviderStatus": "FAIL_UNVERIFIED"
}
+92
View File
@@ -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
}
}
]
}
@@ -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는 유지한다.
@@ -0,0 +1,103 @@
# VD-09: 공급망 inventory, license, vulnerability, SBOM과 provenance
- 상태: Accepted
- 결정일: 2026-07-26
- 적용 브랜치: `feature-frontend-supply-chain-verification`
- 재검토: 조직 vulnerability scanner, signing/attestation provider와 dependency
exception 승인 체계가 선정될 때
## 배경
기존 release script는 `package.json`의 직접 dependency 이름과 버전, lockfile
전체 digest, `dist` checksum만 기록했다. 전이 dependency, 패키지별 integrity와
license, 실제 baseline diff가 없었고 `highRiskUnreviewed: []`는 계산 결과가 아닌
고정값이었다. secret scan도 `src``dist`만 검사해 config, scripts, test와
generated release metadata를 놓쳤다.
반면 저장소에는 조직이 선택한 vulnerability source, severity exception 승인자,
signing identity와 attestation 저장소가 없다. 외부 provider가 없는 상태를 빈
finding과 서명 성공으로 표현하면 local 검증과 release promotion을 혼동한다.
## 결정
1. `pnpm-lock.yaml`의 모든 `packages` row와 `pnpm list --depth Infinity`의 실제
graph를 결합해 직접/전이, production/development, required/platform-optional,
version, SHA-512 SRI, license와 dependency edge를 기록한다.
2. inventory row 수는 lockfile package row 수와 같아야 한다. 누락된 전이
dependency, malformed integrity와 non-optional `NOASSERTION`은 local gate를
실패시킨다.
3. license는 설치된 package manifest에서 읽고 closed allow/deny policy로
검사한다. 현재 OS에 materialize되지 않은 platform optional만
`NOASSERTION`과 그 이유를 명시적으로 허용한다.
4. 승인 dependency baseline과 approval digest를 보존하고 현재 lock inventory와
actual add/remove/change/upgrade diff를 계산한다. 새 direct production
dependency는 owner와 서로 다른 reviewer, reason과 rollback evidence가
필요하다.
5. inventory를 CycloneDX 1.6 SBOM으로 투영한다. component 수, lockfile digest,
SRI, license와 dependency edge가 inventory와 일치해야 한다.
6. local in-toto/SLSA 형태 provenance statement는 source set, lockfile, SBOM과
`dist` digest를 연결하되 `LOCAL_UNSIGNED`로 표시한다. 외부 attestation은
provider, signer와 동일 dist subject digest가 있어야 한다.
7. vulnerability adapter는 `VULNERABILITY_REPORT_PATH`가 가리키는
machine-readable provider report를 검증한다. report의 lock digest, provider,
severity와 exception owner/reviewer/reason/expiry가 유효해야 한다.
8. provider report가 없으면 local inventory/license/SBOM/coherence는 `PASS`,
promotion은 `FAIL_UNVERIFIED`다. 빈 finding을 만들어 vulnerability PASS로
표시하지 않는다.
9. secret scan은 source, scripts, tests, tracked config/schema, public, `dist`
generated release metadata를 검사한다. allowlist는 test path에만 허용하며
owner, reason과 expiry가 필요하다. 발견한 secret 원문은 artifact에 쓰지 않고
rule, path, line과 fingerprint만 남긴다.
10. `SOURCE_DATE_EPOCH`를 지원하고 같은 source/lock/config의 production build를
두 번 실행해 전체 dist digest 일치를 검증한 뒤 일반 build를 복원한다.
## 실행 경계와 증적
```text
package.json + frozen pnpm-lock.yaml + installed graph
-> deterministic dependency inventory
-> license policy + approved actual baseline diff
-> CycloneDX SBOM
source/config/lock + production dist
-> local provenance statement
-> optional vulnerability/attestation provider inputs
-> LOCAL PASS | promotion PASS/FAIL_UNVERIFIED
```
- policy: `config/security/`
- generator: `scripts/generate-supply-chain.mjs`
- coherence: `scripts/verify-supply-chain-artifacts.mjs`
- secret scan: `scripts/security-scan.mjs`
- reproducibility: `scripts/verify-reproducible-build.mjs`
- inventory: `artifacts/release/dependency-inventory.json`
- SBOM/provenance: `artifacts/release/sbom.cdx.json`,
`artifacts/release/provenance.json`
- local/promotion status:
`artifacts/security/supply-chain-verification.json`
## 검증
- 현재 lockfile의 561개 package row와 inventory row가 양방향 일치한다.
- ordering-only digest, removal, integrity tamper, baseline tamper, high-risk
self approval, denied license, critical vulnerability와 만료 exception,
provider/digest 오류, SBOM/provenance 불일치 fixture를 검사한다.
- synthetic provider/attestation fixture는 promotion `PASS`를 증명한 후 기본
`FAIL_UNVERIFIED` 상태를 복원한다.
- frozen install은 manifest/lock mismatch fixture를 실제 pnpm으로 거절한다.
- source/config/dist 각각의 synthetic secret fixture가 실제 scan을 실패시키고
scoped test allowlist만 통과한다.
## 한계와 재검토 조건
로컬 manifest license는 법률 검토가 아니며 vulnerability report도 외부 scanner가
제공한 데이터의 최신성 자체를 보증하지 않는다. 실제 프로젝트는 provider 버전,
database freshness, network outage, exception 승인 조직, signing identity,
attestation transparency/retention과 비밀 관리를 결정해야 한다.
## Rollback
외부 scanner/attestor adapter는 환경 입력을 제거하면 즉시
`FAIL_UNVERIFIED`로 돌아간다. local inventory, lock integrity, license, SBOM,
secret, reproducibility와 actual diff gate는 유지한다. scanner 장애를 이유로
promotion을 PASS로 변경하지 않는다.
@@ -12,7 +12,7 @@
- 기본 번들에 포함할 역량과 필요할 때 설치할 확장 역량을 구분한다.
- 특정 벤더를 채택하더라도 제품 코드가 벤더 API에 직접 결합되지 않는지 확인한다.
최초 검토 기준은 `develop``cb195f8`이며, RP-01~RP-08 구현 결과를 이 문서에
최초 검토 기준은 `develop``cb195f8`이며, RP-01~RP-10 구현 결과를 이 문서에
누적 반영했다. 이후 구현으로 경로나 세부 내용이 달라질 수 있으므로, 각 항목은
문서의 경로뿐 아니라 해당 테스트와 아키텍처 게이트로 계속 검증해야 한다.
@@ -39,15 +39,15 @@ recovery 계약, 제거 가능한 reference 수직 슬라이스, form/page, desi
i18n 실행 경계와 diagnostics/telemetry production wiring은 구현됐다. 현재 선행 해결
대상은 다음과 같다.
1. registry evidence와 실제 compatibility diff
2. 공급망과 optional adapter recipe 심화 게이트
1. optional adapter의 opt-in 경계와 제거 가능한 recipe
따라서 현재 상태를 “프론트 공통부가 모두 구현됐다”고 표현하면 범위가 과장된다.
더 정확한 표현은 다음과 같다.
> 운영·안전 계약과 범용 앱 셸은 갖춰졌지만, 기능 개발자가 사용하는 application
> API, 서버 상태, 폼, 라우팅, 페이지 패턴의 표준 수직 경로는 아직 보강이
> 필요하다.
> application API, 서버 상태, 폼, 라우팅, 페이지, 디자인 시스템, 테스트와
> local 공급망 증적의 표준 수직 경로는 갖춰졌다. 현재 남은 저장소 내부 범위는
> opt-in adapter recipe이며 실제 hosting·IdP·취약점/서명/운영 provider는
> 프로젝트 통합 범위다.
## 3. 판정 기준
@@ -77,18 +77,18 @@ 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을 자동 검증 |
| 공급망 검사 | 부분 준비 | lockfile·문서·gate는 있으나 실제 transitive 취약점/license/SBOM 깊이가 부족 | pinned scanner와 policy exception/증적 연결 |
| 단위·통합·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 유지 |
| 공급망 검사 | 준비됨/프로젝트 선택 | 561개 transitive inventory/integrity/license, actual diff, CycloneDX, local provenance, secret/reproducible build gate | 실제 vulnerability scanner와 signed attestation 없이는 promotion `FAIL_UNVERIFIED` |
| realtime·offline·file 등 | 프로젝트 선택 | 현재 없음 | port/adapter recipe와 선택 기준 제공 |
## 5. 우선순위별 발견 사항
@@ -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`
**목표**
@@ -863,6 +892,31 @@ owner와 만료 시한이 있는 quarantine만 허용한다.
RP-11은 P1 최종 저장소 기준선이다. scanner outage를 무검증 승인으로 우회하지
않고 promotion을 보류한다.
**구현 증거 (2026-07-26)**
- VD-09에서 frozen pnpm graph와 lockfile을 local SSOT로, package manifest
license policy와 CycloneDX 1.6을 local evidence로 채택했다. 외부 vulnerability
report와 signed attestation이 없으면 promotion은 `FAIL_UNVERIFIED`다.
- 현재 직접 35개, 전체 전이 561개 dependency의 name/version, direct/scope/
optional, SHA-512 integrity, license와 dependency edge를 deterministic
inventory로 생성한다. lockfile row와 inventory가 양방향 일치하지 않으면
실패한다.
- 승인 baseline digest와 actual add/remove/change/upgrade diff를 계산하고 새
direct production dependency에는 owner와 다른 reviewer, reason과 rollback을
요구한다.
- CycloneDX SBOM component/edge와 local in-toto/SLSA 형태 provenance의
source/lock/SBOM/dist digest를 coherence gate로 다시 계산한다.
- license allow/deny, vulnerability severity와 독립·만료 exception 정책,
provider lock digest와 attestation subject를 machine-readable하게 검증한다.
provider fixture는 promotion PASS를 증명한 뒤 unconfigured
`FAIL_UNVERIFIED`를 복원한다.
- secret scan을 source/scripts/tests/config/schema/public/dist/generated release
metadata로 확장하고 원문 대신 rule/path/line/fingerprint만 SARIF에 남긴다.
test-only allowlist도 owner/reason/expiry를 강제한다.
- `SOURCE_DATE_EPOCH` 기반 동일 build 2회 digest, 실제 frozen install mismatch,
transitive omission/integrity/baseline/self-review/license/vulnerability/
provider/SBOM/provenance/secret negative fixture를 blocking gate에 연결했다.
## 9. P1 exit gate
- 현실적인 form의 validation/dirty/pending/422/conflict가 작동한다.
+42 -13
View File
@@ -1,18 +1,47 @@
# Build and supply-chain gate
Merge and release controls:
## Local blocking controls
- frozen `pnpm-lock.yaml` installation; drift is blocking
- clean production build with hashed assets and build manifest
- machine-readable bundle sizes and checksums
- source plus built-asset credential-pattern scan
- direct dependency inventory and lockfile digest
- base/head dependency diff review record
- `pnpm install --frozen-lockfile` and a real manifest/lock mismatch fixture
- all direct and transitive lockfile rows with package SHA-512 integrity
- production/development, direct/transitive and platform-optional classification
- package-manifest license allow/deny policy
- approved inventory baseline digest and actual add/remove/change/upgrade diff
- independent review for new direct production dependencies
- CycloneDX 1.6 SBOM and inventory component/edge coherence
- source/lock/SBOM/dist-linked local provenance statement
- source, scripts, tests, tracked config/schema, public, built asset and generated
release metadata secret scan
- two-build `SOURCE_DATE_EPOCH` reproducibility check
Organization-specific vulnerability severity, denied-license list, SBOM format,
and scanner selection remain policy inputs. An approved suppression must record
reason, owner, expiry, affected package, and compensating control. Expired
suppressions are blocking.
The canonical commands are:
`artifacts/security/dependency-diff.json` is a local baseline. CI replaces it
with the actual base/head direct and transitive lockfile diff before release.
```bash
corepack pnpm verify:lockfile
corepack pnpm verify:reproducible-build
corepack pnpm build:release
corepack pnpm verify:supply-chain
corepack pnpm check:supply-chain:fixtures
```
`config/security/dependency-baseline.json` is the approved local baseline.
Changing it requires `DEPENDENCY_BASELINE_OWNER` and
`DEPENDENCY_BASELINE_REASON`; editing the digest or hardcoding an empty diff is
rejected.
## External promotion controls
The vulnerability adapter reads the file named by
`VULNERABILITY_REPORT_PATH`. It requires a provider, the exact lockfile digest,
severity findings and valid independent, unexpired exception evidence.
`PROVENANCE_ATTESTATION_PATH` must name a provider, signer and the exact built
dist subject digest.
If either provider input is absent, local verification remains meaningful but
`artifacts/security/supply-chain-verification.json` records
`promotionStatus: FAIL_UNVERIFIED`. `verify:supply-chain:promotion` then exits
non-zero. Scanner or signing outages are not converted to an empty PASS.
Approved vulnerability exceptions require vulnerability/package identity,
owner, a different reviewer, reason and expiry. Expired or self-approved
exceptions are blocking.
@@ -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하지
않고 테스트 내부의 `<TestShell>`만 렌더링한다. 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
+6
View File
@@ -293,6 +293,12 @@ export default [
},
},
},
{
files: [`tests/support/browser/**/*.${sourceExtensions}`],
rules: {
"react-hooks/rules-of-hooks": "off",
},
},
{
files: [
`tests/fixtures/architecture/forbidden/**/*.${sourceExtensions}`,
+26 -1
View File
@@ -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,37 @@
"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",
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.mjs",
"generate:supply-chain": "node scripts/generate-supply-chain.mjs",
"verify:supply-chain": "node scripts/verify-supply-chain-artifacts.mjs",
"update:dependency-baseline": "node scripts/update-dependency-baseline.mjs",
"check:supply-chain:fixtures": "node scripts/check-supply-chain-fixtures.mjs",
"check:supply-chain:provider-fixtures": "node scripts/check-supply-chain-provider-fixtures.mjs",
"verify:supply-chain:promotion": "node scripts/verify-supply-chain-promotion.mjs",
"verify:reproducible-build": "node scripts/verify-reproducible-build.mjs",
"scan:security": "node scripts/security-scan.mjs",
"scan:security:fixture": "node scripts/security-scan.mjs --policy tests/fixtures/security/secret-detection/forbidden-policy.json --artifact artifacts/security/scan-fixture.sarif",
"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 +103,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 +113,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",
+31 -7
View File
@@ -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,
},
},
],
});
+16
View File
@@ -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,
},
});
+34
View File
@@ -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"] } },
],
});
+37
View File
@@ -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" }],
});
+1470 -16
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,4 +1,5 @@
allowBuilds:
esbuild: true
msw: true
minimumReleaseAgeExclude:
- '@playwright/test@1.62.0'
@@ -0,0 +1,60 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://clean-architecture-frontend.local/schemas/dependency-inventory.schema.json",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"packageManager",
"lockfileSha256",
"dependencyCount",
"directDependencyCount",
"dependencies"
],
"properties": {
"schemaVersion": { "const": 2 },
"packageManager": { "type": "string", "minLength": 1 },
"lockfileSha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"dependencyCount": { "type": "integer", "minimum": 1 },
"directDependencyCount": { "type": "integer", "minimum": 1 },
"dependencies": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"version",
"direct",
"scope",
"optional",
"license",
"integrity",
"dependencies"
],
"properties": {
"name": { "type": "string", "minLength": 1 },
"version": { "type": "string", "minLength": 1 },
"direct": { "type": "boolean" },
"scope": {
"enum": ["production", "development"]
},
"optional": { "type": "boolean" },
"license": { "type": "string", "minLength": 1 },
"integrity": {
"type": "string",
"pattern": "^sha512-"
},
"dependencies": {
"type": "array",
"items": { "type": "string", "minLength": 1 }
}
}
}
}
}
}
@@ -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"
]
}
}
},
@@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://clean-architecture-frontend.local/schemas/supply-chain-verification.schema.json",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"localStatus",
"promotionStatus",
"lockfileSha256",
"sourceSetSha256",
"distSha256",
"sbomSha256",
"dependencyDiff",
"highRiskReview",
"vulnerabilityStatus",
"provenanceAttestationStatus",
"failures"
],
"properties": {
"schemaVersion": { "const": 1 },
"localStatus": { "enum": ["PASS", "FAIL"] },
"promotionStatus": {
"enum": ["PASS", "FAIL_UNVERIFIED"]
},
"lockfileSha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"sourceSetSha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"distSha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"sbomSha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"dependencyDiff": { "type": "object" },
"highRiskReview": { "type": "array" },
"vulnerabilityStatus": {
"enum": ["PASS", "FAIL", "FAIL_UNVERIFIED"]
},
"provenanceAttestationStatus": {
"enum": ["PASS", "FAIL_UNVERIFIED"]
},
"failures": {
"type": "array",
"items": { "type": "string" }
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import { spawnSync } from "node:child_process";
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
const fixtureRoot = await mkdtemp(
path.join(tmpdir(), "ca-frontend-frozen-lockfile-"),
);
try {
await cp("pnpm-lock.yaml", path.join(fixtureRoot, "pnpm-lock.yaml"));
const manifest = JSON.parse(await readFile("package.json", "utf8"));
manifest.dependencies.react = "0.0.0-invalid-fixture";
await writeFile(
path.join(fixtureRoot, "package.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
);
const result = spawnSync(
"corepack",
[
"pnpm",
"install",
"--frozen-lockfile",
"--lockfile-only",
"--ignore-scripts",
],
{
cwd: fixtureRoot,
encoding: "utf8",
},
);
if (result.status === 0) {
process.stderr.write("Tampered manifest unexpectedly passed frozen install.\n");
process.exitCode = 1;
} else {
process.stdout.write("Frozen lockfile mismatch fixture: rejected PASS\n");
}
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
+243 -56
View File
@@ -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<string[]>} */
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<Record<string, unknown>>} */ (
canonicalizeRegistryValue({
schemaVersion: 2,
registries: snapshots,
})
);
let baselineDigest = null;
let currentDigest = registrySnapshotDigest(currentSnapshot);
let compatibility =
/** @type {{impact: string, changes: readonly Record<string, unknown>[]}} */ ({
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`,
);
@@ -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`,
);
+88
View File
@@ -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<string, {pct: number}>} actual
* @param {Record<string, number>} 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`,
);
+148
View File
@@ -0,0 +1,148 @@
import { mkdir, writeFile } from "node:fs/promises";
import {
diffDependencyInventories,
isValidSha512Integrity,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
const integrity = `sha512-${Buffer.alloc(64, 1).toString("base64")}`;
const baseDependency = {
name: "base",
version: "1.0.0",
direct: false,
scope: "production",
optional: false,
license: "MIT",
integrity,
dependencies: [],
};
const directDependency = {
...baseDependency,
name: "new-direct",
direct: true,
};
const before = { dependencies: [baseDependency] };
const after = { dependencies: [baseDependency, directDependency] };
const diff = diffDependencyInventories(before, after);
const selfReview = validateDependencyReview(diff, after, {
changes: [
{
changeId: "add:new-direct@1.0.0",
owner: "same-person",
reviewer: "same-person",
reason: "fixture",
rollback: "remove",
},
],
});
const deniedLicense = validateLicensePolicy(
{
dependencies: [{ ...baseDependency, license: "AGPL-3.0" }],
},
{
allowedLicenses: ["MIT"],
deniedLicensePatterns: ["AGPL"],
},
);
const vulnerable = validateVulnerabilityReport(
{
provider: "fixture",
scannedLockfileSha256: "lock",
findings: [
{
id: "CVE-FIXTURE",
packageName: "base",
version: "1.0.0",
severity: "critical",
},
],
},
{ blockAtSeverity: "high" },
{
exceptions: [
{
vulnerabilityId: "CVE-FIXTURE",
packageName: "base",
owner: "owner",
reviewer: "reviewer",
reason: "expired fixture",
expiresAt: "2000-01-01T00:00:00.000Z",
},
],
},
"lock",
new Date("2026-07-26T00:00:00.000Z"),
);
const mismatchedCoherence = verifySupplyChainCoherence(
{
components: [],
metadata: {
properties: [{ name: "ca:lockfileSha256", value: "wrong" }],
},
},
{ dependencies: [baseDependency], lockfileSha256: "lock" },
{
subject: [{ digest: { sha256: "wrong" } }],
predicate: { materials: { lockfileSha256: "wrong" } },
},
"dist",
);
const orderingStable =
supplyChainDigest({ dependencies: [baseDependency, directDependency] }) ===
supplyChainDigest({ dependencies: [directDependency, baseDependency] });
const approvedDigest = supplyChainDigest(before);
const tamperedBaselineRejected =
approvedDigest !==
supplyChainDigest({
dependencies: [{ ...baseDependency, version: "9.9.9-tampered" }],
});
const providerFailure = validateVulnerabilityReport(
{
provider: "",
scannedLockfileSha256: "wrong",
findings: [],
},
{ blockAtSeverity: "high" },
{ exceptions: [] },
"lock",
);
const results = [
{
id: "transitive-removal-is-real-diff",
passed:
diffDependencyInventories(after, before).removed[0] ===
"new-direct@1.0.0",
},
{
id: "tampered-integrity-rejected",
passed: !isValidSha512Integrity("sha512-dGFtcGVyZWQ="),
},
{ id: "high-risk-self-approval-rejected", passed: !selfReview.passed },
{ id: "denied-license-rejected", passed: !deniedLicense.passed },
{
id: "critical-vulnerability-expired-exception-rejected",
passed: !vulnerable.passed,
},
{ id: "sbom-provenance-mismatch-rejected", passed: !mismatchedCoherence.passed },
{ id: "dependency-ordering-deterministic", passed: orderingStable },
{ id: "baseline-digest-tamper-rejected", passed: tamperedBaselineRejected },
{
id: "vulnerability-provider-evidence-invalid",
passed: !providerFailure.passed,
},
];
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-fixtures.json",
`${JSON.stringify({ schemaVersion: 1, results }, null, 2)}\n`,
);
if (results.some((result) => !result.passed)) {
process.stderr.write("Supply-chain negative fixture failed.\n");
process.exit(1);
}
process.stdout.write(`Supply-chain fixtures: ${results.length} PASS\n`);
@@ -0,0 +1,105 @@
import { spawnSync } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
const fixtureDirectory = path.resolve(".tmp/supply-chain-provider-fixture");
await rm(fixtureDirectory, { recursive: true, force: true });
await mkdir(fixtureDirectory, { recursive: true });
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
);
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
);
const vulnerabilityPath = path.join(
fixtureDirectory,
"vulnerability-report.json",
);
const attestationPath = path.join(fixtureDirectory, "attestation.json");
await writeFile(
vulnerabilityPath,
`${JSON.stringify(
{
schemaVersion: 1,
provider: "fixture-scanner",
scannedLockfileSha256: inventory.lockfileSha256,
generatedAt: "2026-07-26T00:00:00.000Z",
findings: [],
},
null,
2,
)}\n`,
);
await writeFile(
attestationPath,
`${JSON.stringify(
{
schemaVersion: 1,
provider: "fixture-attestor",
signer: "fixture-workload-identity",
subject: {
name: "dist",
digest: { sha256: verification.distSha256 },
},
},
null,
2,
)}\n`,
);
const providerRun = spawnSync(
"node",
["scripts/generate-supply-chain.mjs"],
{
env: {
...process.env,
VULNERABILITY_REPORT_PATH: vulnerabilityPath,
PROVENANCE_ATTESTATION_PATH: attestationPath,
},
encoding: "utf8",
},
);
let promotionStatus = "MISSING";
if (providerRun.status === 0) {
promotionStatus = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
).promotionStatus;
}
const restore = spawnSync(
"node",
["scripts/generate-supply-chain.mjs"],
{ encoding: "utf8" },
);
await rm(fixtureDirectory, { recursive: true, force: true });
const passed =
providerRun.status === 0 &&
promotionStatus === "PASS" &&
restore.status === 0;
await writeFile(
"artifacts/security/supply-chain-provider-fixtures.json",
`${JSON.stringify(
{
schemaVersion: 1,
providerAccepted: providerRun.status === 0,
promotionStatus,
unverifiedDefaultRestored: restore.status === 0,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write(
`Supply-chain provider fixture failed: ${providerRun.stderr || restore.stderr}\n`,
);
process.exit(1);
}
process.stdout.write(
"Supply-chain provider fixture: verified PASS and unconfigured default restored\n",
);
+167
View File
@@ -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<string[]>} */
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`,
);
+7 -1
View File
@@ -15,7 +15,13 @@ const buildId = process.env.VITE_BUILD_ID ?? "local-build";
const commitSha = process.env.VITE_COMMIT_SHA ?? "local";
const releaseId = process.env.RELEASE_ID ?? "local-release";
const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`;
const builtAt = new Date().toISOString();
const buildTime = process.env.SOURCE_DATE_EPOCH
? new Date(Number(process.env.SOURCE_DATE_EPOCH) * 1_000)
: new Date();
if (!Number.isFinite(buildTime.getTime())) {
throw new Error("SOURCE_DATE_EPOCH must be epoch seconds");
}
const builtAt = buildTime.toISOString();
const viteManifest = await readFile("dist/.vite/manifest.json", "utf8");
const viteManifestObject =
/** @type {Record<string, {file: string, name?: string, isDynamicEntry?: boolean}>} */ (
+425 -36
View File
@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { gzipSync } from "node:zlib";
import {
@@ -9,47 +10,404 @@ import {
} from "node:fs/promises";
import path from "node:path";
import {
diffDependencyInventories,
flattenPnpmDependencyTree,
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
try {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
} catch {
return [];
}
}
/** @param {string} file */
async function sha256File(file) {
return createHash("sha256").update(await readFile(file)).digest("hex");
}
/** @param {string[]} files */
async function digestFileSet(files) {
const rows = await Promise.all(
files.sort().map(async (file) => ({
path: file.replaceAll("\\", "/"),
sha256: await sha256File(file),
})),
);
return supplyChainDigest(rows);
}
/** @param {string} file @returns {Promise<Record<string, unknown> | null>} */
async function optionalJson(file) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch {
return null;
}
}
export async function buildDependencyInventory() {
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
const lockfileSha256 = createHash("sha256")
.update(lockfileText)
.digest("hex");
const listed = spawnSync(
"corepack",
["pnpm", "list", "--json", "--depth", "Infinity"],
{
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
},
);
if (listed.status !== 0) {
throw new Error(`pnpm dependency graph failed: ${listed.stderr}`);
}
const roots = JSON.parse(listed.stdout);
const root = roots[0];
const flattened = await flattenPnpmDependencyTree(
root,
packageJson.dependencies ?? {},
packageJson.devDependencies ?? {},
);
const lockRows = parsePnpmLockfilePackages(lockfileText);
const lockByIdentity = new Map(
lockRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const failures = [];
const dependencies = flattened.map((dependency) => {
const identity = `${dependency.name}@${dependency.version}`;
const lockRow = lockByIdentity.get(identity);
if (!lockRow) failures.push(`dependency missing from lockfile: ${identity}`);
if (lockRow && !isValidSha512Integrity(lockRow.integrity)) {
failures.push(`dependency has invalid sha512 integrity: ${identity}`);
}
return {
...dependency,
integrity: lockRow?.integrity ?? "missing",
};
});
const inventoryIds = new Set(
dependencies.map((dependency) => `${dependency.name}@${dependency.version}`),
);
for (const lockRow of lockRows) {
const identity = `${lockRow.name}@${lockRow.version}`;
if (!inventoryIds.has(identity)) {
failures.push(`transitive lockfile dependency omitted: ${identity}`);
}
}
if (failures.length > 0) {
throw new Error(failures.join("\n"));
}
return {
schemaVersion: 2,
packageManager: packageJson.packageManager,
lockfileSha256,
dependencyCount: dependencies.length,
directDependencyCount: dependencies.filter((entry) => entry.direct).length,
dependencies,
};
}
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const lockfile = await readFile("pnpm-lock.yaml");
const outputFiles = await filesWithin("dist");
if (outputFiles.length === 0) {
throw new Error("dist is missing; run the production build first");
}
const outputs = await Promise.all(
outputFiles.map(async (outputFile) => {
const content = await readFile(outputFile);
const metadata = await stat(outputFile);
return {
path: outputFile,
path: outputFile.replaceAll("\\", "/"),
bytes: metadata.size,
gzipBytes: gzipSync(content).byteLength,
sha256: createHash("sha256").update(content).digest("hex"),
};
}),
);
const distDigest = supplyChainDigest(
outputs.map(({ path: outputPath, bytes, sha256 }) => ({
path: outputPath,
bytes,
sha256,
})),
);
const inventory = await buildDependencyInventory();
const licensePolicy = JSON.parse(
await readFile("config/security/dependency-policy.json", "utf8"),
);
const licenseResult = validateLicensePolicy(inventory, licensePolicy);
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
const baseline = await optionalJson(
"config/security/dependency-baseline.json",
);
const baselineApproval = await optionalJson(
"config/security/dependency-baseline.approval.json",
);
const dependencyEvidence = JSON.parse(
await readFile(
"config/security/dependency-change-evidence.json",
"utf8",
),
);
const skipsBaseline = process.argv.includes("--no-baseline");
const baselineFailures = [];
let dependencyDiff =
/** @type {ReturnType<typeof diffDependencyInventories>} */ ({
added: [],
removed: [],
changed: [],
upgrades: [],
});
let reviewResult =
/** @type {ReturnType<typeof validateDependencyReview>} */ ({
passed: skipsBaseline,
highRisk: [],
failures: skipsBaseline ? [] : ["dependency baseline unavailable"],
});
if (baseline && baselineApproval) {
const actualBaselineDigest = supplyChainDigest(baseline);
if (
baselineApproval.schemaVersion !== 1 ||
baselineApproval.snapshotDigest !== actualBaselineDigest ||
typeof baselineApproval.owner !== "string" ||
!baselineApproval.owner
) {
baselineFailures.push("dependency baseline approval digest mismatch");
}
dependencyDiff = diffDependencyInventories(baseline, inventory);
reviewResult = validateDependencyReview(
dependencyDiff,
inventory,
dependencyEvidence,
);
} else if (!skipsBaseline) {
baselineFailures.push("dependency baseline and approval are required");
}
const vulnerabilityPolicy = JSON.parse(
await readFile("config/security/vulnerability-policy.json", "utf8"),
);
const vulnerabilityExceptions = JSON.parse(
await readFile("config/security/vulnerability-exceptions.json", "utf8"),
);
const vulnerabilityInput = process.env.VULNERABILITY_REPORT_PATH
? await optionalJson(process.env.VULNERABILITY_REPORT_PATH)
: null;
const vulnerabilityResult = vulnerabilityInput
? validateVulnerabilityReport(
vulnerabilityInput,
vulnerabilityPolicy,
vulnerabilityExceptions,
inventory.lockfileSha256,
)
: {
passed: false,
failures: ["external vulnerability provider report is missing"],
blocking: [],
};
const vulnerabilityReport = {
schemaVersion: 1,
provider: vulnerabilityInput?.provider ?? "UNCONFIGURED",
scannedLockfileSha256:
vulnerabilityInput?.scannedLockfileSha256 ?? inventory.lockfileSha256,
status: vulnerabilityInput
? vulnerabilityResult.passed
? "PASS"
: "FAIL"
: "FAIL_UNVERIFIED",
findings: vulnerabilityInput?.findings ?? [],
exceptionsApplied:
vulnerabilityInput && vulnerabilityResult.passed
? vulnerabilityExceptions.exceptions
: [],
failures: vulnerabilityResult.failures,
blocking: vulnerabilityResult.blocking,
};
const sourceFiles = (
await Promise.all(
[
"src",
"scripts",
"config",
"public",
"schemas",
"package.json",
"pnpm-lock.yaml",
"vite.config.js",
].map(async (target) => {
try {
const metadata = await stat(target);
return metadata.isDirectory() ? filesWithin(target) : [target];
} catch {
return [];
}
}),
)
).flat();
const sourceSetSha256 = await digestFileSet(sourceFiles);
const components = inventory.dependencies.map((dependency) => ({
type: "library",
"bom-ref": `pkg:npm/${encodeURIComponent(dependency.name)}@${dependency.version}`,
name: dependency.name,
version: dependency.version,
scope: dependency.optional ? "optional" : "required",
hashes: [
{
alg: "SHA-512",
content: dependency.integrity.slice("sha512-".length),
},
],
licenses:
dependency.license === "NOASSERTION"
? [{ expression: "NOASSERTION" }]
: [{ expression: dependency.license }],
properties: [
{ name: "ca:direct", value: String(dependency.direct) },
{ name: "ca:scope", value: dependency.scope },
],
}));
const serialSeed = supplyChainDigest({
lockfileSha256: inventory.lockfileSha256,
components: components.map((component) => component["bom-ref"]),
});
const sbom = {
bomFormat: "CycloneDX",
specVersion: "1.6",
serialNumber: `urn:uuid:${serialSeed.slice(0, 8)}-${serialSeed.slice(8, 12)}-${serialSeed.slice(12, 16)}-${serialSeed.slice(16, 20)}-${serialSeed.slice(20, 32)}`,
version: 1,
metadata: {
component: {
type: "application",
name: packageJson.name,
version: packageJson.version,
},
properties: [
{
name: "ca:lockfileSha256",
value: inventory.lockfileSha256,
},
],
},
components,
dependencies: inventory.dependencies.map((dependency) => ({
ref: `pkg:npm/${encodeURIComponent(dependency.name)}@${dependency.version}`,
dependsOn: dependency.dependencies.map((identity) => {
const separator = identity.lastIndexOf("@");
return `pkg:npm/${encodeURIComponent(identity.slice(0, separator))}@${identity.slice(separator + 1)}`;
}),
})),
};
const provenance = {
_type: "https://in-toto.io/Statement/v1",
subject: [{ name: "dist", digest: { sha256: distDigest } }],
predicateType: "https://slsa.dev/provenance/v1",
predicate: {
buildDefinition: {
buildType: "https://vite.dev/build/v1",
externalParameters: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
},
internalParameters: {
sourceSetSha256,
},
resolvedDependencies: [
{
uri: "pnpm-lock.yaml",
digest: { sha256: inventory.lockfileSha256 },
},
],
},
runDetails: {
builder: { id: "local:clean-architecture-frontend-template" },
metadata: { invocationId: "LOCAL_UNSIGNED" },
},
materials: {
lockfileSha256: inventory.lockfileSha256,
sourceSetSha256,
sbomSha256: supplyChainDigest(sbom),
},
},
};
const coherence = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
);
const attestationInput = process.env.PROVENANCE_ATTESTATION_PATH
? await optionalJson(process.env.PROVENANCE_ATTESTATION_PATH)
: null;
const attestationSubject =
/** @type {Record<string, unknown>} */ (
/** @type {Record<string, unknown>} */ (
attestationInput?.subject ?? {}
).digest ?? {}
);
const attestationPassed =
attestationSubject.sha256 === distDigest &&
typeof attestationInput?.provider === "string" &&
Boolean(attestationInput.provider) &&
typeof attestationInput?.signer === "string" &&
Boolean(attestationInput.signer);
const localFailures = [
...licenseResult.failures,
...baselineFailures,
...reviewResult.failures,
...coherence.failures,
];
if (vulnerabilityInput && !vulnerabilityResult.passed) {
localFailures.push(
...vulnerabilityResult.failures,
...vulnerabilityResult.blocking,
);
}
const localPassed = localFailures.length === 0;
const promotionPassed =
localPassed && vulnerabilityResult.passed && attestationPassed;
const verification = {
schemaVersion: 1,
localStatus: localPassed ? "PASS" : "FAIL",
promotionStatus: promotionPassed ? "PASS" : "FAIL_UNVERIFIED",
lockfileSha256: inventory.lockfileSha256,
sourceSetSha256,
distSha256: distDigest,
sbomSha256: supplyChainDigest(sbom),
dependencyDiff,
highRiskReview: reviewResult.highRisk,
vulnerabilityStatus: vulnerabilityReport.status,
provenanceAttestationStatus: attestationPassed
? "PASS"
: "FAIL_UNVERIFIED",
failures: localFailures,
};
const inventory = Object.entries(dependencies)
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, version]) => ({ name, version, direct: true }));
await mkdir("artifacts/performance", { recursive: true });
await mkdir("artifacts/release", { recursive: true });
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/performance/bundle.json",
`${JSON.stringify(
@@ -59,7 +417,8 @@ await writeFile(
context: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
runnerImage: process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
runnerImage:
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
},
outputs,
},
@@ -67,36 +426,66 @@ await writeFile(
2,
)}\n`,
);
await writeFile(
"artifacts/release/dependency-inventory.json",
`${JSON.stringify(
{
schemaVersion: 1,
lockfileSha256: createHash("sha256").update(lockfile).digest("hex"),
dependencies: inventory,
},
null,
2,
)}\n`,
`${JSON.stringify(inventory, null, 2)}\n`,
);
await writeFile(
"artifacts/release/sbom.cdx.json",
`${JSON.stringify(sbom, null, 2)}\n`,
);
await writeFile(
"artifacts/release/provenance.json",
`${JSON.stringify(provenance, null, 2)}\n`,
);
await writeFile(
"artifacts/release/checksums.txt",
`${outputs.map((output) => `${output.sha256} ${output.path}`).join("\n")}\n`,
);
await writeFile(
"artifacts/security/dependency-diff.json",
`${JSON.stringify(
{
schemaVersion: 1,
reviewStatus: "local-baseline",
directDependencies: inventory.length,
highRiskUnreviewed: [],
lockfileSha256: createHash("sha256").update(lockfile).digest("hex"),
schemaVersion: 2,
baselineDigest: baseline ? supplyChainDigest(baseline) : null,
currentDigest: supplyChainDigest(inventory),
...dependencyDiff,
highRisk: reviewResult.highRisk,
reviewFailures: reviewResult.failures,
},
null,
2,
)}\n`,
);
await writeFile(
"artifacts/security/license-report.json",
`${JSON.stringify(
{
schemaVersion: 1,
status: licenseResult.passed ? "PASS" : "FAIL",
dependencyCount: inventory.dependencyCount,
results: licenseResult.results,
failures: licenseResult.failures,
},
null,
2,
)}\n`,
);
await writeFile(
"artifacts/security/vulnerability-report.json",
`${JSON.stringify(vulnerabilityReport, null, 2)}\n`,
);
await writeFile(
"artifacts/security/supply-chain-verification.json",
`${JSON.stringify(verification, null, 2)}\n`,
);
if (!localPassed) {
process.stderr.write(
`Local supply-chain verification failed:\n- ${localFailures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Supply chain: LOCAL PASS (${inventory.dependencyCount} dependencies); promotion=${verification.promotionStatus}\n`,
);
+321
View File
@@ -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<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffRegistrySnapshots(before, after) {
const changes = /** @type {Array<Record<string, unknown>>} */ ([]);
let impact = "none";
const beforeRegistries =
/** @type {Map<string, Record<string, unknown>>} */ (new Map(
/** @type {Array<Record<string, unknown>>} */ (before.registries ?? []).map(
(registry) => [String(registry.registryId), registry],
),
));
const afterRegistries =
/** @type {Map<string, Record<string, unknown>>} */ (new Map(
/** @type {Array<Record<string, unknown>>} */ (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<string, unknown>} */ (previous.contract ?? {});
const currentContract =
/** @type {Record<string, unknown>} */ (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<string, Record<string, unknown>>} */ (
previous.rows ?? {}
);
const afterRows =
/** @type {Record<string, Record<string, unknown>>} */ (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<Record<string, unknown>>} snapshot
* @param {Readonly<Record<string, unknown>>} 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<typeof diffRegistrySnapshots>} diff
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateBreakingEvidence(diff, evidenceFile) {
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
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),
});
}
+547
View File
@@ -0,0 +1,547 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
/** @param {unknown} value @returns {unknown} */
export function canonicalizeSupplyChainValue(value) {
if (Array.isArray(value)) {
return value
.map(canonicalizeSupplyChainValue)
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]),
);
}
return value;
}
/** @param {unknown} value */
export function supplyChainDigest(value) {
return createHash("sha256")
.update(JSON.stringify(canonicalizeSupplyChainValue(value)))
.digest("hex");
}
/** @param {string} lockfile */
export function parsePnpmLockfilePackages(lockfile) {
const entries =
/** @type {Array<{name: string, version: string, integrity: string}>} */ (
[]
);
let inPackages = false;
/** @type {{name: string, version: string, integrity: string} | null} */
let current = null;
for (const line of lockfile.split(/\r?\n/)) {
if (line === "packages:") {
inPackages = true;
continue;
}
if (line === "snapshots:") {
if (current) entries.push(current);
break;
}
if (!inPackages) continue;
const packageMatch = line.match(/^ {2}(\S.*):$/);
if (packageMatch) {
if (current) entries.push(current);
const key = packageMatch[1].replace(/^['"]|['"]$/g, "");
const separator = key.lastIndexOf("@");
current = {
name: key.slice(0, separator),
version: key.slice(separator + 1),
integrity: "",
};
continue;
}
const integrityMatch = line.match(/\bintegrity:\s*([^,}\s]+)/);
if (current && integrityMatch) {
current.integrity = integrityMatch[1];
}
}
return entries.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/** @param {string} integrity */
export function isValidSha512Integrity(integrity) {
if (!integrity.startsWith("sha512-")) return false;
try {
return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64;
} catch {
return false;
}
}
/**
* @param {unknown} raw
* @returns {string}
*/
export function normalizeLicense(raw) {
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (
raw &&
typeof raw === "object" &&
"type" in raw &&
typeof raw.type === "string"
) {
return raw.type;
}
if (Array.isArray(raw)) {
const licenses = raw.map(normalizeLicense).filter(
(license) => license !== "NOASSERTION",
);
return licenses.length > 0 ? licenses.join(" OR ") : "NOASSERTION";
}
return "NOASSERTION";
}
/**
* @param {Record<string, unknown>} root
* @param {Readonly<Record<string, string>>} directProduction
* @param {Readonly<Record<string, string>>} directDevelopment
*/
export async function flattenPnpmDependencyTree(
root,
directProduction,
directDevelopment,
) {
const records =
/** @type {Map<string, {
* name: string,
* version: string,
* direct: boolean,
* scope: "production" | "development",
* optional: boolean,
* packagePath: string,
* dependencies: Set<string>
* }>} */ (new Map());
const directIds = new Set();
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
)) {
if (
Object.hasOwn(directProduction, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
)) {
if (
Object.hasOwn(directDevelopment, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
/**
* @param {Record<string, unknown>} node
* @param {"production" | "development"} scope
* @param {boolean} optionalPath
*/
function visit(node, scope, optionalPath) {
for (const [groupName, group] of Object.entries({
dependencies: node.dependencies,
devDependencies: node.devDependencies,
optionalDependencies: node.optionalDependencies,
})) {
if (!group || typeof group !== "object" || Array.isArray(group)) continue;
for (const [name, rawDependency] of Object.entries(group)) {
if (
!rawDependency ||
typeof rawDependency !== "object" ||
Array.isArray(rawDependency)
) {
continue;
}
const dependency =
/** @type {Record<string, unknown>} */ (rawDependency);
const version = String(dependency.version ?? "");
const packagePath = String(dependency.path ?? "");
const identity = `${name}@${version}`;
const childScope =
scope === "production" && groupName !== "devDependencies"
? "production"
: "development";
const childOptional =
optionalPath || groupName === "optionalDependencies";
const previous = records.get(identity);
const dependencies = previous?.dependencies ?? new Set();
for (const childGroup of [
dependency.dependencies,
dependency.optionalDependencies,
]) {
if (
!childGroup ||
typeof childGroup !== "object" ||
Array.isArray(childGroup)
) {
continue;
}
for (const [childName, rawChild] of Object.entries(childGroup)) {
if (
rawChild &&
typeof rawChild === "object" &&
!Array.isArray(rawChild)
) {
dependencies.add(
`${childName}@${String(rawChild.version ?? "")}`,
);
}
}
}
records.set(identity, {
name,
version,
direct: directIds.has(identity),
scope:
previous?.scope === "production" || childScope === "production"
? "production"
: "development",
optional: previous ? previous.optional && childOptional : childOptional,
packagePath: previous?.packagePath || packagePath,
dependencies,
});
visit(dependency, childScope, childOptional);
}
}
}
const productionRoot = {
dependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
).filter(([name]) => Object.hasOwn(directProduction, name)),
),
};
const developmentRoot = {
devDependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
).filter(([name]) => Object.hasOwn(directDevelopment, name)),
),
};
visit(productionRoot, "production", false);
visit(developmentRoot, "development", false);
const result = [];
for (const record of records.values()) {
let license = "NOASSERTION";
let optional = record.optional;
if (record.packagePath) {
try {
const manifest = JSON.parse(
await readFile(`${record.packagePath}/package.json`, "utf8"),
);
license = normalizeLicense(manifest.license ?? manifest.licenses);
} catch {
// Platform-specific optional packages may not be materialized locally.
optional = true;
}
}
result.push({
name: record.name,
version: record.version,
direct: record.direct,
scope: record.scope,
optional,
license,
dependencies: [...record.dependencies].sort(),
});
}
return result.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/**
* @param {Readonly<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffDependencyInventories(before, after) {
const beforeRows =
/** @type {Array<Record<string, unknown>>} */ (before.dependencies ?? []);
const afterRows =
/** @type {Array<Record<string, unknown>>} */ (after.dependencies ?? []);
const beforeMap = new Map(
beforeRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const afterMap = new Map(
afterRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key));
const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key));
const changed = [];
for (const key of [...beforeMap.keys()].filter((item) => afterMap.has(item))) {
if (
supplyChainDigest(beforeMap.get(key)) !==
supplyChainDigest(afterMap.get(key))
) {
changed.push(key);
}
}
const upgrades = [];
for (const removedKey of removed) {
const previous = beforeMap.get(removedKey);
const replacement = added.find(
(addedKey) => afterMap.get(addedKey)?.name === previous?.name,
);
if (replacement) {
upgrades.push({
name: previous?.name,
from: previous?.version,
to: afterMap.get(replacement)?.version,
});
}
}
return Object.freeze({
added: Object.freeze(added.sort()),
removed: Object.freeze(removed.sort()),
changed: Object.freeze(changed.sort()),
upgrades: Object.freeze(
upgrades.sort((left, right) =>
String(left.name).localeCompare(String(right.name)),
),
),
});
}
/**
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} policy
*/
export function validateLicensePolicy(inventory, policy) {
const allowed = new Set(
/** @type {string[]} */ (policy.allowedLicenses ?? []),
);
const denied = /** @type {string[]} */ (policy.deniedLicensePatterns ?? []);
const failures = [];
const results = [];
for (const dependency of /** @type {Array<Record<string, unknown>>} */ (
inventory.dependencies ?? []
)) {
const license = String(dependency.license ?? "NOASSERTION");
const explicitlyDenied = denied.some((pattern) =>
new RegExp(pattern, "i").test(license),
);
const unknownAccepted =
license === "NOASSERTION" && dependency.optional === true;
const passed =
!explicitlyDenied && (allowed.has(license) || unknownAccepted);
results.push({
package: `${dependency.name}@${dependency.version}`,
license,
passed,
reason: unknownAccepted ? "platform-optional-not-materialized" : null,
});
if (!passed) {
failures.push(
`${dependency.name}@${dependency.version} has disallowed license ${license}`,
);
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
results: Object.freeze(results),
});
}
/**
* @param {ReturnType<typeof diffDependencyInventories>} diff
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateDependencyReview(diff, inventory, evidenceFile) {
const rows =
/** @type {Array<Record<string, unknown>>} */ (inventory.dependencies ?? []);
const byIdentity = new Map(
rows.map((row) => [`${row.name}@${row.version}`, row]),
);
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
evidenceFile.changes ?? []
).map((entry) => [entry.changeId, entry]),
);
const highRisk = diff.added.filter((identity) => {
const row = byIdentity.get(identity);
return row?.direct === true && row.scope === "production";
});
const failures = [];
for (const identity of highRisk) {
const changeId = `add:${identity}`;
const entry = evidence.get(changeId);
if (!entry) {
failures.push(`high-risk dependency missing review: ${changeId}`);
continue;
}
for (const field of ["owner", "reviewer", "reason", "rollback"]) {
if (typeof entry[field] !== "string" || !entry[field].trim()) {
failures.push(`${changeId} missing ${field}`);
}
}
if (entry.owner === entry.reviewer) {
failures.push(`${changeId} may not be self-approved`);
}
}
return Object.freeze({
passed: failures.length === 0,
highRisk: Object.freeze(highRisk),
failures: Object.freeze(failures),
});
}
const severityRank = new Map([
["unknown", 0],
["low", 1],
["moderate", 2],
["high", 3],
["critical", 4],
]);
/**
* @param {Readonly<Record<string, unknown>>} report
* @param {Readonly<Record<string, unknown>>} policy
* @param {Readonly<Record<string, unknown>>} exceptionFile
* @param {string} lockfileSha256
* @param {Date} [now]
*/
export function validateVulnerabilityReport(
report,
policy,
exceptionFile,
lockfileSha256,
now = new Date(),
) {
const failures = [];
if (report.scannedLockfileSha256 !== lockfileSha256) {
failures.push("vulnerability report lockfile digest mismatch");
}
if (typeof report.provider !== "string" || !report.provider.trim()) {
failures.push("vulnerability report provider missing");
}
const threshold = severityRank.get(String(policy.blockAtSeverity)) ?? 3;
const exceptions =
/** @type {Array<Record<string, unknown>>} */ (
exceptionFile.exceptions ?? []
);
const blocking = [];
for (const finding of /** @type {Array<Record<string, unknown>>} */ (
report.findings ?? []
)) {
const severity = String(finding.severity ?? "unknown").toLowerCase();
if ((severityRank.get(severity) ?? 0) < threshold) continue;
const exception = exceptions.find(
(entry) =>
entry.vulnerabilityId === finding.id &&
entry.packageName === finding.packageName,
);
const expiry =
typeof exception?.expiresAt === "string"
? Date.parse(exception.expiresAt)
: Number.NaN;
const validException =
exception &&
typeof exception.owner === "string" &&
exception.owner.trim() &&
typeof exception.reviewer === "string" &&
exception.reviewer.trim() &&
exception.owner !== exception.reviewer &&
typeof exception.reason === "string" &&
exception.reason.trim() &&
Number.isFinite(expiry) &&
expiry > now.getTime();
if (!validException) {
blocking.push(
`${finding.id}:${finding.packageName}@${finding.version}:${severity}`,
);
}
}
return Object.freeze({
passed: failures.length === 0 && blocking.length === 0,
failures: Object.freeze(failures),
blocking: Object.freeze(blocking),
});
}
/**
* @param {Readonly<Record<string, unknown>>} sbom
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} provenance
* @param {string} distDigest
*/
export function verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
) {
const failures = [];
const componentCount = Array.isArray(sbom.components)
? sbom.components.length
: -1;
const dependencyCount = Array.isArray(inventory.dependencies)
? inventory.dependencies.length
: -2;
if (componentCount !== dependencyCount) {
failures.push("SBOM component count does not match inventory");
}
const metadata =
/** @type {Record<string, unknown>} */ (sbom.metadata ?? {});
const properties =
/** @type {Array<{name?: string, value?: string}>} */ (
metadata.properties ?? []
);
if (properties.find(
/** @param {{name?: string, value?: string}} property */
(property) =>
property.name === "ca:lockfileSha256" &&
property.value === inventory.lockfileSha256,
) === undefined) {
failures.push("SBOM lockfile digest does not match inventory");
}
const subject =
/** @type {Array<Record<string, unknown>>} */ (provenance.subject ?? [])[0];
const subjectDigest =
/** @type {Record<string, unknown>} */ (subject?.digest ?? {});
if (subjectDigest.sha256 !== distDigest) {
failures.push("provenance subject does not match built dist digest");
}
const predicate =
/** @type {Record<string, unknown>} */ (provenance.predicate ?? {});
const materials =
/** @type {Record<string, unknown>} */ (predicate.materials ?? {});
if (materials.lockfileSha256 !== inventory.lockfileSha256) {
failures.push("provenance lockfile material does not match inventory");
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
});
}
+149 -44
View File
@@ -1,10 +1,37 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
const scanRoots = ["src", "dist"];
const findings = /** @type {Array<{ruleId: string, file: string}>} */ ([]);
/** @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/security/secret-scan-policy.json",
);
const artifactPath = argumentValue(
"--artifact",
"artifacts/security/scan.sarif",
);
const policy = JSON.parse(await readFile(policyPath, "utf8"));
const findings =
/** @type {Array<{
* ruleId: string,
* file: string,
* line: number,
* fingerprint: string
* }>} */ ([]);
const policyFailures = [];
const patterns = [
{ id: "private-key", expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g },
{
id: "private-key",
expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g,
},
{ id: "aws-access-key", expression: /\bAKIA[0-9A-Z]{16}\b/g },
{ id: "github-token", expression: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g },
{
@@ -14,35 +41,101 @@ const patterns = [
},
];
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat();
/** @param {string} target @returns {Promise<string[]>} */
async function filesWithin(target) {
try {
const metadata = await stat(target);
if (metadata.isFile()) return [target];
const entries = await readdir(target, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const child = path.join(target, entry.name);
return entry.isDirectory() ? filesWithin(child) : [child];
}),
));
return nested.flat();
} catch {
return [];
}
}
for (const root of scanRoots) {
for (const scanFile of await filesWithin(root)) {
if (/\.(png|jpg|jpeg|gif|woff2?|zip)$/i.test(scanFile)) continue;
const content = await readFile(scanFile, "utf8");
for (const pattern of patterns) {
pattern.expression.lastIndex = 0;
if (pattern.expression.test(content)) {
findings.push({ ruleId: pattern.id, file: scanFile });
}
const excluded = new Set(
/** @type {string[]} */ (policy.excludedPaths ?? []).map((entry) =>
entry.replaceAll("\\", "/"),
),
);
const allowlist =
/** @type {Array<{
* path: string,
* ruleId: string,
* owner: string,
* reason: string,
* expiresAt: string
* }>} */ (policy.allowlist ?? []);
for (const entry of allowlist) {
const expiry = Date.parse(entry.expiresAt);
if (
!entry.path.startsWith("tests/") ||
!entry.owner?.trim() ||
!entry.reason?.trim() ||
!Number.isFinite(expiry) ||
expiry <= Date.now()
) {
policyFailures.push(
`invalid or expired secret allowlist entry: ${entry.path}:${entry.ruleId}`,
);
}
}
const roots = [
...(/** @type {string[]} */ (policy.trackedRoots ?? [])),
...(/** @type {string[]} */ (policy.generatedRoots ?? [])),
];
const scanFiles = (
await Promise.all(roots.map((root) => filesWithin(root)))
).flat();
for (const scanFile of [...new Set(scanFiles)].sort()) {
const normalized = scanFile.replaceAll("\\", "/");
if (
[...excluded].some(
(entry) => normalized === entry || normalized.startsWith(`${entry}/`),
) ||
/\.(?:png|jpe?g|gif|webp|woff2?|zip|gz|sarif)$/i.test(normalized)
) {
continue;
}
let content;
try {
content = await readFile(scanFile, "utf8");
} catch {
continue;
}
for (const pattern of patterns) {
pattern.expression.lastIndex = 0;
for (const match of content.matchAll(pattern.expression)) {
const isAllowed = allowlist.some(
(entry) =>
entry.path === normalized &&
entry.ruleId === pattern.id &&
Date.parse(entry.expiresAt) > Date.now(),
);
if (isAllowed) continue;
const prefix = content.slice(0, match.index);
findings.push({
ruleId: pattern.id,
file: normalized,
line: prefix.split(/\r?\n/).length,
fingerprint: createHash("sha256")
.update(`${pattern.id}:${normalized}:${String(match.index)}`)
.digest("hex"),
});
}
}
}
const sarif = {
version: "2.1.0",
$schema:
"https://json.schemastore.org/sarif-2.1.0.json",
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
runs: [
{
tool: {
@@ -54,29 +147,41 @@ const sarif = {
})),
},
},
results: findings.map((finding) => ({
ruleId: finding.ruleId,
message: { text: "Potential secret material must be removed." },
locations: [
{
physicalLocation: {
artifactLocation: { uri: finding.file },
},
results: [
...findings.map((finding) => ({
ruleId: finding.ruleId,
message: {
text: "Potential secret material must be removed.",
},
],
})),
partialFingerprints: {
primaryLocationLineHash: finding.fingerprint,
},
locations: [
{
physicalLocation: {
artifactLocation: { uri: finding.file },
region: { startLine: finding.line },
},
},
],
})),
...policyFailures.map((failure) => ({
ruleId: "invalid-allowlist",
message: { text: failure },
})),
],
},
],
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/scan.sarif",
`${JSON.stringify(sarif, null, 2)}\n`,
);
if (findings.length > 0) {
process.stderr.write(`Security scan found ${findings.length} blocking result(s).\n`);
await mkdir(path.dirname(artifactPath), { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(sarif, null, 2)}\n`);
if (findings.length > 0 || policyFailures.length > 0) {
process.stderr.write(
`Security scan found ${findings.length + policyFailures.length} blocking result(s).\n`,
);
process.exit(1);
}
process.stdout.write("Source and built-asset secret scan: PASS\n");
process.stdout.write(
`Tracked source, config, built asset and artifact secret scan: PASS (${scanFiles.length} files)\n`,
);
+47
View File
@@ -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<Record<string, string>>} */ ({
".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)));
}
+38 -1
View File
@@ -18,6 +18,8 @@ const featureOwnedPaths = [
featureSource,
featureTests,
"tests/e2e/reference-form.spec.js",
"tests/e2e/reference-route.spec.js",
"tests/mocks",
];
const copyTargets = [
"src",
@@ -41,6 +43,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 +51,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 +149,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<string, unknown>} 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 +202,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",
+47
View File
@@ -0,0 +1,47 @@
import { spawnSync } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import { supplyChainDigest } from "./lib/supply-chain.mjs";
const owner = process.env.DEPENDENCY_BASELINE_OWNER;
const reason = process.env.DEPENDENCY_BASELINE_REASON;
if (!owner?.trim() || !reason?.trim()) {
process.stderr.write(
"DEPENDENCY_BASELINE_OWNER and DEPENDENCY_BASELINE_REASON are required.\n",
);
process.exit(2);
}
const commands = /** @type {Array<[string, string[]]>} */ ([
["corepack", ["pnpm", "build"]],
["node", ["scripts/generate-supply-chain.mjs", "--no-baseline"]],
]);
for (const [command, args] of commands) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status ?? 1);
}
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
);
await writeFile(
"config/security/dependency-baseline.json",
`${JSON.stringify(inventory, null, 2)}\n`,
);
await writeFile(
"config/security/dependency-baseline.approval.json",
`${JSON.stringify(
{
schemaVersion: 1,
snapshotDigest: supplyChainDigest(inventory),
owner,
reason,
approvedAt: new Date().toISOString(),
},
null,
2,
)}\n`,
);
process.stdout.write(
`Dependency baseline approved: ${inventory.dependencyCount} packages\n`,
);
+41
View File
@@ -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`);
+76
View File
@@ -0,0 +1,76 @@
import { spawnSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { supplyChainDigest } from "./lib/supply-chain.mjs";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
}
async function distDigest() {
const rows = await Promise.all(
(await filesWithin("dist")).map(async (file) => ({
path: path.relative("dist", file).replaceAll("\\", "/"),
bytes: (await readFile(file)).byteLength,
content: supplyChainDigest(await readFile(file)),
})),
);
return supplyChainDigest(rows);
}
function build(environment = process.env) {
return spawnSync("corepack", ["pnpm", "build"], {
env: environment,
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
}
const deterministicEnvironment = {
...process.env,
SOURCE_DATE_EPOCH: "946684800",
};
const firstBuild = build(deterministicEnvironment);
const firstDigest = firstBuild.status === 0 ? await distDigest() : "BUILD_FAILED";
const secondBuild = build(deterministicEnvironment);
const secondDigest =
secondBuild.status === 0 ? await distDigest() : "BUILD_FAILED";
const restoreBuild = build();
const passed =
firstBuild.status === 0 &&
secondBuild.status === 0 &&
restoreBuild.status === 0 &&
firstDigest === secondDigest;
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/reproducible-build.json",
`${JSON.stringify(
{
schemaVersion: 1,
sourceDateEpoch: deterministicEnvironment.SOURCE_DATE_EPOCH,
firstDigest,
secondDigest,
restored: restoreBuild.status === 0,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write(
`Reproducible build failed: first=${firstDigest} second=${secondDigest}\n`,
);
process.exit(1);
}
process.stdout.write(`Reproducible build: PASS (${firstDigest})\n`);
+121
View File
@@ -0,0 +1,121 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import {
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
}
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
);
const sbom = JSON.parse(
await readFile("artifacts/release/sbom.cdx.json", "utf8"),
);
const provenance = JSON.parse(
await readFile("artifacts/release/provenance.json", "utf8"),
);
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
);
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
const lockfileSha256 = createHash("sha256")
.update(lockfileText)
.digest("hex");
const outputs = await Promise.all(
(await filesWithin("dist")).map(async (file) => {
const content = await readFile(file);
return {
path: file.replaceAll("\\", "/"),
bytes: (await stat(file)).size,
sha256: createHash("sha256").update(content).digest("hex"),
};
}),
);
const distDigest = supplyChainDigest(outputs);
const coherence = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
);
const failures = [...coherence.failures];
if (
inventory.lockfileSha256 !== lockfileSha256 ||
verification.lockfileSha256 !== lockfileSha256
) {
failures.push("inventory/verification lockfile digest mismatch");
}
if (
verification.distSha256 !== distDigest ||
verification.sbomSha256 !== supplyChainDigest(sbom)
) {
failures.push("verification digest set is incoherent");
}
const lockRows = parsePnpmLockfilePackages(lockfileText);
const inventoryRows =
/** @type {Array<Record<string, unknown>>} */ (
inventory.dependencies ?? []
);
const inventoryByIdentity = new Map(
inventoryRows.map((entry) => [
`${entry.name}@${entry.version}`,
entry,
]),
);
if (lockRows.length !== inventoryRows.length) {
failures.push("transitive dependency count differs from lockfile");
}
for (const lockRow of lockRows) {
const identity = `${lockRow.name}@${lockRow.version}`;
const dependency = inventoryByIdentity.get(identity);
if (
!dependency ||
dependency.integrity !== lockRow.integrity ||
!isValidSha512Integrity(lockRow.integrity)
) {
failures.push(`lockfile inventory integrity mismatch: ${identity}`);
}
}
const report = {
schemaVersion: 1,
status: failures.length === 0 ? "PASS" : "FAIL",
dependencyCount: inventoryRows.length,
lockfileSha256,
distSha256: distDigest,
sbomSha256: supplyChainDigest(sbom),
failures,
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-coherence.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (failures.length > 0) {
process.stderr.write(
`Supply-chain artifact coherence failed:\n- ${failures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Supply-chain artifact coherence: PASS (${inventoryRows.length} dependencies)\n`,
);
+30
View File
@@ -0,0 +1,30 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
);
const passed = verification.promotionStatus === "PASS";
const report = {
schemaVersion: 1,
status: passed ? "PASS" : "FAIL_UNVERIFIED",
vulnerabilityStatus: verification.vulnerabilityStatus,
provenanceAttestationStatus:
verification.provenanceAttestationStatus,
lockfileSha256: verification.lockfileSha256,
distSha256: verification.distSha256,
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/promotion-verification.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(
"Supply-chain promotion is FAIL_UNVERIFIED: external vulnerability and signed provenance evidence are required.\n",
);
process.exit(1);
}
process.stdout.write("Supply-chain promotion evidence: PASS\n");
+2
View File
@@ -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(<RuntimeApplication composition={composition} />);
} catch (error) {
+27
View File
@@ -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<Record<string, Readonly<SchemaDefinition>>>} */ (
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",
}),
})
);
@@ -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)
@@ -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",
@@ -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),
@@ -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<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primitives: Story = {
render: () => (
<div className="ui-stack">
<Card title="Actions and status">
<div className="ui-cluster">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button pending pendingLabel="Processing">
Save
</Button>
</div>
<div className="ui-cluster">
<Badge variant="neutral">Neutral</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
</div>
</Card>
<TextField
description="A stable accessible description"
label="Name"
placeholder="Example"
/>
<TextField error="Enter a name" label="Invalid name" value="" readOnly />
<TextArea
defaultValue="Long-form content"
label="Notes"
maxLength={100}
/>
<Alert title="Platform status" variant="info">
The component workshop uses the same tokens and providers as the app.
</Alert>
<ProgressBar label="Build readiness" value={72} />
<Skeleton label="Loading example" />
</div>
),
};
function OverlayExample() {
const [open, setOpen] = useState(false);
return (
<div className="ui-stack">
<Button onClick={() => setOpen(true)}>Open dialog</Button>
<Dialog
actions={<Button onClick={() => setOpen(false)}>Confirm</Button>}
onClose={() => setOpen(false)}
open={open}
title="Confirm platform action"
>
Keyboard dismissal must restore focus to the trigger.
</Dialog>
<Menu
items={[
{ id: "first", label: "First action", onSelect() {} },
{ id: "second", label: "Second action", onSelect() {} },
]}
triggerLabel="Open menu"
/>
<Tabs
defaultValue="one"
label="Example sections"
tabs={[
{ id: "one", label: "One", panel: "First panel" },
{ id: "two", label: "Two", panel: "Second panel" },
]}
/>
</div>
);
}
export const OverlayInteraction: Story = {
render: () => <OverlayExample />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const trigger = canvas.getByRole("button", { name: "Open dialog" });
await userEvent.click(trigger);
const dialog = within(document.body).getByRole("dialog", {
name: "Confirm platform action",
});
await expect(dialog).toBeVisible();
await userEvent.keyboard("{Escape}");
await expect(dialog).not.toBeVisible();
await expect(trigger).toHaveFocus();
},
};
export const LongPseudoLikeContent: Story = {
render: () => (
<Card title="[!! Ćømƥøñëñţ ţøķëñ åñđ ļøñğ ţëжţ vëŕïƒïćåţïøñ !!]">
<p>
[!! Ţhïš šţøŕÿ vëŕïƒïëš ţhåţ å ƥŕïmïţïvë ŕëmåïñš ŕëåđåɓļë
ïñ å ćømƥåćţ ćøñţåïñëŕ. !!]
</p>
<Button>[!! Ćøñţïñüë !!]</Button>
</Card>
),
};
+1 -1
View File
@@ -1,5 +1,5 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
for (const route of Object.values(ROUTE_REGISTRY).map((definition) => {
+1 -22
View File
@@ -1,5 +1,4 @@
import { expect, test } from "@playwright/test";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("boots the public app shell", async ({ page }) => {
await page.goto("/");
@@ -25,26 +24,6 @@ test("navigates to a registry-backed example without a page reload", async ({
).toBeFocused();
});
test("opens the protected integration route through the local demo seam", async ({
page,
}) => {
const protectedRoute = Object.values(ROUTE_REGISTRY).find(
(definition) => definition.access === "integration-defined",
);
if (!protectedRoute) throw new Error("An integration route is required");
await page.goto(protectedRoute.path);
await expect(
page.getByRole("heading", { name: "세션이 필요합니다." }),
).toBeVisible();
await page.getByRole("button", { name: "로그인 시작" }).click();
await expect(
page.getByRole("heading", { name: protectedRoute.title }),
).toBeVisible();
await expect(page.getByText("인증됨")).toBeVisible();
});
test("provides an escape-dismissible mobile navigation", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
test("boots and navigates the compact production shell", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("main")).toBeVisible();
const menu = page.getByRole("button", { name: "메뉴", exact: true });
await expect(menu).toHaveCSS("min-width", "44px");
await menu.click();
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
await page.getByRole("link", { name: "UI 구성요소" }).click();
await expect(page).toHaveURL(/\/examples\/ui$/);
await expect(page.locator("html")).toHaveAttribute("data-build-id", "local-build");
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - window.innerWidth,
);
expect(overflow).toBeLessThanOrEqual(1);
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("runs menu typeahead, tabs and duplicate toast interactions", async ({
page,
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("switches the shell locale and keeps pseudo-locale copy within compact layout", async ({
page,
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
/** @param {import("@playwright/test").Page} page */
async function openReferenceForm(page) {
+36
View File
@@ -0,0 +1,36 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
import { successEnvelope } from "../mocks/contracts/envelopes.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
test("opens the protected integration route through the local demo seam", async ({
page,
}) => {
const protectedRoute = Object.values(ROUTE_REGISTRY).find(
(definition) => definition.access === "integration-defined",
);
if (!protectedRoute) throw new Error("An integration route is required");
await page.route(
"http://localhost:8080/api/reference-resources?*",
(route) =>
route.fulfill({
json: successEnvelope([
{
id: "browser-reference",
name: "Browser reference",
createdAt: "2026-07-26T00:00:00.000Z",
},
]),
}),
);
await page.goto(protectedRoute.path);
await expect(
page.getByRole("heading", { name: "세션이 필요합니다." }),
).toBeVisible();
await page.getByRole("button", { name: "로그인 시작" }).click();
await expect(
page.getByRole("heading", { name: protectedRoute.title }),
).toBeVisible();
await expect(page.getByText("인증됨")).toBeVisible();
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
page,
+1 -1
View File
@@ -1,5 +1,5 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("persists an explicit color scheme through the storage contract", async ({
page,
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("validates and reports the common text-field flow", async ({ page }) => {
await page.goto("/examples/ui");
@@ -1,7 +1,5 @@
// @vitest-environment jsdom
import { HttpResponse, http } from "msw";
import { setupServer } from "msw/node";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
@@ -16,6 +14,9 @@ import {
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.js";
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.jsx";
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.js";
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.js";
import { createStrictMockServer } from "../../mocks/server.js";
const runtimeConfig = {
APP_ENV: "local",
@@ -56,41 +57,23 @@ const releaseManifest = {
const listRequests = vi.fn();
const createRequests = vi.fn();
const resources = [{ id: "reference-1", name: "Existing" }];
const server = setupServer(
http.get("http://app.test/config.json", () =>
HttpResponse.json(runtimeConfig),
),
http.get("http://app.test/release-manifest.json", () =>
HttpResponse.json(releaseManifest),
),
http.get("https://api.test/api/reference-resources", ({ request }) => {
listRequests(new URL(request.url).search);
return HttpResponse.json({
success: true,
data: resources,
meta: { requestId: "request-list", traceId: "trace-list" },
});
}),
http.post("https://api.test/api/reference-resources", async ({ request }) => {
const body = (await request.json()) as { name: string };
createRequests(body);
const created = { id: "reference-created", name: body.name };
resources.push(created);
return HttpResponse.json({
success: true,
data: created,
meta: { requestId: "request-create", traceId: "trace-create" },
});
const mockApi = createStrictMockServer(
...createBootstrapHandlers(runtimeConfig, releaseManifest),
...createReferenceScenarioHandlers({
resources,
onList: listRequests,
onCreate: createRequests,
}),
);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
beforeAll(mockApi.listen);
afterEach(() => {
mockApi.reset();
listRequests.mockClear();
createRequests.mockClear();
resources.splice(1);
});
afterAll(() => server.close());
afterAll(mockApi.close);
const absoluteFetch: typeof fetch = (input, init) => {
if (input instanceof Request) return fetch(input, init);
+225
View File
@@ -0,0 +1,225 @@
{
"schemaVersion": 1,
"cases": [
{
"id": "ordering-only",
"expected": "none",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": {
"A": { "path": "/a", "tags": ["one", "two"] },
"B": { "path": "/b", "tags": ["three"] }
}
}
]
},
"after": {
"registries": [
{
"rows": {
"B": { "tags": ["three"], "path": "/b" },
"A": { "tags": ["two", "one"], "path": "/a" }
},
"contract": { "breakingFields": ["path"] },
"registryId": "FE-REG-TEST"
}
],
"schemaVersion": 2
}
},
{
"id": "row-addition",
"expected": "additive",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": {
"A": { "path": "/a" },
"B": { "path": "/b" }
}
}
]
}
},
{
"id": "behavior-change",
"expected": "behavior-change",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a", "owner": "one" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a", "owner": "two" } }
}
]
}
},
{
"id": "row-removal",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": {
"A": { "path": "/a" },
"B": { "path": "/b" }
}
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a" } }
}
]
}
},
{
"id": "field-type-narrowing",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "limit": 10 } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "limit": "10" } }
}
]
}
},
{
"id": "route-path-change",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/before" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/after" } }
}
]
}
},
{
"id": "registry-contract-narrowing",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": {
"breakingFields": ["path"],
"allowedValues": { "kind": ["one", "two"] }
},
"rows": { "A": { "path": "/a", "kind": "one" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": {
"breakingFields": ["path"],
"allowedValues": { "kind": ["one"] }
},
"rows": { "A": { "path": "/a", "kind": "one" } }
}
]
}
}
],
"breakingEvidence": {
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/before" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/after" } }
}
]
}
},
"tamperedApproval": {
"snapshot": {
"schemaVersion": 2,
"registries": []
},
"approval": {
"schemaVersion": 1,
"snapshotDigest": "tampered",
"owner": "fixture-owner",
"approvedAt": "2026-07-26T00:00:00.000Z"
}
}
}
@@ -0,0 +1,6 @@
{
"schemaVersion": 1,
"snapshotDigest": "0000000000000000000000000000000000000000000000000000000000000000",
"owner": "negative-fixture",
"approvedAt": "2026-07-26T00:00:00.000Z"
}
@@ -0,0 +1 @@
export const client_secret = "fixture-only-secret-value";
@@ -0,0 +1,9 @@
{
"schemaVersion": 1,
"trackedRoots": [
"tests/fixtures/security/secret-detection/forbidden"
],
"generatedRoots": [],
"excludedPaths": [],
"allowlist": []
}
@@ -0,0 +1,3 @@
{
"client_secret": "synthetic-forbidden-secret"
}
@@ -0,0 +1 @@
globalThis.password = "synthetic-built-secret";
@@ -0,0 +1 @@
export const leaked = "AKIAABCDEFGHIJKLMNOP";
@@ -0,0 +1,5 @@
test("false visual pass", async ({ page }) => {
await expect(page).toHaveScreenshot({
mask: [page.locator("body")]
});
});
@@ -0,0 +1 @@
test.skip("unowned indefinite quarantine", async () => {});
+30
View File
@@ -0,0 +1,30 @@
export function successEnvelope<Value>(
data: Value,
requestId = "fixture-request",
) {
return Object.freeze({
success: true as const,
data: structuredClone(data),
meta: Object.freeze({
requestId,
traceId: "fixture-trace",
}),
});
}
export function failureEnvelope(
code: string,
details?: Readonly<Record<string, unknown>>,
) {
return Object.freeze({
success: false as const,
error: Object.freeze({
code,
...(details ? { details: structuredClone(details) } : {}),
}),
meta: Object.freeze({
requestId: "fixture-request",
traceId: "fixture-trace",
}),
});
}
+16
View File
@@ -0,0 +1,16 @@
import { http, HttpResponse } from "msw";
export function createBootstrapHandlers(
runtimeConfig: Readonly<Record<string, unknown>>,
releaseManifest: Readonly<Record<string, unknown>>,
baseUrl = "http://app.test",
) {
return [
http.get(`${baseUrl}/config.json`, () =>
HttpResponse.json(structuredClone(runtimeConfig)),
),
http.get(`${baseUrl}/release-manifest.json`, () =>
HttpResponse.json(structuredClone(releaseManifest)),
),
] as const;
}
+171
View File
@@ -0,0 +1,171 @@
import { delay, http, HttpResponse } from "msw";
import {
failureEnvelope,
successEnvelope,
} from "../contracts/envelopes.js";
import type { HttpScenarioId } from "../scenarios/catalog.js";
import { assertOperationScenario } from "../scenarios/catalog.js";
export type ReferenceResourceFixture = Readonly<{
id: string;
name: string;
createdAt?: string;
}>;
type ScenarioOptions = Readonly<{
baseUrl?: string;
scenarios?: Partial<
Record<
| "LIST_REFERENCE_RESOURCES"
| "CREATE_REFERENCE_RESOURCE"
| "GET_REFERENCE_RESOURCE",
HttpScenarioId
>
>;
resources?: ReferenceResourceFixture[];
onList?(search: string): void;
onCreate?(body: Readonly<Record<string, unknown>>): void;
}>;
const DEFAULT_RESOURCE = Object.freeze({
id: "reference-1",
name: "Reference",
createdAt: "2026-07-26T00:00:00.000Z",
});
async function scenarioResponse(
scenario: HttpScenarioId,
payload: unknown,
attempt: number,
) {
if (scenario === "slow") await delay(50);
if (scenario === "timeout") await delay(30_000);
if (scenario === "network-error") return HttpResponse.error();
if (scenario === "content-type-mismatch") {
return new HttpResponse("<html>not json</html>", {
headers: { "Content-Type": "text/html" },
});
}
if (scenario === "malformed-json") {
return new HttpResponse("{invalid", {
headers: { "Content-Type": "application/json" },
});
}
if (scenario === "envelope-mismatch") {
return HttpResponse.json({ data: payload });
}
if (scenario === "schema-mismatch") {
return HttpResponse.json(successEnvelope({ unexpected: true }));
}
if (
scenario === "auth-persistent-401" ||
(scenario === "auth-recover-once" && attempt === 1)
) {
return HttpResponse.json(failureEnvelope("AUTH_REQUIRED"), {
status: 401,
});
}
if (scenario === "forbidden-403") {
return HttpResponse.json(failureEnvelope("FORBIDDEN"), { status: 403 });
}
if (scenario === "not-found-404") {
return HttpResponse.json(failureEnvelope("NOT_FOUND"), { status: 404 });
}
if (scenario === "conflict-409") {
return HttpResponse.json(failureEnvelope("CONFLICT"), { status: 409 });
}
if (scenario === "validation-422") {
return HttpResponse.json(
failureEnvelope("VALIDATION_REJECTED", {
issues: [{ path: "name", code: "too_small" }],
}),
{ status: 422 },
);
}
if (scenario === "rate-limited-429") {
return HttpResponse.json(failureEnvelope("RATE_LIMITED"), {
status: 429,
headers: { "Retry-After": "1" },
});
}
if (
scenario === "server-terminal-500" ||
(scenario === "server-retry-success" && attempt === 1)
) {
return HttpResponse.json(failureEnvelope("SERVER_FAILURE"), {
status: 503,
});
}
return HttpResponse.json(successEnvelope(payload));
}
export function createReferenceScenarioHandlers(options: ScenarioOptions = {}) {
const baseUrl = options.baseUrl ?? "https://api.test";
const resources = options.resources ?? [{ ...DEFAULT_RESOURCE }];
const attempts = new Map<string, number>();
const scenarioFor = (
operationId: keyof NonNullable<ScenarioOptions["scenarios"]>,
) =>
assertOperationScenario(
operationId,
options.scenarios?.[operationId] ?? "success",
);
const nextAttempt = (operationId: string) => {
const next = (attempts.get(operationId) ?? 0) + 1;
attempts.set(operationId, next);
return next;
};
return [
http.get(`${baseUrl}/api/reference-resources`, ({ request }) => {
options.onList?.(new URL(request.url).search);
const scenario = scenarioFor("LIST_REFERENCE_RESOURCES");
const payload = scenario === "empty" ? [] : resources;
return scenarioResponse(
scenario,
payload,
nextAttempt("LIST_REFERENCE_RESOURCES"),
);
}),
http.post(
`${baseUrl}/api/reference-resources`,
async ({ request }) => {
const rawBody = await request.json();
const body =
rawBody &&
typeof rawBody === "object" &&
!Array.isArray(rawBody)
? (rawBody as Readonly<Record<string, unknown>>)
: {};
options.onCreate?.(body);
const scenario = scenarioFor("CREATE_REFERENCE_RESOURCE");
const created = {
id: "reference-created",
name: String(body.name ?? "Created"),
createdAt: "2026-07-26T00:00:00.000Z",
};
if (scenario === "success") resources.push(created);
return scenarioResponse(
scenario,
created,
nextAttempt("CREATE_REFERENCE_RESOURCE"),
);
},
),
http.get(
`${baseUrl}/api/reference-resources/:resourceId`,
({ params }) => {
const scenario = scenarioFor("GET_REFERENCE_RESOURCE");
const resource =
resources.find((entry) => entry.id === params.resourceId) ??
DEFAULT_RESOURCE;
return scenarioResponse(
scenario,
resource,
nextAttempt("GET_REFERENCE_RESOURCE"),
);
},
),
] as const;
}
+45
View File
@@ -0,0 +1,45 @@
export const HTTP_SCENARIO_IDS = Object.freeze([
"success",
"empty",
"slow",
"network-error",
"timeout",
"aborted",
"content-type-mismatch",
"malformed-json",
"envelope-mismatch",
"schema-mismatch",
"auth-recover-once",
"auth-persistent-401",
"forbidden-403",
"not-found-404",
"conflict-409",
"validation-422",
"rate-limited-429",
"server-retry-success",
"server-terminal-500",
] as const);
export type HttpScenarioId = (typeof HTTP_SCENARIO_IDS)[number];
export const OPERATION_SCENARIO_CATALOG = Object.freeze({
LIST_REFERENCE_RESOURCES: HTTP_SCENARIO_IDS,
CREATE_REFERENCE_RESOURCE: Object.freeze(
HTTP_SCENARIO_IDS.filter(
(scenario) => !["empty", "aborted"].includes(scenario),
),
),
GET_REFERENCE_RESOURCE: HTTP_SCENARIO_IDS,
} satisfies Readonly<Record<string, readonly HttpScenarioId[]>>);
export function assertOperationScenario(
operationId: keyof typeof OPERATION_SCENARIO_CATALOG,
scenario: HttpScenarioId,
) {
if (!OPERATION_SCENARIO_CATALOG[operationId].includes(scenario)) {
throw new Error(
`Scenario ${scenario} is not declared for ${operationId}`,
);
}
return scenario;
}
+13
View File
@@ -0,0 +1,13 @@
import { setupServer } from "msw/node";
export function createStrictMockServer(
...handlers: Parameters<typeof setupServer>
) {
const server = setupServer(...handlers);
return Object.freeze({
server,
listen: () => server.listen({ onUnhandledRequest: "error" }),
reset: () => server.resetHandlers(),
close: () => server.close(),
});
}
+24
View File
@@ -0,0 +1,24 @@
import AxeBuilder from "@axe-core/playwright";
import {
expect,
test,
} from "../support/browser/strict-browser-test.js";
test("runs the isolated overlay interaction with blocking accessibility", async ({
page,
}) => {
await page.goto(
"/iframe.html?id=platform-design-system--overlay-interaction&viewMode=story",
);
const trigger = page.getByRole("button", { name: "Open dialog" });
await expect(trigger).toBeVisible();
await trigger.click();
await expect(
page.getByRole("dialog", { name: "Confirm platform action" }),
).toBeVisible();
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
await page.keyboard.press("Escape");
await expect(trigger).toBeFocused();
});
@@ -0,0 +1,46 @@
import {
expect,
test as base,
type ConsoleMessage,
type Page,
} from "@playwright/test";
function ignoredConsole(message: ConsoleMessage) {
return (
message.type() === "warning" &&
message.text().includes("NO_COLOR")
);
}
export const test = base.extend({
page: async ({ page }, use) => {
const failures: string[] = [];
const onConsole = (message: ConsoleMessage) => {
if (
["error", "warning"].includes(message.type()) &&
!ignoredConsole(message)
) {
failures.push(`console:${message.type()}:${message.text()}`);
}
};
const onPageError = (error: Error) => {
failures.push(`pageerror:${error.name}`);
};
const onRequestFailed = (request: import("@playwright/test").Request) => {
failures.push(
`requestfailed:${request.method()}:${new URL(request.url()).pathname}:${request.failure()?.errorText ?? "unknown"}`,
);
};
page.on("console", onConsole);
page.on("pageerror", onPageError);
page.on("requestfailed", onRequestFailed);
await use(page);
page.off("console", onConsole);
page.off("pageerror", onPageError);
page.off("requestfailed", onRequestFailed);
expect(failures, "unexpected browser console/page errors").toEqual([]);
},
});
export { expect };
export type { Page };
+69
View File
@@ -0,0 +1,69 @@
import { vi } from "vitest";
export function createDeterministicClock(start = 0) {
let current = start;
return Object.freeze({
now: () => current,
sleep: async (milliseconds: number, signal?: AbortSignal) => {
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
current += Math.max(0, milliseconds);
},
advance(milliseconds: number) {
current += Math.max(0, milliseconds);
},
});
}
export function createSeededRandom(seed = 1) {
let state = seed >>> 0;
return () => {
state = (state * 1_664_525 + 1_013_904_223) >>> 0;
return state / 0x1_0000_0000;
};
}
export function createControlledScheduler() {
const callbacks: Array<() => void> = [];
return Object.freeze({
callbacks,
setTimeout: vi.fn((callback: () => void) => {
callbacks.push(callback);
return callbacks.length - 1;
}),
clearTimeout: vi.fn(),
runNext() {
callbacks.shift()?.();
},
});
}
export function createRecordingStorage() {
const values = new Map<string, string>();
const operations: Array<
Readonly<{ operation: "get" | "set" | "remove"; key: string }>
> = [];
const storage: Storage = {
getItem(key) {
operations.push({ operation: "get", key });
return values.get(key) ?? null;
},
setItem(key, value) {
operations.push({ operation: "set", key });
values.set(key, value);
},
removeItem(key) {
operations.push({ operation: "remove", key });
values.delete(key);
},
clear() {
values.clear();
},
key(index) {
return [...values.keys()][index] ?? null;
},
get length() {
return values.size;
},
};
return Object.freeze({ storage, operations, values });
}
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import {
canonicalRegistryJson,
diffRegistrySnapshots,
registrySnapshotDigest,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "../../scripts/lib/registry-compatibility.mjs";
function snapshot(
rows: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
) {
return {
schemaVersion: 2,
registries: [
{
registryId: "FE-REG-TEST",
contract: { breakingFields: ["path"] },
rows,
},
],
};
}
describe("registry compatibility evidence", () => {
it("canonicalizes object and primitive-array ordering", () => {
expect(
canonicalRegistryJson({
second: ["b", "a"],
first: { z: 1, a: 2 },
}),
).toBe('{"first":{"a":2,"z":1},"second":["a","b"]}');
});
it("classifies additive, behavioral and breaking actual diffs", () => {
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a" } }),
snapshot({ A: { path: "/a" }, B: { path: "/b" } }),
).impact,
).toBe("additive");
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a", owner: "one" } }),
snapshot({ A: { path: "/a", owner: "two" } }),
).impact,
).toBe("behavior-change");
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a" } }),
snapshot({ A: { path: "/moved" } }),
).impact,
).toBe("breaking");
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a" } }),
snapshot({}),
).impact,
).toBe("breaking");
});
it("verifies the approved digest and complete breaking evidence", () => {
const before = snapshot({ A: { path: "/before" } });
const after = snapshot({ A: { path: "/after" } });
const digest = registrySnapshotDigest(before);
expect(
verifyRegistryBaselineApproval(before, {
schemaVersion: 1,
snapshotDigest: digest,
owner: "platform",
approvedAt: "2026-07-26T00:00:00.000Z",
}).passed,
).toBe(true);
expect(
verifyRegistryBaselineApproval(before, {
schemaVersion: 1,
snapshotDigest: "tampered",
owner: "platform",
approvedAt: "2026-07-26T00:00:00.000Z",
}).passed,
).toBe(false);
const diff = diffRegistrySnapshots(before, after);
expect(validateBreakingEvidence(diff, { changes: [] }).passed).toBe(false);
const changeId = diff.changes[0]?.changeId;
expect(
validateBreakingEvidence(diff, {
changes: [
{
changeId,
versionBump: "2",
migration: "dual-read",
compatibilityWindow: "one release",
rollback: "restore previous registry snapshot",
owner: "platform",
},
],
}).passed,
).toBe(true);
});
});
+18 -10
View File
@@ -2,24 +2,32 @@ import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
describe("registry governance manifest", () => {
it("declares exactly nine single-owner registries and impact labels", async () => {
it("declares ten typed, single-owner executable registries", async () => {
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
);
const registries =
/** @type {Array<{registryId: string, owner: string}>} */ (
/** @type {Array<{
* registryId: string,
* owner: string,
* requiredFields: string[],
* fieldTypes: Record<string, string>
* }>} */ (
governance.registries
);
expect(governance.registries).toHaveLength(9);
expect(governance.registries).toHaveLength(10);
expect(new Set(registries.map((entry) => entry.registryId)).size).toBe(
9,
10,
);
expect(registries.every((entry) => entry.owner)).toBe(true);
expect(governance.compatibilityImpact.allowed).toEqual([
"none",
"additive",
"behavior-change",
"breaking",
]);
expect(
registries.every(
(entry) =>
Array.isArray(entry.requiredFields) &&
entry.requiredFields.length > 0 &&
entry.fieldTypes &&
Object.keys(entry.fieldTypes).length > 0,
),
).toBe(true);
});
});
+89
View File
@@ -0,0 +1,89 @@
import { describe, expect, it } from "vitest";
import {
diffDependencyInventories,
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
} from "../../scripts/lib/supply-chain.mjs";
const integrity = `sha512-${Buffer.alloc(64, 7).toString("base64")}`;
const dependency = {
name: "fixture",
version: "1.0.0",
direct: true,
scope: "production",
optional: false,
license: "MIT",
integrity,
dependencies: [],
};
describe("supply-chain policy", () => {
it("parses every top-level lockfile package and validates SRI", () => {
const parsed = parsePnpmLockfilePackages(`
packages:
'@scope/one@1.0.0':
resolution: {integrity: ${integrity}}
two@2.0.0:
resolution: {integrity: ${integrity}}
snapshots:
`);
expect(parsed).toEqual([
{ name: "@scope/one", version: "1.0.0", integrity },
{ name: "two", version: "2.0.0", integrity },
]);
expect(parsed.every((entry) => isValidSha512Integrity(entry.integrity))).toBe(
true,
);
});
it("keeps inventory digests stable when dependency ordering changes", () => {
const other = { ...dependency, name: "other" };
expect(supplyChainDigest([dependency, other])).toBe(
supplyChainDigest([other, dependency]),
);
});
it("calculates actual additions and requires independent high-risk review", () => {
const before = { dependencies: [] };
const after = { dependencies: [dependency] };
const diff = diffDependencyInventories(before, after);
expect(diff.added).toEqual(["fixture@1.0.0"]);
expect(
validateDependencyReview(diff, after, {
changes: [
{
changeId: "add:fixture@1.0.0",
owner: "one",
reviewer: "one",
reason: "fixture",
rollback: "remove",
},
],
}).passed,
).toBe(false);
});
it("allows explicit policy licenses and rejects denied licenses", () => {
expect(
validateLicensePolicy(
{ dependencies: [dependency] },
{ allowedLicenses: ["MIT"], deniedLicensePatterns: ["AGPL"] },
).passed,
).toBe(true);
expect(
validateLicensePolicy(
{
dependencies: [{ ...dependency, license: "AGPL-3.0" }],
},
{ allowedLicenses: ["MIT"], deniedLicensePatterns: ["AGPL"] },
).passed,
).toBe(false);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+43
View File
@@ -0,0 +1,43 @@
import {
expect,
test,
} from "../support/browser/strict-browser-test.js";
test("wide application shell visual contract", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await expect(page.getByRole("main")).toBeVisible();
await expect(page).toHaveScreenshot("app-shell-wide-light.png", {
fullPage: true,
});
});
test("compact drawer and pseudo-locale visual contract", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await page.locator("#locale-preference").selectOption("en-XA");
await page.locator(".app-shell__menu-button").click();
await expect(page.locator(".ui-drawer")).toHaveJSProperty("open", true);
await expect(page).toHaveScreenshot("app-shell-compact-pseudo-drawer.png", {
fullPage: true,
});
});
test("design-system gallery dark visual contract", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto("/examples/ui");
await page.locator("#theme-preference").selectOption("dark");
await expect(page.getByRole("main")).toHaveScreenshot(
"design-system-gallery-dark.png",
);
});
test("loading empty error and access surfaces visual contract", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 1100 });
await page.goto("/examples/states");
await expect(page.getByRole("main")).toHaveScreenshot(
"state-surfaces-light.png",
);
});
+1 -1
View File
@@ -4,6 +4,6 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node"]
},
"include": ["scripts", "vite.config.js"],
"include": ["scripts", ".storybook", "vite.config.js"],
"exclude": ["dist", "node_modules", "tests"]
}
+1 -1
View File
@@ -10,7 +10,7 @@
"tests/**/*.ts",
"tests/**/*.tsx",
"vitest.config.js",
"playwright.config.js"
"playwright*.config.js"
],
"exclude": [
"dist",
+13 -1
View File
@@ -10,7 +10,19 @@ export default defineConfig({
testTimeout: 10_000,
exclude: [...configDefaults.exclude, ".tmp/**"],
coverage: {
reporter: ["text", "json-summary"],
provider: "v8",
reportsDirectory: "artifacts/tests/coverage",
reporter: ["text", "json-summary", "lcov"],
include: [
"src/application/policies/**/*.{js,ts}",
"src/application/use-cases/**/*.{js,ts}",
"src/contracts/diagnostics.ts",
"src/adapters/http/retry-policy.js",
"src/adapters/storage/browser-storage-adapter.js",
"src/adapters/telemetry/best-effort-telemetry.js",
"scripts/lib/registry-compatibility.mjs",
],
exclude: ["**/*.d.ts", "**/*.stories.*"],
},
},
});