234 lines
13 KiB
JSON
234 lines
13 KiB
JSON
{
|
|
"$schema": "../../schemas/config/frontend-capability-recipes.schema.json",
|
|
"schemaVersion": 1,
|
|
"decisionId": "VD-10",
|
|
"defaultStatus": "NOT_INSTALLED",
|
|
"productionRuntimeDependencies": [],
|
|
"catalogOwner": "frontend-platform",
|
|
"reviewOn": "project-capability-selection",
|
|
"vendorPackagePatterns": [
|
|
"@launchdarkly/*",
|
|
"@sentry/*",
|
|
"@opentelemetry/*",
|
|
"@openapitools/openapi-generator-cli",
|
|
"@reduxjs/toolkit",
|
|
"@tanstack/react-virtual",
|
|
"@uppy/*",
|
|
"firebase",
|
|
"idb",
|
|
"react-window",
|
|
"redux",
|
|
"socket.io-client",
|
|
"tus-js-client",
|
|
"workbox-window",
|
|
"xstate",
|
|
"zustand"
|
|
],
|
|
"recipes": [
|
|
{
|
|
"id": "realtime",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "The backend exposes ordered push events with a documented resume and authorization protocol.",
|
|
"forbiddenWhen": ["Polling satisfies the measured freshness requirement.", "Event ordering and reconnect ownership are undefined."],
|
|
"boundary": "outbound connection plus inbound validated event adapter",
|
|
"port": "RealtimePort",
|
|
"fake": "FakeRealtimeAdapter",
|
|
"failureKinds": ["disconnect", "duplicate", "out-of-order", "auth-expiry"],
|
|
"lifecycleMethods": ["unsubscribe"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Validate every event envelope.", "Never place credentials in URLs or telemetry.", "Refresh authorization through the session boundary."],
|
|
"bundleBudgetGzipBytes": 12000,
|
|
"fallback": "Bounded polling or explicitly stale UI.",
|
|
"removal": ["Remove composition registration.", "Remove adapter and vendor dependency.", "Run recipe-removal and production-bundle gates."],
|
|
"serverStatePolicy": "query-cache-owned"
|
|
},
|
|
{
|
|
"id": "offline-indexeddb",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A product requirement needs durable offline data or a durable command queue beyond small public preferences.",
|
|
"forbiddenWhen": ["The data contains credentials.", "The browser would connect directly to a database or object store.", "A normal HTTP cache is sufficient."],
|
|
"boundary": "application-owned versioned repository output port",
|
|
"port": "VersionedOfflineRepository",
|
|
"fake": "MemoryOfflineRepository",
|
|
"failureKinds": ["quota", "corruption", "migration-rollback"],
|
|
"lifecycleMethods": ["close"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Classify persisted fields.", "Encrypting in the same client is not a credential protection boundary.", "Version and test every migration."],
|
|
"bundleBudgetGzipBytes": 8000,
|
|
"fallback": "Online-only query path with an explicit offline state.",
|
|
"removal": ["Stop writes.", "Migrate or purge owned stores.", "Remove repository composition and dependency."],
|
|
"serverStatePolicy": "reference-or-command-only"
|
|
},
|
|
{
|
|
"id": "service-worker-pwa",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "Installability or a measured offline-shell requirement is approved with cache ownership.",
|
|
"forbiddenWhen": ["Hosting cache and worker cache ownership conflict.", "Update and rollback UX is undefined."],
|
|
"boundary": "bootstrap update controller and cache policy adapter",
|
|
"port": "ServiceWorkerUpdatePort",
|
|
"fake": "FakeServiceWorkerUpdateAdapter",
|
|
"failureKinds": ["stale-worker", "update-loop", "offline-fallback"],
|
|
"lifecycleMethods": ["unregister", "rollback"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Never cache authenticated API responses by default.", "Bind cache names to release identity.", "Fail closed on malformed update metadata."],
|
|
"bundleBudgetGzipBytes": 10000,
|
|
"fallback": "Normal network application with hosting cache headers.",
|
|
"removal": ["Deploy an unregister migration.", "Delete owned caches.", "Remove worker registration and manifest."],
|
|
"serverStatePolicy": "network-cache-policy-only"
|
|
},
|
|
{
|
|
"id": "file-transfer",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "The product accepts or delivers files with progress and cancellation requirements.",
|
|
"forbiddenWhen": ["Allowed size and MIME policy is missing.", "Long-lived credentials would be embedded in URLs."],
|
|
"boundary": "application file transfer output port behind an authorized backend protocol",
|
|
"port": "FileTransferPort",
|
|
"fake": "FakeFileTransferAdapter",
|
|
"failureKinds": ["size-rejection", "type-rejection", "abort", "expired-url"],
|
|
"lifecycleMethods": ["cancel-via-AbortSignal"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Treat MIME as untrusted metadata.", "Use short-lived opaque resource identifiers.", "Redact file names when classified as personal data."],
|
|
"bundleBudgetGzipBytes": 6000,
|
|
"fallback": "Standard request with bounded size and no background continuation.",
|
|
"removal": ["Cancel active transfers.", "Remove route actions and composition.", "Remove transfer dependency."],
|
|
"serverStatePolicy": "query-cache-metadata-only"
|
|
},
|
|
{
|
|
"id": "generated-api",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A versioned backend contract justifies generated transport code.",
|
|
"forbiddenWhen": ["Generated DTOs would escape into domain or presentation.", "Contract drift cannot block CI."],
|
|
"boundary": "generated client wrapped by a feature gateway facade and mapper",
|
|
"port": "GeneratedApiFacade",
|
|
"fake": "FakeGeneratedApiAdapter",
|
|
"failureKinds": ["contract-drift", "unsupported-field"],
|
|
"lifecycleMethods": ["cancel-via-AbortSignal"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Generate from an authenticated source.", "Review generator execution and output.", "Do not log request bodies."],
|
|
"bundleBudgetGzipBytes": 16000,
|
|
"fallback": "Existing typed request builder and runtime response schema.",
|
|
"removal": ["Restore handwritten gateway.", "Remove generated output and generator.", "Verify DTOs do not remain in public types."],
|
|
"serverStatePolicy": "query-cache-owned"
|
|
},
|
|
{
|
|
"id": "feature-flag",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A staged rollout or kill switch has a named owner, default and stale policy.",
|
|
"forbiddenWhen": ["A flag is used as authorization.", "Unknown and unavailable behavior is undefined."],
|
|
"boundary": "application feature policy output port",
|
|
"port": "FeatureFlagPort",
|
|
"fake": "FakeFeatureFlagAdapter",
|
|
"failureKinds": ["provider-unavailable", "unknown-flag", "stale-value"],
|
|
"lifecycleMethods": ["dispose-provider-if-installed"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Flags are hints, never access control.", "Minimize targeting attributes.", "Apply consent rules to personal attributes."],
|
|
"bundleBudgetGzipBytes": 10000,
|
|
"fallback": "Typed local default with an explicit stale decision.",
|
|
"removal": ["Resolve the rollout permanently.", "Delete flag key and branches.", "Remove provider composition and dependency."],
|
|
"serverStatePolicy": "policy-cache-only"
|
|
},
|
|
{
|
|
"id": "web-worker",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "Profiling shows CPU work blocking the main thread beyond the performance budget.",
|
|
"forbiddenWhen": ["The task is primarily network I/O.", "Cancellation and stale-result ownership are undefined."],
|
|
"boundary": "request/result/cancel output port with a validated message adapter",
|
|
"port": "WorkerTaskPort",
|
|
"fake": "FakeWorkerTaskAdapter",
|
|
"failureKinds": ["crash", "stale-result", "transfer-failure"],
|
|
"lifecycleMethods": ["cancel", "dispose"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Validate worker messages.", "Do not send credentials.", "Bound transferred data and worker count."],
|
|
"bundleBudgetGzipBytes": 14000,
|
|
"fallback": "Chunked or deferred main-thread execution within a measured limit.",
|
|
"removal": ["Stop and dispose workers.", "Restore synchronous facade implementation.", "Remove worker entry and chunk."],
|
|
"serverStatePolicy": "no-server-state"
|
|
},
|
|
{
|
|
"id": "multi-tab",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A documented workflow must synchronize non-sensitive events across tabs.",
|
|
"forbiddenWhen": ["The server is the correct conflict authority.", "Event version and source identity are undefined."],
|
|
"boundary": "versioned browser event output/input adapter",
|
|
"port": "MultiTabPort",
|
|
"fake": "FakeMultiTabAdapter",
|
|
"failureKinds": ["self-echo", "duplicate", "conflict"],
|
|
"lifecycleMethods": ["unsubscribe", "close"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Broadcast no credentials or personal payload.", "Validate versions.", "Treat events as hints rather than authorization."],
|
|
"bundleBudgetGzipBytes": 4000,
|
|
"fallback": "Refresh from the authoritative server on focus.",
|
|
"removal": ["Close channels.", "Remove event registry entries.", "Restore focus-based refresh."],
|
|
"serverStatePolicy": "invalidation-only"
|
|
},
|
|
{
|
|
"id": "browser-permission",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A user-initiated flow requires clipboard, notification or media access.",
|
|
"forbiddenWhen": ["Permission would be requested at boot.", "Denied, dismissed and unsupported UX are not designed."],
|
|
"boundary": "presentation input action through a browser capability output port",
|
|
"port": "BrowserPermissionPort",
|
|
"fake": "FakeBrowserPermissionAdapter",
|
|
"failureKinds": ["denied", "dismissed", "unsupported"],
|
|
"lifecycleMethods": ["stop-media-tracks-if-opened"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Require an explicit user gesture.", "Minimize requested scope.", "Do not persist permission as authorization."],
|
|
"bundleBudgetGzipBytes": 3000,
|
|
"fallback": "Manual input or copy/download instruction.",
|
|
"removal": ["Stop acquired resources.", "Remove permission action and adapter.", "Retest denied-path accessibility."],
|
|
"serverStatePolicy": "no-server-state"
|
|
},
|
|
{
|
|
"id": "client-workflow",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A measured cross-page client-only workflow cannot be represented by URL, local state, context or query cache.",
|
|
"forbiddenWhen": ["The store would duplicate server response collections.", "A library is selected before state ownership is documented.", "Zustand and Redux Toolkit would both be installed."],
|
|
"boundary": "workflow-specific local facade; vendor types remain in its adapter",
|
|
"port": "ClientWorkflowPort",
|
|
"fake": "FakeClientWorkflowAdapter",
|
|
"failureKinds": ["reset", "version-mismatch", "server-state-duplication"],
|
|
"lifecycleMethods": ["unsubscribe", "reset"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Persist only explicitly classified workflow fields.", "Never persist credentials.", "Define logout and version reset."],
|
|
"bundleBudgetGzipBytes": 9000,
|
|
"fallback": "URL, component state, context and TanStack Query ownership.",
|
|
"removal": ["Move remaining state to its natural owner.", "Remove facade and one selected store dependency.", "Verify logout/reset."],
|
|
"serverStatePolicy": "reference-only"
|
|
},
|
|
{
|
|
"id": "large-data-ui",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "Production-like profiling proves a list or grid exceeds interaction and rendering budgets.",
|
|
"forbiddenWhen": ["Pagination solves the scale requirement.", "Keyboard and screen-reader focus behavior is undefined."],
|
|
"boundary": "presentation facade around virtualizer or data-grid behavior",
|
|
"port": "LargeDataUiFacade",
|
|
"fake": "FakeLargeDataUiAdapter",
|
|
"failureKinds": ["focus-loss", "stale-row", "scale-limit"],
|
|
"lifecycleMethods": ["dispose-observers-if-installed"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Render only authorized rows.", "Do not expose hidden row data to telemetry.", "Preserve accessible row identity."],
|
|
"bundleBudgetGzipBytes": 30000,
|
|
"fallback": "Accessible pagination and bounded result sets.",
|
|
"removal": ["Restore paginated primitive.", "Remove facade adapter and dependency.", "Run keyboard and performance evidence."],
|
|
"serverStatePolicy": "query-cache-owned"
|
|
},
|
|
{
|
|
"id": "analytics-error-sink",
|
|
"status": "RECIPE_AVAILABLE",
|
|
"trigger": "A production provider, consent policy, retention owner and event registry are approved.",
|
|
"forbiddenWhen": ["Consent and essential diagnostics are not separated.", "Arbitrary message or attribute keys can bypass redaction."],
|
|
"boundary": "closed diagnostics/analytics port with provider adapter",
|
|
"port": "AnalyticsErrorSink",
|
|
"fake": "RecordingAnalyticsAdapter",
|
|
"failureKinds": ["consent-denied", "queue-full", "provider-unavailable"],
|
|
"lifecycleMethods": ["flush", "dispose"],
|
|
"owner": "project-owner-required",
|
|
"securityPrivacy": ["Allowlist events and attributes.", "Redact before queueing.", "Apply consent, sampling and retention policy."],
|
|
"bundleBudgetGzipBytes": 25000,
|
|
"fallback": "Existing bounded local diagnostics and best-effort telemetry port.",
|
|
"removal": ["Disable provider delivery.", "Flush or discard by policy.", "Remove adapter, runtime config and dependency."],
|
|
"serverStatePolicy": "no-server-state"
|
|
}
|
|
]
|
|
}
|