diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34eaa19d..c715934d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,15 @@ jobs: - name: Generate Prisma client run: pnpm --filter @ice/db exec prisma generate + - name: Lint (ESLint) + run: pnpm lint:check + - name: Format check (Prettier) run: pnpm format:check + - name: Typecheck + run: pnpm typecheck + - name: Unit tests run: pnpm test:unit env: diff --git a/docs/architecture/desktop.md b/docs/architecture/desktop.md index 426777ba..627c6b19 100644 --- a/docs/architecture/desktop.md +++ b/docs/architecture/desktop.md @@ -101,6 +101,8 @@ pnpm dist:desktop:linux # AppImage + .deb The `prebuild` script runs `@ice/gateway build` and `@ice/web build` and copies the Prisma client - all three are required because the distributable has to carry the gateway source, the web bundle, and the native Prisma bindings. +To cut a release (build all three platforms, publish to GitHub Releases), use the one-command flow: `pnpm release patch`. See [docs/build/releasing.md](../build/releasing.md). + ## Packaging configuration `apps/desktop/electron-builder.yml` controls what ships: @@ -150,4 +152,5 @@ Delete the file to reset the app to first-run state. - [README.md](README.md) - how the embedded gateway fits. - [database.md](database.md) - the SQLite schema the desktop uses. +- [docs/build/releasing.md](../build/releasing.md) - cutting a release. - [ROADMAP.md](../../ROADMAP.md) - signing + notarization status. diff --git a/package.json b/package.json index 11b78f71..6d1dcbfb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ice", "license": "Apache-2.0", - "version": "0.1.899", + "version": "0.1.900", "description": "ICE - Integrated Cloud Environment (Web + Backend)", "private": true, "type": "module", @@ -25,6 +25,7 @@ "dist:desktop:mac": "pnpm --filter @ice/desktop dist:mac", "dist:desktop:win": "pnpm --filter @ice/desktop dist:win", "dist:desktop:linux": "pnpm --filter @ice/desktop dist:linux", + "release": "node scripts/release.mjs", "test:unit": "vitest run --root .", "test:coverage": "vitest run --root . --coverage", "test:int": "vitest run --root . --include '**/*.int.test.{ts,tsx}'", diff --git a/packages/core/src/deploy/providers/__tests__/live/_live-helpers.ts b/packages/core/src/deploy/providers/__tests__/live/_live-helpers.ts index b2aa99ea..8396147e 100644 --- a/packages/core/src/deploy/providers/__tests__/live/_live-helpers.ts +++ b/packages/core/src/deploy/providers/__tests__/live/_live-helpers.ts @@ -32,7 +32,7 @@ import { randomBytes } from 'node:crypto'; import { describe } from 'vitest'; import { AWSDeployer, create_aws_deployer } from '../../aws-deployer'; import { AzureDeployer, create_azure_deployer } from '../../azure-deployer'; -import { AlibabaDeployer, create_alibaba_deployer } from '../../alibaba/alibaba-deployer'; +import { AlibabaDeployer, type AlibabaDeployOptions, create_alibaba_deployer } from '../../alibaba/alibaba-deployer'; import { DigitalOceanDeployer, create_digitalocean_deployer } from '../../digitalocean/digitalocean-deployer'; import { IBMDeployer, create_ibm_deployer } from '../../ibm/ibm-deployer'; import { KubernetesDeployer, create_kubernetes_deployer } from '../../kubernetes/kubernetes-deployer'; @@ -293,7 +293,7 @@ export function uniqueIbmName(service: string, maxLen = 63): string { export async function createAwsDeployer(): Promise { const region = process.env.AWS_REGION!; const deployer = create_aws_deployer(); - await deployer.initialize({ regions: [region] }); + await deployer.initialize({ provider: 'aws', regions: [region] }); return deployer; } @@ -323,6 +323,7 @@ export async function createAzureDeployer(): Promise { const resourceGroup = azureTestResourceGroup(); const deployer = create_azure_deployer(); await deployer.initialize({ + provider: 'azure', subscriptions: [subscription], resource_groups: [resourceGroup], regions: [location], @@ -358,7 +359,7 @@ export interface AlibabaLiveContext { export async function createAlibabaDeployer(): Promise { const region = process.env.ALIBABA_CLOUD_REGION!; const deployer = create_alibaba_deployer(); - await deployer.initialize({ provider: 'alibaba', region }); + await deployer.initialize({ provider: 'alibaba', region } as AlibabaDeployOptions); return { deployer, region }; } diff --git a/packages/core/src/deploy/providers/__tests__/live/_live-types.ts b/packages/core/src/deploy/providers/__tests__/live/_live-types.ts index c0cf5d26..c39f3f63 100644 --- a/packages/core/src/deploy/providers/__tests__/live/_live-types.ts +++ b/packages/core/src/deploy/providers/__tests__/live/_live-types.ts @@ -8,7 +8,7 @@ import type { ResourceDeployResult } from '../../../types'; -export type LiveProvider = 'aws' | 'azure'; +export type LiveProvider = 'aws' | 'azure' | 'kubernetes' | 'alibaba' | 'oci' | 'digitalocean' | 'ibm'; export interface RunStartEvent { kind: 'run-start'; diff --git a/packages/core/src/deploy/providers/alibaba/alibaba-deployer.ts b/packages/core/src/deploy/providers/alibaba/alibaba-deployer.ts index 7074ad81..16b28e34 100644 --- a/packages/core/src/deploy/providers/alibaba/alibaba-deployer.ts +++ b/packages/core/src/deploy/providers/alibaba/alibaba-deployer.ts @@ -119,6 +119,8 @@ function unsupported( export interface AlibabaDeployOptions extends DeployOptions { alibaba_credentials?: AlibabaCredentials; + /** Single deploy region. Falls back to ALIBABA_CLOUD_REGION, then the default. */ + region?: string; } export class AlibabaDeployer implements ProviderDeployer { diff --git a/packages/core/src/deploy/providers/alibaba/sdk-loader.ts b/packages/core/src/deploy/providers/alibaba/sdk-loader.ts index 604260dc..0739dcf3 100644 --- a/packages/core/src/deploy/providers/alibaba/sdk-loader.ts +++ b/packages/core/src/deploy/providers/alibaba/sdk-loader.ts @@ -99,7 +99,9 @@ export async function initialize_alibaba_clients( // Attach a thunk per service rather than instantiating up-front: the // sdk-loader resolves on first `clients.get()`. for (const svc of Object.keys(SERVICE_PACKAGES)) { - const { pkg, endpoint_prefix } = SERVICE_PACKAGES[svc]; + const entry = SERVICE_PACKAGES[svc]; + if (!entry) continue; + const { pkg, endpoint_prefix } = entry; let client: unknown = undefined; const lazyClient = { async resolve() { diff --git a/packages/core/src/deploy/providers/aws/types.ts b/packages/core/src/deploy/providers/aws/types.ts index eb1b47eb..9ed40e9d 100644 --- a/packages/core/src/deploy/providers/aws/types.ts +++ b/packages/core/src/deploy/providers/aws/types.ts @@ -49,6 +49,11 @@ export interface AWSHandlerContext { * etc.) honour this so a cancel actually stops the remote work. */ abort_signal?: AbortSignal; + /** + * Optional IAM role ARN/name for the Lambda CodeBuild fallback path. + * When unset, the builder uses the `codebuild-service-role` default. + */ + codebuild_service_role?: string; } /** diff --git a/packages/core/src/deploy/providers/azure/handlers/subnet.ts b/packages/core/src/deploy/providers/azure/handlers/subnet.ts index cc8d6bcf..30cde7c2 100644 --- a/packages/core/src/deploy/providers/azure/handlers/subnet.ts +++ b/packages/core/src/deploy/providers/azure/handlers/subnet.ts @@ -18,7 +18,7 @@ const SDK = '@azure/arm-network'; function extract_vnet_name_from_id(provider_id: string, fallback: string): string { const match = provider_id.match(/\/virtualNetworks\/([^/]+)\/subnets\//); - return match ? match[1] : fallback; + return match?.[1] ?? fallback; } export const subnet_handler: AzureResourceHandler = { diff --git a/packages/core/src/deploy/providers/digitalocean/digitalocean-deployer.ts b/packages/core/src/deploy/providers/digitalocean/digitalocean-deployer.ts index b01f5211..a37cd025 100644 --- a/packages/core/src/deploy/providers/digitalocean/digitalocean-deployer.ts +++ b/packages/core/src/deploy/providers/digitalocean/digitalocean-deployer.ts @@ -78,6 +78,8 @@ function unsupported( export interface DODeployOptions extends DeployOptions { digitalocean_credentials?: DOCredentials; + /** Single deploy region. Falls back to DIGITALOCEAN_REGION, then the default. */ + region?: string; } export class DigitalOceanDeployer implements ProviderDeployer { diff --git a/packages/core/src/deploy/providers/ibm/handlers/databases-instance.ts b/packages/core/src/deploy/providers/ibm/handlers/databases-instance.ts index 16bcf7cc..22187de3 100644 --- a/packages/core/src/deploy/providers/ibm/handlers/databases-instance.ts +++ b/packages/core/src/deploy/providers/ibm/handlers/databases-instance.ts @@ -17,7 +17,7 @@ import type { IBMResourceHandler } from '../types'; const SDK = '@ibm-cloud/platform-services'; -const PLAN_IDS: Record = { +const PLAN_IDS: Record<'postgresql' | 'mysql' | 'mongodb' | 'redis', { service_name: string; plan_id: string }> = { postgresql: { service_name: 'databases-for-postgresql', plan_id: '38774ff2-9eef-4ee5-bef5-d8d2d0671c2c' }, mysql: { service_name: 'databases-for-mysql', plan_id: 'standard' }, mongodb: { service_name: 'databases-for-mongodb', plan_id: '4929e7bb-25e9-4ce0-a4f1-c8a87ff39b32' }, diff --git a/packages/core/src/deploy/providers/ibm/ibm-deployer.ts b/packages/core/src/deploy/providers/ibm/ibm-deployer.ts index b272007b..0db15a89 100644 --- a/packages/core/src/deploy/providers/ibm/ibm-deployer.ts +++ b/packages/core/src/deploy/providers/ibm/ibm-deployer.ts @@ -102,6 +102,8 @@ function unsupported( export interface IBMDeployOptions extends DeployOptions { ibm_credentials?: IBMCredentials; + /** Single deploy region. Falls back to IBMCLOUD_REGION, then the default. */ + region?: string; } export class IBMDeployer implements ProviderDeployer { diff --git a/packages/core/src/deploy/providers/ibm/sdk-loader.ts b/packages/core/src/deploy/providers/ibm/sdk-loader.ts index eeca656b..fb7bdd3c 100644 --- a/packages/core/src/deploy/providers/ibm/sdk-loader.ts +++ b/packages/core/src/deploy/providers/ibm/sdk-loader.ts @@ -53,7 +53,9 @@ export async function initialize_ibm_clients( const authenticator = await build_authenticator(credentials); const clients = new Map(); for (const svc of Object.keys(SERVICE_PACKAGES)) { - const { pkg, clientName } = SERVICE_PACKAGES[svc]; + const entry = SERVICE_PACKAGES[svc]; + if (!entry) continue; + const { pkg, clientName } = entry; let client: unknown = undefined; const lazyClient = { async resolve() { diff --git a/packages/core/src/deploy/providers/kubernetes/handlers/_crd.ts b/packages/core/src/deploy/providers/kubernetes/handlers/_crd.ts index 723bdc0f..53c62577 100644 --- a/packages/core/src/deploy/providers/kubernetes/handlers/_crd.ts +++ b/packages/core/src/deploy/providers/kubernetes/handlers/_crd.ts @@ -81,6 +81,6 @@ export function parseCrdProviderId( fallbackNamespace: string, ): { namespace: string; name: string } { const parts = provider_id.split('/'); - if (parts.length === 5) return { namespace: parts[3], name: parts[4] }; + if (parts.length === 5) return { namespace: parts[3] ?? fallbackNamespace, name: parts[4] ?? '' }; return { namespace: fallbackNamespace, name: parts.pop() ?? '' }; } diff --git a/packages/core/src/deploy/providers/kubernetes/namespace.ts b/packages/core/src/deploy/providers/kubernetes/namespace.ts index 5f91ac12..7b4c05d0 100644 --- a/packages/core/src/deploy/providers/kubernetes/namespace.ts +++ b/packages/core/src/deploy/providers/kubernetes/namespace.ts @@ -48,7 +48,7 @@ export async function ensure_namespace(ctx: KubernetesHandlerContext): Promise/// - if (parts.length === 4) return parts[2]; + if (parts.length === 4) return parts[2] ?? fallback; return fallback; } diff --git a/packages/core/src/deploy/providers/oci/oci-deployer.ts b/packages/core/src/deploy/providers/oci/oci-deployer.ts index be724122..4e815412 100644 --- a/packages/core/src/deploy/providers/oci/oci-deployer.ts +++ b/packages/core/src/deploy/providers/oci/oci-deployer.ts @@ -105,6 +105,8 @@ function unsupported( export interface OCIDeployOptions extends DeployOptions { oci_credentials?: OCICredentials; + /** Single deploy region. Falls back to OCI_REGION, then the default. */ + region?: string; } export class OCIDeployer implements ProviderDeployer { diff --git a/packages/core/src/deploy/providers/oci/sdk-loader.ts b/packages/core/src/deploy/providers/oci/sdk-loader.ts index 95435a70..29c21251 100644 --- a/packages/core/src/deploy/providers/oci/sdk-loader.ts +++ b/packages/core/src/deploy/providers/oci/sdk-loader.ts @@ -82,7 +82,9 @@ export async function initialize_oci_clients( const clients = new Map(); const provider = await build_auth_provider(credentials); for (const svc of Object.keys(SERVICE_PACKAGES)) { - const { pkg, clientName } = SERVICE_PACKAGES[svc]; + const entry = SERVICE_PACKAGES[svc]; + if (!entry) continue; + const { pkg, clientName } = entry; let client: unknown = undefined; const lazyClient = { async resolve() { diff --git a/packages/core/tsconfig.tsbuildinfo b/packages/core/tsconfig.tsbuildinfo index 94926521..ee55130d 100644 --- a/packages/core/tsconfig.tsbuildinfo +++ b/packages/core/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./src/types/graph.ts","./src/types/providers.ts","./src/types/deployment.ts","./src/types/errors.ts","./src/types/result.ts","./src/types/index.ts","./src/schema/schema-provider.ts","./src/schema/resource-validator-types.ts","./src/schema/validation/error-conversion.ts","./src/schema/validation/constraints.ts","./src/schema/validation/type-checker.ts","./src/schema/validation/property-validator.ts","./src/schema/resource-validator.ts","./src/schema/type-mapper.ts","./src/schema/embedded/events.ts","./src/schema/embedded/sqlite-types.ts","./src/schema/embedded/converters.ts","./src/schema/embedded/graph-queries.ts","./src/schemas/db/index.ts","./src/schema/embedded/initialization.ts","./src/schema/embedded/queries.ts","./src/schema/embedded-schema-provider.ts","./src/schema/unified-type-resolver.ts","./src/schema/customization/base-db.ts","./src/schema/customization/paths.ts","./src/schema/customization/example-files.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/line-counter.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/errors.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/applyreviver.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/log.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/tojs.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/scalar.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/stringify.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/collection.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/yamlseq.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/types.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/common/map.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/common/seq.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/common/string.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/foldflowlines.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/stringifynumber.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/stringifystring.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/util.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/yamlmap.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/schema.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/createnode.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/addpairtojsmap.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/pair.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/tags.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/options.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/node.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst-scalar.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst-stringify.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst-visit.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/alias.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/document.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/directives.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/compose/composer.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/lexer.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/parser.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/public-api.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/visit.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/index.d.ts","./src/schema/customization/file-validators.ts","./src/schema/customization/scanner.ts","./src/schema/customization-loader.ts","./src/schema/index.ts","./src/state/state-store.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+better-sqlite3@7.6.13/node_modules/@types/better-sqlite3/index.d.ts","./src/state/sqlite/types.ts","./src/state/sqlite/resources.ts","./src/state/sqlite/deployments.ts","./src/state/sqlite/lifecycle.ts","./src/state/sqlite/locks.ts","./src/state/sqlite/snapshots.ts","./src/state/sqlite-state-store.ts","./src/state/index.ts","./src/graph/parser/tokens.ts","./src/graph/parser/ast/types/base.ts","./src/graph/parser/ast/types/expressions.ts","./src/graph/parser/ast/types/blocks.ts","./src/graph/parser/ast/types/statements.ts","./src/graph/parser/ast/types.ts","./src/graph/parser/ast/helpers.ts","./src/graph/parser/ast.ts","./src/graph/parser/lexer-state.ts","./src/graph/parser/lexer-scanners.ts","./src/graph/parser/lexer-heredoc.ts","./src/graph/parser/lexer.ts","./src/graph/parser/parser-state.ts","./src/graph/parser/parser-literals.ts","./src/graph/parser/parser-primary.ts","./src/graph/parser/parser-binary-exprs.ts","./src/graph/parser/parser-block-body.ts","./src/graph/parser/parser-statements.ts","./src/graph/parser/parser.ts","../../node_modules/.pnpm/@types+js-yaml@4.0.9/node_modules/@types/js-yaml/index.d.ts","../../node_modules/.pnpm/@types+js-yaml@4.0.9/node_modules/@types/js-yaml/index.d.mts","./src/graph/parser/format-parser.ts","./src/graph/parser/index.ts","./src/graph/mutable-graph/types.ts","./src/graph/mutable-graph/edges.ts","../constants/src/providers.ts","../constants/src/ice-types.ts","../constants/src/categories.ts","../constants/src/feature-flags.ts","../constants/src/ai.ts","../constants/src/derived.ts","../constants/src/grid.ts","../constants/src/connections.ts","../constants/src/node-traits.ts","../constants/src/block-classifiers.ts","../constants/src/templates.ts","../constants/src/integrations.ts","../constants/src/index.ts","./src/graph/classifier/category-classifier.ts","./src/graph/mutable-graph/nodes.ts","./src/graph/mutable-graph/stats-serialize.ts","./src/graph/mutable-graph/traversal.ts","./src/graph/mutable-graph.ts","./src/graph/validator/base-validator.ts","./src/graph/validator/validators/schema.ts","./src/graph/validator/validators/security.ts","./src/graph/algorithms/topo-cycle.ts","./src/graph/algorithms/paths.ts","./src/graph/algorithms/components.ts","./src/graph/algorithms/analysis.ts","./src/graph/algorithms.ts","./src/graph/validator/validators/structure.ts","./src/graph/validator/validators.ts","./src/graph/validator/index.ts","./src/graph/classifier/index.ts","./src/graph/inference/relationship-inferrer.ts","./src/graph/inference/index.ts","./src/graph/index.ts","./src/providers/provider-registry.ts","./src/providers/index.ts","./src/importers/terraform/types.ts","./src/importers/terraform/type-mapper.ts","./src/importers/terraform/sensitive.ts","./src/importers/terraform/resource-conversion.ts","./src/importers/terraform/graph-conversion.ts","./src/importers/terraform/state-importer.ts","./src/importers/terraform/index.ts","./src/importers/pulumi/types.ts","./src/importers/pulumi/type-mapper/parse.ts","./src/importers/pulumi/type-mapper/data.ts","./src/importers/pulumi/type-mapper/mapping.ts","./src/importers/pulumi/type-mapper.ts","./src/importers/pulumi/parsing.ts","./src/importers/pulumi/resource-conversion.ts","./src/importers/pulumi/graph-conversion.ts","./src/importers/pulumi/state-importer.ts","./src/importers/pulumi/index.ts","./src/importers/gcp/types.ts","./src/importers/gcp/relationships.ts","./src/importers/gcp/services/base-service.ts","./src/importers/gcp/services/compute.ts","./src/importers/gcp/services/storage.ts","./src/errors/import-errors/types.ts","./src/errors/import-errors/gcp.ts","./src/errors/import-errors/aws.ts","./src/errors/import-errors/azure.ts","./src/errors/import-errors.ts","./src/resources/high-level-resources/types.ts","./src/resources/high-level-resources/categories/compute.ts","./src/resources/high-level-resources/categories/database.ts","./src/resources/high-level-resources/categories/messaging.ts","./src/resources/high-level-resources/categories/monitoring.ts","./src/resources/high-level-resources/categories/networking.ts","./src/resources/high-level-resources/categories/security.ts","./src/resources/high-level-resources/categories/storage.ts","./src/resources/high-level-resources/helpers.ts","./src/resources/high-level-resources.ts","./src/importers/gcp/services/asset-inventory.ts","./src/importers/gcp/services/index.ts","./src/importers/gcp/type-mapper.ts","./src/importers/gcp/gcp-importer.ts","./src/importers/gcp/index.ts","./src/importers/aws/arn-helpers.ts","./src/importers/aws/sdk-init.ts","./src/importers/aws/types.ts","./src/importers/aws/discovery.ts","./src/importers/aws/graph-conversion.ts","./src/importers/aws/type-mapper.ts","./src/importers/aws/aws-importer.ts","./src/importers/aws/index.ts","./src/importers/azure/type-mapper.ts","./src/importers/azure/types.ts","./src/importers/azure/azure-importer.ts","./src/importers/azure/index.ts","./src/importers/index.ts","./src/plan/diff.ts","./src/plan/plan-engine.ts","./src/plan/index.ts","./src/apply/types.ts","./src/providers/mock-provider.ts","./src/apply/apply-engine.ts","./src/apply/index.ts","./src/diff/types.ts","./src/diff/diff.ts","./src/diff/index.ts","./src/deploy/providers/gcp/messages.ts","./src/deploy/messages.ts","./src/deploy/types.ts","./src/deploy/scheduler/types.ts","./src/deploy/scheduler/dag.ts","./src/deploy/scheduler/predicates.ts","./src/deploy/scheduler/dispatch.ts","./src/deploy/scheduler/progress-wrapper.ts","./src/deploy/scheduler.ts","./src/deploy/deploy-engine.ts","./src/deploy/providers/gcp/types.ts","./src/deploy/providers/gcp/handlers/api-gateway.ts","./src/deploy/providers/gcp/handlers/backend-bucket.ts","./src/deploy/providers/gcp/handlers/bigquery.ts","./src/deploy/providers/gcp/handlers/cloud-armor.ts","./src/deploy/providers/gcp/handlers/cloud-functions.ts","./src/deploy/providers/gcp/handlers/cloud-build-helper.ts","./src/deploy/providers/gcp/handlers/cloud-run/image-resolver.ts","./src/deploy/providers/gcp/handlers/cloud-run/result-helpers.ts","./src/deploy/providers/gcp/handlers/cloud-run/utils.ts","./src/deploy/providers/gcp/handlers/cloud-run/create-job.ts","./src/deploy/providers/gcp/handlers/cloud-run/iam.ts","./src/deploy/providers/gcp/handlers/cloud-run/create-service.ts","./src/deploy/providers/gcp/handlers/cloud-run.ts","./src/deploy/providers/gcp/handlers/cloud-scheduler.ts","./src/deploy/providers/gcp/handlers/cloud-sql.ts","./src/deploy/providers/gcp/handlers/cloud-storage/bucket-creator.ts","./src/deploy/providers/gcp/handlers/cloud-storage/bucket-updater.ts","./src/deploy/providers/gcp/handlers/cloud-storage/bucket-utils.ts","./src/deploy/providers/gcp/handlers/cloud-storage/public-access-granter.ts","./src/deploy/providers/gcp/handlers/cloud-storage/placeholder-uploader.ts","./src/deploy/providers/gcp/handlers/cloud-storage/result-helpers.ts","./src/deploy/providers/gcp/handlers/cloud-storage.ts","./src/deploy/providers/gcp/handlers/dataflow.ts","./src/deploy/providers/gcp/handlers/discovery-engine.ts","./src/deploy/providers/gcp/handlers/domain-mapping.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/dns-extractor.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/rest-client.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/domain-registrar.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/tar-parser.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/github-downloader.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/result-helpers.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/site-provisioner.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/site-utils.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/version-publisher.ts","./src/deploy/providers/gcp/handlers/firebase-hosting.ts","./src/deploy/providers/gcp/handlers/firestore.ts","./src/deploy/providers/gcp/handlers/gke.ts","./src/deploy/providers/gcp/handlers/identity-platform.ts","./src/deploy/providers/gcp/handlers/load-balancer/result-helpers.ts","./src/deploy/providers/gcp/handlers/load-balancer/compute-ops.ts","./src/deploy/providers/gcp/handlers/load-balancer/backend-creator.ts","./src/deploy/providers/gcp/handlers/load-balancer/cert-fetcher.ts","./src/deploy/providers/gcp/handlers/load-balancer/url-builder.ts","./src/deploy/providers/gcp/handlers/load-balancer/lb-builder.ts","./src/deploy/providers/gcp/handlers/load-balancer.ts","./src/deploy/providers/gcp/handlers/logging.ts","./src/deploy/providers/gcp/handlers/managed-ssl-certificate.ts","./src/deploy/providers/gcp/handlers/memorystore.ts","./src/deploy/providers/gcp/handlers/pubsub.ts","./src/deploy/providers/gcp/handlers/secret-manager.ts","./src/deploy/providers/gcp/handlers/subnet.ts","./src/deploy/providers/gcp/handlers/vpc.ts","./src/deploy/providers/gcp/handlers/vertex-ai.ts","./src/deploy/providers/gcp/sdk-loader.ts","./src/deploy/providers/gcp/gcp-deployer.ts","./src/deploy/providers/gcp/auth.ts","./src/deploy/providers/gcp/index.ts","./src/deploy/providers/aws/sdk-loader.ts","./src/deploy/providers/aws/account.ts","./src/deploy/providers/aws/handlers/_result.ts","./src/deploy/providers/aws/types.ts","./src/deploy/providers/aws/handlers/api-gateway.ts","./src/deploy/providers/aws/handlers/bedrock.ts","./src/deploy/providers/aws/handlers/cloudfront.ts","./src/deploy/providers/aws/handlers/cloudwatch-logs.ts","./src/deploy/providers/aws/handlers/cognito.ts","./src/deploy/providers/aws/handlers/docdb.ts","./src/deploy/providers/aws/handlers/dynamodb.ts","./src/deploy/providers/aws/handlers/ec2.ts","./src/deploy/providers/aws/iam-roles.ts","./src/deploy/providers/aws/handlers/ecs.ts","./src/deploy/providers/aws/handlers/elasticache.ts","./src/deploy/providers/aws/handlers/elbv2.ts","./src/deploy/providers/aws/handlers/events-rule.ts","./src/deploy/providers/aws/handlers/lambda-builder.ts","./src/deploy/providers/aws/handlers/lambda.ts","./src/deploy/providers/aws/handlers/opensearch.ts","./src/deploy/providers/aws/handlers/rds.ts","./src/deploy/providers/aws/handlers/redshift.ts","./src/deploy/providers/aws/handlers/s3.ts","./src/deploy/providers/aws/handlers/sagemaker.ts","./src/deploy/providers/aws/handlers/secrets-manager.ts","./src/deploy/providers/aws/handlers/sns.ts","./src/deploy/providers/aws/handlers/sqs.ts","./src/deploy/providers/aws/aws-deployer.ts","./src/deploy/providers/aws/index.ts","./src/deploy/providers/aws-deployer.ts","./src/deploy/providers/azure-deployer.ts","./src/deploy/providers/index.ts","./src/deploy/block-deploy-classifiers.ts","./src/deploy/edge-classifier.ts","./src/deploy/extractors/ancillary.ts","./src/deploy/extractors/aws/ai.ts","./src/deploy/extractors/aws/ancillary.ts","./src/deploy/utils/name-utils.ts","./src/deploy/extractors/compute.ts","./src/deploy/extractors/aws/compute.ts","./src/deploy/extractors/aws/database.ts","./src/deploy/extractors/aws/network.ts","./src/deploy/extractors/database.ts","./src/deploy/extractors/network.ts","./src/deploy/extractors/dispatch.ts","./src/deploy/internal-ingress-overrides.ts","./src/deploy/passes/deploy-expansion.ts","./src/deploy/passes/pass-1-4-repo-wiring.ts","./src/deploy/passes/pass-1-45-domain-propagation.ts","./src/deploy/passes/pass-1-46-socket-port-targeting.ts","./src/deploy/self-serving-resources.ts","./src/deploy/passes/pass-1-5-endpoint-wiring.ts","./src/deploy/type-maps.ts","./src/deploy/utils/stable-name.ts","./src/deploy/card-translator.ts","./src/deploy/state-bridge.ts","./src/deploy/state-store-adapter.ts","./src/deploy/environment-config.ts","./src/deploy/index.ts","./src/compute/types.ts","./src/compute/propagation-rules.ts","./src/compute/compute-derived.ts","./src/compute/index.ts","./src/export/terraform/case-utils.ts","./src/export/terraform/types.ts","./src/export/terraform/hcl-formatter.ts","./src/export/terraform/type-mapping.ts","./src/export/terraform/value-transform.ts","./src/export/terraform/converter.ts","./src/export/terraform-exporter.ts","./src/export/pulumi/case-utils.ts","./src/export/pulumi/type-mapping.ts","./src/export/pulumi/types.ts","./src/export/pulumi/typescript-formatter.ts","./src/export/pulumi/value-transform.ts","./src/export/pulumi/yaml-formatter.ts","./src/export/pulumi/converter.ts","./src/export/pulumi-exporter.ts","./src/export/index.ts","./src/errors/index.ts","./src/resources/cloud-providers.ts","./src/resources/blueprint-factory.ts","./src/resources/cloud-blocks-types.ts","./src/resources/cloud-blocks-data/backend.ts","./src/resources/cloud-blocks-data/compute.ts","./src/resources/cloud-blocks-data/data.ts","./src/resources/cloud-blocks-data/frontend.ts","./src/resources/cloud-blocks-data/messaging.ts","./src/resources/cloud-blocks-data/networking.ts","./src/resources/cloud-blocks-data/observability.ts","./src/resources/cloud-blocks-data/security.ts","./src/resources/cloud-blocks-data/storage.ts","./src/resources/cloud-blocks-data.ts","./src/resources/cloud-blocks.ts","./src/validation/classifiers.ts","./src/validation/types.ts","./src/validation/architecture-rules.ts","./src/validation/connection-rules.ts","./src/validation/schema-bridge.ts","./src/validation/deploy-rules.ts","./src/validation/property-rules.ts","./src/validation/structure-rules.ts","./src/validation/canvas-validator.ts","./src/validation/template-validator.ts","./src/validation/index.ts","./src/index.ts","./src/__tests__/card-translator.test.d.ts","./src/__tests__/core.test.d.ts","./src/__tests__/pulumi-importer.test.d.ts","./src/__tests__/terraform-importer.test.d.ts","./src/cli/index.ts","./src/cli/messages.ts","./src/cli/bin/ice.ts","./src/cli/commands/apply.ts","./src/cli/commands/config.ts","./src/cli/commands/deploy.ts","./src/cli/commands/destroy.ts","./src/cli/commands/diff.ts","./src/cli/commands/graph.ts","./src/cli/commands/import.ts","./src/cli/commands/plan.ts","./src/cli/commands/providers.ts","./src/cli/commands/schema.ts","./src/cli/commands/state.ts","./src/cli/utils/config.ts","./src/cli/utils/index.ts","./src/cli/utils/output.ts","../../node_modules/.pnpm/@vitest+pretty-format@4.1.0/node_modules/@vitest/pretty-format/dist/index.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/display.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/types.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/helpers.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/timers.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/index.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/types.d-bcelap-c.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/diff.d.ts","../../node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/tasks.d-d2gkpdwq.d.ts","../../node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/traces.d.402v_yfi.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/types/hmrpayload.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/dist/node/chunks/modulerunnertransport.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/types/customevent.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/types/hot.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/dist/node/module-runner.d.ts","../../node_modules/.pnpm/@vitest+snapshot@4.1.0/node_modules/@vitest/snapshot/dist/environment.d-dojxxzv9.d.ts","../../node_modules/.pnpm/@vitest+snapshot@4.1.0/node_modules/@vitest/snapshot/dist/rawsnapshot.d-u2kjuxdr.d.ts","../../node_modules/.pnpm/@vitest+snapshot@4.1.0/node_modules/@vitest/snapshot/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/config.d.ejlve3es.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/environment.d.crsxczp1.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/rpc.d.bfmwpdph.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/worker.d.b84svry0.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/browser.d.x3sxoocv.d.ts","../../node_modules/.pnpm/@vitest+spy@4.1.0/node_modules/@vitest/spy/dist/index.d.ts","../../node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.d.ts","../../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../node_modules/.pnpm/@types+deep-eql@4.0.2/node_modules/@types/deep-eql/index.d.ts","../../node_modules/.pnpm/assertion-error@2.0.1/node_modules/assertion-error/index.d.ts","../../node_modules/.pnpm/@types+chai@5.2.3/node_modules/@types/chai/index.d.ts","../../node_modules/.pnpm/@vitest+expect@4.1.0/node_modules/@vitest/expect/dist/index.d.ts","../../node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/utils.d.ts","../../node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/benchmark.d.daahlpsq.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/global.d.x-ilcfae.d.ts","../../node_modules/.pnpm/@vitest+mocker@4.1.0_vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3_/node_modules/@vitest/mocker/dist/types.d-bji5eawu.d.ts","../../node_modules/.pnpm/@vitest+mocker@4.1.0_vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3_/node_modules/@vitest/mocker/dist/index.d-b41z0auw.d.ts","../../node_modules/.pnpm/@vitest+mocker@4.1.0_vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3_/node_modules/@vitest/mocker/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/suite.d.udjtyagw.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/evaluatedmodules.d.bxj5omdx.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/runners.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/index.d.ts","./src/deploy/providers/__tests__/_aws-test-harness.ts","./src/graph/algorithms/__tests__/fixtures.ts","./src/graph/mutable-graph/index.ts","./src/resources/scale-presets-types.ts","./src/resources/scale-presets-data/compute.ts","./src/resources/scale-presets-data/database.ts","./src/resources/scale-presets-data/messaging.ts","./src/resources/scale-presets-data/monitoring.ts","./src/resources/scale-presets-data/networking.ts","./src/resources/scale-presets-data/security.ts","./src/resources/scale-presets-data/storage.ts","./src/resources/scale-presets-data.ts","./src/resources/scale-presets.ts","./src/resources/index.ts","./src/schemas/index.ts","./src/schemas/db/graph-queries.ts","./src/schemas/db/schema-merger.ts","./src/schemas/db/sqlite-registry.ts","./src/schemas/embedded/schema-registry.ts"],"fileIdsList":[[135,184,201,202],[135,184,201,202,234],[135,184,201,202,591,592],[135,184,201,202,263],[135,181,182,184,201,202],[135,183,184,201,202],[184,201,202],[135,184,189,201,202,219],[135,184,185,190,195,201,202,204,216,227],[135,184,185,186,195,201,202,204],[130,131,132,135,184,201,202],[135,184,187,201,202,228],[135,184,188,189,196,201,202,205],[135,184,189,201,202,216,224],[135,184,190,192,195,201,202,204],[135,183,184,191,201,202],[135,184,192,193,201,202],[135,184,194,195,201,202],[135,183,184,195,201,202],[135,184,195,196,197,201,202,216,227],[135,184,195,196,197,201,202,211,216,219],[135,177,184,192,195,198,201,202,204,216,227],[135,184,195,196,198,199,201,202,204,216,224,227],[135,184,198,200,201,202,216,224,227],[133,134,135,136,137,138,139,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233],[135,184,195,201,202],[135,184,201,202,203,227],[135,184,192,195,201,202,204,216],[135,184,201,202,205],[135,184,201,202,206],[135,183,184,201,202,207],[135,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233],[135,184,201,202,209],[135,184,201,202,210],[135,184,195,201,202,211,212],[135,184,201,202,211,213,228,230],[135,184,196,201,202],[135,184,195,201,202,216,217,219],[135,184,201,202,218,219],[135,184,201,202,216,217],[135,184,201,202,219],[135,184,201,202,220],[135,181,184,201,202,216,221,227],[135,184,195,201,202,222,223],[135,184,201,202,222,223],[135,184,189,201,202,204,216,224],[135,184,201,202,225],[135,184,201,202,204,226],[135,184,198,201,202,210,227],[135,184,189,201,202,228],[135,184,201,202,216,229],[135,184,201,202,203,230],[135,184,201,202,231],[135,177,184,201,202],[135,177,184,195,197,201,202,207,216,219,227,229,230,232],[135,184,201,202,216,233],[135,184,201,202,565,571,573,588,589,590,593,598],[135,184,201,202,599],[135,184,201,202,599,600],[135,184,201,202,569,571,572],[135,184,201,202,569,571],[135,184,201,202,569],[135,184,201,202,564,569,580,581],[135,184,201,202,564,580],[135,184,201,202,564,570],[135,184,201,202,564],[135,184,201,202,566],[135,184,201,202,564,565,566,567,568],[135,184,201,202,605,606],[135,184,201,202,605,606,607,608],[135,184,201,202,605,607],[135,184,201,202,605],[135,149,153,184,201,202,227],[135,149,184,201,202,216,227],[135,144,184,201,202],[135,146,149,184,201,202,224,227],[135,184,201,202,204,224],[135,144,184,201,202,234],[135,146,149,184,201,202,204,227],[135,141,142,145,148,184,195,201,202,216,227],[135,149,156,184,201,202],[135,141,147,184,201,202],[135,149,170,171,184,201,202],[135,145,149,184,201,202,219,227,234],[135,170,184,201,202,234],[135,143,144,184,201,202,234],[135,149,184,201,202],[135,143,144,145,146,147,148,149,150,151,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,171,172,173,174,175,176,184,201,202],[135,149,164,184,201,202],[135,149,156,157,184,201,202],[135,147,149,157,158,184,201,202],[135,148,184,201,202],[135,141,144,149,184,201,202],[135,149,153,157,158,184,201,202],[135,153,184,201,202],[135,147,149,152,184,201,202,227],[135,141,146,149,156,184,201,202],[135,184,201,202,216],[135,144,149,170,184,201,202,232,234],[135,184,201,202,575],[135,184,201,202,575,576,577,578],[135,184,201,202,577],[135,184,201,202,573,595,596,598],[135,184,201,202,573,574,586,598],[135,184,201,202,564,571,573,582,598],[135,184,201,202,579],[135,184,201,202,564,573,582,585,594,597,598],[135,184,201,202,573,574,579,582,598],[135,184,201,202,573,595,596,597,598],[135,184,201,202,573,579,583,584,585,598],[135,184,201,202,564,569,571,573,574,579,582,583,584,585,586,587,588,594,595,596,597,598,601,602,603,604,609],[135,184,201,202,564,571,573,574,582,583,595,596,597,598,602],[85,108,109,113,115,116,135,184,201,202],[93,103,109,115,135,184,201,202],[115,135,184,201,202],[85,89,92,101,102,103,106,108,109,114,116,135,184,201,202],[84,135,184,201,202],[84,85,89,92,93,101,102,103,106,107,108,109,113,114,115,117,118,119,120,121,122,123,135,184,201,202],[88,101,106,135,184,201,202],[88,89,90,92,101,109,113,115,135,184,201,202],[102,103,109,135,184,201,202],[89,92,101,106,109,114,115,135,184,201,202],[88,89,90,92,101,102,108,113,114,115,135,184,201,202],[88,90,102,103,104,105,109,113,135,184,201,202],[88,109,113,135,184,201,202],[109,115,135,184,201,202],[88,89,90,91,100,103,106,109,113,135,184,201,202],[88,89,90,91,103,104,106,109,113,135,184,201,202],[84,86,87,89,93,103,106,107,109,116,135,184,201,202],[85,89,109,113,135,184,201,202],[113,135,184,201,202],[110,111,112,135,184,201,202],[86,108,109,115,117,135,184,201,202],[93,135,184,201,202],[93,102,106,108,135,184,201,202],[93,108,135,184,201,202],[89,90,92,101,103,104,108,109,135,184,201,202],[88,92,93,100,101,103,135,184,201,202],[88,89,90,93,100,101,103,106,135,184,201,202],[108,114,115,135,184,201,202],[89,135,184,201,202],[89,90,135,184,201,202],[87,88,90,94,95,96,97,98,99,101,104,106,135,184,201,202],[135,184,201,202,270],[135,184,201,202,269,271],[135,184,201,202,269,270,271,272,273,274,275,276,277,278,279,280],[58,59,60,135,184,201,202,286,360,362,363],[135,184,201,202,362,364],[58,59,60,135,184,201,202],[135,184,201,202,496,497],[135,184,201,202,496,497,498],[135,184,201,202,281,496],[58,135,184,201,202,281,286,340,470,474,481,482,483,484,485,486,488,489,490],[58,135,184,201,202,366,367,370,371,377],[58,135,184,201,202,281,469],[135,184,201,202,475],[135,184,201,202,474],[135,184,201,202,281],[135,184,201,202,471,472,473,475,476,477,478,479,480],[135,184,189,201,202,281],[135,184,201,202,370,371,377,378,433,435,468,491,492,493,494],[135,184,201,202,369],[135,184,201,202,286,340,474,491],[135,184,201,202,286,491],[135,184,201,202,286,469,491],[135,184,201,202,281,286,470,474,487,491],[135,184,201,202,610],[135,184,201,202,465],[135,184,201,202,437],[135,184,201,202,371,437,438,440,441,442,443,444,445,446,447,448,450,451,452,453,455,456,457,458,459,460,461,462,463],[135,184,201,202,371],[135,184,201,202,437,439,440],[135,184,201,202,371,437,440],[135,184,201,202,437,439,440,449],[135,184,185,196,201,202,205,206,437,440],[135,184,201,202,371,437,440,454],[135,184,201,202,437,438,440,449,464],[135,184,201,202,371,438],[135,184,201,202,370,433],[135,184,201,202,369,370,371,379,380,381,382,383,384,392,393,394,401,402,403,404,414,415,416,417,424,425,426,427,428,429,430,431,432,433],[135,184,201,202,369,371,379],[135,184,201,202,369,379],[135,184,201,202,369,379,386,387,388,389,390,391],[135,184,201,202,369,371,379,386,387,388],[135,184,201,202,369,371,379,386,387,388,390],[135,184,201,202,379],[135,184,201,202,369,379,385],[135,184,201,202,369,379,395,396,397,398,399,400],[135,184,201,202,379,397,398],[135,184,201,202,371,379],[135,184,201,202,379,405,406,407,409,410,411,412,413],[135,184,201,202,379,405,406],[135,184,201,202,233,379,408],[135,184,201,202,379,406],[135,184,189,201,202,233,379,406,408],[135,184,201,202,379,418,419,420,421,422,423],[135,184,201,202,379,418,419],[135,184,201,202,379,418],[135,184,201,202,369,379,418],[135,184,201,202,379,418,419,420,422],[135,184,201,202,379,434,435],[135,184,201,202,370,379],[135,184,201,202,436,466,467],[135,184,201,202,371,372,373,374,375,376],[58,135,184,201,202,366,372],[135,184,201,202,366,371,372,374],[135,184,201,202,372],[135,184,201,202,366,371],[58,135,184,201,202,366,371],[58,135,184,201,202,371],[58,135,184,201,202,242,492],[135,184,201,202,491],[135,184,189,201,202,474],[58,135,184,201,202,366],[135,184,201,202,366,367],[135,184,201,202,326,327,328,329],[135,184,201,202,326],[135,184,201,202,330],[135,184,201,202,506,514],[79,135,184,201,202,286,509,513],[58,64,79,135,184,201,202,286,507,508,509,510,511,512],[135,184,201,202,507],[135,184,201,202,507,508,509],[135,184,201,202,507,509],[135,184,201,202,509],[79,135,184,201,202,286,501,505],[58,64,79,135,184,201,202,286,500,501,502,503,504],[135,184,201,202,501],[135,184,201,202,290,291,292,293],[135,184,201,202,286],[58,135,184,201,202,286,290,292],[58,135,184,201,202,286],[135,184,201,202,282],[135,184,201,202,266,286,294,297,298,300],[135,184,201,202,299],[58,135,184,201,202],[58,135,184,201,202,267,268,283,284,285],[58,135,184,201,202,267],[135,184,201,202,267,268,283,284,285],[58,135,184,201,202,267,268,282],[58,135,184,201,202,267,268],[135,184,201,202,249,250],[135,184,201,202,244,249],[135,184,201,202,245,246,247,248],[135,184,201,202,244],[135,184,201,202,245,246],[135,184,201,202,245],[135,184,201,202,245,246,247],[135,184,201,202,244,251,264],[135,184,201,202,244,251,255,262,265],[135,184,201,202,252,253],[135,184,201,202,244,252],[135,184,201,202,244,255],[135,184,201,202,244,252,253,254],[135,184,201,202,251,256,257,258],[135,184,201,202,251,256,257,259],[135,184,201,202,244,251,256],[135,184,201,202,244,251,256,257,259],[135,184,201,202,244,262],[135,184,201,202,244,251,256,257,260,261],[135,184,201,202,287,296],[64,135,184,201,202,287,288,289,295],[64,135,184,201,202,286,287],[135,184,201,202,286,287],[135,184,201,202,286,287,294],[135,184,201,202,286,330,347,348,349,350,351],[135,184,201,202,346,347,348],[58,135,184,201,202,286,348],[135,184,201,202,348,351,352],[58,135,184,201,202,286,330,354,355],[135,184,201,202,354,355,356],[58,135,184,201,202,286,321,322,342,343],[135,184,201,202,321,322,342,343,344],[135,184,201,202,321],[135,184,196,201,202,321,323,330,340],[135,184,201,202,321,323],[135,184,201,202,323,324,325,341],[135,184,201,202,340],[135,184,201,202,310,320,345,353,357],[58,135,184,201,202,286,311,319],[135,184,201,202,311,315,319],[135,184,201,202,311,315],[135,184,201,202,311,315,316,319],[135,184,196,197,201,202,286,311,315,316,317,318],[135,184,201,202,312,314],[135,184,201,202,312,313],[135,184,201,202,311],[58,135,184,201,202,286,304,309],[135,184,201,202,304,305,309],[135,184,201,202,304,305,306,309],[135,184,201,202,304],[135,184,196,197,201,202,286,304,306,307,308],[63,128,135,184,201,202,243,301,303,340,358,361,363,365,368,495,499,515,516,517,518,530,541],[60,135,184,201,202],[135,184,201,202,359,360],[58,59,60,135,184,201,202,286,294,359],[135,184,201,202,302],[58,59,135,184,201,202],[59,61,62,135,184,201,202],[135,184,201,202,519,520,521,522,523,524,525,526,527,528],[135,184,201,202,519],[135,184,201,202,519,529],[135,184,201,202,331,339],[135,184,201,202,331],[135,184,201,202,281,331,332,333,334,335,336,337,338],[135,184,201,202,340,517,518,530,623],[135,184,201,202,614,615,616,617,618,619,620,621],[135,184,201,202,614],[135,184,201,202,614,622],[81,82,83,125,126,135,184,196,201,202,206],[135,184,196,201,202,206],[82,135,184,196,201,202,206],[124,135,184,196,201,202],[61,62,64,72,73,75,77,78,135,184,201,202],[64,73,135,184,201,202],[64,135,184,201,202],[61,62,64,73,74,135,184,201,202],[73,76,135,184,196,201,202,206],[64,70,71,79,80,127,135,184,201,202],[61,62,64,65,66,69,135,184,201,202],[61,62,135,184,201,202],[64,79,135,184,201,202],[64,65,135,184,201,202],[61,65,135,184,201,202],[64,65,67,68,135,184,201,202],[65,135,184,201,202],[129,135,184,201,202,242],[58,60,61,62,129,135,184,201,202,236,237,238,239,240,241],[60,61,62,129,135,184,201,202,236,237],[61,62,135,184,201,202,235,236],[58,59,60,61,62,129,135,184,201,202,235,236],[61,62,129,135,184,201,202,236,237],[129,135,184,201,202,235],[58,59,60,61,62,135,184,201,202],[61,135,184,201,202],[135,184,201,202,531,532],[135,184,201,202,532,533,534,536,537,538],[135,184,201,202,281,531,532,535],[135,184,201,202,532,533,534,535,536,537,538,539,540],[135,184,201,202,340,532,535],[135,184,201,202,281,340],[135,184,201,202,531,532,535]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},"9f422a1bcf9b6de329433e9846e2de072714d0feb659261d0915ed89b7227871","9f06cca3f1b2c3d560cdbec5c0a3bfafdf58dc2439d7065ddc5f23af72fdaa73","8c467364768b887a7c3f3af90e02fcd1cf443ce162bb498082bfbcc99beb2a88","905a105127753c3ce4f2d19d866c0d5565758691579204519bb9b8fd838b790b","62d8d9c7b326f1d1392cf56c82d9c6b042039875caa7b503b0a0b41d0c3123c8","44a47ae3fa050377c57d37fb051a6c6cbc08405bd0efda40861653de399a0818","7170f18d5cdfb2d301f0470233e281da189a3b0a8d9ea4358edcb21b4c9a5728","cbbede9be700512a66d94e48c4ef981a6d1a1678698ee36f7dbbb4ac747aeaf3","a92c3adb47ae83dffb2b67e18f045efb376efd1547941de56366340c06d61077","341494baae56b3810de282a6932c5c4edf0b44adf7aa00e1ced71a9258141315","c1e5340e6e2c73765454ef0056093661c0b4159518797e442c58d9e0dfbbe8cc","92b52627eb99e20e437d33fc58808896145bc40ae551a991844e7658395bdfce","21b14a29d87dcddf64f59d7865144dadf7c455696b7c509d973d46d73af2eedf","0685b918bf7e38ff946e8feb260e193f48626b7eed70669f1a4c5ecd0b3cd444","33943d74593fcd8670b682fbda86001d2e728b13fceb5d22961074f186619743","36cfb8ac02cece02f64db23c7ba4fb95ec2b56fa8d753250aff3f2b1663a9e34","64d61d96ca6dffcff65d3021718e83ed23bb14517aeb1f02e7c3ad993eb0a0bc","8ebea2af5e5b387063de6535a5ecb197f32b1ae6614df257432c087725161c6f","c0bd5e212ce61814fcdc9240b90868b1108c37f21da55d21cabddd3e180f195e","326a61d40b8bdb5b3c35f83e3cb0a78af05cfcd4539f9098c9fe46e32b5c6326","044f2620626e0bd1ce6161513ccc00cc107e502eb2d540c543ea7dcc54f18cba","bdaaa669518e13907293d935c2fe379936525a1226cbfe7b5ed1b407bac5234a","f6cd6b7cda0141a3f0d629aff6c529e3f55245d1c198c450737d7cea81b56d1a","b5981034fd2b7724d3b4f4acdba358dc34ddaa3ad509ef9399c630c86cb9ccc1","0cc85eed70c33b0d113321a7fe2a8493c530cdec9babef148241f7203f35cc4b","c442972d48882efb43e3e64e1b30a68effd736227742a3ff936162d0ef320a4c",{"version":"3dfcd0a3bfa70b53135db3cf2e4ddcb7eccc3e4418ce833ae24eecd06928328f","impliedFormat":1},{"version":"bea7cae6a8b2d41fd1a9d70475b54d741dd7ca2103904934858108eec0336a69","impliedFormat":1},{"version":"bc41a8e33caf4d193b0c49ec70d1e8db5ce3312eafe5447c6c1d5a2084fece12","impliedFormat":1},{"version":"7c33f11a56ba4e79efc4ddae85f8a4a888e216d2bf66c863f344d403437ffc74","impliedFormat":1},{"version":"cbef1abd1f8987dee5c9ed8c768a880fbfbff7f7053e063403090f48335c8e4e","impliedFormat":1},{"version":"9249603c91a859973e8f481b67f50d8d0b3fa43e37878f9dfc4c70313ad63065","impliedFormat":1},{"version":"0132f67b7f128d4a47324f48d0918ec73cf4220a5e9ea8bd92b115397911254f","impliedFormat":1},{"version":"06b37153d512000a91cad6fcbae75ca795ecec00469effaa8916101a00d5b9e2","impliedFormat":1},{"version":"8a641e3402f2988bf993007bd814faba348b813fc4058fce5b06de3e81ed511a","impliedFormat":1},{"version":"281744305ba2dcb2d80e2021fae211b1b07e5d85cfc8e36f4520325fcf698dbb","impliedFormat":1},{"version":"e1b042779d17b69719d34f31822ddba8aa6f5eb15f221b02105785f4447e7f5b","impliedFormat":1},{"version":"6858337936b90bd31f1674c43bedda2edbab2a488d04adc02512aef47c792fd0","impliedFormat":1},{"version":"15cb3deecc635efb26133990f521f7f1cc95665d5db8d87e5056beaea564b0ce","impliedFormat":1},{"version":"e27605c8932e75b14e742558a4c3101d9f4fdd32e7e9a056b2ca83f37f973945","impliedFormat":1},{"version":"f0443725119ecde74b0d75c82555b1f95ee1c3cd371558e5528a83d1de8109de","impliedFormat":1},{"version":"7794810c4b3f03d2faa81189504b953a73eb80e5662a90e9030ea9a9a359a66f","impliedFormat":1},{"version":"b074516a691a30279f0fe6dff33cd76359c1daacf4ae024659e44a68756de602","impliedFormat":1},{"version":"57cbeb55ec95326d068a2ce33403e1b795f2113487f07c1f53b1eaf9c21ff2ce","impliedFormat":1},{"version":"a00362ee43d422bcd8239110b8b5da39f1122651a1809be83a518b1298fa6af8","impliedFormat":1},{"version":"a820499a28a5fcdbf4baec05cc069362041d735520ab5a94c38cc44db7df614c","impliedFormat":1},{"version":"33a6d7b07c85ac0cef9a021b78b52e2d901d2ebfd5458db68f229ca482c1910c","impliedFormat":1},{"version":"8f648847b52020c1c0cdfcc40d7bcab72ea470201a631004fde4d85ccbc0c4c7","impliedFormat":1},{"version":"7821d3b702e0c672329c4d036c7037ecf2e5e758eceb5e740dde1355606dc9f2","impliedFormat":1},{"version":"213e4f26ee5853e8ba314ecad3a73cd06ab244a0809749bb777cbc1619aa07d8","impliedFormat":1},{"version":"1720be851bdb7cdbff68061522a71d9ddaa69db1fe90c6819a26953da05942f2","impliedFormat":1},{"version":"961fa18e1658f3f8e38c23e1a9bc3f4d7be75b056a94700291d5f82f57524ff0","impliedFormat":1},{"version":"079c02dc397960da2786db71d7c9e716475377bcedd81dede034f8a9f94c71b8","impliedFormat":1},{"version":"a7595cbb1b354b54dff14a6bb87d471e6d53b63de101a1b4d9d82d3d3f6eddec","impliedFormat":1},{"version":"1f49a85a97e01a26245fd74232b3b301ebe408fb4e969e72e537aa6ffbd3fe14","impliedFormat":1},{"version":"9c38563e4eabfffa597c4d6b9aa16e11e7f9a636f0dd80dd0a8bce1f6f0b2108","impliedFormat":1},{"version":"a971cba9f67e1c87014a2a544c24bc58bad1983970dfa66051b42ae441da1f46","impliedFormat":1},{"version":"df9b266bceb94167c2e8ae25db37d31a28de02ae89ff58e8174708afdec26738","impliedFormat":1},{"version":"9e5b8137b7ee679d31b35221503282561e764116d8b007c5419b6f9d60765683","impliedFormat":1},{"version":"3e7ae921a43416e155d7bbe5b4229b7686cfa6a20af0a3ae5a79dfe127355c21","impliedFormat":1},{"version":"c7200ae85e414d5ed1d3c9507ae38c097050161f57eb1a70bef021d796af87a7","impliedFormat":1},{"version":"4edb4ff36b17b2cf19014b2c901a6bdcdd0d8f732bcf3a11aa6fd0a111198e27","impliedFormat":1},{"version":"810f0d14ce416a343dcdd0d3074c38c094505e664c90636b113d048471c292e2","impliedFormat":1},{"version":"9c37dc73c97cd17686edc94cc534486509e479a1b8809ef783067b7dde5c6713","impliedFormat":1},{"version":"5fe2ef29b33889d3279d5bc92f8e554ffd32145a02f48d272d30fc1eea8b4c89","impliedFormat":1},{"version":"e39090ffe9c45c59082c3746e2aa2546dc53e3c5eeb4ad83f8210be7e2e58022","impliedFormat":1},{"version":"9f85a1810d42f75e1abb4fc94be585aae1fdac8ae752c76b912d95aef61bf5de","impliedFormat":1},"db9e4672c7c667a0deabd6276b696e5994d3730e5586b5b87267aa63d9f72328","6ba80b835f61ba3a1e68f225ae88fa6e3f7c5f65ca051bc1756b7c899d3e9274","4631550a659e584f008891ce1c715ce29d0cbc3dd1ffbfc76ad2b50eff702948","ab0970f1b7769b6a37dd837ce570dfeb1124740a38f15c9294b246eb1bc780c4","116f359450e3ce8b94d3a4e321826d56d77ac599b68f9c07b4a4d34b2e8bf3ed",{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"6b039f55681caaf111d5eb84d292b9bee9e0131d0db1ad0871eef0964f533c73","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"c2a6a737189ced24ffe0634e9239b087e4c26378d0490f95141b9b9b042b746c","impliedFormat":1},"8ab9053eb153fdd8636f6b3f30208e0aaaefd7c23cd7c87be7ef913d36aaebe8","57430b85fd84c95c5b5f867dfbdbdffbd11ef2e48965eb9ecc7f4fb9d78be325","08eb64a1afdeda9c7cd87114906037388d7af9d91bce27c293f55dee9ce5a8c2","b5076e65f873347e41f18a2d239083a544c080b880fd62de907bd064465e1290","b9e4ff74d366947b2c7ff9158b5c878480c1523bffa20590914fa53c29144e07","63a9ab201a2da78d4d5c505c1c4c9fe380fffc7368f9d22fe7084b4e491977c4","903dd73508a4b3ae23daa597ca913eac9f75e3621539691b0a216047278e9efa","52741694e94b7b6b04c12285da7e10103e66b57126303a593a257fb5e086a491","b28b6136a5678e5b01ed6019b0b9ef8b22446f7d291f081bcddc71051d2a6492","583610054995599f0ed590a55b9f81466909e2087ee04b6c2461d6e0c8b08b43","c8449a6bc016461d1269ba1cd5aaeadd845e458bde39966f1fb434e5e99204ce","7ea0e0cdd8e14703d723a50b6c0d07f620d825f0e148d6c9aa97af9950fcb44c","2fd75662e4cd0cd568d6c0857e7a71648addda2a1c6d01918fae951dce5b519a","a2ef968f2f25e45f7a634cba822bc2e7542678615fe0161826a8b2e82e071acb","88b62f5279f98c2c529bc562a99dcd1c1cf96c5f3698b04936ec7cd95722ddda","59b46ed04998fc50de4f3a8896fa90c08e79530e15da952bf1aade4498f63694","c4d2a21697551d6e34d05cc2d20920bf142723d8f9f9ff4396b1b7d700e0795e","98a3ed3fac7a473fa6fc4beca3de620139d52f9863497e943f053c124eae15a8","cafaee243ca4ca03a097af441baadf576180f1f6b0c0a71bd2cf27c92dc3c635","d5132708f5591db907f3adda24e0a67970e99d588082cf0cc673359c4140c1d4","5ac4bd7d11ce705620a56ed6903e3f0c2a432f616bfbd96d408d5e77d40773a5","54ae1f916462a6ec28561bf5c31fa31411c2b95421419952f82fb29efe33714e","5a226d5a51ad9fabfe0849f5d5c81b9460ccdd9184796f1467cb992bbc5ce012","5ddfa20a02c6207915c3c9d2fbdb57c70f549307638c4608a63c4049c591dc6a","c35b355506a0ec45a8488207a4df84cde8d696832d6157f61b6f851863ee1b66","edb83dc67f334db478790a845e10b191ab51d035160a88e5fd3876664dec1612","df984b0bafb5e53912367951ffc923a81ca6e15e6164b634055880e4344cac36",{"version":"7a1dd1e9c8bf5e23129495b10718b280340c7500570e0cfe5cffcdee51e13e48","impliedFormat":1},{"version":"95bf7c19205d7a4c92f1699dae58e217bb18f324276dfe06b1c2e312c7c75cf2","impliedFormat":99},"b045297393a1167259e546c9c8f790f11d1bb9e56faee2a96ebf9dce560f198d","9ca31fd2cb6a4a2a79f0b9655deaec5de960871406084ddc7834e5f0caf6825d","25fdd9095e3c88304fcacabc177f915c88f911ec76eec1bef97b0a5296b69b42","1b6a4d8614e087ac52dbff37fc366bdb9d9f6ded3aed6b9fc6a42c9a07c7df6e","76cc26cd0f1fc54af550f8841264bfffae32d47ceb78f0234554492a2009fa6f","649415d05765a968aad9b1906b023aa680e332b6fe32bf31d4e96ae972797107","3803a665d8434387ab3b02307f2724bf49e2c74295f930e7fe4ba58bca51a078","cc395f85c2a42db8cbc0e1749695af300abad568b8ff801b4ff1929071c2ffcd","e6f912a58bbeba4d07663b3655e1ef880f01e7a194a86e351d4a9b96785b9dc4","b93c566bb2c61594e867c53306a6d56b086e7658013d2b9d233e89fab5acbb35","7a0aed8f3aa05ec618437c5c7fbbfd2295321316c197a171bc12de0fbb2bb922","791e111b25123db45cda32073ba13222b5a34a947b0156eda9e77187f2ef91ca","fb0547afd53f48e07ad555fd6efd0de07fecf26615cc93a4607c0220211bb114","998feccbffe65bfb424ba1acc11357db6e7fd611eb99ebd237d860eda1e97138","7be4ecdc1589196748775376a609a917fd731034f8a1fafa7b5ebc9cf5e5bd53","62bbb0c5c7b0e2a53d11f4cadb782609af4e12e79170fd58163110088393b05a","0ca8883957ce65b76ec86ef9d3aac955dc3c4c46530cb729b88a6d2d737af186","542267bec9796016127a97865a0f97ea180cb8fc3f89606fb8c9cc4253126acf","28b5fc5754f4ebdf042cb5b27931e63b4ed8f5b64df7bf3be4b77d974640396c","89e0e9ac65c95aea0a539bd60ace8484b3a5c3cc4d897a68812217af830fc23d","0988e2afa78428c0d1cd50268b54dbcf421f80c992d8a1a6055f4133730ec6a9","f2905ff1575a84c410192ec46aa3f4996765c1e849ba421a259e05c7b2fc340d","aa34083e206a5f93e16d7c8ad6974bf445da6be4b5a7722d2ee98789faf7f8ae","ffe50ea4d20591a5940538606dd44f33a5dc343b6cc9d043377bb2c5f72dca13","38e01627ea43a1196ab44549409015e5c6f5b301fd332346a2e0fe38fddc6472","c3de1901e84728e2075eb45ca5df0d75ecd0e35e919e826fa8339404ed5536fb","676403375489df3da1edc59ed83d950fb937530b99d1691e495e7e84920510c1","8f8327bf5762a69f257f2eb84e3cf865c83a5a9c134adf38be12ce69bae4eb7b","11833b3c60fc9ab463a626d92764f42597c64fc93119ebadbbf9790756db7d5a","126096fa7c786d9d03b90f9e81f2145a2237e4781504957d3aa2ee4382f5b39f","1d095cf9256ec12ecd33cf34815253020c89230be4212f1fbbd9b5dc345bf2a0","ec7fc48032dd77a91fba9a4b98a4ab79135026a5364eef928ca03ce4c6936c7a","d2b46d9c36e9927cc6b59679dbc45a92ed19909fcc25412772af6bf7cdcc5a4b","1500929393a66eac684b644318dec8d947f7cece77bceac84395a96cd0e819db","cc998ddaa317600c0c56e53e6e76cfd3b3e23a0783fbf371f294201fd3b15e39","0fa7d91ac72697950720581ea9265cc0d7aaf5e559ae21bee6cb6ec3f7e4f3c4","6aa749045a7e565ab60023cd6cdaa21f9f0c0eb724ab4cc82f8b567182679813","42661bdd3babb7325e5c1050855251dc1ebdf4b14e69ef732bb948cd3fadb831","d202cab2cfd4555a3f2cc559ed805fe0e9d8890dd79e34646d142c0c24b95141","ea2f9f2010acbf2c1e02e79513fcbc6715fc69483b0b270e26b7b16592790111","36d25724c00295ceadde4a1f69ea32c94d749823dd5ede6258b928ed699f1768","2efef5f483459d0177a69b02baef9b931fd045a852f27a4f295f6ed958eb57f2","222ab086b785ed8c221acbd2922699f3411227a510d23788e52c303515b01d28","09b127a60bb3bd8625f2e0910af7c7e86afacc05fcdfe7ea0d9f73c381d7c8aa","fd0cecf30f9f963f1b0148733981087da93014fb0f69a9e065dab17fe67643ae","d9bac95e5cc44d0ea516e83cddda764f54dc5dd698cee1c64356ec968ebf57c0","2ec1e34570bc9b528ed078112f31576dc9ba1c973dc83a8c61c80b7bc425d4bf","b4ca0b2ae78101bdc252134c8519eacb2ceb931c9c38176706ac467b4eff7f55","b6c3f5b183e5428d20d6f6e3290a1e8efdbdc5cd8ec31184bb3a018b17c3d7cc","37cb6bb48aba80a010297eb815aecc07c0ea334509944c66305018a8fdaa2c65","be7b800e572c75d9eb786fa7448c9e108dab1cece51f55f73b4144e91b8906e4","0cb6b0a4e69bd922c9fae22e5d4cdb24b148ec0d29766c200738fdf5255a40d1","c34561f348d2cad7395bc203ab64e2edac6184a5f0184ecf19e22a71724e4fb9","9bb87d1dcfb06b265c96c8bff31a95d7003128fdd24750dc6ffc7ba2e4cd357f","00a6ccbc8dcd86c5d895ad39c2dbf030f2b6064c421bb49a021fae08f266e16a","5971247075b05ed88679485c1d056aec4cae062eb6951b995788c69a654a1c3c","f731d9570dc4e6c8a828c6b9f0228877820afaac8d2fdc00f4e8aff8d85c0311","dbcfc7d4a685b33c38be3dc2611615e692604bfdea5625f14d9f1156ad6722cb","83a184bbff2dd763f8e8051aed20b7b620d55778918945b23ef7241e6792abf7","fb73e07df3a40fbb9daf4ec269ac01946ce2a35cae550dc79e34d7ea8bdcecee","be6fe6c54c0b3a07b8667b02fc6981b5b40758ede42caccb7fa81646ba567d24","031f18d0b2394a9704c2f96564fd098bc5d9b8770a0467623cccb2d74f9f6d6a","670b9a2aafef1ec78813620c5a738bb1df2235a8c2def1702b8d58332b52c177","6e07a05d78279eb87db944470d6b16bf3c5ffc8a43b7a17bbaa9d80d34c638f1","9c2eb90da480fa93d78e04cc441c282a07a2c7428bdade5cd35ee20de7cd888a","3d3f74d81aa3ee49f3218c9f7ad9a9202891581cdc4cf3dde9b92ddde4e781d2","e9dea9b7f3586beb174d10099b644685798d15fb3d6dd5e57e7c4f2bb5068188","0d9237c67866eadb203d211bd7eaa1b3aba550a0c8a6bc1260d344c2c4a4ae7a","c60b24102572f64fb0023f5413404293c35c73b9734bd71da9d571b08111a81a","f479af6869f5f6e70b8d8e19f59995c6ae59e23bb56670c96ce30485ac98a1d9","f0bc31f07565c26ec58b2d3a26453b9019a83109cdb2254601b3e1cdd6fedd43","dd94387420ae72f77718a1eb8373de4687dcd6267f92411de88bdd3da5de2312","c66384b76689bf7ab5bb36915013d4836b7eff025a220b807bcdc106b1d4adf5","7f95716c1c1e73c9c27e91618a727846a86b6f69b8b9f16f84faf5f2a7fe7580","c24cb3c039b97ba6f6ae397234350e7f6f52e4088bc6d6b03de6d08907d5e3eb","01384cb437c106f902313bf25b0e8e65ac7b9ce0811bee3bfe7dfd073cd9b383","3c0a996c5eccc7c17f68c1ebb45c299ca6d7dbb9daf58c8fff3392409c31a9ea","ea2635643910097eba211a15f93faffd343817feb00af8efefd97c81b48e2fed","ca06a25e3a9bf82bfdb6f9f61d6d67f6c90960b6e20df43abea3c9eab515bbea","d1b1da0800ae66d723c43f2a07965d216addd736382b674a363d086b755f5efb","0d597693230d3eca85524fa33430fe1aed403b459510b0abe00a4ead3aeb4025","5d2667f3261713c3ed1dc5fdf5532bd49fdd74f399fb7e5d21901e29c7f6f1dd","d050988d2be49157325e97f3319532684b8ede2b83b87abcd8832b651b29b471","5a8fd21287dc8e39f13cbcb1ec3908f0f36428b4d575c02ef76c709d4cd07e3b","4e02c2619e7a81dfd2b13b89917c7320d5ebd87d46f79ea113d088a0f122f7f8","9f2de64058d15197c6e93170f8a8e2a10a3ae0f181d17c4b3a482bd3500d077c","ef8865f11a87d159281b1bfb08427a05413fb012a60e624d33589ba16957c607","5dcc475ba4e86db8610afe2ebce0f05da3d98be85ff9a0fd5e4bee1e293caaf2","f3afd6cb637b62f3184da2fed4948253e7476e9d6ec8e590359056d1ddf23b77","3f80582eca2101e3627134b99632333a665bc2837d1c4f7d07d0b3bdaba0aa0f","001dec038b0f816e352d106eb1d2b436d74462a9ae0f09487a533d08ebfa56b2","e645dc71a241c5fb97402bb0bafd2be8031abbdc9ea0726b44bf79f104022a61","4dd4f634c626a6f4365a71159448fe3ff154636f3413ef5a876ac0ac40fd751a","6b2078cf551ddb6ba3c5a47a50b78f3c3202d4bfffc06f2d63c46b1275969a40","ba881b8d5b6e3df354cc26faf2ebe93d6c3c1dde02534542c716970ae9862549","181dab8c3c89994ffd0f00c80a4af48d62aa9fa40c07316e8c3cd1825a62eccf","6d6336b1b4df7f1f65d3f84c9c020c205865ae237e19633d1fafd6aadad9a067","c12c64d0d3ac1be4fa925ba87f3de675601fb413ae25cc50396b192a9e176425","5402794b0084c917353387d102cfcc303842e39852c263451c1a77863cf28a9e","287d0baf43e643c7c740b9e8770f9300b2bbf98b8a518e76e8e217ec8aaead64","91298e6a51bb1962d39eceb6371aada140acfa24417011da38a2c50d2f0da168","4bde15551a94df07a4010f995704efb12ffdc4bd752851a7f95f591092bab4f3","9dfe82c07da9a54e56a248ba387ce383fc6a7743af92b17fed1527b0d5b6ebbd","290d8f4ffa2ed9c837bf5d1ff05c7637ff021d486fcae6d898aaf2df69dda769","231b7e837fc604212aa02a5b148f585ad0b2ba995032e37eea7dddbb3a46088b","4115ce5ac014390c8cff8d3c51b52fa13f6294fb13d1803d6c40fe6cfdd6901c","b2419ec464344d9a111778d1d616889685934ba3d0915801a156f20295da39ef","c2d0ba4e3ef6f4510841f61190862a4376c6515a4af1e44290d94e876f699251","632532295af106d8dd11653a780e944492849c55bc026c0425fe067c30e44bc7","d9a8dd605a9db2828cb9c2117053bd44bb318092e4bc8ac12297ecae52e48408","a40659fe11d83da3b659ad4287b7c1b141378fce8962e95592cf4ee53258f190","0e23ec9cf75425a296be7ee9bb1f85e499078bc492dae72e2fdc5d5fca98fb7d","4bdf7deeb5d15eda7a4a4246622cc7321e2d4d605c3dcd9a516625815e772e22","6bf062bf83fe661c697b7122c883b56f982b85438ade4e43731c2efa36a48032","fdc556b6075fae393c48cc18dc34519af2d448664447be8b7ca2126b02affb19","4678397e8b0ed1445aa088771c9d8572e7bf45cffc52c14b039f86b66fc20252","fe49dc0bf1a296cb927061f74f7fc6d74d2e418b9362c4640881b4e42a503031","d301b86d1d7050c88fcb6ca92ff9b138cfb0a0ca8a04b3a1b61ebfd911cb1a6f","fae2bc89da0a93f9a0266d21ac2c33bf03f2674c5ed33ff980e887f397c8e06d","02d6d2563d3b1928e87f184b0c8cf3bb35398e2c3aa7b6a29ffc24ddf282b6c5","43cb773c20a53ad1001df0133aeaf26b52855445031c295b9a37154ae35343af","0e481a28d565f1a503dbada5d810dde3113157cf6563016b213cffec90a79993","10719c6dd6b07a17cb9be468bafce43a61e5afaeaa4f571c5b49866ac337a8b6","fe0a9db55849377425bef5a6f180779d0c7370cd9f4a41b520880c5ee1cb11fb","c5c99d680e713636b83e2e037b5a292adf0b2dcac011b1b188552e5907f6d7a5","0e606c69a1200fccb0e927c090393936f2ac1c11920b0e55753f0c4fc45fb778","6b4c5c71b0aa3cac94c8b3b819f68a30d0248709e6bce38465bd8bfd7d7cf395","5f314bd27e54d77f98289eb1e174636987e346c78b9631a2a1bc14f5d8595725","fa1124d16b3dff467594eb52257875ba98e68e40f39e10dba0611c7cf8d657f1","4f6ab5fa695e1f3d02881def8d878313b2f103220b91e48db7ac3bc21239313b","0b2d2659a5ea7be13b0b7fe40f2c4019e3be87c4dd921a8025ee382320a7b63e","f354dde829d0ca02f714015e8ab8535b7e57d46d492c3e4e3e6aa2b3bc47eedd","9c24c78a38da64fd818c7488f69b9d3e3290ecca8d2d2e01389fffd60fbdcb11","508d52244a0011a2812d56877281530ea463ba6cdcaaabacee8d11116796be74","b47ee19e60f569336d7b5fb2933e702b83d57ed60a253edd66cb1227432a5004","9fcd9e148321b51599f5e159452bac3942fe8e5152e379d40eca23209c0a0fda","ee485f14ae7dfc8c0f38490472d1f546e61c030fe440c0ad68d0adc75b09e4db","62f0646b465914396e6910bc62de871a623d2277db1da460209380168be11788","d6ea9568e4855c97e8126e299a4df99ab0cc53ae477b032a27045172e54d41a5","39c340dec9bcc6bcc6f291a47dc6bbb6d298a114ad9f32b9829733b3e0830e38","c1c5a4d1a72757a92a31329e8f86753bb5ff0c4d0196b8466e168a171e5f4fda","3d7bdaf3ed469369002a47e51fc3c03af0ca6e657da5297480e20e4ecd6b5ece","cf0a557968c3c332ded37b6a899c82d605412fdff51756d1bf99c9251006ecde","f1f4867152604ba908a356c724970b6fa7c372d2f5f47ea04d37af4c043646b1","2e25a1451ed168766182a729e70cf978f6666827bd7fbebdfb0a12600be3557b","97b1f9f98dad479221ad2169fa7f94c6c4b049ed3b5bb2f88c9e2296973efb2a","d6852d894450cf567f3aa71d0052f1de7ec00992f4d00159b0d9130f9ac156ab","928b3deb2de8a4e91cd4473631de7d098f7cefba8279b527db2fbe9b440919be","91ff3d9748b6abfd395824f0fee820c5638871b86e6400c21d0c1dbe162b836f","1ee5b9e592624caba1ba0fcf518851c824f448376408819372bf950dce560322","225d85ad926f680f65b70a76869691ca99e633e3e4240810eaaa62dc45b5e092","1d217f400428c50966aa0342db49abc2e57c37143be7fb243e4d610d7680b717","02cd73f7d88b1a50873ac66115bec20abe49433c35f0cad5dd09fe6f2f5940f8","04134f50f9c61432f199b9419775578a04691eb5dcff09492e5af4f409793ce1","29136ee2c5932f09c7eaee1608d3ef92f1535d6fbfd8857e5b5cdbf91a85bf6c","1711236bdd3baa5e1bba2640cec90d8ce98eb9ef752b1cf24ab70209aed78b4c","106cad458688e9688f525b07808597fafbf7e2dbf27986a1bb7fa04772893965","38888437f31ec5e2d39aef9c46f5092d5722bf43f472436ad1b1af08d0ffdc57","284363ad97f3206b55bdd30c19b6727051203e030bd3d4008374d135f789b11a","87f0e1655eed475c75e8f8565b2c111b222eff3a0be57122e5dd7f54349cd26d","90d293744f66eb2cd999f5a414d75c0f6eb572505814c1186f29563a03554550","516a0644a6400b8ad2699d64219cd623ac085cd72a6256caa17f6ebe0da01c7a","5da3a91b2b5294885b3dec780c177bfde9132af5980be86b54a3e7734e3c26af","91ca499e3074bf63918558870f50849bf4a60be52d5a07c5f7b4e1c81361ece6",{"version":"2a078a3d4ff94609475f554e331ea7979b9f340f73ae327a0e28609aa455a671","signature":"12342600947933152b059cb567e68f067df23df03834804a6f8b2e0163db451f"},"f97aa7fd4854ae4781f63fd5d481f76cb728a8501f254748b195054547cffce6","4808bce828d7d7195dc01061460b99aea69a268113007177eb07d09fbcb9f285","7dfece00e91dedb5e6f262fe95608e17d93b0e3bcc88d3221de3bdf82c22a4a9","184e1728672781b439d0053f03e41aa2474569efa05bb2429e4ae000a9aed934","b6b55f2ed87d0e7a97b6a552c4becfd465f99598fe6e0a531b36beb2b98c4258","bbb46b0fbcdc576355c8044c59fc6041147db11505e5c4335bf3e97a5289efb5","a3d89d72d4526e41ccb86f770a5f55edf937a5f667b0befde576c1d4bdb0c9b2",{"version":"8cd5c8f46fc7915ce58a13621837e4101fbe9fc948ce01549b36f4852b70cf58","signature":"106b3f8678b78fce3a921b4b10b77490f25b8f54faad30c4696136697741d759"},{"version":"d73abe26d86d76b9291215d6e55011c8fb57b931ddddc807033fbbfc96529d0e","signature":"a1d6316feb75f247c21e648ab2ed07ef58c54a6fb2071823fca13e8a344a1ebc"},{"version":"2b096428749378118129ebc7a85424f79dc2b8bc958968dc024a65049e313009","signature":"196783e4317f27aa9a8fbaf75d12fd0a68557ed455301114d24e820aa081b064"},{"version":"33eb7d195211e397eda255795af34acfb955079c69faa09bff2703e253e7fb42","signature":"45bef13917c6670ebb0d0a0925ee7c155c549e22db117bd4b98f0dc1411c089e"},{"version":"a991ed0662c2fdf75cbcc03dde481fe711e94f05c20b08dfa460abd7ae375041","signature":"6cf6827828a5f7925d59312ee0fdbbe7b610dc07ff45d4614c6e1aefd3b21eb9"},{"version":"527ddf406dc12fb329bfe5ee50afeb4143094f1f67cbccddc5c6b236e2842205","signature":"39538c0611f7e5171b0f1f05f63f52f204bd20350a450b2a403282bcd891be22"},{"version":"e560c6f84daef82b9b6ff6f0b60221a49b26dd6904132848a55322a6e297a79f","signature":"4937e3a48705847042e97d5e2a35fc1d311b9f969eeebb138ec7dc5e7161986a"},{"version":"64fd9d10ab6441c5ab6d6e66aeb7b60aa8fa99e81e5f2499046dcbc979dd5a91","signature":"333265ebe414dc5827b0cac654bbc9a17a33a6daefac366df1aeb3308205fa36"},{"version":"e0e1d0d69bb366ffdee63473cc9f813dca0a5aaa66f4c13bfa73f92d7e78aa98","signature":"6f184ea8e4fbbdae888aa665da17e449e9b316b4addbee12b13fea75ca5935d7"},{"version":"11235f5d63ebe09a849f09bec0504b247057c26fb72a96939d0b84a3cda9b7e6","signature":"1777c629dc50335faf229a42a6b979b057cf9bcc1a15bf031b8839ca50930560"},{"version":"e1d2be54ff56dd0cbc96b9b43b04579944cdd2547f193e9ff55fe2e5fb8ca620","signature":"a5e8701ea9836bc26137ffd002658c9ea77074e5c9f68c26a6816200033c2472"},{"version":"042ffd4c05b68e236f5226eb57f587c39ed8dfce8fd108365fbadfdf07f89879","signature":"060722df62d4aae0f273cefb7357538dd42ced41abbc0617e4809202d1294090"},{"version":"e9ceaeffbab576dd1c624f75214e896c5d2b8905fe08c605af2bb1512462253e","signature":"ceec29d2b665eda0b8a2c09f3e8561b52a31f89e337be12d68a2dbcf44e7d63f"},{"version":"1d83022a1a09d133f30366342ca157834027c022d22825d192b05dc3ba72205d","signature":"ffb3057a72b3671dae0bf6fb216ac26727ecdb7d96ca6c8a8c204b61daaa174d"},{"version":"5ec565237f90f6ffdd87af68f25f3ca86e8674ae1b9aaa117362ab5ccd181b4c","signature":"e43c20b05f1a036867b1350190fdd0e1aa353a7d283af17109340ae323c6b1e4"},{"version":"9865c353f1dbb31026fb9c2a974926188b6a04cac529bd5ba115de094c8b8069","signature":"01ab14c2f02d1f2c37daf40ff3d276687dd5b8168b148cd41001977831de8ef4"},{"version":"72c7cbbd490ab5f383e635eecda98a62844cccf630348e1b720cce61f2d1d102","signature":"17f63890415e853bf59bdc3a148f0db5681e41c62661b7e0d2fb02474f3aa7a5"},{"version":"3cf1e8ef6a548f5a99da18b1a42d2748f34312f649060f81abb8ccee97f43bc1","signature":"4de980b028050932f168d5a5249e1b30bebf820dae4bb86312b1979ef4e86092"},{"version":"d087812fbad5bfcfb8475d947924b1f5329c04caca8f69890eb9263af7590c6d","signature":"7b339f209feae139c4efa581b90dedbd1a04d9cbcb385b7a0435ee10f7b6c609"},{"version":"c57294050388bcb6b61beb23ae6fe202e60efe8eefc32910bcabd8f579134146","signature":"f69b245b4ae7fc7b73308440f9f4f0f5c22e827ae21e0ec7d959e35b0c6a000f"},{"version":"15b8f0d1fbe927d19bc38a678308022fdfaf39d5bcea3c555cbecd1b2c0c7f47","signature":"a0f157f6ec209e1baafb123233f40a4eb18dd702766a0f4792e6ab44c409a1bf"},{"version":"4eb5784ab2b376ea97fdfc4c25cb16a4c7cf61c8d9e962a63661a3aa75ab396d","signature":"0909d57619ed061f78838866b346b08315f14af7a370c2f76bac160274f49882"},{"version":"59138c34c47d26972210b261a0c875a72ffa626ea037e22ec6520bdaa1be7477","signature":"f27ea12fb23392518764e75a358f799bfe90407bbff1449854634cab3dcde2c1"},{"version":"917b63fda998cfb36511cc218d33f5dab5ab7d45b9560b09d42b8e9f791f3c1e","signature":"bbb5c6397a256407b2e3238ca5b4f340b61da8df317b132464b0aca216834b96"},{"version":"cd82ca985cd336a0b3fb7f11c7638c10115ef8d075c284ba5396270ec9db5aca","signature":"ff0dd6ae109e2ed64128cde417e91dd1e44b3b9a5b1021bbf555a86e8bd7fe0e"},{"version":"5834a7e9d1e23542abc0e5c3635051cff315f98f5178f1518b5aab65ba65d602","signature":"c88096f6a70b828ed96808c40830feb934d77901d5dbd6b777d4e04bd0af03cc"},{"version":"c35e5a75228d0fbee3f2d1d59bd8b74c62bc8752c54d3018b6b475b24967e380","signature":"87b7066a0a2463cd0fec6a6609f558e5c84bcc7842a1f464a86eb3e67afcd763"},{"version":"b27acac9d5dfac6e1e1e8afbbb453ea1fde5c073ab4cd5f9c2e9785e9d192fa9","signature":"64e7a53d631709d0a5fc4d4d065a512de8371f86ad20c822c11e8c4fc88828aa"},{"version":"a713743e21042a5d96c0d9b42e265f7686fe478aff836b7fedaed5908540c7a1","signature":"8f16a1e5b9c89392599d6faede167d0d4abb25515e1e56b79fad2e57a8e03fa8"},{"version":"c86c0661f5ec2d684ae73aea3d94c6abdd1edef9cc2ca9e3c656e5fa3f5713e4","signature":"0a1900a2d38369652c46d9dfed9167f8f6b06136338886a985d12389d4a69c94"},"7c1ebde823b390f4e738aa1384e6106f0246ce1d73b1d6d20d4e9c67623c26d0","18e839a0576d60b32ba7865379783c9cc434bde58eaeb267aa5be1f6f1472529",{"version":"9bdabd0c7274d9cd8009a717087ec17431228901cd1b475955cf053adf837813","signature":"d56a9c1ad9bfaa5b9aded4ebeed606295470271fcb9a137d7205f69bb62d1241"},"b0a66b71460581bda148485bd214bdcaaf706776cc8969601eb9e9ae73107833",{"version":"988fad22b03f068fa42ba0f21ca11a30b144c5b6dc7fe378dd0d5993cb0ea021","signature":"337d47027050863e14974d13d062f1f85a9b2869a84bb91bbe24df61ec852a93"},{"version":"9f463411f685d3f2cc13ff4395ee2ef9b222f62048f74ee699c528cf4551a02c","signature":"77902db1e0ea5d4209241c6f0bd67e74d3f3665a0a009eb797deff03b678f655"},{"version":"8de4e575e3639288d0d181da4f8d304d99683bb382dc5c2e1e643b33210f94c0","signature":"a4e69719bab3096f0d889cb7702273aeb1663ec1bcb660828a36e75e8bf709b1"},"56fd389ea239c6259f7ee1eaa9f82f0441c6566da29d670f5ec9df6b10b55d6e",{"version":"4726730040de266dee3f85ff088f7e86c81d34f8e89a3658ec668d2451cd8b20","signature":"1ab2b8fa37eae578b30743deb1fe446bef44135e76a66eb438d772d7ee309ad9"},{"version":"a470d4f6c0fd45144ec0e97c0e9299fa8595cf88d599901ce49ef86fc2105c1f","signature":"79b4b71c08d175da7cb27e11ea1bcadd758cddf20b2ffe0e75cfbeedd89584b5"},{"version":"3dc7bca9a9ce7368cce1cfb457f560219e5e8503f5eeb84915c4d7278be08f6a","signature":"70552cd64daeb1bc28e5570f5f93139fbe4f61be463fac96009373eac9446730"},"d302144628e8e8bb096aa3668b545511f46f0c3b787b5de6fa887892a702387b","2a2584577164a5f7520b29f83472b5125fde512794ff5ec5b2226eed0f098f6a","a17c3792bfb09725c7024bd1e5cf1e7496136d42a3ef83deb3697ff92edb7af6","673ea4e20151fbfa745feaf284c1c85545fafaed03911015c6a7791160acc1e9",{"version":"889fbbf9ea80ded4dc038d353dba84a398434c58c22fb1fe2bbf4de3ed181e38","signature":"322916fa199c1182ea2faca1b6d488cb6ec497f7c48491623c440815c1033e97"},"c201951b6841ed5d1303eed429a901c9a0e3eb14cc8e5948bb0e56d12efb615b","bb89661a4eb77298a7369fb43414990eb6041032f9138e9f75798832d1bc4441","660bc470b457b104a4d9d80159b1d348d6026025f42f7690a4c184d2b66ae623","2732c099efc3745e4ba388f0d56b3d3767fb0c7d43aeefe1b2613f09b90c7f31",{"version":"a4a4f6a3a827bd8674d5b4230d6ee7ebabda6d637e76dd6667b524e61df71300","signature":"3d3e8d8e570c2d70ad678766c40a35c535b5fa76cf7bc73c6333c54823b0b752"},"0eb9fd5052728a548116c94d7954723095c9a292edf83f9d9d1ea45963d0f8e8","54b7abaab7423e9c952f1aea2eaab68266c85565f21c656c9de31d80a38a7282","040fd3a93bc770d200ecfbd83cab66b22e0c104fdaed29daea5f1ecc05f8dc74","19d8af7bc1beb5fcb22aae8b44fe31dd7af39500f54fe5f212721ba8d6a297d1","fb59f620807e4d1dbc5cc95639262f89395c1d20fcab03efc323a221b07bca43","922d4ab256e7db3a63b52e0e4a8f2c7019491b58b0961f297c5b613a2783d970","85b6daaabfe2c275b7013d13df14031bbaf623357e3746bed9ca1afaab46bedf","2ccb0e02fb165ef588673868e5e1b78d7f2b8f22cd463c8e015f9c9fcb8f3eb6","f594f5af79c5a5c75dd9e04e39314d6a193b2a18369d57195ae3675ea25925e2","8b76ee888686669a7bb8b6c5ebeb72e5a7bc0e7263152ff5daa0c486d34d870d","1cad8db035a510050fb929577f148153ef41f0fd2773df0149cbec01556c7050","43b3564d0973387eb4877c5e3a996883331e8c73a2a675aa4a708afcb4f12521","7ef1eabb4e5ec071c4aca406d80abf528ea9a15b968dbc3e275d8453c1924003","ee5d9091cc397c259f406c229cecbb893af5548944ec9b7c3dbf0daa9cced037","cdaa28c764ef022a285afecf6a4b1dc9287843dd2b45c703056e880467257cb6","57a452c40651b548c85a342f28241ee37a84e1967630ed318e515ee21d4c8ee5","e0611572c6aa5500e703c4332eecb1235d618fc91315bd39d4ccedbe3a6fe710","be4f9c6ac98f48266c88db65806e5dc4084170a229c6205159a53f0507c4c666","bb299294f09f4dd9932c8ad94bb16d7d062353d172f72e0a99d7226f3ec5b659","83962d24b7752d8aa205f38ab58ba7fd4464017403e4eb8d2481234248a626d5","260da6fd223535a3d498a1af94f4249a3e67d28e56d7648dcc72e069911cc2c8","d1f0095428cf1de664d9594850b63567b7ca7325fedac6dfd425130041b420a2","6c430c13fabc220d5e2053a81d1c993ba4341c9f461d98270b1ff503c8c48030","0740a1887c4e1c1c71fff7925a3feef85a6ab4d5b2ee4c24fb84357583da3402","a8124c363cdc2f4bbd08a4283e216b1d1fd358ddbdc658afd6fa510ed16740dd","1c6cecc8cf9d39e2501347175b1be54b8dca76fed82b87f9fa0b8b7268cb5b61","cdc15c17185913bb5b56f5f240728e7096ca133d78464876e98b6d38185c6d19","58fc9b0a79e29fa867e347981ecbef6ef52ffacfc848be358cdc41a5d9e133c8","b3bb55f630f4d3113bd13408c49d2d7a2ea1ddf96061cb5952d0bf778d491eba","f10912d31d9010c44d3aa471b34e77be99329c64f69675b00104a21e4cc50470","20e5482e50895255d12953211f19710988ab278b9f342db385f7985d34ec02bf",{"version":"d4bc691857a8aa60b09cebdfda30afb2eb8ab13d7fa8c29bcd93082803223eff","signature":"e847e443a0d4a2648051133817a02200e0b9c4b2f616a992c7ca8325a7c71d24"},"75304d5f2ccf463507df23f4b8066e7e758225a1a36d6ebe83c1b714da6c2ac5","049398af73a373ead19379fd1da4809d18bf6cf7123e44116c1491ad58d5acf0","9d8c851e30a432fadf9709f5b7802b9346d7c4aa3db0f25907669f98178bdd6e","4eaf53474be1f3e9182de786375b87893f47081e491b45321cac3a8427250f5c","79804619db6df7144de589d927d3c861d5e037ee91c9d55590ac14f927e371ac","5f6104030d0571c22ccb732c1ff1ee3f9ce38d54d0157fb895fd99d62222103c","5a64c9b17d19f3d6f73be414c83c4b4a37bb1898fc8552e2d63f71f2f6904724","a17ffb4463a3144d95960125ae3ea541a055bfa1494a908dc8688a21a0a58216","9428fa53b4d4910ec04a42be6e8bc915242800381bf5782013965ae8b4f6e3ae","dc9928627dc636458117a0f2888131ab222a160ed58d4942ebdba75d32beb69b","632cf9f97223c29fa65cf7eb81ba15d2d21a851dff8f5e0921b7e428a837af84","5e789cac338582e7c1522ce9fde6b2c4cfd626989fb5dedbdcbd1fdb7028e82d","1ccf807c1e706cf662a2475227e453da3a57cd9f6c21aaa843d3358d619e78f4","dad7f9496da68b21150b690b1bf00dcb5ddc05f575f082b9f79a0316e3cf0923","356173560bdd68ce8d6f93eb359829b4b93fdc9799369052964544c0c0551303","0ef5075c8c118e4b9f6d6e61164849a3da33cfa8a6d709b9e76b9e187cc502d7","30b18dfd86c195a4e504a9c7cab74e253cc9a81e5150a1e078a60116f8413a7b","eb572a47bd52202d9046b74cd08e11b817a09d7da9d411221c209605b53d50bf","507d41570c8cad3489d272cba504fd33baa7d60903086b2b1ea6a44a32cab870","4f4dad224f28ae73a636eaca502d71b374a4095d5a3f540f66cb56a688e067f1","4139b027a8724e900dc887009ac6df809cc5f6a5fa5f58d2d684a6f6d96ff245","09e6acd94660ad0acf93ee0cce96ba7c540b67bd65ab5b043decb5ad9976c00d","f90eaf7c39bcd43cca5e307991878da4c86947ade4c9c999534c75647e9356cf","5b9a0c2f29da015a2b552e9890b67dd33cbe9fd15c69384b8eb2e65b0be19320","01101a850ef55e575e5c27672a5788b954101ab3ee2009dc5d94820fd9aeda1d","6c26420749d17833efe84a50d61db06fbd4082e497e65ae2e4b82f2d23476279","d6c4e7806d89a5b3fe00eb53b08ae06a8d26226bc9cfbec2758d4db28462d0c4","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",{"version":"cadeb2c96f1c964d7e49c0f17d6805e1b4dee62f0862c49bb178dae6ab277e8e","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"eb64a68249f1ee45e496a23cd0be8fe8c84aecb4c038b86d4abcc765c5ba115e","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"3b0a56d056d81a011e484b9c05d5e430711aaecd561a788bad1d0498aad782c7","impliedFormat":99},{"version":"3354286ef917d22c72e0c830324062f950134d8882e9ea57ad6ade3d8ad943cf","impliedFormat":99},{"version":"3dedc468e9b0ed804c0226482e344bd769417f834988af838d814504af81cba6","impliedFormat":99},{"version":"ac3d263474022e9a14c43f588f485d549641d839b159ecc971978b90f34bdf6b","impliedFormat":99},{"version":"3b89216a7e38a454985ad17bb2ff85792837dc812f2a89fa5f60ad0a2e216fa7","impliedFormat":99},{"version":"10073cdcf56982064c5337787cc59b79586131e1b28c106ede5bff362f912b70","impliedFormat":99},{"version":"82179358c2d9d7347f1602dc9300039a2250e483137b38ebf31d4d2e5519c181","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"091546ac9077cddcd7b9479cc2e0c677238bf13e39eab4b13e75046c3328df93","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"727858fb893b87791876dee5e3cd4c187a721d2de516fd19d3d00dc6e8a894b3","impliedFormat":99},{"version":"5bfaa2ee33e63a1b17b08dbefd7a3c42d1e0f914e52aca5bef679b420bd7a07c","impliedFormat":99},{"version":"7d5c6cc5d537c47c7723a1fe76411b99373eb55c487045dfd076c1956e87389a","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"a86701e56b10a6d1ef9b2ecaeedbab94ed7b957a646cd71fd09d02b323c6d3d7","impliedFormat":99},{"version":"b3f0791a73b6521da68107c5ba1bfed4bc21ff7099b892700fd65670e88ef6ee","impliedFormat":99},{"version":"d0411dddbef50f9ad568ee9d24b108153bcb8f0db1094de6dfbadf02feb3aa70","impliedFormat":99},{"version":"25249ca5fe64ca60d7bfb7fbbf0cb084324853b03a265dbbbc45fb4949de7567","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"31d5fb0aeb0368909fbe8cd9b893c16350aa94d48a2f909fdd393982ceb4814d","affectsGlobalScope":true,"impliedFormat":99},{"version":"90fe5875e2c7519711442683a9489416819c6cec8d395e48ff568e94254533e7","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"72a863bc6c0fc7a6263d8e07279167d86467a3869b5f002b1df6eaf5000ccc7b","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"42f0f7e74d73ae5873ed666373e09a367d62232ca378677094d0dc06020d6e00","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"ebb9b9fa684d70aef64614a59b7582f46f4982139b8b632b911ef98e10c4d117","impliedFormat":99},{"version":"2c34caeb0e7809e93a2baa06872c49707b1c96e229e1061c6b9ee364607f563f","signature":"b39a7180afe792f0975be5e4841a05ccf9365eca0b213207c44829c618b565be"},"3bb648f566e49849b20bbef6a3b737a875ef4e774b33f94f53dfdbd9deac403f","3d8e5168a19d52d77dc69b6785a069ed2f14000e24eb275dd5846fd31e473801","eb76714199edf4f96ba125503ae4114b4d557a55ad0fc85ec5bc30f09310bbaf","ac9ad025410d9d9a641205333a48c94a3de3caae3f5c3c4d155cf70b520556bc","e51177d723e64eb327102e10399d61ed9e89bddcf69260ac0600ca0a39b3f33b","b1bd895022be16cfda91bd37db27aab863df16082d760afe55ec3d912f1743b6","4143a3ab0eafd782c745bb4ce3b5ca2be6ca96cba8d23dc69755dcc348e319c7","6efb415baab05a5830113c471acf3c82cae9b9560036644f937cb34924921dcb","de5e7d2f6e5328b18b5dc3a39f3ca67df73577cee8b5ca8af48759f1264dca01","f24c8d553c4178e3bca9e6988140a5b98064a1f8820b53adc58803b6317c84c7","043dc788a719c8ead6f63a3f38809be150dfab433068fd674cbe491c7b0af2fd","99af15d20e90e9b50cdcfca19a43ae519039ba1c747da2e3f1eb0dff85345180","af62c3ac94d0a249d8c26a6a285b86557617dfb08f8f665cd58505232f74f78c","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"root":[[58,83],[125,129],[236,262],[265,268],[282,563],[611,629]],"options":{"allowSyntheticDefaultImports":true,"alwaysStrict":true,"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"exactOptionalPropertyTypes":false,"module":99,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitReturns":true,"noImplicitThis":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":false,"noUnusedParameters":false,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"strictBindCallApply":true,"strictFunctionTypes":true,"strictNullChecks":true,"strictPropertyInitialization":true,"target":9,"tsBuildInfoFile":"./tsconfig.tsbuildinfo","useDefineForClassFields":true,"verbatimModuleSyntax":true},"referencedMap":[[590,1],[235,2],[593,3],[591,1],[264,4],[263,1],[181,5],[182,5],[183,6],[135,7],[184,8],[185,9],[186,10],[130,1],[133,11],[131,1],[132,1],[187,12],[188,13],[189,14],[190,15],[191,16],[192,17],[193,17],[194,18],[195,19],[196,20],[197,21],[136,1],[134,1],[198,22],[199,23],[200,24],[234,25],[201,26],[202,1],[203,27],[204,28],[205,29],[206,30],[207,31],[208,32],[209,33],[210,34],[211,35],[212,35],[213,36],[214,1],[215,37],[216,38],[218,39],[217,40],[219,41],[220,42],[221,43],[222,44],[223,45],[224,46],[225,47],[226,48],[227,49],[228,50],[229,51],[230,52],[231,53],[137,1],[138,1],[139,1],[178,54],[179,1],[180,1],[232,55],[233,56],[594,57],[600,58],[601,59],[599,1],[564,1],[573,60],[572,61],[595,60],[580,62],[582,63],[581,64],[588,1],[571,65],[565,66],[567,67],[569,68],[568,1],[570,66],[566,1],[592,1],[140,1],[607,69],[609,70],[608,71],[606,72],[605,1],[596,1],[589,1],[56,1],[57,1],[11,1],[10,1],[2,1],[12,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[3,1],[20,1],[21,1],[4,1],[22,1],[26,1],[23,1],[24,1],[25,1],[27,1],[28,1],[29,1],[5,1],[30,1],[31,1],[32,1],[33,1],[6,1],[37,1],[34,1],[35,1],[36,1],[38,1],[7,1],[39,1],[44,1],[45,1],[40,1],[41,1],[42,1],[43,1],[8,1],[49,1],[46,1],[47,1],[48,1],[50,1],[9,1],[51,1],[52,1],[53,1],[55,1],[54,1],[1,1],[156,73],[166,74],[155,73],[176,75],[147,76],[146,77],[175,2],[169,78],[174,79],[149,80],[163,81],[148,82],[172,83],[144,84],[143,2],[173,85],[145,86],[150,87],[151,1],[154,87],[141,1],[177,88],[167,89],[158,90],[159,91],[161,92],[157,93],[160,94],[170,2],[152,95],[153,96],[162,97],[142,98],[165,89],[164,87],[168,1],[171,99],[576,100],[579,101],[577,100],[575,1],[578,102],[597,103],[587,104],[583,105],[584,62],[603,106],[598,107],[585,108],[602,109],[574,1],[586,110],[610,111],[604,112],[117,113],[86,1],[104,114],[116,115],[115,116],[85,117],[124,118],[87,1],[105,119],[114,120],[91,121],[102,122],[109,123],[106,124],[89,125],[88,126],[101,127],[92,128],[108,129],[110,130],[111,131],[112,131],[113,132],[118,1],[84,1],[119,131],[120,133],[94,134],[95,134],[96,134],[103,135],[107,136],[93,137],[121,138],[122,139],[97,1],[90,140],[98,141],[99,142],[100,143],[123,122],[273,1],[278,1],[271,144],[276,1],[274,144],[272,145],[275,1],[270,1],[281,146],[280,1],[277,1],[269,1],[279,1],[543,1],[544,1],[545,1],[546,1],[364,147],[365,148],[362,149],[549,1],[550,1],[551,1],[552,1],[553,1],[554,1],[555,1],[556,1],[557,1],[558,1],[559,1],[560,1],[547,1],[548,1],[561,1],[562,1],[563,1],[498,150],[499,151],[497,152],[496,1],[469,1],[491,153],[378,154],[470,155],[494,1],[471,1],[472,1],[473,1],[476,156],[477,157],[478,158],[475,157],[479,157],[481,159],[480,160],[495,161],[482,1],[370,162],[483,163],[484,164],[485,165],[486,164],[488,166],[611,167],[466,168],[438,169],[464,170],[439,171],[441,172],[442,172],[443,172],[444,172],[445,172],[446,172],[447,172],[448,173],[450,174],[451,172],[452,172],[453,172],[454,175],[455,176],[456,172],[457,172],[458,172],[459,173],[460,172],[461,172],[462,172],[463,172],[449,169],[465,177],[437,1],[440,178],[467,171],[435,179],[434,180],[380,181],[381,181],[382,181],[383,181],[385,182],[384,181],[392,183],[389,184],[391,185],[390,186],[386,187],[387,171],[388,186],[393,181],[394,181],[401,188],[395,186],[396,186],[397,1],[399,189],[398,186],[400,171],[402,190],[403,181],[404,190],[414,191],[405,1],[407,192],[409,193],[406,186],[410,171],[411,194],[412,1],[408,1],[413,195],[415,190],[416,181],[417,190],[424,196],[420,197],[421,198],[419,199],[423,200],[418,171],[422,1],[425,181],[426,181],[427,181],[428,181],[429,181],[430,181],[432,181],[431,181],[436,201],[369,1],[433,202],[379,171],[468,203],[377,204],[373,205],[375,206],[374,207],[376,208],[372,209],[487,1],[492,210],[493,211],[489,212],[371,1],[474,1],[490,213],[367,214],[368,215],[366,1],[330,216],[328,217],[329,217],[327,217],[326,1],[516,218],[515,219],[514,220],[507,1],[513,221],[508,222],[509,1],[510,223],[511,224],[512,225],[506,226],[500,1],[505,227],[502,228],[503,1],[501,1],[504,1],[294,229],[612,230],[293,231],[292,232],[291,232],[290,232],[282,158],[298,233],[301,234],[300,235],[299,236],[286,237],[268,238],[613,239],[283,240],[284,238],[285,241],[267,236],[251,242],[250,243],[249,244],[245,245],[247,246],[246,247],[248,248],[265,249],[266,250],[254,251],[253,252],[252,253],[255,254],[259,255],[260,256],[257,257],[258,258],[256,259],[261,256],[262,260],[244,1],[287,232],[297,261],[296,262],[288,263],[289,264],[295,265],[346,1],[352,266],[349,267],[350,268],[353,269],[347,1],[351,1],[348,1],[356,270],[357,271],[354,1],[355,1],[344,272],[345,273],[322,274],[341,275],[323,274],[324,276],[342,277],[325,276],[343,278],[321,1],[358,279],[318,280],[320,281],[316,282],[317,283],[319,284],[315,285],[313,1],[314,286],[312,287],[311,1],[308,288],[310,289],[307,290],[306,291],[309,292],[305,1],[304,1],[542,293],[359,294],[361,295],[360,296],[303,297],[363,298],[302,299],[518,278],[529,300],[520,301],[521,301],[522,301],[523,301],[524,301],[525,301],[526,301],[527,301],[528,301],[519,1],[530,302],[517,158],[340,303],[332,304],[333,304],[334,304],[335,304],[336,304],[337,304],[338,304],[339,305],[331,158],[624,306],[622,307],[615,308],[616,308],[617,308],[618,308],[619,308],[620,308],[621,308],[614,1],[623,309],[127,310],[81,311],[83,312],[125,313],[82,1],[126,311],[79,314],[74,315],[72,316],[75,317],[77,318],[78,317],[73,316],[128,319],[65,316],[70,320],[64,321],[71,316],[80,322],[67,323],[66,324],[69,325],[68,326],[626,1],[76,1],[627,1],[628,1],[629,1],[625,1],[243,327],[242,328],[238,329],[239,330],[240,329],[237,331],[241,332],[236,333],[129,334],[60,298],[61,1],[58,1],[63,334],[59,236],[62,335],[533,336],[539,337],[531,158],[534,336],[536,338],[541,339],[537,340],[535,341],[538,336],[540,342],[532,1]],"affectedFilesPendingEmit":[[364,51],[365,51],[362,51],[549,51],[550,51],[551,51],[552,51],[553,51],[554,51],[555,51],[556,51],[557,51],[558,51],[559,51],[560,51],[547,51],[548,51],[561,51],[562,51],[563,51],[498,51],[499,51],[497,51],[496,51],[469,51],[491,51],[378,51],[470,51],[494,51],[471,51],[472,51],[473,51],[476,51],[477,51],[478,51],[475,51],[479,51],[481,51],[480,51],[495,51],[482,51],[370,51],[483,51],[484,51],[485,51],[486,51],[488,51],[611,51],[466,51],[438,51],[464,51],[439,51],[441,51],[442,51],[443,51],[444,51],[445,51],[446,51],[447,51],[448,51],[450,51],[451,51],[452,51],[453,51],[454,51],[455,51],[456,51],[457,51],[458,51],[459,51],[460,51],[461,51],[462,51],[463,51],[449,51],[465,51],[437,51],[440,51],[467,51],[435,51],[434,51],[380,51],[381,51],[382,51],[383,51],[385,51],[384,51],[392,51],[389,51],[391,51],[390,51],[386,51],[387,51],[388,51],[393,51],[394,51],[401,51],[395,51],[396,51],[397,51],[399,51],[398,51],[400,51],[402,51],[403,51],[404,51],[414,51],[405,51],[407,51],[409,51],[406,51],[410,51],[411,51],[412,51],[408,51],[413,51],[415,51],[416,51],[417,51],[424,51],[420,51],[421,51],[419,51],[423,51],[418,51],[422,51],[425,51],[426,51],[427,51],[428,51],[429,51],[430,51],[432,51],[431,51],[436,51],[369,51],[433,51],[379,51],[468,51],[377,51],[373,51],[375,51],[374,51],[376,51],[372,51],[487,51],[492,51],[493,51],[489,51],[371,51],[474,51],[490,51],[367,51],[368,51],[366,51],[330,51],[328,51],[329,51],[327,51],[326,51],[516,51],[515,51],[514,51],[507,51],[513,51],[508,51],[509,51],[510,51],[511,51],[512,51],[506,51],[500,51],[505,51],[502,51],[503,51],[501,51],[504,51],[294,51],[612,51],[293,51],[292,51],[291,51],[290,51],[282,51],[298,51],[301,51],[300,51],[299,51],[286,51],[268,51],[613,51],[283,51],[284,51],[285,51],[267,51],[251,51],[250,51],[249,51],[245,51],[247,51],[246,51],[248,51],[265,51],[266,51],[254,51],[253,51],[252,51],[255,51],[259,51],[260,51],[257,51],[258,51],[256,51],[261,51],[262,51],[244,51],[287,51],[297,51],[296,51],[288,51],[289,51],[295,51],[346,51],[352,51],[349,51],[350,51],[353,51],[347,51],[351,51],[348,51],[356,51],[357,51],[354,51],[355,51],[344,51],[345,51],[322,51],[341,51],[323,51],[324,51],[342,51],[325,51],[343,51],[321,51],[358,51],[318,51],[320,51],[316,51],[317,51],[319,51],[315,51],[313,51],[314,51],[312,51],[311,51],[308,51],[310,51],[307,51],[306,51],[309,51],[305,51],[304,51],[542,51],[359,51],[361,51],[360,51],[303,51],[363,51],[302,51],[518,51],[529,51],[520,51],[521,51],[522,51],[523,51],[524,51],[525,51],[526,51],[527,51],[528,51],[519,51],[530,51],[517,51],[340,51],[332,51],[333,51],[334,51],[335,51],[336,51],[337,51],[338,51],[339,51],[331,51],[624,51],[622,51],[615,51],[616,51],[617,51],[618,51],[619,51],[620,51],[621,51],[614,51],[623,51],[127,51],[81,51],[83,51],[125,51],[82,51],[126,51],[79,51],[74,51],[72,51],[75,51],[77,51],[78,51],[73,51],[128,51],[65,51],[70,51],[64,51],[71,51],[80,51],[67,51],[66,51],[69,51],[68,51],[626,51],[76,51],[627,51],[628,51],[629,51],[625,51],[243,51],[242,51],[238,51],[239,51],[240,51],[237,51],[241,51],[236,51],[129,51],[60,51],[61,51],[58,51],[63,51],[59,51],[62,51],[533,51],[539,51],[531,51],[534,51],[536,51],[541,51],[537,51],[535,51],[538,51],[540,51],[532,51]],"emitSignatures":[58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,125,126,127,128,129,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,265,266,267,268,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629],"version":"5.9.3"} \ No newline at end of file +{"fileNames":["../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./src/types/graph.ts","./src/types/providers.ts","./src/types/deployment.ts","./src/types/errors.ts","./src/types/result.ts","./src/types/index.ts","./src/schema/schema-provider.ts","./src/schema/resource-validator-types.ts","./src/schema/validation/error-conversion.ts","./src/schema/validation/constraints.ts","./src/schema/validation/type-checker.ts","./src/schema/validation/property-validator.ts","./src/schema/resource-validator.ts","./src/schema/type-mapper.ts","./src/schema/embedded/events.ts","./src/schema/embedded/sqlite-types.ts","./src/schema/embedded/converters.ts","./src/schema/embedded/graph-queries.ts","./src/schemas/db/index.ts","./src/schema/embedded/initialization.ts","./src/schema/embedded/queries.ts","./src/schema/embedded-schema-provider.ts","./src/schema/unified-type-resolver.ts","./src/schema/customization/base-db.ts","./src/schema/customization/paths.ts","./src/schema/customization/example-files.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/line-counter.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/errors.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/applyreviver.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/log.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/tojs.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/scalar.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/stringify.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/collection.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/yamlseq.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/types.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/common/map.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/common/seq.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/common/string.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/foldflowlines.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/stringifynumber.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/stringify/stringifystring.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/util.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/yamlmap.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/schema.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/createnode.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/addpairtojsmap.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/pair.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/tags.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/options.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/node.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst-scalar.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst-stringify.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst-visit.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/cst.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/alias.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/document.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/doc/directives.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/compose/composer.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/lexer.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/parse/parser.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/public-api.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/visit.d.ts","../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/index.d.ts","./src/schema/customization/file-validators.ts","./src/schema/customization/scanner.ts","./src/schema/customization-loader.ts","./src/schema/index.ts","./src/state/state-store.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.19.15/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+better-sqlite3@7.6.13/node_modules/@types/better-sqlite3/index.d.ts","./src/state/sqlite/types.ts","./src/state/sqlite/resources.ts","./src/state/sqlite/deployments.ts","./src/state/sqlite/lifecycle.ts","./src/state/sqlite/locks.ts","./src/state/sqlite/snapshots.ts","./src/state/sqlite-state-store.ts","./src/state/index.ts","./src/graph/parser/tokens.ts","./src/graph/parser/ast/types/base.ts","./src/graph/parser/ast/types/expressions.ts","./src/graph/parser/ast/types/blocks.ts","./src/graph/parser/ast/types/statements.ts","./src/graph/parser/ast/types.ts","./src/graph/parser/ast/helpers.ts","./src/graph/parser/ast.ts","./src/graph/parser/lexer-state.ts","./src/graph/parser/lexer-scanners.ts","./src/graph/parser/lexer-heredoc.ts","./src/graph/parser/lexer.ts","./src/graph/parser/parser-state.ts","./src/graph/parser/parser-literals.ts","./src/graph/parser/parser-primary.ts","./src/graph/parser/parser-binary-exprs.ts","./src/graph/parser/parser-block-body.ts","./src/graph/parser/parser-statements.ts","./src/graph/parser/parser.ts","../../node_modules/.pnpm/@types+js-yaml@4.0.9/node_modules/@types/js-yaml/index.d.ts","../../node_modules/.pnpm/@types+js-yaml@4.0.9/node_modules/@types/js-yaml/index.d.mts","./src/graph/parser/format-parser.ts","./src/graph/parser/index.ts","./src/graph/mutable-graph/types.ts","./src/graph/mutable-graph/edges.ts","../constants/src/providers.ts","../constants/src/ice-types.ts","../constants/src/categories.ts","../constants/src/feature-flags.ts","../constants/src/ai.ts","../constants/src/derived.ts","../constants/src/grid.ts","../constants/src/connections.ts","../constants/src/node-traits.ts","../constants/src/block-classifiers.ts","../constants/src/templates.ts","../constants/src/integrations.ts","../constants/src/index.ts","./src/graph/classifier/category-classifier.ts","./src/graph/mutable-graph/nodes.ts","./src/graph/mutable-graph/stats-serialize.ts","./src/graph/mutable-graph/traversal.ts","./src/graph/mutable-graph.ts","./src/graph/validator/base-validator.ts","./src/graph/validator/validators/schema.ts","./src/graph/validator/validators/security.ts","./src/graph/algorithms/topo-cycle.ts","./src/graph/algorithms/paths.ts","./src/graph/algorithms/components.ts","./src/graph/algorithms/analysis.ts","./src/graph/algorithms.ts","./src/graph/validator/validators/structure.ts","./src/graph/validator/validators.ts","./src/graph/validator/index.ts","./src/graph/classifier/index.ts","./src/graph/inference/relationship-inferrer.ts","./src/graph/inference/index.ts","./src/graph/index.ts","./src/providers/provider-registry.ts","./src/providers/index.ts","./src/importers/terraform/types.ts","./src/importers/terraform/type-mapper.ts","./src/importers/terraform/sensitive.ts","./src/importers/terraform/resource-conversion.ts","./src/importers/terraform/graph-conversion.ts","./src/importers/terraform/state-importer.ts","./src/importers/terraform/index.ts","./src/importers/pulumi/types.ts","./src/importers/pulumi/type-mapper/parse.ts","./src/importers/pulumi/type-mapper/data.ts","./src/importers/pulumi/type-mapper/mapping.ts","./src/importers/pulumi/type-mapper.ts","./src/importers/pulumi/parsing.ts","./src/importers/pulumi/resource-conversion.ts","./src/importers/pulumi/graph-conversion.ts","./src/importers/pulumi/state-importer.ts","./src/importers/pulumi/index.ts","./src/importers/gcp/types.ts","./src/importers/gcp/relationships.ts","./src/importers/gcp/services/base-service.ts","./src/importers/gcp/services/compute.ts","./src/importers/gcp/services/storage.ts","./src/errors/import-errors/types.ts","./src/errors/import-errors/gcp.ts","./src/errors/import-errors/aws.ts","./src/errors/import-errors/azure.ts","./src/errors/import-errors.ts","./src/resources/high-level-resources/types.ts","./src/resources/high-level-resources/categories/compute.ts","./src/resources/high-level-resources/categories/database.ts","./src/resources/high-level-resources/categories/messaging.ts","./src/resources/high-level-resources/categories/monitoring.ts","./src/resources/high-level-resources/categories/networking.ts","./src/resources/high-level-resources/categories/security.ts","./src/resources/high-level-resources/categories/storage.ts","./src/resources/high-level-resources/helpers.ts","./src/resources/high-level-resources.ts","./src/importers/gcp/services/asset-inventory.ts","./src/importers/gcp/services/index.ts","./src/importers/gcp/type-mapper.ts","./src/importers/gcp/gcp-importer.ts","./src/importers/gcp/index.ts","./src/importers/aws/arn-helpers.ts","./src/importers/aws/sdk-init.ts","./src/importers/aws/types.ts","./src/importers/aws/discovery.ts","./src/importers/aws/graph-conversion.ts","./src/importers/aws/type-mapper.ts","./src/importers/aws/aws-importer.ts","./src/importers/aws/index.ts","./src/importers/azure/types.ts","./src/importers/azure/relationships.ts","./src/importers/azure/type-mapper.ts","./src/importers/azure/azure-importer.ts","./src/importers/azure/index.ts","./src/importers/index.ts","./src/plan/diff.ts","./src/plan/plan-engine.ts","./src/plan/index.ts","./src/apply/types.ts","./src/providers/mock-provider.ts","./src/apply/apply-engine.ts","./src/apply/index.ts","./src/diff/types.ts","./src/diff/diff.ts","./src/diff/index.ts","./src/deploy/providers/gcp/messages.ts","./src/deploy/messages.ts","./src/deploy/types.ts","./src/deploy/scheduler/types.ts","./src/deploy/scheduler/dag.ts","./src/deploy/scheduler/predicates.ts","./src/deploy/scheduler/dispatch.ts","./src/deploy/scheduler/progress-wrapper.ts","./src/deploy/scheduler.ts","./src/deploy/deploy-engine.ts","./src/deploy/providers/gcp/types.ts","./src/deploy/providers/gcp/handlers/api-gateway.ts","./src/deploy/providers/gcp/handlers/backend-bucket.ts","./src/deploy/providers/gcp/handlers/bigquery.ts","./src/deploy/providers/gcp/handlers/cloud-armor.ts","./src/deploy/providers/gcp/handlers/cloud-functions.ts","./src/deploy/providers/gcp/handlers/cloud-build-helper.ts","./src/deploy/providers/gcp/handlers/cloud-run/image-resolver.ts","./src/deploy/providers/gcp/handlers/cloud-run/result-helpers.ts","./src/deploy/providers/gcp/handlers/cloud-run/utils.ts","./src/deploy/providers/gcp/handlers/cloud-run/create-job.ts","./src/deploy/providers/gcp/handlers/cloud-run/iam.ts","./src/deploy/providers/gcp/handlers/cloud-run/create-service.ts","./src/deploy/providers/gcp/handlers/cloud-run.ts","./src/deploy/providers/gcp/handlers/cloud-scheduler.ts","./src/deploy/providers/gcp/handlers/cloud-sql.ts","./src/deploy/providers/gcp/handlers/cloud-storage/bucket-creator.ts","./src/deploy/providers/gcp/handlers/cloud-storage/bucket-updater.ts","./src/deploy/providers/gcp/handlers/cloud-storage/bucket-utils.ts","./src/deploy/providers/gcp/handlers/cloud-storage/public-access-granter.ts","./src/deploy/providers/gcp/handlers/cloud-storage/placeholder-uploader.ts","./src/deploy/providers/gcp/handlers/cloud-storage/result-helpers.ts","./src/deploy/providers/gcp/handlers/cloud-storage.ts","./src/deploy/providers/gcp/handlers/compute-firewall.ts","./src/deploy/providers/gcp/handlers/compute-instance.ts","./src/deploy/providers/gcp/handlers/dataflow.ts","./src/deploy/providers/gcp/handlers/discovery-engine.ts","./src/deploy/providers/gcp/handlers/domain-mapping.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/dns-extractor.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/rest-client.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/domain-registrar.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/tar-parser.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/github-downloader.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/result-helpers.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/site-provisioner.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/site-utils.ts","./src/deploy/providers/gcp/handlers/firebase-hosting/version-publisher.ts","./src/deploy/providers/gcp/handlers/firebase-hosting.ts","./src/deploy/providers/gcp/handlers/firestore.ts","./src/deploy/providers/gcp/handlers/gke.ts","./src/deploy/providers/gcp/handlers/identity-platform.ts","./src/deploy/providers/gcp/handlers/load-balancer/result-helpers.ts","./src/deploy/providers/gcp/handlers/load-balancer/compute-ops.ts","./src/deploy/providers/gcp/handlers/load-balancer/backend-creator.ts","./src/deploy/providers/gcp/handlers/load-balancer/cert-fetcher.ts","./src/deploy/providers/gcp/handlers/load-balancer/url-builder.ts","./src/deploy/providers/gcp/handlers/load-balancer/lb-builder.ts","./src/deploy/providers/gcp/handlers/load-balancer.ts","./src/deploy/providers/gcp/handlers/logging.ts","./src/deploy/providers/gcp/handlers/managed-ssl-certificate.ts","./src/deploy/providers/gcp/handlers/memorystore.ts","./src/deploy/providers/gcp/handlers/private-service-connect.ts","./src/deploy/providers/gcp/handlers/pubsub.ts","./src/deploy/providers/gcp/handlers/secret-manager.ts","./src/deploy/providers/gcp/handlers/subnet.ts","./src/deploy/providers/gcp/handlers/vpc.ts","./src/deploy/providers/gcp/handlers/vertex-ai.ts","./src/deploy/providers/gcp/sdk-loader.ts","./src/deploy/providers/gcp/gcp-deployer.ts","./src/deploy/providers/gcp/auth.ts","./src/deploy/providers/gcp/index.ts","./src/deploy/providers/aws/sdk-loader.ts","./src/deploy/providers/aws/account.ts","./src/deploy/providers/aws/handlers/_result.ts","./src/deploy/providers/aws/types.ts","./src/deploy/providers/aws/handlers/acm.ts","./src/deploy/providers/aws/handlers/amazon-mq.ts","./src/deploy/providers/aws/handlers/amplify-hosting.ts","./src/deploy/providers/aws/handlers/api-gateway.ts","./src/deploy/providers/aws/handlers/bedrock.ts","./src/deploy/providers/aws/handlers/cloudfront.ts","./src/deploy/providers/aws/handlers/cloudwatch-logs.ts","./src/deploy/providers/aws/handlers/codebuild.ts","./src/deploy/providers/aws/handlers/cognito.ts","./src/deploy/providers/aws/handlers/docdb.ts","./src/deploy/providers/aws/handlers/dynamodb.ts","./src/deploy/providers/aws/handlers/ec2.ts","./src/deploy/providers/aws/iam-roles.ts","./src/deploy/providers/aws/network-resolver.ts","./src/deploy/providers/aws/handlers/ecs.ts","./src/deploy/providers/aws/subnet-groups.ts","./src/deploy/providers/aws/handlers/elasticache.ts","./src/deploy/providers/aws/handlers/elbv2.ts","./src/deploy/providers/aws/handlers/events-rule.ts","./src/deploy/providers/aws/handlers/lambda-builder.ts","./src/deploy/providers/aws/handlers/lambda.ts","./src/deploy/providers/aws/handlers/opensearch.ts","./src/deploy/providers/aws/handlers/opensearch-serverless.ts","./src/deploy/providers/aws/handlers/rds.ts","./src/deploy/providers/aws/handlers/redshift.ts","./src/deploy/providers/aws/handlers/route53.ts","./src/deploy/providers/aws/handlers/s3.ts","./src/deploy/providers/aws/handlers/sagemaker.ts","./src/deploy/providers/aws/handlers/secrets-manager.ts","./src/deploy/providers/aws/handlers/security-group.ts","./src/deploy/providers/aws/handlers/sns.ts","./src/deploy/providers/aws/handlers/sqs.ts","./src/deploy/providers/aws/handlers/subnet.ts","./src/deploy/providers/aws/handlers/vpc.ts","./src/deploy/providers/aws/handlers/vpc-endpoint.ts","./src/deploy/providers/aws/handlers/wafv2.ts","./src/deploy/providers/aws/aws-deployer.ts","./src/deploy/providers/aws/index.ts","./src/deploy/providers/aws-deployer.ts","./src/deploy/providers/azure/sdk-loader.ts","./src/deploy/providers/azure/types.ts","./src/deploy/providers/azure/resource-group.ts","./src/deploy/providers/azure/handlers/_result.ts","./src/deploy/providers/azure/handlers/acr.ts","./src/deploy/providers/azure/handlers/acr-task.ts","./src/deploy/providers/azure/handlers/aks.ts","./src/deploy/providers/azure/handlers/apim.ts","./src/deploy/providers/azure/handlers/app-gateway.ts","./src/deploy/providers/azure/handlers/app-insights.ts","./src/deploy/providers/azure/handlers/app-service.ts","./src/deploy/providers/azure/handlers/azure-ml.ts","./src/deploy/providers/azure/handlers/azure-openai.ts","./src/deploy/providers/azure/handlers/azure-waf.ts","./src/deploy/providers/azure/handlers/cognitive-search.ts","./src/deploy/providers/azure/handlers/container-apps.ts","./src/deploy/providers/azure/handlers/cosmosdb.ts","./src/deploy/providers/azure/handlers/data-explorer.ts","./src/deploy/providers/azure/handlers/dns-zone.ts","./src/deploy/providers/azure/handlers/entra-b2c.ts","./src/deploy/providers/azure/handlers/event-grid.ts","./src/deploy/providers/azure/handlers/event-hubs.ts","./src/deploy/providers/azure/handlers/front-door.ts","./src/deploy/providers/azure/handlers/functions.ts","./src/deploy/providers/azure/handlers/key-vault.ts","./src/deploy/providers/azure/handlers/log-analytics.ts","./src/deploy/providers/azure/handlers/logic-apps.ts","./src/deploy/providers/azure/handlers/mysql-flex.ts","./src/deploy/providers/azure/handlers/nsg.ts","./src/deploy/providers/azure/handlers/postgresql-flex.ts","./src/deploy/providers/azure/handlers/private-endpoint.ts","./src/deploy/providers/azure/handlers/redis-cache.ts","./src/deploy/providers/azure/handlers/service-bus.ts","./src/deploy/providers/azure/handlers/sql-database.ts","./src/deploy/providers/azure/handlers/static-web-apps.ts","./src/deploy/providers/azure/handlers/storage-account.ts","./src/deploy/providers/azure/handlers/subnet.ts","./src/deploy/providers/azure/handlers/synapse.ts","./src/deploy/providers/azure/handlers/virtual-machine.ts","./src/deploy/providers/azure/handlers/vnet.ts","./src/deploy/providers/azure/handlers/web-app.ts","./src/deploy/providers/azure/azure-deployer.ts","./src/deploy/providers/azure-deployer.ts","./src/deploy/providers/kubernetes/types.ts","./src/deploy/providers/kubernetes/handlers/_crd.ts","./src/deploy/providers/kubernetes/handlers/_result.ts","./src/deploy/providers/kubernetes/handlers/cert-manager.ts","./src/deploy/providers/kubernetes/namespace.ts","./src/deploy/providers/kubernetes/handlers/configmap.ts","./src/deploy/providers/kubernetes/handlers/cronjob.ts","./src/deploy/providers/kubernetes/handlers/deployment.ts","./src/deploy/providers/kubernetes/handlers/hpa.ts","./src/deploy/providers/kubernetes/handlers/ingress.ts","./src/deploy/providers/kubernetes/handlers/job.ts","./src/deploy/providers/kubernetes/handlers/knative-service.ts","./src/deploy/providers/kubernetes/handlers/namespace.ts","./src/deploy/providers/kubernetes/handlers/networkpolicy.ts","./src/deploy/providers/kubernetes/handlers/persistentvolumeclaim.ts","./src/deploy/providers/kubernetes/handlers/poddisruptionbudget.ts","./src/deploy/providers/kubernetes/handlers/prometheus-rule.ts","./src/deploy/providers/kubernetes/handlers/secret.ts","./src/deploy/providers/kubernetes/handlers/service.ts","./src/deploy/providers/kubernetes/handlers/serviceaccount.ts","./src/deploy/providers/kubernetes/handlers/statefulset.ts","./src/deploy/providers/kubernetes/sdk-loader.ts","./src/deploy/providers/kubernetes/kubernetes-deployer.ts","./src/deploy/providers/alibaba/types.ts","./src/deploy/providers/alibaba/handlers/_client.ts","./src/deploy/providers/alibaba/handlers/_result.ts","./src/deploy/providers/alibaba/handlers/alidns-record.ts","./src/deploy/providers/alibaba/handlers/amqp-instance.ts","./src/deploy/providers/alibaba/handlers/apigateway-api.ts","./src/deploy/providers/alibaba/handlers/cas-certificate.ts","./src/deploy/providers/alibaba/handlers/cdn-domain.ts","./src/deploy/providers/alibaba/handlers/cr-build-task.ts","./src/deploy/providers/alibaba/handlers/cr-instance.ts","./src/deploy/providers/alibaba/handlers/cs-managed-cluster.ts","./src/deploy/providers/alibaba/handlers/dds-db-instance.ts","./src/deploy/providers/alibaba/handlers/eci-container-group.ts","./src/deploy/providers/alibaba/handlers/ecs-instance.ts","./src/deploy/providers/alibaba/handlers/ecs-security-group.ts","./src/deploy/providers/alibaba/handlers/eventbridge-rule.ts","./src/deploy/providers/alibaba/handlers/fc-function.ts","./src/deploy/providers/alibaba/handlers/kms-secret.ts","./src/deploy/providers/alibaba/handlers/kvstore-instance.ts","./src/deploy/providers/alibaba/handlers/maxcompute-project.ts","./src/deploy/providers/alibaba/handlers/mns-queue.ts","./src/deploy/providers/alibaba/handlers/mns-topic.ts","./src/deploy/providers/alibaba/handlers/opensearch-app.ts","./src/deploy/providers/alibaba/handlers/oss-bucket.ts","./src/deploy/providers/alibaba/handlers/pai-eas-service.ts","./src/deploy/providers/alibaba/handlers/pai-workspace.ts","./src/deploy/providers/alibaba/handlers/privatelink-endpoint.ts","./src/deploy/providers/alibaba/handlers/ram-user.ts","./src/deploy/providers/alibaba/handlers/rds-db-instance.ts","./src/deploy/providers/alibaba/handlers/sae-application.ts","./src/deploy/providers/alibaba/handlers/slb-load-balancer.ts","./src/deploy/providers/alibaba/handlers/sls-project.ts","./src/deploy/providers/alibaba/handlers/vpc.ts","./src/deploy/providers/alibaba/handlers/vpc-vswitch.ts","./src/deploy/providers/alibaba/handlers/waf-policy.ts","./src/deploy/providers/alibaba/region.ts","./src/deploy/providers/alibaba/sdk-loader.ts","./src/deploy/providers/alibaba/alibaba-deployer.ts","./src/deploy/providers/oci/types.ts","./src/deploy/providers/oci/handlers/_client.ts","./src/deploy/providers/oci/handlers/_result.ts","./src/deploy/providers/oci/handlers/analytics-instance.ts","./src/deploy/providers/oci/handlers/apigateway-gateway.ts","./src/deploy/providers/oci/handlers/artifacts-repository.ts","./src/deploy/providers/oci/handlers/certificates-certificate.ts","./src/deploy/providers/oci/handlers/containerengine-cluster.ts","./src/deploy/providers/oci/handlers/containerinstance.ts","./src/deploy/providers/oci/handlers/core-instance.ts","./src/deploy/providers/oci/handlers/core-nsg.ts","./src/deploy/providers/oci/handlers/core-privateaccessgateway.ts","./src/deploy/providers/oci/handlers/core-subnet.ts","./src/deploy/providers/oci/handlers/core-vcn.ts","./src/deploy/providers/oci/handlers/database-autonomous.ts","./src/deploy/providers/oci/handlers/datascience-modeldeployment.ts","./src/deploy/providers/oci/handlers/dns-zone.ts","./src/deploy/providers/oci/handlers/functions-function.ts","./src/deploy/providers/oci/handlers/generativeai-endpoint.ts","./src/deploy/providers/oci/handlers/identitydomains-user.ts","./src/deploy/providers/oci/handlers/loadbalancer.ts","./src/deploy/providers/oci/handlers/logging-loggroup.ts","./src/deploy/providers/oci/handlers/monitoring-alarm.ts","./src/deploy/providers/oci/handlers/mysql-dbsystem.ts","./src/deploy/providers/oci/handlers/nosql-table.ts","./src/deploy/providers/oci/handlers/objectstorage-bucket.ts","./src/deploy/providers/oci/handlers/ons-topic.ts","./src/deploy/providers/oci/handlers/psql-dbsystem.ts","./src/deploy/providers/oci/handlers/queue.ts","./src/deploy/providers/oci/handlers/redis-cluster.ts","./src/deploy/providers/oci/handlers/resourcescheduler.ts","./src/deploy/providers/oci/handlers/streaming-stream.ts","./src/deploy/providers/oci/handlers/vault-secret.ts","./src/deploy/providers/oci/handlers/waf-policy.ts","./src/deploy/providers/oci/sdk-loader.ts","./src/deploy/providers/oci/oci-deployer.ts","./src/deploy/providers/digitalocean/handlers/_result.ts","./src/deploy/providers/digitalocean/types.ts","./src/deploy/providers/digitalocean/handlers/apps-app.ts","./src/deploy/providers/digitalocean/handlers/apps-envvar.ts","./src/deploy/providers/digitalocean/handlers/apps-static.ts","./src/deploy/providers/digitalocean/handlers/container-registry.ts","./src/deploy/providers/digitalocean/handlers/databases-cluster.ts","./src/deploy/providers/digitalocean/handlers/domain-record.ts","./src/deploy/providers/digitalocean/handlers/droplet.ts","./src/deploy/providers/digitalocean/handlers/firewall.ts","./src/deploy/providers/digitalocean/handlers/functions-function.ts","./src/deploy/providers/digitalocean/handlers/functions-namespace.ts","./src/deploy/providers/digitalocean/handlers/kubernetes-cluster.ts","./src/deploy/providers/digitalocean/handlers/loadbalancer.ts","./src/deploy/providers/digitalocean/handlers/monitoring-alertpolicy.ts","./src/deploy/providers/digitalocean/handlers/reserved-ip.ts","./src/deploy/providers/digitalocean/handlers/snapshot.ts","./src/deploy/providers/digitalocean/handlers/spaces-bucket.ts","./src/deploy/providers/digitalocean/handlers/volume.ts","./src/deploy/providers/digitalocean/handlers/vpc-network.ts","./src/deploy/providers/digitalocean/sdk-loader.ts","./src/deploy/providers/digitalocean/digitalocean-deployer.ts","./src/deploy/providers/ibm/types.ts","./src/deploy/providers/ibm/handlers/_client.ts","./src/deploy/providers/ibm/handlers/_result.ts","./src/deploy/providers/ibm/handlers/codeengine-application.ts","./src/deploy/providers/ibm/handlers/codeengine-function.ts","./src/deploy/providers/ibm/handlers/codeengine-job.ts","./src/deploy/providers/ibm/sdk-loader.ts","./src/deploy/providers/ibm/handlers/cos-bucket.ts","./src/deploy/providers/ibm/handlers/databases-instance.ts","./src/deploy/providers/ibm/handlers/resource-instance.ts","./src/deploy/providers/ibm/handlers/secretsmanager-secret.ts","./src/deploy/providers/ibm/handlers/vpc.ts","./src/deploy/providers/ibm/handlers/vpc-instance.ts","./src/deploy/providers/ibm/handlers/vpc-loadbalancer.ts","./src/deploy/providers/ibm/handlers/vpc-securitygroup.ts","./src/deploy/providers/ibm/handlers/vpc-subnet.ts","./src/deploy/providers/ibm/ibm-deployer.ts","./src/deploy/providers/index.ts","./src/deploy/block-deploy-classifiers.ts","./src/deploy/edge-classifier.ts","./src/deploy/extractors/alibaba/index.ts","./src/deploy/extractors/ancillary.ts","./src/deploy/extractors/aws/ai.ts","./src/deploy/extractors/aws/ancillary.ts","./src/deploy/utils/name-utils.ts","./src/deploy/extractors/compute.ts","./src/deploy/extractors/aws/compute.ts","./src/deploy/extractors/aws/database.ts","./src/deploy/extractors/aws/network.ts","./src/deploy/extractors/azure/ai.ts","./src/deploy/extractors/azure/ancillary.ts","./src/deploy/extractors/azure/compute.ts","./src/deploy/extractors/azure/database.ts","./src/deploy/extractors/azure/messaging.ts","./src/deploy/extractors/azure/network.ts","./src/deploy/extractors/database.ts","./src/deploy/extractors/digitalocean/index.ts","./src/deploy/extractors/gcp/new-services.ts","./src/deploy/extractors/ibm/index.ts","./src/deploy/extractors/kubernetes/index.ts","./src/deploy/extractors/network.ts","./src/deploy/extractors/oci/index.ts","./src/deploy/extractors/dispatch.ts","./src/deploy/internal-ingress-overrides.ts","./src/deploy/passes/deploy-expansion.ts","./src/deploy/passes/pass-1-4-repo-wiring.ts","./src/deploy/passes/pass-1-45-domain-propagation.ts","./src/deploy/passes/pass-1-46-socket-port-targeting.ts","./src/deploy/self-serving-resources.ts","./src/deploy/passes/pass-1-5-endpoint-wiring.ts","./src/deploy/passes/pass-1-6-aws-network-wiring.ts","./src/deploy/type-maps.ts","./src/deploy/utils/stable-name.ts","./src/deploy/card-translator.ts","./src/deploy/state-bridge.ts","./src/deploy/state-store-adapter.ts","./src/deploy/environment-config.ts","./src/deploy/index.ts","./src/compute/types.ts","./src/compute/propagation-rules.ts","./src/compute/compute-derived.ts","./src/compute/index.ts","./src/export/terraform/case-utils.ts","./src/export/terraform/types.ts","./src/export/terraform/hcl-formatter.ts","./src/export/terraform/type-mapping.ts","./src/export/terraform/value-transform.ts","./src/export/terraform/converter.ts","./src/export/terraform-exporter.ts","./src/export/pulumi/case-utils.ts","./src/export/pulumi/type-mapping.ts","./src/export/pulumi/types.ts","./src/export/pulumi/typescript-formatter.ts","./src/export/pulumi/value-transform.ts","./src/export/pulumi/yaml-formatter.ts","./src/export/pulumi/converter.ts","./src/export/pulumi-exporter.ts","./src/export/index.ts","./src/errors/index.ts","./src/resources/cloud-providers.ts","./src/resources/blueprint-factory.ts","./src/resources/cloud-blocks-types.ts","./src/resources/cloud-blocks-data/backend.ts","./src/resources/cloud-blocks-data/compute.ts","./src/resources/cloud-blocks-data/data.ts","./src/resources/cloud-blocks-data/frontend.ts","./src/resources/cloud-blocks-data/messaging.ts","./src/resources/cloud-blocks-data/networking.ts","./src/resources/cloud-blocks-data/observability.ts","./src/resources/cloud-blocks-data/security.ts","./src/resources/cloud-blocks-data/storage.ts","./src/resources/cloud-blocks-data.ts","./src/resources/cloud-blocks.ts","./src/validation/classifiers.ts","./src/validation/types.ts","./src/validation/architecture-rules.ts","./src/validation/connection-rules.ts","./src/validation/schema-bridge.ts","./src/validation/deploy-rules.ts","./src/validation/property-rules.ts","./src/validation/structure-rules.ts","./src/validation/canvas-validator.ts","./src/validation/template-validator.ts","./src/validation/index.ts","./src/index.ts","./src/__tests__/card-translator.test.d.ts","./src/__tests__/core.test.d.ts","./src/__tests__/pulumi-importer.test.d.ts","./src/__tests__/terraform-importer.test.d.ts","./src/cli/index.ts","./src/cli/messages.ts","./src/cli/bin/ice.ts","./src/cli/commands/apply.ts","./src/cli/commands/config.ts","./src/cli/commands/deploy.ts","./src/cli/commands/destroy.ts","./src/cli/commands/diff.ts","./src/cli/commands/graph.ts","./src/cli/commands/import.ts","./src/cli/commands/plan.ts","./src/cli/commands/providers.ts","./src/cli/commands/schema.ts","./src/cli/commands/state.ts","./src/cli/utils/config.ts","./src/cli/utils/index.ts","./src/cli/utils/output.ts","../../node_modules/.pnpm/@vitest+pretty-format@4.1.0/node_modules/@vitest/pretty-format/dist/index.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/display.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/types.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/helpers.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/timers.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/index.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/types.d-bcelap-c.d.ts","../../node_modules/.pnpm/@vitest+utils@4.1.0/node_modules/@vitest/utils/dist/diff.d.ts","../../node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/tasks.d-d2gkpdwq.d.ts","../../node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/traces.d.402v_yfi.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/types/hmrpayload.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/dist/node/chunks/modulerunnertransport.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/types/customevent.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/types/hot.d.ts","../../node_modules/.pnpm/vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3/node_modules/vite/dist/node/module-runner.d.ts","../../node_modules/.pnpm/@vitest+snapshot@4.1.0/node_modules/@vitest/snapshot/dist/environment.d-dojxxzv9.d.ts","../../node_modules/.pnpm/@vitest+snapshot@4.1.0/node_modules/@vitest/snapshot/dist/rawsnapshot.d-u2kjuxdr.d.ts","../../node_modules/.pnpm/@vitest+snapshot@4.1.0/node_modules/@vitest/snapshot/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/config.d.ejlve3es.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/environment.d.crsxczp1.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/rpc.d.bfmwpdph.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/worker.d.b84svry0.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/browser.d.x3sxoocv.d.ts","../../node_modules/.pnpm/@vitest+spy@4.1.0/node_modules/@vitest/spy/dist/index.d.ts","../../node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.d.ts","../../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../node_modules/.pnpm/@types+deep-eql@4.0.2/node_modules/@types/deep-eql/index.d.ts","../../node_modules/.pnpm/assertion-error@2.0.1/node_modules/assertion-error/index.d.ts","../../node_modules/.pnpm/@types+chai@5.2.3/node_modules/@types/chai/index.d.ts","../../node_modules/.pnpm/@vitest+expect@4.1.0/node_modules/@vitest/expect/dist/index.d.ts","../../node_modules/.pnpm/@vitest+runner@4.1.0/node_modules/@vitest/runner/dist/utils.d.ts","../../node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/benchmark.d.daahlpsq.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/global.d.x-ilcfae.d.ts","../../node_modules/.pnpm/@vitest+mocker@4.1.0_vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3_/node_modules/@vitest/mocker/dist/types.d-bji5eawu.d.ts","../../node_modules/.pnpm/@vitest+mocker@4.1.0_vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3_/node_modules/@vitest/mocker/dist/index.d-b41z0auw.d.ts","../../node_modules/.pnpm/@vitest+mocker@4.1.0_vite@8.0.1_@types+node@25.5.0_esbuild@0.27.4_jiti@2.6.1_tsx@4.21.0_yaml@2.8.3_/node_modules/@vitest/mocker/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/suite.d.udjtyagw.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/chunks/evaluatedmodules.d.bxj5omdx.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/runners.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.d.ts","../../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.d.ts","../../node_modules/.pnpm/vitest@4.1.0_@opentelemetry+api@1.9.0_@types+node@25.5.0_jsdom@29.1.1_vite@8.0.1_@types_e7fcc74193e0e73f2abf87bac0e3d214/node_modules/vitest/dist/index.d.ts","./src/deploy/providers/__tests__/_aws-test-harness.ts","./src/deploy/providers/__tests__/_azure-test-harness.ts","./src/deploy/providers/__tests__/live/_live-types.ts","./src/deploy/providers/__tests__/live/_live-helpers.ts","./src/deploy/providers/alibaba/auth.ts","./src/deploy/providers/aws/handlers/cloudwatch-alarm.ts","./src/deploy/providers/aws/handlers/ecr.ts","./src/deploy/providers/aws/handlers/kinesis.ts","./src/deploy/providers/aws/handlers/timestream.ts","./src/deploy/providers/azure/auth.ts","./src/deploy/providers/gcp/handlers/artifact-registry.ts","./src/deploy/providers/gcp/handlers/cloud-build.ts","./src/deploy/providers/gcp/handlers/cloud-dns.ts","./src/deploy/providers/gcp/handlers/monitoring.ts","./src/deploy/providers/kubernetes/auth.ts","./src/graph/algorithms/__tests__/fixtures.ts","./src/graph/mutable-graph/index.ts","./src/resources/scale-presets-types.ts","./src/resources/scale-presets-data/compute.ts","./src/resources/scale-presets-data/database.ts","./src/resources/scale-presets-data/messaging.ts","./src/resources/scale-presets-data/monitoring.ts","./src/resources/scale-presets-data/networking.ts","./src/resources/scale-presets-data/security.ts","./src/resources/scale-presets-data/storage.ts","./src/resources/scale-presets-data.ts","./src/resources/scale-presets.ts","./src/resources/index.ts","./src/schemas/index.ts","./src/schemas/db/graph-queries.ts","./src/schemas/db/schema-merger.ts","./src/schemas/db/sqlite-registry.ts","./src/schemas/embedded/schema-registry.ts"],"fileIdsList":[[135,183,200,201],[135,183,200,201,233],[135,183,200,201,798,799],[135,183,200,201,262],[135,180,181,183,200,201],[135,182,183,200,201],[183,200,201],[135,183,188,200,201,218],[135,183,184,189,194,200,201,203,215,226],[135,183,184,185,194,200,201,203],[130,131,132,135,183,200,201],[135,183,186,200,201,227],[135,183,187,188,195,200,201,204],[135,183,188,200,201,215,223],[135,183,189,191,194,200,201,203],[135,182,183,190,200,201],[135,183,191,192,200,201],[135,183,193,194,200,201],[135,182,183,194,200,201],[135,183,194,195,196,200,201,215,226],[135,183,194,195,196,200,201,210,215,218],[135,176,183,191,194,197,200,201,203,215,226],[135,183,194,195,197,198,200,201,203,215,223,226],[135,183,197,199,200,201,215,223,226],[133,134,135,136,137,138,139,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232],[135,183,194,200,201],[135,183,200,201,202,226],[135,183,191,194,200,201,203,215],[135,183,200,201,204],[135,183,200,201,205],[135,182,183,200,201,206],[135,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232],[135,183,200,201,208],[135,183,200,201,209],[135,183,194,200,201,210,211],[135,183,200,201,210,212,227,229],[135,183,195,200,201],[135,183,194,200,201,215,216,218],[135,183,200,201,217,218],[135,183,200,201,215,216],[135,183,200,201,218],[135,183,200,201,219],[135,180,183,200,201,215,220,226],[135,183,194,200,201,221,222],[135,183,200,201,221,222],[135,183,188,200,201,203,215,223],[135,183,200,201,224],[135,183,200,201,203,225],[135,183,197,200,201,209,226],[135,183,188,200,201,227],[135,183,200,201,215,228],[135,183,200,201,202,229],[135,183,200,201,230],[135,176,183,200,201],[135,176,183,194,196,200,201,206,215,218,226,228,229,231],[135,183,200,201,215,232],[135,183,200,201,772,778,780,795,796,797,800,805],[135,183,200,201,806],[135,183,200,201,806,807],[135,183,200,201,776,778,779],[135,183,200,201,776,778],[135,183,200,201,776],[135,183,200,201,771,776,787,788],[135,183,200,201,771,787],[135,183,200,201,771,777],[135,183,200,201,771],[135,183,200,201,773],[135,183,200,201,771,772,773,774,775],[135,183,200,201,812,813],[135,183,200,201,812,813,814,815],[135,183,200,201,812,814],[135,183,200,201,812],[135,148,152,183,200,201,226],[135,148,183,200,201,215,226],[135,143,183,200,201],[135,145,148,183,200,201,223,226],[135,183,200,201,203,223],[135,143,183,200,201,233],[135,145,148,183,200,201,203,226],[135,140,141,144,147,183,194,200,201,215,226],[135,148,155,183,200,201],[135,140,146,183,200,201],[135,148,169,170,183,200,201],[135,144,148,183,200,201,218,226,233],[135,169,183,200,201,233],[135,142,143,183,200,201,233],[135,148,183,200,201],[135,142,143,144,145,146,147,148,149,150,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,170,171,172,173,174,175,183,200,201],[135,148,163,183,200,201],[135,148,155,156,183,200,201],[135,146,148,156,157,183,200,201],[135,147,183,200,201],[135,140,143,148,183,200,201],[135,148,152,156,157,183,200,201],[135,152,183,200,201],[135,146,148,151,183,200,201,226],[135,140,145,148,155,183,200,201],[135,183,200,201,215],[135,143,148,169,183,200,201,231,233],[135,183,200,201,782],[135,183,200,201,782,783,784,785],[135,183,200,201,784],[135,183,200,201,780,802,803,805],[135,183,200,201,780,781,793,805],[135,183,200,201,771,778,780,789,805],[135,183,200,201,786],[135,183,200,201,771,780,789,792,801,804,805],[135,183,200,201,780,781,786,789,805],[135,183,200,201,780,802,803,804,805],[135,183,200,201,780,786,790,791,792,805],[135,183,200,201,771,776,778,780,781,786,789,790,791,792,793,794,795,801,802,803,804,805,808,809,810,811,816],[135,183,200,201,771,778,780,781,789,790,802,803,804,805,809],[85,108,109,113,115,116,135,183,200,201],[93,103,109,115,135,183,200,201],[115,135,183,200,201],[85,89,92,101,102,103,106,108,109,114,116,135,183,200,201],[84,135,183,200,201],[84,85,89,92,93,101,102,103,106,107,108,109,113,114,115,117,118,119,120,121,122,123,135,183,200,201],[88,101,106,135,183,200,201],[88,89,90,92,101,109,113,115,135,183,200,201],[102,103,109,135,183,200,201],[89,92,101,106,109,114,115,135,183,200,201],[88,89,90,92,101,102,108,113,114,115,135,183,200,201],[88,90,102,103,104,105,109,113,135,183,200,201],[88,109,113,135,183,200,201],[109,115,135,183,200,201],[88,89,90,91,100,103,106,109,113,135,183,200,201],[88,89,90,91,103,104,106,109,113,135,183,200,201],[84,86,87,89,93,103,106,107,109,116,135,183,200,201],[85,89,109,113,135,183,200,201],[113,135,183,200,201],[110,111,112,135,183,200,201],[86,108,109,115,117,135,183,200,201],[93,135,183,200,201],[93,102,106,108,135,183,200,201],[93,108,135,183,200,201],[89,90,92,101,103,104,108,109,135,183,200,201],[88,92,93,100,101,103,135,183,200,201],[88,89,90,93,100,101,103,106,135,183,200,201],[108,114,115,135,183,200,201],[89,135,183,200,201],[89,90,135,183,200,201],[87,88,90,94,95,96,97,98,99,101,104,106,135,183,200,201],[135,183,200,201,269],[135,183,200,201,268,270],[135,183,200,201,268,269,270,271,272,273,274,275,276,277,278,279],[58,59,60,135,183,200,201,285,360,362,363],[135,183,200,201,362,364],[58,59,60,135,183,200,201],[135,183,200,201,703,704],[135,183,200,201,703,704,705],[135,183,200,201,280,703],[58,135,183,200,201,280,285,339,664,669,687,688,689,690,691,692,694,695,696,697],[58,135,183,200,201,366,367,370,371,377],[58,135,183,200,201,280,663],[135,183,200,201,670],[135,183,200,201,669],[135,183,200,201,280],[135,183,200,201,665,666,667,668,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686],[135,183,188,200,201,280],[135,183,200,201,370,371,377,378,436,438,662,698,699,700,701],[135,183,200,201,369],[135,183,200,201,285,339,669,698],[135,183,200,201,285,698],[135,183,200,201,285,663,698],[135,183,200,201,280,285,664,669,693,698],[135,183,200,201,817],[135,183,188,195,200,201,205,482,525,548,586,622,644,661,817,820],[135,183,200,201,371],[135,183,200,201,371,549,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585],[135,183,200,201,549,584,585],[135,183,200,201,549],[135,183,200,201,549,550,551],[135,183,200,201,549,584],[135,183,200,201,481],[135,183,200,201,440],[135,183,200,201,371,440,441,443,444,445,446,447,448,449,450,451,452,453,454,455,458,460,461,462,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479],[135,183,200,201,440,442,443],[135,183,200,201,371,440,443],[135,183,200,201,440,442,443,456,457],[135,183,200,201,440,442,443,457,459],[135,183,200,201,440,442,443,457],[135,183,184,195,200,201,204,205,440,443],[135,183,200,201,371,440,443,463],[135,183,200,201,440,441,443,456,480],[135,183,200,201,440,443],[135,183,200,201,440,443,457],[135,183,200,201,371,441],[135,183,200,201,484,524],[135,183,200,201,483],[135,183,200,201,371,483,484,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523],[135,183,200,201,484,486],[135,183,200,201,484,485,486],[135,183,200,201,483,484,485,486],[135,183,200,201,483,484],[135,183,200,201,371,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643],[135,183,200,201,623,624],[135,183,200,201,624],[135,183,200,201,370,436],[135,183,200,201,369,370,371,379,380,381,382,383,384,392,393,394,401,402,403,404,405,406,416,417,418,419,426,427,428,429,430,431,432,433,434,435,436],[135,183,200,201,369,371,379],[135,183,200,201,369,379],[135,183,200,201,369,379,386,387,388,389,390,391],[135,183,200,201,369,371,379,386,387,388],[135,183,200,201,369,371,379,386,387,388,390],[135,183,200,201,379],[135,183,200,201,369,379,385],[135,183,200,201,369,379,395,396,397,398,399,400],[135,183,200,201,379,397,398],[135,183,200,201,371,379],[135,183,200,201,379,407,408,409,411,412,413,414,415],[135,183,200,201,379,407,408],[135,183,200,201,232,379,410],[135,183,200,201,379,408],[135,183,188,200,201,232,379,408,410],[135,183,200,201,379,420,421,422,423,424,425],[135,183,200,201,379,420,421],[135,183,200,201,379,420],[135,183,200,201,369,379,420],[135,183,200,201,379,420,421,422,424],[135,183,200,201,379,437,438],[135,183,200,201,370,379],[135,183,200,201,645],[135,183,200,201,645,646,647],[135,183,200,201,645,647,651],[135,183,200,201,371,645,648,649,650,651,652,653,654,655,656,657,658,659,660],[135,183,200,201,439,482,525,526,548,549,586,587,622,624,644,645,661],[135,183,200,201,547],[135,183,200,201,526],[135,183,200,201,526,527,528],[135,183,200,201,526,528,530],[135,183,200,201,371,526,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547],[135,183,200,201,587],[135,183,200,201,587,588,589],[135,183,200,201,371,587,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621],[135,183,200,201,371,372,373,374,375,376],[58,135,183,200,201,366,372],[135,183,200,201,366,371,372,374],[135,183,200,201,372],[135,183,200,201,366,371],[58,135,183,200,201,366,371],[58,135,183,200,201,371],[58,135,183,200,201,241,699],[135,183,200,201,698],[135,183,188,200,201,669],[58,135,183,200,201,366],[135,183,200,201,366,367],[135,183,200,201,325,326,327,328],[135,183,200,201,325],[135,183,200,201,329],[135,183,200,201,713,721],[79,135,183,200,201,285,716,720],[58,64,79,135,183,200,201,285,714,715,716,717,718,719],[135,183,200,201,714],[135,183,200,201,714,715,716],[135,183,200,201,714,716],[135,183,200,201,716],[79,135,183,200,201,285,708,712],[58,64,79,135,183,200,201,285,707,708,709,710,711],[135,183,200,201,708],[135,183,200,201,289,290,291,292],[135,183,200,201,285],[58,135,183,200,201,285,289,291],[58,135,183,200,201,285],[135,183,200,201,281],[135,183,200,201,265,285,293,296,297,299],[135,183,200,201,298],[58,135,183,200,201],[58,135,183,200,201,266,267,282,283,284],[58,135,183,200,201,266],[135,183,200,201,266,267,282,283,284],[58,135,183,200,201,266,267,281],[58,135,183,200,201,266,267],[135,183,200,201,248,249],[135,183,200,201,243,248],[135,183,200,201,244,245,246,247],[135,183,200,201,243],[135,183,200,201,244,245],[135,183,200,201,244],[135,183,200,201,244,245,246],[135,183,200,201,243,250,263],[135,183,200,201,243,250,254,261,264],[135,183,200,201,251,252],[135,183,200,201,243,251],[135,183,200,201,243,254],[135,183,200,201,243,251,252,253],[135,183,200,201,250,255,256,257],[135,183,200,201,250,255,256,258],[135,183,200,201,243,250,255],[135,183,200,201,243,250,255,256,258],[135,183,200,201,243,261],[135,183,200,201,243,250,255,256,259,260],[135,183,200,201,286,295],[64,135,183,200,201,286,287,288,294],[64,135,183,200,201,285,286],[135,183,200,201,285,286],[135,183,200,201,285,286,293],[135,183,200,201,285,329,346,347,348,349,350],[135,183,200,201,345,346,347],[58,135,183,200,201,285,347],[135,183,200,201,347,350,351],[58,135,183,200,201,285,329,353,354,355],[135,183,200,201,353,355,356],[135,183,200,201,353],[58,135,183,200,201,285,320,321,341,342],[135,183,200,201,320,321,341,342,343],[135,183,200,201,320],[135,183,195,200,201,320,322,329,339],[135,183,200,201,320,322],[135,183,200,201,322,323,324,340],[135,183,200,201,339],[135,183,200,201,309,319,344,352,357],[58,135,183,200,201,285,310,318],[135,183,200,201,310,314,318],[135,183,200,201,310,314],[135,183,200,201,310,314,315,318],[135,183,195,196,200,201,285,310,314,315,316,317],[135,183,200,201,311,313],[135,183,200,201,311,312],[135,183,200,201,310],[58,135,183,200,201,285,303,308],[135,183,200,201,303,304,308],[135,183,200,201,303,304,305,308],[135,183,200,201,303],[135,183,195,196,200,201,285,303,305,306,307],[63,128,135,183,200,201,242,300,302,339,358,361,363,365,368,702,706,722,723,724,725,737,748],[60,135,183,200,201],[135,183,200,201,359,360],[58,59,60,135,183,200,201,285,293,359],[135,183,200,201,301],[58,59,135,183,200,201],[59,61,62,135,183,200,201],[135,183,200,201,726,727,728,729,730,731,732,733,734,735],[135,183,200,201,726],[135,183,200,201,726,736],[135,183,200,201,330,338],[135,183,200,201,330],[135,183,200,201,280,330,331,332,333,334,335,336,337],[135,183,200,201,339,724,725,737,844],[135,183,200,201,835,836,837,838,839,840,841,842],[135,183,200,201,835],[135,183,200,201,835,843],[81,82,83,125,126,135,183,195,200,201,205],[135,183,195,200,201,205],[82,135,183,195,200,201,205],[124,135,183,195,200,201],[61,62,64,72,73,75,77,78,135,183,200,201],[64,73,135,183,200,201],[64,135,183,200,201],[61,62,64,73,74,135,183,200,201],[73,76,135,183,195,200,201,205],[64,70,71,79,80,127,135,183,200,201],[61,62,64,65,66,69,135,183,200,201],[61,62,135,183,200,201],[64,79,135,183,200,201],[64,65,135,183,200,201],[61,65,135,183,200,201],[64,65,67,68,135,183,200,201],[65,135,183,200,201],[129,135,183,200,201,241],[58,60,61,62,129,135,183,200,201,235,236,237,238,239,240],[60,61,62,129,135,183,200,201,235,236],[61,62,135,183,200,201,234,235],[58,59,60,61,62,129,135,183,200,201,234,235],[61,62,129,135,183,200,201,235,236],[129,135,183,200,201,234],[58,59,60,61,62,135,183,200,201],[61,135,183,200,201],[135,183,200,201,738,739],[135,183,200,201,739,740,741,743,744,745],[135,183,200,201,280,738,739,742],[135,183,200,201,739,740,741,742,743,744,745,746,747],[135,183,200,201,339,739,742],[135,183,200,201,280,339],[135,183,200,201,738,739,742]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},"9f422a1bcf9b6de329433e9846e2de072714d0feb659261d0915ed89b7227871","9f06cca3f1b2c3d560cdbec5c0a3bfafdf58dc2439d7065ddc5f23af72fdaa73","8c467364768b887a7c3f3af90e02fcd1cf443ce162bb498082bfbcc99beb2a88","905a105127753c3ce4f2d19d866c0d5565758691579204519bb9b8fd838b790b","62d8d9c7b326f1d1392cf56c82d9c6b042039875caa7b503b0a0b41d0c3123c8","44a47ae3fa050377c57d37fb051a6c6cbc08405bd0efda40861653de399a0818","7170f18d5cdfb2d301f0470233e281da189a3b0a8d9ea4358edcb21b4c9a5728","cbbede9be700512a66d94e48c4ef981a6d1a1678698ee36f7dbbb4ac747aeaf3","a92c3adb47ae83dffb2b67e18f045efb376efd1547941de56366340c06d61077","341494baae56b3810de282a6932c5c4edf0b44adf7aa00e1ced71a9258141315","c1e5340e6e2c73765454ef0056093661c0b4159518797e442c58d9e0dfbbe8cc","92b52627eb99e20e437d33fc58808896145bc40ae551a991844e7658395bdfce","21b14a29d87dcddf64f59d7865144dadf7c455696b7c509d973d46d73af2eedf","0685b918bf7e38ff946e8feb260e193f48626b7eed70669f1a4c5ecd0b3cd444","33943d74593fcd8670b682fbda86001d2e728b13fceb5d22961074f186619743","36cfb8ac02cece02f64db23c7ba4fb95ec2b56fa8d753250aff3f2b1663a9e34","64d61d96ca6dffcff65d3021718e83ed23bb14517aeb1f02e7c3ad993eb0a0bc","8ebea2af5e5b387063de6535a5ecb197f32b1ae6614df257432c087725161c6f","c0bd5e212ce61814fcdc9240b90868b1108c37f21da55d21cabddd3e180f195e","326a61d40b8bdb5b3c35f83e3cb0a78af05cfcd4539f9098c9fe46e32b5c6326","044f2620626e0bd1ce6161513ccc00cc107e502eb2d540c543ea7dcc54f18cba","bdaaa669518e13907293d935c2fe379936525a1226cbfe7b5ed1b407bac5234a","f6cd6b7cda0141a3f0d629aff6c529e3f55245d1c198c450737d7cea81b56d1a","b5981034fd2b7724d3b4f4acdba358dc34ddaa3ad509ef9399c630c86cb9ccc1","0cc85eed70c33b0d113321a7fe2a8493c530cdec9babef148241f7203f35cc4b","c442972d48882efb43e3e64e1b30a68effd736227742a3ff936162d0ef320a4c",{"version":"3dfcd0a3bfa70b53135db3cf2e4ddcb7eccc3e4418ce833ae24eecd06928328f","impliedFormat":1},{"version":"bea7cae6a8b2d41fd1a9d70475b54d741dd7ca2103904934858108eec0336a69","impliedFormat":1},{"version":"bc41a8e33caf4d193b0c49ec70d1e8db5ce3312eafe5447c6c1d5a2084fece12","impliedFormat":1},{"version":"7c33f11a56ba4e79efc4ddae85f8a4a888e216d2bf66c863f344d403437ffc74","impliedFormat":1},{"version":"cbef1abd1f8987dee5c9ed8c768a880fbfbff7f7053e063403090f48335c8e4e","impliedFormat":1},{"version":"9249603c91a859973e8f481b67f50d8d0b3fa43e37878f9dfc4c70313ad63065","impliedFormat":1},{"version":"0132f67b7f128d4a47324f48d0918ec73cf4220a5e9ea8bd92b115397911254f","impliedFormat":1},{"version":"06b37153d512000a91cad6fcbae75ca795ecec00469effaa8916101a00d5b9e2","impliedFormat":1},{"version":"8a641e3402f2988bf993007bd814faba348b813fc4058fce5b06de3e81ed511a","impliedFormat":1},{"version":"281744305ba2dcb2d80e2021fae211b1b07e5d85cfc8e36f4520325fcf698dbb","impliedFormat":1},{"version":"e1b042779d17b69719d34f31822ddba8aa6f5eb15f221b02105785f4447e7f5b","impliedFormat":1},{"version":"6858337936b90bd31f1674c43bedda2edbab2a488d04adc02512aef47c792fd0","impliedFormat":1},{"version":"15cb3deecc635efb26133990f521f7f1cc95665d5db8d87e5056beaea564b0ce","impliedFormat":1},{"version":"e27605c8932e75b14e742558a4c3101d9f4fdd32e7e9a056b2ca83f37f973945","impliedFormat":1},{"version":"f0443725119ecde74b0d75c82555b1f95ee1c3cd371558e5528a83d1de8109de","impliedFormat":1},{"version":"7794810c4b3f03d2faa81189504b953a73eb80e5662a90e9030ea9a9a359a66f","impliedFormat":1},{"version":"b074516a691a30279f0fe6dff33cd76359c1daacf4ae024659e44a68756de602","impliedFormat":1},{"version":"57cbeb55ec95326d068a2ce33403e1b795f2113487f07c1f53b1eaf9c21ff2ce","impliedFormat":1},{"version":"a00362ee43d422bcd8239110b8b5da39f1122651a1809be83a518b1298fa6af8","impliedFormat":1},{"version":"a820499a28a5fcdbf4baec05cc069362041d735520ab5a94c38cc44db7df614c","impliedFormat":1},{"version":"33a6d7b07c85ac0cef9a021b78b52e2d901d2ebfd5458db68f229ca482c1910c","impliedFormat":1},{"version":"8f648847b52020c1c0cdfcc40d7bcab72ea470201a631004fde4d85ccbc0c4c7","impliedFormat":1},{"version":"7821d3b702e0c672329c4d036c7037ecf2e5e758eceb5e740dde1355606dc9f2","impliedFormat":1},{"version":"213e4f26ee5853e8ba314ecad3a73cd06ab244a0809749bb777cbc1619aa07d8","impliedFormat":1},{"version":"1720be851bdb7cdbff68061522a71d9ddaa69db1fe90c6819a26953da05942f2","impliedFormat":1},{"version":"961fa18e1658f3f8e38c23e1a9bc3f4d7be75b056a94700291d5f82f57524ff0","impliedFormat":1},{"version":"079c02dc397960da2786db71d7c9e716475377bcedd81dede034f8a9f94c71b8","impliedFormat":1},{"version":"a7595cbb1b354b54dff14a6bb87d471e6d53b63de101a1b4d9d82d3d3f6eddec","impliedFormat":1},{"version":"1f49a85a97e01a26245fd74232b3b301ebe408fb4e969e72e537aa6ffbd3fe14","impliedFormat":1},{"version":"9c38563e4eabfffa597c4d6b9aa16e11e7f9a636f0dd80dd0a8bce1f6f0b2108","impliedFormat":1},{"version":"a971cba9f67e1c87014a2a544c24bc58bad1983970dfa66051b42ae441da1f46","impliedFormat":1},{"version":"df9b266bceb94167c2e8ae25db37d31a28de02ae89ff58e8174708afdec26738","impliedFormat":1},{"version":"9e5b8137b7ee679d31b35221503282561e764116d8b007c5419b6f9d60765683","impliedFormat":1},{"version":"3e7ae921a43416e155d7bbe5b4229b7686cfa6a20af0a3ae5a79dfe127355c21","impliedFormat":1},{"version":"c7200ae85e414d5ed1d3c9507ae38c097050161f57eb1a70bef021d796af87a7","impliedFormat":1},{"version":"4edb4ff36b17b2cf19014b2c901a6bdcdd0d8f732bcf3a11aa6fd0a111198e27","impliedFormat":1},{"version":"810f0d14ce416a343dcdd0d3074c38c094505e664c90636b113d048471c292e2","impliedFormat":1},{"version":"9c37dc73c97cd17686edc94cc534486509e479a1b8809ef783067b7dde5c6713","impliedFormat":1},{"version":"5fe2ef29b33889d3279d5bc92f8e554ffd32145a02f48d272d30fc1eea8b4c89","impliedFormat":1},{"version":"e39090ffe9c45c59082c3746e2aa2546dc53e3c5eeb4ad83f8210be7e2e58022","impliedFormat":1},{"version":"9f85a1810d42f75e1abb4fc94be585aae1fdac8ae752c76b912d95aef61bf5de","impliedFormat":1},"db9e4672c7c667a0deabd6276b696e5994d3730e5586b5b87267aa63d9f72328","6ba80b835f61ba3a1e68f225ae88fa6e3f7c5f65ca051bc1756b7c899d3e9274","4631550a659e584f008891ce1c715ce29d0cbc3dd1ffbfc76ad2b50eff702948","ab0970f1b7769b6a37dd837ce570dfeb1124740a38f15c9294b246eb1bc780c4","116f359450e3ce8b94d3a4e321826d56d77ac599b68f9c07b4a4d34b2e8bf3ed",{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"6b039f55681caaf111d5eb84d292b9bee9e0131d0db1ad0871eef0964f533c73","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"c2a6a737189ced24ffe0634e9239b087e4c26378d0490f95141b9b9b042b746c","impliedFormat":1},"8ab9053eb153fdd8636f6b3f30208e0aaaefd7c23cd7c87be7ef913d36aaebe8","57430b85fd84c95c5b5f867dfbdbdffbd11ef2e48965eb9ecc7f4fb9d78be325","08eb64a1afdeda9c7cd87114906037388d7af9d91bce27c293f55dee9ce5a8c2","b5076e65f873347e41f18a2d239083a544c080b880fd62de907bd064465e1290","b9e4ff74d366947b2c7ff9158b5c878480c1523bffa20590914fa53c29144e07","63a9ab201a2da78d4d5c505c1c4c9fe380fffc7368f9d22fe7084b4e491977c4","903dd73508a4b3ae23daa597ca913eac9f75e3621539691b0a216047278e9efa","52741694e94b7b6b04c12285da7e10103e66b57126303a593a257fb5e086a491","b28b6136a5678e5b01ed6019b0b9ef8b22446f7d291f081bcddc71051d2a6492","583610054995599f0ed590a55b9f81466909e2087ee04b6c2461d6e0c8b08b43","c8449a6bc016461d1269ba1cd5aaeadd845e458bde39966f1fb434e5e99204ce","7ea0e0cdd8e14703d723a50b6c0d07f620d825f0e148d6c9aa97af9950fcb44c","2fd75662e4cd0cd568d6c0857e7a71648addda2a1c6d01918fae951dce5b519a","a2ef968f2f25e45f7a634cba822bc2e7542678615fe0161826a8b2e82e071acb","88b62f5279f98c2c529bc562a99dcd1c1cf96c5f3698b04936ec7cd95722ddda","59b46ed04998fc50de4f3a8896fa90c08e79530e15da952bf1aade4498f63694","c4d2a21697551d6e34d05cc2d20920bf142723d8f9f9ff4396b1b7d700e0795e","98a3ed3fac7a473fa6fc4beca3de620139d52f9863497e943f053c124eae15a8","cafaee243ca4ca03a097af441baadf576180f1f6b0c0a71bd2cf27c92dc3c635","d5132708f5591db907f3adda24e0a67970e99d588082cf0cc673359c4140c1d4","5ac4bd7d11ce705620a56ed6903e3f0c2a432f616bfbd96d408d5e77d40773a5","54ae1f916462a6ec28561bf5c31fa31411c2b95421419952f82fb29efe33714e","5a226d5a51ad9fabfe0849f5d5c81b9460ccdd9184796f1467cb992bbc5ce012","5ddfa20a02c6207915c3c9d2fbdb57c70f549307638c4608a63c4049c591dc6a","c35b355506a0ec45a8488207a4df84cde8d696832d6157f61b6f851863ee1b66","edb83dc67f334db478790a845e10b191ab51d035160a88e5fd3876664dec1612","df984b0bafb5e53912367951ffc923a81ca6e15e6164b634055880e4344cac36",{"version":"7a1dd1e9c8bf5e23129495b10718b280340c7500570e0cfe5cffcdee51e13e48","impliedFormat":1},{"version":"95bf7c19205d7a4c92f1699dae58e217bb18f324276dfe06b1c2e312c7c75cf2","impliedFormat":99},"b045297393a1167259e546c9c8f790f11d1bb9e56faee2a96ebf9dce560f198d","9ca31fd2cb6a4a2a79f0b9655deaec5de960871406084ddc7834e5f0caf6825d","25fdd9095e3c88304fcacabc177f915c88f911ec76eec1bef97b0a5296b69b42","1b6a4d8614e087ac52dbff37fc366bdb9d9f6ded3aed6b9fc6a42c9a07c7df6e","76cc26cd0f1fc54af550f8841264bfffae32d47ceb78f0234554492a2009fa6f","649415d05765a968aad9b1906b023aa680e332b6fe32bf31d4e96ae972797107","3803a665d8434387ab3b02307f2724bf49e2c74295f930e7fe4ba58bca51a078","5183e29c1682fa5a8b7fcb70788b7005e39fec1fe92163af24345d991be58867","e6f912a58bbeba4d07663b3655e1ef880f01e7a194a86e351d4a9b96785b9dc4","b93c566bb2c61594e867c53306a6d56b086e7658013d2b9d233e89fab5acbb35","7a0aed8f3aa05ec618437c5c7fbbfd2295321316c197a171bc12de0fbb2bb922","791e111b25123db45cda32073ba13222b5a34a947b0156eda9e77187f2ef91ca","fb0547afd53f48e07ad555fd6efd0de07fecf26615cc93a4607c0220211bb114","998feccbffe65bfb424ba1acc11357db6e7fd611eb99ebd237d860eda1e97138","7be4ecdc1589196748775376a609a917fd731034f8a1fafa7b5ebc9cf5e5bd53","62bbb0c5c7b0e2a53d11f4cadb782609af4e12e79170fd58163110088393b05a","0ca8883957ce65b76ec86ef9d3aac955dc3c4c46530cb729b88a6d2d737af186","542267bec9796016127a97865a0f97ea180cb8fc3f89606fb8c9cc4253126acf","28b5fc5754f4ebdf042cb5b27931e63b4ed8f5b64df7bf3be4b77d974640396c","89e0e9ac65c95aea0a539bd60ace8484b3a5c3cc4d897a68812217af830fc23d","0988e2afa78428c0d1cd50268b54dbcf421f80c992d8a1a6055f4133730ec6a9","f2905ff1575a84c410192ec46aa3f4996765c1e849ba421a259e05c7b2fc340d","aa34083e206a5f93e16d7c8ad6974bf445da6be4b5a7722d2ee98789faf7f8ae","ffe50ea4d20591a5940538606dd44f33a5dc343b6cc9d043377bb2c5f72dca13","38e01627ea43a1196ab44549409015e5c6f5b301fd332346a2e0fe38fddc6472","c3de1901e84728e2075eb45ca5df0d75ecd0e35e919e826fa8339404ed5536fb","676403375489df3da1edc59ed83d950fb937530b99d1691e495e7e84920510c1","8f8327bf5762a69f257f2eb84e3cf865c83a5a9c134adf38be12ce69bae4eb7b","11833b3c60fc9ab463a626d92764f42597c64fc93119ebadbbf9790756db7d5a","126096fa7c786d9d03b90f9e81f2145a2237e4781504957d3aa2ee4382f5b39f","1d095cf9256ec12ecd33cf34815253020c89230be4212f1fbbd9b5dc345bf2a0","ec7fc48032dd77a91fba9a4b98a4ab79135026a5364eef928ca03ce4c6936c7a","d2b46d9c36e9927cc6b59679dbc45a92ed19909fcc25412772af6bf7cdcc5a4b","1500929393a66eac684b644318dec8d947f7cece77bceac84395a96cd0e819db","cc998ddaa317600c0c56e53e6e76cfd3b3e23a0783fbf371f294201fd3b15e39","0fa7d91ac72697950720581ea9265cc0d7aaf5e559ae21bee6cb6ec3f7e4f3c4","6aa749045a7e565ab60023cd6cdaa21f9f0c0eb724ab4cc82f8b567182679813","42661bdd3babb7325e5c1050855251dc1ebdf4b14e69ef732bb948cd3fadb831","d202cab2cfd4555a3f2cc559ed805fe0e9d8890dd79e34646d142c0c24b95141","ea2f9f2010acbf2c1e02e79513fcbc6715fc69483b0b270e26b7b16592790111","36d25724c00295ceadde4a1f69ea32c94d749823dd5ede6258b928ed699f1768","2efef5f483459d0177a69b02baef9b931fd045a852f27a4f295f6ed958eb57f2","222ab086b785ed8c221acbd2922699f3411227a510d23788e52c303515b01d28","09b127a60bb3bd8625f2e0910af7c7e86afacc05fcdfe7ea0d9f73c381d7c8aa","fd0cecf30f9f963f1b0148733981087da93014fb0f69a9e065dab17fe67643ae","d9bac95e5cc44d0ea516e83cddda764f54dc5dd698cee1c64356ec968ebf57c0","2ec1e34570bc9b528ed078112f31576dc9ba1c973dc83a8c61c80b7bc425d4bf","b4ca0b2ae78101bdc252134c8519eacb2ceb931c9c38176706ac467b4eff7f55","b6c3f5b183e5428d20d6f6e3290a1e8efdbdc5cd8ec31184bb3a018b17c3d7cc","37cb6bb48aba80a010297eb815aecc07c0ea334509944c66305018a8fdaa2c65","be7b800e572c75d9eb786fa7448c9e108dab1cece51f55f73b4144e91b8906e4","0cb6b0a4e69bd922c9fae22e5d4cdb24b148ec0d29766c200738fdf5255a40d1","c34561f348d2cad7395bc203ab64e2edac6184a5f0184ecf19e22a71724e4fb9","9bb87d1dcfb06b265c96c8bff31a95d7003128fdd24750dc6ffc7ba2e4cd357f","00a6ccbc8dcd86c5d895ad39c2dbf030f2b6064c421bb49a021fae08f266e16a","5971247075b05ed88679485c1d056aec4cae062eb6951b995788c69a654a1c3c","f731d9570dc4e6c8a828c6b9f0228877820afaac8d2fdc00f4e8aff8d85c0311","dbcfc7d4a685b33c38be3dc2611615e692604bfdea5625f14d9f1156ad6722cb","83a184bbff2dd763f8e8051aed20b7b620d55778918945b23ef7241e6792abf7","fb73e07df3a40fbb9daf4ec269ac01946ce2a35cae550dc79e34d7ea8bdcecee","be6fe6c54c0b3a07b8667b02fc6981b5b40758ede42caccb7fa81646ba567d24","031f18d0b2394a9704c2f96564fd098bc5d9b8770a0467623cccb2d74f9f6d6a","670b9a2aafef1ec78813620c5a738bb1df2235a8c2def1702b8d58332b52c177","6e07a05d78279eb87db944470d6b16bf3c5ffc8a43b7a17bbaa9d80d34c638f1","9c2eb90da480fa93d78e04cc441c282a07a2c7428bdade5cd35ee20de7cd888a","3d3f74d81aa3ee49f3218c9f7ad9a9202891581cdc4cf3dde9b92ddde4e781d2","e9dea9b7f3586beb174d10099b644685798d15fb3d6dd5e57e7c4f2bb5068188","0d9237c67866eadb203d211bd7eaa1b3aba550a0c8a6bc1260d344c2c4a4ae7a","c60b24102572f64fb0023f5413404293c35c73b9734bd71da9d571b08111a81a","f479af6869f5f6e70b8d8e19f59995c6ae59e23bb56670c96ce30485ac98a1d9","f0bc31f07565c26ec58b2d3a26453b9019a83109cdb2254601b3e1cdd6fedd43","dd94387420ae72f77718a1eb8373de4687dcd6267f92411de88bdd3da5de2312","c66384b76689bf7ab5bb36915013d4836b7eff025a220b807bcdc106b1d4adf5","7f95716c1c1e73c9c27e91618a727846a86b6f69b8b9f16f84faf5f2a7fe7580","c24cb3c039b97ba6f6ae397234350e7f6f52e4088bc6d6b03de6d08907d5e3eb","01384cb437c106f902313bf25b0e8e65ac7b9ce0811bee3bfe7dfd073cd9b383","3c0a996c5eccc7c17f68c1ebb45c299ca6d7dbb9daf58c8fff3392409c31a9ea","ea2635643910097eba211a15f93faffd343817feb00af8efefd97c81b48e2fed","ca06a25e3a9bf82bfdb6f9f61d6d67f6c90960b6e20df43abea3c9eab515bbea","d1b1da0800ae66d723c43f2a07965d216addd736382b674a363d086b755f5efb","0d597693230d3eca85524fa33430fe1aed403b459510b0abe00a4ead3aeb4025","5d2667f3261713c3ed1dc5fdf5532bd49fdd74f399fb7e5d21901e29c7f6f1dd","d050988d2be49157325e97f3319532684b8ede2b83b87abcd8832b651b29b471","5a8fd21287dc8e39f13cbcb1ec3908f0f36428b4d575c02ef76c709d4cd07e3b","4e02c2619e7a81dfd2b13b89917c7320d5ebd87d46f79ea113d088a0f122f7f8","9f2de64058d15197c6e93170f8a8e2a10a3ae0f181d17c4b3a482bd3500d077c","ef8865f11a87d159281b1bfb08427a05413fb012a60e624d33589ba16957c607","5dcc475ba4e86db8610afe2ebce0f05da3d98be85ff9a0fd5e4bee1e293caaf2","f3afd6cb637b62f3184da2fed4948253e7476e9d6ec8e590359056d1ddf23b77","001dec038b0f816e352d106eb1d2b436d74462a9ae0f09487a533d08ebfa56b2",{"version":"eecc465018e34af899d8ac0d98d9b8dce9969bd093faa18fa8c50b361283eded","signature":"45cd0b2575a97ede8904f65006321e712b837e94d3301435060bbc1ce3b17370"},{"version":"d1b0d22e6e4e90c3d094b8fbb49d311224cb2eecd3b34efd8800caa76e61f3cc","signature":"584ed46622f07013d9f37a1f83e9e5f289a5f0c0670eb4495cea9b96e92eda75"},{"version":"a4d773dcfee1612ad27ef4c4b30b1933c2fff82f2aa27068f449ca64f8c1b72c","signature":"62cc9b5fbe9f7d7047d4158d132b77856eb829dd28292515f8bfbb5ed93ac080"},"4dd4f634c626a6f4365a71159448fe3ff154636f3413ef5a876ac0ac40fd751a","6b2078cf551ddb6ba3c5a47a50b78f3c3202d4bfffc06f2d63c46b1275969a40","ba881b8d5b6e3df354cc26faf2ebe93d6c3c1dde02534542c716970ae9862549","181dab8c3c89994ffd0f00c80a4af48d62aa9fa40c07316e8c3cd1825a62eccf","6d6336b1b4df7f1f65d3f84c9c020c205865ae237e19633d1fafd6aadad9a067","c12c64d0d3ac1be4fa925ba87f3de675601fb413ae25cc50396b192a9e176425","5402794b0084c917353387d102cfcc303842e39852c263451c1a77863cf28a9e","287d0baf43e643c7c740b9e8770f9300b2bbf98b8a518e76e8e217ec8aaead64","91298e6a51bb1962d39eceb6371aada140acfa24417011da38a2c50d2f0da168","4bde15551a94df07a4010f995704efb12ffdc4bd752851a7f95f591092bab4f3","9dfe82c07da9a54e56a248ba387ce383fc6a7743af92b17fed1527b0d5b6ebbd","290d8f4ffa2ed9c837bf5d1ff05c7637ff021d486fcae6d898aaf2df69dda769",{"version":"519bb18f0794a8ff471d102dd8b9938026ef3709800aa1be85ac498efb410c51","signature":"1262ca92c805ce7e145342140673a363ed2be99348f0cce10868d6a188d82b14"},"4115ce5ac014390c8cff8d3c51b52fa13f6294fb13d1803d6c40fe6cfdd6901c",{"version":"fb22c7976ba03bc78985174791b59a9fae0f998634ab888420a8a9a337a7ce6b","signature":"14db1a79905115dfbd3aeb0d23aeb9dac65cb849ce18bd29d2f391b5db2f6339"},"c2d0ba4e3ef6f4510841f61190862a4376c6515a4af1e44290d94e876f699251","632532295af106d8dd11653a780e944492849c55bc026c0425fe067c30e44bc7","d9a8dd605a9db2828cb9c2117053bd44bb318092e4bc8ac12297ecae52e48408","a40659fe11d83da3b659ad4287b7c1b141378fce8962e95592cf4ee53258f190","0e23ec9cf75425a296be7ee9bb1f85e499078bc492dae72e2fdc5d5fca98fb7d","4bdf7deeb5d15eda7a4a4246622cc7321e2d4d605c3dcd9a516625815e772e22","6bf062bf83fe661c697b7122c883b56f982b85438ade4e43731c2efa36a48032","fdc556b6075fae393c48cc18dc34519af2d448664447be8b7ca2126b02affb19","4678397e8b0ed1445aa088771c9d8572e7bf45cffc52c14b039f86b66fc20252","fe49dc0bf1a296cb927061f74f7fc6d74d2e418b9362c4640881b4e42a503031","d301b86d1d7050c88fcb6ca92ff9b138cfb0a0ca8a04b3a1b61ebfd911cb1a6f","fae2bc89da0a93f9a0266d21ac2c33bf03f2674c5ed33ff980e887f397c8e06d","02d6d2563d3b1928e87f184b0c8cf3bb35398e2c3aa7b6a29ffc24ddf282b6c5","43cb773c20a53ad1001df0133aeaf26b52855445031c295b9a37154ae35343af","0e481a28d565f1a503dbada5d810dde3113157cf6563016b213cffec90a79993","10719c6dd6b07a17cb9be468bafce43a61e5afaeaa4f571c5b49866ac337a8b6","fe0a9db55849377425bef5a6f180779d0c7370cd9f4a41b520880c5ee1cb11fb","c5c99d680e713636b83e2e037b5a292adf0b2dcac011b1b188552e5907f6d7a5","0e606c69a1200fccb0e927c090393936f2ac1c11920b0e55753f0c4fc45fb778","6b4c5c71b0aa3cac94c8b3b819f68a30d0248709e6bce38465bd8bfd7d7cf395","5f314bd27e54d77f98289eb1e174636987e346c78b9631a2a1bc14f5d8595725","fa1124d16b3dff467594eb52257875ba98e68e40f39e10dba0611c7cf8d657f1","4f6ab5fa695e1f3d02881def8d878313b2f103220b91e48db7ac3bc21239313b","0b2d2659a5ea7be13b0b7fe40f2c4019e3be87c4dd921a8025ee382320a7b63e","f354dde829d0ca02f714015e8ab8535b7e57d46d492c3e4e3e6aa2b3bc47eedd","9c24c78a38da64fd818c7488f69b9d3e3290ecca8d2d2e01389fffd60fbdcb11","508d52244a0011a2812d56877281530ea463ba6cdcaaabacee8d11116796be74","b47ee19e60f569336d7b5fb2933e702b83d57ed60a253edd66cb1227432a5004","9fcd9e148321b51599f5e159452bac3942fe8e5152e379d40eca23209c0a0fda","ee485f14ae7dfc8c0f38490472d1f546e61c030fe440c0ad68d0adc75b09e4db",{"version":"2b1b85a4eafee769832745c5286b1cc6e59f63ebd598cc25933f167d08e6e791","signature":"47feb828a1f41847fe6773923b758eaef3d4ceea64653fedba186a53353c3e3e"},{"version":"67b48fe3bf693a70e27ea2748afd1f604c54aec829b8d5fc96b3f628e5733500","signature":"acc55cdd103bab05e0b8c879b73dcff56bdeb063f69e167b19cb10b456f7e725"},"62f0646b465914396e6910bc62de871a623d2277db1da460209380168be11788","d6ea9568e4855c97e8126e299a4df99ab0cc53ae477b032a27045172e54d41a5","39c340dec9bcc6bcc6f291a47dc6bbb6d298a114ad9f32b9829733b3e0830e38","c1c5a4d1a72757a92a31329e8f86753bb5ff0c4d0196b8466e168a171e5f4fda","3d7bdaf3ed469369002a47e51fc3c03af0ca6e657da5297480e20e4ecd6b5ece","cf0a557968c3c332ded37b6a899c82d605412fdff51756d1bf99c9251006ecde","f1f4867152604ba908a356c724970b6fa7c372d2f5f47ea04d37af4c043646b1","2e25a1451ed168766182a729e70cf978f6666827bd7fbebdfb0a12600be3557b","97b1f9f98dad479221ad2169fa7f94c6c4b049ed3b5bb2f88c9e2296973efb2a","d6852d894450cf567f3aa71d0052f1de7ec00992f4d00159b0d9130f9ac156ab","928b3deb2de8a4e91cd4473631de7d098f7cefba8279b527db2fbe9b440919be","91ff3d9748b6abfd395824f0fee820c5638871b86e6400c21d0c1dbe162b836f","1ee5b9e592624caba1ba0fcf518851c824f448376408819372bf950dce560322","225d85ad926f680f65b70a76869691ca99e633e3e4240810eaaa62dc45b5e092",{"version":"ac59d5b03e47598d112161a3608e188c1319ac0c03ab8e0f5e8462064063962c","signature":"9a4d746d25858cc7572c54b3a28100c70b9c1afc14ccac1449072b27dc37499a"},"02cd73f7d88b1a50873ac66115bec20abe49433c35f0cad5dd09fe6f2f5940f8","04134f50f9c61432f199b9419775578a04691eb5dcff09492e5af4f409793ce1","29136ee2c5932f09c7eaee1608d3ef92f1535d6fbfd8857e5b5cdbf91a85bf6c","1711236bdd3baa5e1bba2640cec90d8ce98eb9ef752b1cf24ab70209aed78b4c","106cad458688e9688f525b07808597fafbf7e2dbf27986a1bb7fa04772893965","38888437f31ec5e2d39aef9c46f5092d5722bf43f472436ad1b1af08d0ffdc57","284363ad97f3206b55bdd30c19b6727051203e030bd3d4008374d135f789b11a","87f0e1655eed475c75e8f8565b2c111b222eff3a0be57122e5dd7f54349cd26d","90d293744f66eb2cd999f5a414d75c0f6eb572505814c1186f29563a03554550","516a0644a6400b8ad2699d64219cd623ac085cd72a6256caa17f6ebe0da01c7a","5da3a91b2b5294885b3dec780c177bfde9132af5980be86b54a3e7734e3c26af",{"version":"e531578e24a613e74f7ca51a5c8a6b8202fee90cd75a11e043076536168677df","signature":"146e08edc6c3b09d6068918c5d08f6c41161471eb62537a64f32ed30077a99ab"},"91ca499e3074bf63918558870f50849bf4a60be52d5a07c5f7b4e1c81361ece6","2a078a3d4ff94609475f554e331ea7979b9f340f73ae327a0e28609aa455a671","f97aa7fd4854ae4781f63fd5d481f76cb728a8501f254748b195054547cffce6","4808bce828d7d7195dc01061460b99aea69a268113007177eb07d09fbcb9f285","7dfece00e91dedb5e6f262fe95608e17d93b0e3bcc88d3221de3bdf82c22a4a9",{"version":"882ba582bca780ea29e33264013ae0772f87eef120a74f604cf2024ba1dd023b","signature":"e04cd4f239a6efd036f72e29cdfd0ee48794a4a0fe1b4961760b49c122eb050b"},{"version":"045f1da158a705e887ab4bd0040ae26691219a5e3e25d2a9a4056100f53a454b","signature":"ad942337787a4c93558068574acacef507da8382ab59596670748c72513479f7"},"bbb46b0fbcdc576355c8044c59fc6041147db11505e5c4335bf3e97a5289efb5","a3d89d72d4526e41ccb86f770a5f55edf937a5f667b0befde576c1d4bdb0c9b2",{"version":"6b81d9ddd26fbdbe6ccf437518b8233f6e7872fc6e845c219529f148d5fa95eb","signature":"106b3f8678b78fce3a921b4b10b77490f25b8f54faad30c4696136697741d759"},{"version":"d73abe26d86d76b9291215d6e55011c8fb57b931ddddc807033fbbfc96529d0e","signature":"a1d6316feb75f247c21e648ab2ed07ef58c54a6fb2071823fca13e8a344a1ebc"},"2b096428749378118129ebc7a85424f79dc2b8bc958968dc024a65049e313009",{"version":"7c80c7a584bb8954bf7f8ad9a39a65bc61260d69b54e536eba6861d2cd47fecc","signature":"3c8cc4e08191dcef24ba64b393ed7b724189e76ff9efe7ee7227a875e7afeb04"},"b7fa6dadb82aa5fccfefb380e7c5014a77a09f73f51bfbbc1ee12a1c7ffa7469","a2f6dda27ce235c2db5ffe43551ca71531dcba8b80a0ec4d4a39691b49f8ceb9","a150ee96e12249171678fb7739a0f1dd1969344233028ac860e68607ae2c1304","a991ed0662c2fdf75cbcc03dde481fe711e94f05c20b08dfa460abd7ae375041","527ddf406dc12fb329bfe5ee50afeb4143094f1f67cbccddc5c6b236e2842205","732d8f47da31a9d428cbe6f8bb36efe5668feb1334240cc7a90f0af42370b106","64fd9d10ab6441c5ab6d6e66aeb7b60aa8fa99e81e5f2499046dcbc979dd5a91","40c4b6ff6923d99741cfe5f6e642196379ac3b014a85766ffe5b64e6605428af","a878601dbcb7bafa7968b6b822fee520fbe453eb6e5a030dc26e18a20d843b66","2112d7752882b843e20e5ce120bb4316936177b2398de0ecc468572d9627ab18","e1d2be54ff56dd0cbc96b9b43b04579944cdd2547f193e9ff55fe2e5fb8ca620","0d784994e950419334ea71a5ec1a558d5e481202bd0ddc03c3b9c540200bb9a8",{"version":"e9ceaeffbab576dd1c624f75214e896c5d2b8905fe08c605af2bb1512462253e","signature":"ceec29d2b665eda0b8a2c09f3e8561b52a31f89e337be12d68a2dbcf44e7d63f"},"0c653e7135c9eb75e4d2fd3d87e22d57b95f5e31a910740e54196ee2a430ac60","793f252b7c71620d98bf68ebe8a2bb1b8c814b77472ed298c1c12db02db642f7","f0a2784f167433dc4a4801ed39ea30ec81f1b0de909ee48892f986378a667650","1bc7b2ef65d3dcf01b0ccd9b9975574b16f57c83250384ac6263a34148cfafd8","7dfa41a118390210af818fa72499e85a964b13c324982927b2a3fe1d9d056fa8","6e2f3199f08eee4a64866edeea4666c5461293a72a4c82d8f28ef29e50e2b5ae","1f1b0722d39e71f3493373716f710c1f4de89dcf9b56dff474b89871f125ece4","d087812fbad5bfcfb8475d947924b1f5329c04caca8f69890eb9263af7590c6d","c57294050388bcb6b61beb23ae6fe202e60efe8eefc32910bcabd8f579134146","d629aae730f7a245688f561386ac656ef874e998a87810ecb7e3c4bddd72746b","3bff5583aba62387764a5afc76e24902d21ad6d3926735833aac69fec68ece68","e0e2a2293c5169d36e50b06131daf695bb1b6f8c5c5fb56c0cca5892ee445dec","e5ab51d67f6b616cc30f933983fe32f6306cbe6ee52ce1d67c8f9374e842a0ad","59138c34c47d26972210b261a0c875a72ffa626ea037e22ec6520bdaa1be7477","917b63fda998cfb36511cc218d33f5dab5ab7d45b9560b09d42b8e9f791f3c1e","cd82ca985cd336a0b3fb7f11c7638c10115ef8d075c284ba5396270ec9db5aca","be394fe821feba8accf0a3f27ed4eefb79abb67b9ef1fa9d2e82eddf6384c0e7","5834a7e9d1e23542abc0e5c3635051cff315f98f5178f1518b5aab65ba65d602","c35e5a75228d0fbee3f2d1d59bd8b74c62bc8752c54d3018b6b475b24967e380","fe8072ef3d585645935b853ade0138c803d730a95a4888c6f877bcd830ff4092","a09eb033edbf6868331baf67da3c28341388527b1d525fedc589783f07bb42c9","36e045faf67a8b8a901a0693936609d0fdea75c5b9334b0f2d2f861f6cff3d0e","459dfbc672d2b4c8570911ab74e41e56b235d969c849dcecfb6fec7f731cddb3","aaea59593b826584d85520b39190ae6e34e34a69f76481727c601ea52cefc9df","a713743e21042a5d96c0d9b42e265f7686fe478aff836b7fedaed5908540c7a1","c86c0661f5ec2d684ae73aea3d94c6abdd1edef9cc2ca9e3c656e5fa3f5713e4",{"version":"f0146d0a947a5fbaa2c7e8d0c5bdf08a3ce2b444eeb91fb41d868b96a6a02a5c","signature":"9efa35ebaecd17cbf8ddfc64affc4f774c0345a4433344257b1906d5b883b55e"},{"version":"40cdfe462505e0533d8b30736092c4497d11328b0f46173568932e27d5e8eb82","signature":"4b3f2f10a58aaafb3a14c1e848bf7e8d23b45aac77eb1be6a7d30e1741de109d"},{"version":"ebb588142d1ad92bdfd19895d131ef36164c0f791d43621660a752f02b92c228","signature":"fd312d9012f8eefa03a44c80c3671bfeea3d53bdf4b82ed69ace7757fa24d86a"},{"version":"ce76d5bd95a820c781afe8ca7a3fbf178bf99327d8ff430b9a4730ad34896922","signature":"d578bcbb89dc8c33854fbbbaf12499f7fd25c2ec9a2a23f546731cd129bfaf8c"},{"version":"cc730349c174b1e2681165ca1b7c8b53eafa565d0f934f9d0c1095b126553c4b","signature":"5cf79ea804fcb5735e28af2cab64241d32da83ce139fce5f048663f9a7ca3bab"},{"version":"ba500b5a406968b75adf6c822ca1c9f5b606497d10cf34c3d05b98f0cf5ec398","signature":"1cf9b3bbf1645f338631a14c31d8aa39ce8e429aad16ec69727b739294b8fcf2"},{"version":"8c9f0ed167548d6a2ad0f8852888f607e28a593558bef9a708abd0cd019c6bcb","signature":"a7b0dc56a1cba56558977a7a1abfee88c41d8b78b19ae454e5eeab6ffae0c6e9"},{"version":"1d00c732522902dfe38b558116d6b0f62604d9825a3cc53829780f107347f8d9","signature":"7da048f2f8bee792008fde64f0e9574c1c13f9ac2e536ff5010d1cc3c0e504b4"},{"version":"1e35ca4f26025eda95880ce0146c93b9f2070eec74b0d36487da8afbf6c7b01b","signature":"b02dcbb13bf980fa9e9ffd15ba7168ca8acb211b653feb9d8c020d23f79cd21c"},{"version":"86558136c47819f41befd54ec24300db0fa0a1c6ae9b31b98857613258c7aee2","signature":"dfb664f09fa801cc8fdb8629a13b2ad8dd408e5caa62899b5a5faf9a30daa474"},{"version":"370e99a7940fbef00aada87ae4a7d4cda4f0d95afb591e2c10c091fbc4f1a98a","signature":"7345879660d01969854a2ca8777b381b6c21ac96ec2f3b94fd142276709b8cb0"},{"version":"00152a417750025cc52f4a372000eb1e79acafecbc404676efcd5bf26e5c2614","signature":"4c73b81dd2b38bac18c8d02c7ce8a5931ddc4d0d635cb99dc893b96a4c1076f0"},{"version":"143b5e51343e6b38333551fbb3b4b61ebf99839e7af1573964003fda1ba78053","signature":"fdb5ca9ef183860584bdf8ba0780d541ee22ee5dc8184de61717898c0c8f3ded"},{"version":"a1ec08f5a004c498aef32a6a27342fa36f9e2f69d2108f1f8c88c68ab3bb0195","signature":"0ea9de0c84d8ded21b70fa5710c186dec99052b76962e0949f4b6466a9e9e640"},{"version":"eb10dc46d98670c1493c4510491497bb09a74077624cef8547fdd0aab7137888","signature":"d5180b4b18ca5d52a144d4690a25f65b99256d406e0a49aa68792760a06ba508"},{"version":"7731053f4746c2313c4a80f9d9504b492aca83793d4cc2c715a05a9443474e02","signature":"f65c729ace7fb1613d8ba0cd4b87af3c060ffd1eb2f1bac512aac28a402666e1"},{"version":"f0a293dab017688c608c9a4390a5befe603e1bd6b0be673cca2eda2ac74bc3ae","signature":"30a0ad1831f08aa705bb63e0c390004e9a59ae89a9f025efb0087f5fcde2b7b5"},{"version":"95bf90e6384c2b8cbf40183d1c39cde0617d0ea31974b8d5ef876471abc7ccf7","signature":"2bcbd027548ddfa7ea54ab2d88ef2cdb5c6cd3f1d551a78af7474eb4826e297a"},{"version":"45d584f88e6264778b316349ed927e7ddf72d31028f27693e4b01dfcd1fe28b3","signature":"4a53a3f28de653f8687db69aadb309d313138ae8c00887197664f295a7028656"},{"version":"9c10a4b956d8821f4ee24ef3c46e4dcce1164bb6a254050672bd820dd01987d7","signature":"243d57671a393d398ebd1ae08c3efb48f4dbc0b2aea6fde2ae8c9a0da743ac01"},{"version":"3b5651357aa700579522001281e7bc52b29994fb26b3ea55d58958545a684c56","signature":"ed3e6b6936afb4bd673e8d8e5815bf565fb3993fdfcc7921629e267dc8db0721"},{"version":"318078da4e607909cb6d4992043beaafd3018466f708ff97e46b86f2cd31c75c","signature":"fe7808dfa3dede2d00dbbfa7917e1eaa31c0eaa9310ad3009453980cdebc2bf9"},{"version":"61c734499e4461cda41893cff5f3953ccf4480a50d8c368546fca3c30bc1c153","signature":"ab9e239f7d4058f33c187377dbf640b3d58154963f1e843aeaeb50ef80d73119"},{"version":"11b89b0eebc162aa9aa6b3444c4dd259bc02e6e0474b8843ddf41ad9ce266fd8","signature":"1d061c46417f91c4e09d98b469048c78c72264e1d39b56a9306c4fd04230340d"},{"version":"f602e50a105b83b80c9153294dc685b335a4c9b44213d280eac2300bb4fae4ff","signature":"c6a45508660007a6500058a3f595117582f5c31a95fcbff39100b6ff4ef61f23"},{"version":"3c632f84cade1714849887190fc6ed4e8c99dc1d7258a4892b2dee59aef9ffc0","signature":"bfb1bdb9992a65d57874db62e12e17aa35caa33ddee5257539e28f50277994db"},{"version":"df0c3f01bab8bd80a337b7e806c4566d6f651dbadcc506c0e5fee9c5413aad1f","signature":"79aa080bae74b59c6e3bfb5588f9938d4db8e1b1c0300956ea30916103d85221"},{"version":"87202d52a489e3e81b12e1928d12561b1922b289d88760d0d99b419de3e8308f","signature":"ff5bee36f2f6d5c59f83c8ff8682cc9cba40b46e76f43b579cf92006d4df774a"},{"version":"44493da813771fc9e5e9466826ec2a79315fa09caf66e67e32cf45728f7c885f","signature":"8d9bdf573a8b01d6745efecc092aaa2f548167cc051192efd1e29b23735f8661"},{"version":"3fc3927e014294941f14817930fc56421835fbbbab415618afa614788a444910","signature":"c21c1e1ac243d48dbe3f4d8a4e64c6f49c528d4d7851fd4cb28b6f4db508c70f"},{"version":"1dee0ff4e6ec722a18d9893ed02a96c17bb5780a0d5ce9b7c292ddbc643a64f7","signature":"7e4b396ae6ee4801dbafc7e08b846314dba90a14a2f6e995e5736ac6367696de"},{"version":"a6cb2e3992cfedf07e86730f22af09ae8efe483ed3331cf287399e85d75617c3","signature":"a58f9eb50c5b3f1843e26b6ea905696d37ce6a57897c13d4f3b0edcb87ce9e48"},{"version":"57e1e506e18c58da7e7842d19f5827c8b1e575d0f5c57234cc688e162586f82f","signature":"b5cb48ad2431d25cc8e2c87c7e3e74c0c240e39ff11019bd4aef760457102dc8"},{"version":"c78094299de37214dd4bf2ad449805adb94681f8cb0aeec0b4fb762848f50514","signature":"89f802118cfc8d8e9ace38508a10a00de801f5620e17c20c37f90096cb4806db"},{"version":"3865f12f6058e41cdfee3269472e89524d12be69fbc008b9d1c84b07c78169e4","signature":"4de55218ded7496d38e3071d3288c3008cc0644b3ad25fa4ec714606030e90d9"},{"version":"956735671b4c6dd5e0a06c33d86025cb3b971b11eb6d408907e5474b6b821a93","signature":"75de71c5dc9f4e6ec6e7064a8137baae409d6196d0ccc7e6ec8ad84d0a340954"},{"version":"f72c7feac788da83cf9f4f3a720bc5efccb310ac601c102f7f9659f9a0d9e36e","signature":"30511bc16bd0fc6563f9fb877daf2efbea510be05636f651ab9de3f1e8e6cb42"},{"version":"78f9af0e339dc463fece2179a90d83be9cc20320e474fbd556f7030ce4481916","signature":"743bf7f1b16ee847f47dde35d20936dbdd1cc7b55644f628a40d2e5b03579c34"},{"version":"a6ce08c86e405e1055d8ffc678c7cfbed9dd45f71c3cf9cd02ca10afe19f39bf","signature":"3f7fea9f3f81b5efd94259c44ee9438e99fce2c3c4293f8a54dcfffa04e3e7ae"},{"version":"e44130f1dfab80e546f7060cd1d60d3ca808129adb6e9215c636a488e5928c66","signature":"0f34290123e85960887b5a868f204ea076a5a1046dc77c846d27b64559853f9e"},{"version":"04208dccac826b72625e442559f74767fb50e87991ee4bab6568aff3060a4339","signature":"d3fab3668f98c7cd8acc8bf650981a554fdb4d96cfa62629fdc404fd6ecbbbe0"},{"version":"f74c77bb444300ec8018eefc13d33244bc25ca9524124977bf053ff48e9cea4a","signature":"53140417761f2fcb3e6c2c09207bc10738d1a95b245a31b04e216369c872224c"},{"version":"1eeea752fd6a93d82bf02bcfdc154e322049acc0acba9295884505c05474865b","signature":"f365ab60c4027fca6c3ec435b2be3b0f6d44547255ffce934839713d9190a136"},{"version":"6e8eb4862bf420bc4fbf3c83dd4e4bfc6ddfbcaec72b6e7b180501bd06c4b2bb","signature":"883b0bab0598b517adcd33f726110b8d6baa859ef8a4b270284259e6204cf1a1"},{"version":"c7b7f08ac20722f2fafb2a0d9ef32de3026504a6039b8d8ef685f50dc9b2ad39","signature":"b65714d825838aed41a3f5c35b1ac56690bf9d19902a54f843f0d53e822fc542"},{"version":"258f1ea0c228ecd77c3bf7b08d647103195d9138b4afa9708f8754589465fa5a","signature":"81ccb5e7c206abfd7a86569b1fff9ca172c7c8e3642786d386d3edb86a513ea0"},{"version":"88cf15903c73c75b1102cf00e570610089bf332ac9ea2db3515a5f15965421fc","signature":"4ea2ccec0a9e5d09e5ed41e1c6638192c93d6a9df18e67f1f9a3b9188d8d24fd"},{"version":"06fc6549018d94a9864ee4de5a6e13bde4b51cbed7fef0694498284d8e988d67","signature":"36f0c111b1e8715f61a6c1431a0ab812021321179b0ea3ac4520e1888c20007c"},{"version":"0af17ca60f1af4798a22f3b96f47287d6363338c2bd6c77d7a2f7558dd2e9aac","signature":"dad694d90afef87976b9074949f7dc88e1f45104e8e9583b0ae1b86dcf85b1db"},{"version":"b3248d9508f7d6264574236fec37bb472ab37455d5e20e25d8171e8617579e2a","signature":"b9d55b9361fec1227ea1eb68e2fe8a6c9464ea87df107270b25bbc809f9b13a7"},{"version":"f6260627f9ae18742d1ae33bf33e0dc433b6ed060a63643beb85d9d3a8a26c1f","signature":"1ffb83d5b196f3d5c864c3f9f2a095caecf6c115dbc6f6de316c9872beae0a54"},{"version":"eaffb4e62baa5ac4f89d4386a1b9f29fa8f455cf6c81a46a4efacf9ff62bfe1a","signature":"71757585f51f973ab815de570030262b0f20d600d57bfe69fdda2f882e2c0f2b"},{"version":"4caecbbbf4040f47d6fca6fe230a7aebc363b2cc09aef7c1355357e2cc1d2c6b","signature":"229784901c74cd4be011f1c90745047d8a37225562b5d70121df926e1f8fd021"},{"version":"b9e062aa4d35677c07dca6f8e63d0c0335eba49dfc90ea72c54899f863e03e82","signature":"f57c2d593aa3fe47d827abff36a4e53560a5d10aa39305e29ef3e4e846a57219"},{"version":"6cc117d5541bed2f3327da889c8868488b15c04530bbdbbb4e5ed7d4f88399d7","signature":"8322586adcfaf85babf5e16eff3bdbab3af2f54137eb4478c2af3df67ca57ec0"},{"version":"2098c50bbbfa6950bfb37b610eb8c0154658e564d0fe54a6869dabdad4bfb4c8","signature":"4f5f9448cd3224615da4d3c442b90ee5e0cb9adf7e6a4743e64f147eb4dc79a1"},{"version":"0cdf15ac9f6c688194b50912cea0746684923607813439de9d395d38d4af0705","signature":"6e12bd9a7606f048ab6f942cfb3841b7df454eaddad9bcf2521050543ddbe337"},{"version":"00b1c542fe9c22d2006ad207bf9d1ed90c5aa7f17c0db5d80aa4c171c248750b","signature":"6735179f2dfc07d1cc461ca002bbd5a746ddabdd6f76bf4e670823f07da7a9b3"},{"version":"57f7479f4bc01eca1b0f086480e25ace6ea8805443052160fb5fac68456c541d","signature":"9b8f78674ab689db7c31f66d5a40d9c86be1b4266a0b8ffef520c7949fcafec8"},{"version":"7f76786790a697e2ad0b40ff597ab2ceadbcd5675d1437cc0263a3d35b38e5e8","signature":"32bb544fe03a176f235623694213eaadca1959dcf251fe7564207bde9b8c5c30"},{"version":"d1903222cdec8c303ba572135d98a835b6940911258041aebca49da244af6312","signature":"a0fbf6bb3775166dd93798982d8ff217466c5a16e240f8638f2621727617a3eb"},{"version":"b2bc6ce89ac0880444ad043a565ecad24a98749c035123cf50a6e191cf197c0e","signature":"29b6713ce3bd73d899396c7130e36611703665d86ff07db7f564eeec4a37ae37"},{"version":"0697e200be8ad5c9d9b53cedbee544246441b1d6474c1510b75941e21639be18","signature":"440bf107ac687b6ca388e81d98d07a66766f02c9746394dfd8e92dbdad834643"},{"version":"b8ddbff4e762b800e2a4c8abb1ce00d82be4d22399d5ef9e1896a9e45673de17","signature":"e2093c45a8b30fbf524113178729a3ed5bb3c9a40df2526fd384fa36f5bb60e6"},{"version":"9253fe17b72f4d7b04d1bd731f1ec64d98f07c661b0807bcd3a2adc7a71974fb","signature":"4d1cd2a53e9708bb5a24a6b068e56987a678a3eb5763cea3b372ebe34211e61b"},{"version":"a95ba4090cfaffee5ab2125c119fe208df50362e4f17fbcb65b0aa0a6ed42292","signature":"9020c0746bb5b98ccf57e8677b3b6c906e3289e3dc19c3cdd95c8be352d924bd"},{"version":"fbf17983ad1c1407af1fb82f32be0beccae47287e68d50efa530bb6171d8cc6d","signature":"60881637252d14d967b0829dfbe4cd41848f4a5e8d6c38c8c45342972f8174fd"},{"version":"b8ceef2e1ac9371aea925cb020953aff19fdfcb0db5108fec067076f33dc4353","signature":"fe82a19cd3f685f2e5bcfc717f475c328515e74db87c6b6691b04fd1aef83385"},{"version":"eef45fa61d588d6c680e8a3660da0266a9a7d537bfdada35b188d25483de8d22","signature":"d97a49fd73f498ed9c5758fad90009e56eeec854c054f222245343e3cc41906b"},{"version":"820a8118464951d0733bf924a06e7f3e3f7f43e3602ddb099f54937143c5a95e","signature":"6a22a44cd113fd171dbe144cd3f978c6738c09a7e7a25118fa90e26108d3538f"},{"version":"4e7dab6457f19e4220828d3e93c9e436c04abc060506c28e0cdc1785da750d16","signature":"4a7f35727f43c7f9acea1691ad8a32269c994fce97fa3bfe733d01f20f3ff7a1"},{"version":"12532bf3b13b52669fcd5e3a7c1513bc3581d6ddd064c8568754551108e2dde2","signature":"db81bf18ee2a7491b0b0b9ac44b7beeb3fb6b15cd4d707b361f96e119dadb032"},{"version":"85d095e8269bf42a66ccab33eb74aa68361cdc85b1ea509560851ec3bc76ea00","signature":"dd7c9b16844b5df8cbb4af8058a03cf24f5bcfbb871444d03650dd7dde522d1f"},{"version":"1c62f6d5bf18d371280a2501b5de6f7191419d230da81d8a34fe7e66bcb43886","signature":"79f90327f6ad1d8eb83206b5c008b1425c1d102c301db3cf9366d5f045617789"},{"version":"a14a98e30f6fe5b21b62cfba2310d7e87cb8f67a645114bb9a0f05b7d6d71bf8","signature":"061414a7c2595b28de252f325b96065a3957e5e785b71905a5c3515143516d9f"},{"version":"bfe437eef48862d1c7cc1b2418b6fcfdf1a3fcc3ddf3dd528fc48165ac895839","signature":"a4d7a82a09db44d2bd4f39aee960d22886ef368938a290bc77d68b8ceda0dd77"},{"version":"be3934e83f3871815a026a52f8f8b33ef28a6ba8a575b14294c6b4e1ef8c61b2","signature":"a4bbfe55c6bf80068968937a9e5de83cd288b0a03760cba8e77b4e2c02d8cd5c"},{"version":"75e8ccb3fdeecd5378020b22b17fe093c616266c246edc18a7015ab9aaa9abb7","signature":"df4954f12c803a14669dd9a3f1ad0cb0e75df4855db825f46fa8b87ec02729ca"},{"version":"5a2f9b7853f066fc3aa4ba1c67cb5b44b57769b07b4cfbda887a4a2ca5aefc5e","signature":"ac646b21f75aa6bc627159f8428abda973e1dd78236b417852ba17c7f102406d"},{"version":"4635c7ff817dfcec23ae3bd91397191345c74c18740302fb2c01d4ca596defe0","signature":"d5b2a5f00e994a6203c81f73d8fc0000c782d4077eba777879e1bcfda9537582"},{"version":"bc1b8932b97e2b875efdbf4c235dc4972b5d9d6b746fc785f3db5b2172e23012","signature":"f49d9f5a190b0daaf9112471276111ad9cf82ad23f78c1b18fd56eb4296f3981"},{"version":"686136fabbf7f6a7514f1725111db9a4af7db13138d64c9f80326956ab75c19b","signature":"091567ccbac786510c4f57b652dd262400e5d887c26791f9c10917acd9ce67b6"},{"version":"4812fc007006cc8174fc8ad34692b7d8e16d9723d476b5bd3c53c4ae2814e3f7","signature":"7bcf2eae824bc0aee9eb8d147edc5af5ba9b9bf961f3dcdda7fda4a4965ded09"},{"version":"6a38dc0d76981f386ce55060a43cd55b7defc748d37079d5b16885300ab63172","signature":"ad16e6aac237d76f4e0f65b2bf77a43829c1e8d2a9ea40c8436e36f04406be97"},{"version":"3ad2bab80f5fc952e7a780e9ebe9674ffcc618a73a7363db039a426e8599781d","signature":"0542824eb9f81c71a33ed4feace2acd691ea171ea4d417f3bcb6f067ae7c5a91"},{"version":"7f8d824287bb83bca1337412118eb8a89d3814710f2ee60692e9fdb3f86c012b","signature":"f8b0e29877c49809bc41c71355585aff514030a6cf041a2b291df36ef4eb1809"},{"version":"e74ccb67ca3f6d550a9408f22a8bb0ce9ba0d77aef5422fbf244b88e1da56373","signature":"00d3c255e6c7836190d4dc75f87ab8653e9899292509308c46a2f82ab2699328"},{"version":"06e21032f4251d956d35910d09c51fada96c1281a9c1784de97ff8bfd7e21f69","signature":"fa5ebea0ad5c20fbd17aa19e8592a3c329ef33ace4ae64f2725f4d58d45b6dc5"},{"version":"91f790da64c011a66d5aad9ad27cc4102c32c126a36717db300312ac3ceb1cdb","signature":"d9bdd7b0a36c13474446e246301c5b3d8371ec2e8215d773abe376cec1dac24c"},{"version":"3f26aab5595694fcd989e93d0e6b6cdc85bb32c96a5846f8fe293dc82cb440f7","signature":"8aea925a77a512cf3e04eae0428576bbeb480adc397a19a66b6e8636f91e1944"},{"version":"d3ca47b70828ff60e7018dda9d3238d2319428cf923ea83db15f7fe66f1b8c33","signature":"965557f5ddc6b61c00c48e6d191eedacf24ef4e1b4d974a7dc5315cd2913aecf"},{"version":"57e5a2e55ee4f9634fde4c23a26a0bb21edfe904fec01aad7bc9dd0a2d68c481","signature":"008e9ad9184ba50ecee4b7f26d3081d8d8ab6335e529a502d97ddae97ccd496f"},{"version":"5a67d02c65843b3e2b2e88a3424f604163f3f2a14628b7f2c8faa56ce1b6e2dd","signature":"f2c3508660fef5300f2184ec59ee7a8517a058c06b6a8e8ad4d05acd702d35fa"},{"version":"ed10c093b6f78fbd2f3b6049ace609ca716481a146fbf1a7f310b1b37fdaa620","signature":"c8eff41b8e7a1f8375abc9eebfc10eea4c2b636c5ff988b49b53e8e4d492231e"},{"version":"da61cebe550713d466e17e82e01c61f9843615d840174f2b3f56a9bea85a680c","signature":"084bdf2ecab4deab77f48edf276709646fbc86a1002f23247a9955461dd556c1"},{"version":"29053fedb149cf66e6a36bed6b1525ea1772971249c952ace8f4f9d4d74bcc9c","signature":"040213e217a35bb6305ad73959b8635667834f1edcda587c3abd74badb8bd05e"},{"version":"6d778790e0604620d873c2ffc496b35bc3218386f3a7f4614a3515ca24e2f5a6","signature":"16e514e8d7a2b102741cc64c9b8226da4d506e1d1c5a3f46adc03537b458de9c"},{"version":"1d63178a71de508bb2d5f6e42c2be283b6ba33620e24063aebdd14ea4bd68a9e","signature":"f93bd2b5fcc975f80eb3cd209d0b19c2f4762c13da43d17172118379702dcb67"},{"version":"c3f364d03499f755ff040690a68de23d218fca0ef9bfd9aae1150501a25a6fd8","signature":"3a68037b391f2f5d85ab29336a340117831bc944581df436a0bb34734946b46a"},{"version":"2e65b0f5b9f4287055f3bcb6b0f995779d680b9a5120d94f4a735b88c721ac56","signature":"56fd0049b93f7d8f5c87c49e34b5119e80be688b6fcea5eaf8a8345ae4dea0dd"},{"version":"0f22232c891a945b20c858fc9484bb9ac4460e2cb954f5af64d365dc9759127e","signature":"5848539abff27522b351a366d56f9de4b18a64757e40c1db4f8b24b95712f6cd"},{"version":"ab5a5fcf2f7871ad5e009c1c5bfef2cbe61f0f632e98862360b2b3d6051afd51","signature":"c425b58f944634d7528e66a16f3833a6966477f63d8c144b06ccb99d15590eeb"},{"version":"8cb6bb6b0b882b523ec6a04fb249ceabc37d0aef4fe40e9a593fea085554d71e","signature":"627c424a01bd1e8d3100391da076f228ffd53bcd4f2868407814d9e4825e4d5c"},{"version":"c1fff4172b3826e595e9a665688a689cc4e2d09004862ea81e0634838f92502c","signature":"fea8570283a40fb5082b973c97117b5d8e28c058bb119f42a6b100c48a5512dd"},{"version":"ca8df91b3ecbcc29fc2239d92d6f5631e5dbd494fc56a86ec6dc4bb3e039c1c3","signature":"3d35d59a040fed72eb378f6c1de19cebb20fc7174dc0faa65903a964bc4803ff"},{"version":"94055daa95928fc0f0e252cc54f24a1e489e8b7d6359b71a9f379212e45f90de","signature":"78d696f3e353df61648e8b17638a3d75966a5ec101b20c794dba8f6d68f730ff"},{"version":"806cbf21b375d541908987673289ba7a17d3cddfef2d3bba5d9c87a2f6e20c28","signature":"4bbcee0f5815853c9cd570dd8cdafd8443b0eb237ae0d00ac60c7f8c4d0a3e95"},{"version":"28d0f21d0136b2a60b4a40e4220df36a890c4abaaf9614dc3bd9621e0e8be323","signature":"d0465f08b925a1223d8a723c237a1578df7b054af1b11d2eb77175a5f9750af6"},{"version":"5d353c7ed080731f8568702d10f46084f5213bbf22801571a07e3936a720d728","signature":"09f61cb3525f36f96269570df8c389b68a117394cda8b0281b8bafdc01e96beb"},{"version":"be5a766ec1eba27a8e6d0faa2862a64e31054ec8886b8aa5a77210535611a82b","signature":"7f1489eefa1cfb270cc2cc1afa2c8384e7642eba0b5c3a9b204e7c5c211d44fe"},{"version":"38c6fa056f52f531ad0dc7ec35c7674a1748736147e7e133a72a3b3bf49cb034","signature":"f92818c1a09c2fb605e09f7abcaa231319ceb97a714228dc53ade0c37e5e237b"},{"version":"1f987565963d255e9134fc35bba761949b087eac1bdf68d89f6013de023fb9cf","signature":"1c0c832e18002e5f13497dc04a0dfadc33ede8583a7e75ff0d8467869f5714f2"},{"version":"949f8e89171b0fc4ec009bb9df1665507d9faddb8fd765c4307df1caeb6a8ede","signature":"4e206ab5d5761814ef041607372eb99c5f5dfdf107beeb9e1fd6a9bb4a948bf1"},{"version":"ec7df466446665edee4a4f25d70f7a0b9411b960728629545e3d98147b9e9c5a","signature":"d6ea454bc7b82a9b3b0a585b62bdef04d5443a56462d22fb00366195689837f2"},{"version":"d884d9ac0319410cd75497bcee95e575ee115d89eefcf77d6403f972365a2f6b","signature":"f8d823babd17d0a3a7dc279ccb6af58c663ef898fa0d129db3dc1bb37b807c5c"},{"version":"b861ba5706636c931c696d6e188708562305b9ac181769f52f39fc2bba10f940","signature":"61814b5d0032018099b454326cda32487620b26411284314b278aa0bc5427992"},{"version":"3de03d3667d2f86be0d82ec1d5e883823df30e7f37900a6146999f259e50df83","signature":"cbab052349418e53f950b089657db6d743dcafe0b3824f59e9a489f955b60acf"},{"version":"3acdd1eea115a147c757d6e798738255bc09f929bfcdd0116a852d4255a29a63","signature":"a74d86730598fba3d047f241fbb663d4e77689f1227a6c17927b74d4989b912f"},{"version":"411e8add7657e41e4b8edc77a8b12fe6e834cda9fb8a2c725a9fc31ad604aaba","signature":"bce83ed2f9c106dd4b0c17f5ef941835f6f2b64d1d00a9933b05d251aa11e42c"},{"version":"770ffdb3ee85040802c915ae3045d0cf414c30b7e058e2de30daab8293bd47fc","signature":"bc44cf8df0703daf13c15f0be40859b7852c4f66073da58f159bece74e786a89"},{"version":"a8890ce4c3b691562062f9fa06cd41f909ec5469e87ac3ca57c95ca0e82dfe58","signature":"dc48e1c6e5c0112b1e800f282e24c6404c7392fb0407828ae5379f723d3ce298"},{"version":"d33ba42ae5208aaaac43286e9eed10a0a1b25f32e85ba576e03a9f0677e52761","signature":"3c22f6a829fc2b049849d732014379c118b710b15963cd225dce44e299ec2d5b"},{"version":"4e72b76979ce24ffa0d8f3a3965f81151fe200a9615ce07e988b88149725a5d9","signature":"b090f5fad3287bfe064688e6bc9f8cc30ec28e9b3b0cc1290ecc2a3f53288338"},{"version":"ea56b23a1013d2db539179267d048afbc66dcbb7668ec76cb1ad633cf77dbd50","signature":"fab51e5fc4099a3ade011b48a702f40ee598759a4c0de1698744aa7bbf5746bd"},{"version":"0882cd1f389f0fe69b537be3d2e258c3267335bf73bc7189dad50f06b12d3086","signature":"9db1b82a41bc7eee5a0b2a05532a49b48b29a1549bfe67c8fd493fe68da80265"},{"version":"58bf40f9dfc5af5691500311230ec75f7017dda78dfaeacd9e4798f3e2f8ae5e","signature":"47e57d3dce434b719771d0d1649473dfe2bb000564e67cc76c2d02d17b1ab947"},{"version":"93c4423df933842a77a9878a691812e92791389802516314d7acaff78c495b52","signature":"98f8ffdb869c50298dffabe585cb61c8e2c0c211e9cb63acc2b50176df75686b"},{"version":"137b0e13e88f687b002c8687f891709a5f77255374c3c50b318ffec4feb7acd7","signature":"3a89237a54e14097acc9ef9a2bf7ee98d41e33008ce3fb26c31a1c2f00fe7929"},{"version":"7da74aa809ee908fe29843062f0cccd068cadad6839071d8b64b2696fd419fea","signature":"407626ebcc7224cb157cbc21f5b52d6873e71cf8af64be9b708b5639619fad06"},{"version":"25a2b8e134ad9cc05f25cd79e4e65efa059bc4dfad35727cb5e24c77bbe21e07","signature":"e8057ba1965c5549a169df1a3341bb6c127f0c45d98b385c330af467093d0ae5"},{"version":"497413b21e9c39f7bfbd0fe6dd5c404802cbf22c84e5033981d99679de20c5e0","signature":"1034c301ed42c6cfdd1a8873236bccffcc25df6e900cb5369e8baf141b80e408"},{"version":"a34e35a6ece8bad9f442e6ae43cb411ad476978450e28384aa10737395376de2","signature":"ff1c1f195d11487fbf418a3b4c77d0bd154e90facde36ed3cff0d39f501d661f"},{"version":"287da319123a401c8258feb8e4bdb27464fcf5926cf99f01cdee038fff8adda2","signature":"44a538a1591b8d512b640888ecf292c61b5d57ab6cf2b16b4b0ad16028e647db"},{"version":"279dccb0bd4c7b52fcdbf2757027520022a36c4bb57050c813021a1a9c4ba8cd","signature":"c986c50649fa04a7861a1402e62584249ab7b9d1327c57b1149181928157acb1"},{"version":"03ccc0a898e32aa52c91080647389a829089e030a3463b6878a232dce5e72d52","signature":"01ebd3bb50b1788c9c983d3ca8c913e27f63c845434db0af537edb42b8d8ced5"},{"version":"057d8920258362c815016e90e956c8837215d5f9d86dfbdd43e7d54d13052f8b","signature":"3cdc04812c2f099080dba4bccc2f650234419072a3174d5d8e8468cf37f18cbe"},{"version":"239f99cc87cf536d345e70b730e8a573b0fcdeddc345715169a293c140afdfc3","signature":"9af444efabf5661e7a7f7bca53230019e6b614298274a3f453e754486bebc80f"},{"version":"42558647194255386fddfd072e4bc9a99faa7322f511c151fae1ccfbc71a5a8a","signature":"5bf92d05aab5ad9b564663d73ea191090b3fa7dd387e0407f6b858fe1e46c61d"},{"version":"01f99c08d616bddbe0cbb68e81ae15216e823467bd86b3bb6c087cf59b796621","signature":"a9638e9f3fad12d6bbb45ed51a680f2b67b2ca67b1d5a2c2bbc0857f096df3ce"},{"version":"15ae7ad71da89aebf52b9bd23f1cb39f16344aea08d2fba31ae27bf0fb948d5c","signature":"c5d37eabe1209949dacda86c16f10f7b82a7a2db7ce8243c2856a3fdc277ca97"},{"version":"0b0de30fcf760540cfd47d93501d211d35e6ef651e9cc568eb0b5799beeca0fd","signature":"ee3f3236a26ecb278b325c388c0e9d329b58b7def3f1aeecec3ba0376a204643"},{"version":"cbfeadae55bbbda9e6672b58277b81853610d5beb6aa192a049986b8e17a3a50","signature":"44c81ea830974e85679b1afa25d1eedc79b3a66ccd9489e7f1900d338015caea"},{"version":"a10fccaf22fd7b7148a858ccd394ec92e713ad3b88ec3b36968109b05b0219e1","signature":"82169b89c409dbbfb5081cc88d2a1ec9397ddc91d3771a7130d36728bdcdd9a7"},{"version":"48b08f9de4ea66834164910296a74e2e16307d86efab9fd0a6d8a83196a0ba73","signature":"b98c5f764939a6f6b39b71893fb2a4f487ca09752995cb5fa34625a90ee823c0"},{"version":"94d17e69da52efde0ccd50071deed15638573f9a95c4fcec307468c15d56d459","signature":"42347c457142141ec7314ac31566f52b554f95e169a3baab6b91a17ba36791f3"},{"version":"115b8f02e70a74fd8adec44ad48f62c380febade10b7ace2c63bdd8fcc26e9ca","signature":"40106ef7012432e0f023d28d0b8b651ecc08546f293f6960087fcbdb48ded702"},{"version":"3c080849da00fe8bbe19638d6fc8e31b505f6c00a6a8b2be0ebff6592bfbb6e8","signature":"1f64ebdc20594ab2b0aab3a0af149147dfa31296037174c8bf978eecbee60e36"},{"version":"fa91d3314e51c3dc9a47e49282781062b0be2d85562e229158c2aa19e1032419","signature":"f33fdd0e7e847972318fdddd6c38f3e9c4449afe9d8156944f4f1298a8b035e9"},{"version":"9b4ff7795bc092a828f9c7a0435995f084ac8628512c56e02f7cc07f24e78303","signature":"1310af8ad5d8fe3f392fdd3fa56f15ce82519f7ee9dcef42066e7931ad0475d8"},{"version":"36227e4ab41ccb253b437ad5c5d178852b4e6790ae7c8d3e9643a61fe0d2235d","signature":"0efb6efd07fa0e9eb7bb01e403e621ca1cf2bd712fec27528a45851c6bec2d8e"},{"version":"a381f18066ed40be760ed5d6e8adea4be9c91efe1cabf990c82ce6302dde5a51","signature":"b8e47f5ffdb4c7cf6fe2153b3ab270517a2f0493ac0f56ae3c87b93ba9fa5ecb"},{"version":"ad4abafed19353a7a1a39e094d506c050c008a26647fc22aab0b3f10a91c8b02","signature":"2005f9ef25e7491deae66d9b627466eafffd2311f09a7d225404d2c76c55eff1"},{"version":"1a877e80e66f4b554650787a7c9e08e92795b5d67cb7d3cced3664efe5cb382c","signature":"7b54c642387d4c761da122065faebf47e133e5acb95287feedfab49560b0a24d"},{"version":"d93f7a0cdcf9bf4dcec12f8c97e60058449d36c854f3da8522a422e1985027a9","signature":"4e311a9d3c0f14200a2d9edc55bd7020b6673f68fbbd5ca690134aa9930665ec"},{"version":"8b1ed9a5915ac82fd127fcd63ccf1b6bee4cf34ce0c90856bde42d52a61b6e6b","signature":"3600a22617df83d4d7e927a2ea4572abe42e4ee9fbe8872e2cec912c8f0dc8c8"},{"version":"e4e881d804b344639bbc12cae92db1e749c6a9de1f9246919a4786edc169f819","signature":"890f9fef202bf20de2f1dff0daa5d4e322fbd60a202519676e9a7b386e8ed223"},{"version":"b79d5c8dced141083b1aa17e6c4234175de891a788a98ffc0031076d85ff14d2","signature":"402a163749dcbc280a68eb32fb932bed7fd231741c6eabb99330852b8924455c"},{"version":"4c032520a52e663b38aa8bbe9f6a84b82792198045af70e63e8bbee89a228334","signature":"09164c2f61bd65d2ea76b29c9077bbbc3db0deadb8abf30ef093e6b99d3880ad"},{"version":"65da082a2e75e13b2c4f73493a2b861c8767d7015d09ef992f37608bc01f8762","signature":"0e94e72a6bcf4e55f408a4084693363cfb13a8143a96eb95df43d69f50cacd4b"},{"version":"21bf6c7856290e1abfd1ba0230834ec6b0dbe975d6b169771a951fa0c736979b","signature":"ab760b792392ce8077b61074cce9c32f6052cebba6db08a5fea7ba51831bf42a"},{"version":"997733db45bf78d13b3a1915dd68ce78e9bd1c1d37d6361b5c9459cbaac68b9b","signature":"822a1bcffd88cd72df460143855c06cd1727ca4c3ca53ff5d69b2d71f5b97381"},{"version":"7d76a29ddc97720499405adf3ffdbc5c707717955987cad657760bc3cadff23b","signature":"32878e1003903901953b72e61b0e4f9c01c7d3d21f0db878bf999508e6f67407"},{"version":"473d9c5e375e3b8ab43f40b38807bbd414e929a865ce7b2ebd12c0ef8c939818","signature":"37f988deff733951a6292455352c0c048b74404fcca9012fa3bde03181335485"},{"version":"63f3cdc5baf4c6efaa0a0d7b1a5f90be2c9815628e125476abca17e7dbf7f75a","signature":"e44111d4386de9e584c0b5b40629e014325e7e60ce128c33092e0f6d7287f863"},{"version":"6c3a621188540af7ff1b5515c49cded29b6c50b403e1e28ea8305e8f06727675","signature":"dcb40fde0db088841ee0bf58fe4d2b662e1dd7ebe576f3facf5ee0b9290f47aa"},{"version":"ab47b80f84ce55ae9225fb74d5d8cd65f8711b042f9e9c07023ea761774b2855","signature":"ea5da833881aec7ebd7cc9b6c2fdfe40aabe616d6149cc745c473fca30f9daba"},{"version":"99c403270d896d09f8daa2300c3cb112475a10ae83246d9c10b7beb77d2e1ac3","signature":"3fc8be48c72293960408aec18600d104fb44b739d08de16648a5917a08ce06d9"},{"version":"0cf7cd444607b288f73a947ef49e2764846f5760fd3b9160d3b7a55f39962248","signature":"698e8fa61e1686e2b4422cfc37283baf3ebd7af1b0113f9217811108ddb265f7"},{"version":"257844624cf04aa66ad3edd5b2bd7505cc4194e6c4ea5478680bab49e6d4cd7a","signature":"07b3aa17f35946ec51aa56c2b23e30fe9eb9989f032dd6c5d4262db3b89dddb1"},{"version":"1d507ff41f41fffe18c638fdcc9539d305e1ebc39acdbf741af6360144f75e58","signature":"06db30a4b8bb5e1f1d108991e96f22984adf6aaeca888bccd44544f0fbba2a1a"},{"version":"646f036db4e5278e044b6059771c902480f481fb8b96579624cd5757ae2322f8","signature":"3f0326f6df90a4a9ee7ebcdfda8d7e4d8213414a4e64f85044af8265b9f88eab"},{"version":"48b4517dd9e757647a2e956505453d74a3cfd30989a90fd591baf1872ff4bbeb","signature":"74f8e4e8ef03773a84e2bfeeb141e1e0ccb3e7249fdd9b1cd064679d6c27701d"},{"version":"33f1307ebc6c86f1b043d9cfcb40d70087617e0269ad1fc1d6f99b4eb7422a5c","signature":"c0bd2b538492828f6fa29fc2d01290d2ab722c981360cd0826cf30c2c5e12a36"},{"version":"19d0e1a19eee5ad0cba7dcac52c5f77594b87976e685f403da26b1891b98f4cf","signature":"ceeefa74b1d0d5505ff0cbb851bd0b4826b5228323379c7c381ac95c7758e97c"},{"version":"84538c760565a8923681549e5946519b1ebc7ee822bcb82ddb9ce7e84c432039","signature":"5f8d91a8dbaf7e381318f5783b31148c64646b129aeedbffe9fe7d54d1decab7"},{"version":"2ca1fb2babb686ef5e7fdb4538cf4a64b49ffcc67ec7ef1a11d28c7ad4cc3f4c","signature":"25b8a26bc4ffce97093842320325af9a5f1d92122458b3702d7efe3800850b31"},{"version":"dcb84fbd15c0ae3b35bb6edf8fea8ae37d6a7cc8d1c1980177f7f6bb0b59fe84","signature":"eec54eaf4db88b0af4a71b6cf13a9a0507709c02876a9b7611cdea523dd30494"},{"version":"febc0e4bbd94f77b2b27dd656d9157f28ba9ed00045c81c2f187c787294018fc","signature":"874bb23c7e2f12a621cd036a133e93dd1b8dd65b5c4a61567f1f6b9f94d1ba11"},{"version":"d0f5a1341ab5e3e73d2ddb2b56d8e6b518f725cf0c5383a400eee403f4ad0b18","signature":"8b11f521b400a236c683015ac9f3acce50d2cc869db052d4a329a565658dc30c"},"2477220d733d169bb38fdff3ab6da80fe498c7cc1b7c93798650235d4192f28f",{"version":"9bdabd0c7274d9cd8009a717087ec17431228901cd1b475955cf053adf837813","signature":"d56a9c1ad9bfaa5b9aded4ebeed606295470271fcb9a137d7205f69bb62d1241"},"b0a66b71460581bda148485bd214bdcaaf706776cc8969601eb9e9ae73107833",{"version":"1d52fb958000fc6969d98ac163e441a60c66ec3f392a404d6e97fdaa3f8c6820","signature":"a1a4f94d8fae59bfa5c8800b9a4998b590429da469182e6dc16f0ae49f601187"},{"version":"988fad22b03f068fa42ba0f21ca11a30b144c5b6dc7fe378dd0d5993cb0ea021","signature":"337d47027050863e14974d13d062f1f85a9b2869a84bb91bbe24df61ec852a93"},{"version":"9057f8a4b24e7910fccc95ac7a735016359dd71ec8128dcf8cac42101d9dee93","signature":"8c197d892b58585c205a0a72c614c62679c100dc241d97e94746f7a7edef17c7"},{"version":"c7042d6a4a917a966b3a4be4cc3301f74e128275f19de80f76ee7f56b4263612","signature":"ce3df779c79bb40ccd33c165d2ebb31eae6757c07dff4f90b7f53fdb5be7faa9"},"56fd389ea239c6259f7ee1eaa9f82f0441c6566da29d670f5ec9df6b10b55d6e",{"version":"4726730040de266dee3f85ff088f7e86c81d34f8e89a3658ec668d2451cd8b20","signature":"1ab2b8fa37eae578b30743deb1fe446bef44135e76a66eb438d772d7ee309ad9"},{"version":"4b80dab6517f99557255bc83e0e10a6b725475431fc17a8b64969f336bf4457d","signature":"902fe9480631261d8d673e0e79422e0b1c98d6bfb340fd71009d58c10f9f53ba"},{"version":"3dc7bca9a9ce7368cce1cfb457f560219e5e8503f5eeb84915c4d7278be08f6a","signature":"70552cd64daeb1bc28e5570f5f93139fbe4f61be463fac96009373eac9446730"},{"version":"c5b3a45900654806aba3fbf3a08a360d828b715f5c5e335e8ddafb8804cce155","signature":"25793cd87b7fe511a90c2ec2157d57d63b80b13017d78ec7a19e38337be3d346"},{"version":"dce6e3c85d41d94ffd8d0783f89c77b87847165925b0ddf364198764b97e91c7","signature":"d373286ef4e09d52e8133bff60629f3542c3cf19fe7061e2ad59780a38ba3e9c"},{"version":"bf02b499dd11bedd635dc31cad194bc3064ea4c50472130312b6c515192cda50","signature":"9dbdbc2fe06b80731789940ac96326bec3ee71187cee735d167bb63b29fd70cd"},{"version":"617e06a025e697927b5b7eb5883a7623d2f6a828d63703b091c9fe471ddee059","signature":"0ba922d824b37dcc25403fc3a5539572a2398cb4f487d56349fcf82bd5fbc83a"},{"version":"0a42ac2fb0c4277db9f4160ffdd31c282ce37f5e9a6ccdc1cb68248a138ccf62","signature":"30feb8c1f83bc852270ec8a61ae147003ed44475712aca456a8da7a12ec8abf3"},{"version":"79bb7b5d5b1d4d828c5b219b435fb902e15e608438532e31c315c1005026329f","signature":"2eac28477656b0ad5d7a9ba7e7b88b8def3920325eb83998a1999aa899f523e5"},{"version":"e3747823338ce60aa21da605a81c1326e99fa6b100cdc24559d8fe9a3d1b9c69","signature":"b99fbc2ce6bfccd65cde53f233ba84889ff5326b863467f73548c26dd328540e"},"2a2584577164a5f7520b29f83472b5125fde512794ff5ec5b2226eed0f098f6a",{"version":"e82b189efd838b8a75d0f1245dc9d1858a2b2cad72f370cbfe7afadcc4f217ef","signature":"24d1c11bf9cf822144bf7ca7bab7e09863c167d7122f75f37181e6454307c44a"},{"version":"668239434ac31cf7da33bc5fa67379c5cc5c95cea8348d9e3162d5036c88b817","signature":"db4e279069f6fb0b6302d5b5aae6ec8bcbecffcd76da901ad48b47476f498668"},{"version":"10589b83334aea024c9df9e20665007660d16754c0df36f55747d212aa5347dc","signature":"afab60e4e1a70240501405654aded882cf2835ef1de56706f767b812dce7fa43"},{"version":"30943bf0f29e0393f93a101b2137e6fad92cbf5e97bf76bf38de424c95572f7f","signature":"4dc3d3abe32ad39c557114ea9ba72d79f40997e02e75ecdf0b81e97b75b136ca"},"a17c3792bfb09725c7024bd1e5cf1e7496136d42a3ef83deb3697ff92edb7af6",{"version":"8ae5613070155b80d7af418318d336cd459236a45877680b04dbdfa442febc0d","signature":"198cb73d5dd5b735d203c6677716ba311376768b6cff39a22150e79dea827168"},{"version":"22051056f2182b72bf226c5a7d5a80f0108aba6881cdb51fee6665ce462c1d26","signature":"69984b8b25cd8e7c40c21ffcd0f1e2212c2a59435f16ce886888fb5becef1b8a"},{"version":"889fbbf9ea80ded4dc038d353dba84a398434c58c22fb1fe2bbf4de3ed181e38","signature":"322916fa199c1182ea2faca1b6d488cb6ec497f7c48491623c440815c1033e97"},"c201951b6841ed5d1303eed429a901c9a0e3eb14cc8e5948bb0e56d12efb615b","bb89661a4eb77298a7369fb43414990eb6041032f9138e9f75798832d1bc4441","660bc470b457b104a4d9d80159b1d348d6026025f42f7690a4c184d2b66ae623","2732c099efc3745e4ba388f0d56b3d3767fb0c7d43aeefe1b2613f09b90c7f31",{"version":"a4a4f6a3a827bd8674d5b4230d6ee7ebabda6d637e76dd6667b524e61df71300","signature":"3d3e8d8e570c2d70ad678766c40a35c535b5fa76cf7bc73c6333c54823b0b752"},"0eb9fd5052728a548116c94d7954723095c9a292edf83f9d9d1ea45963d0f8e8",{"version":"846a5de17aefd2dee5ce18c1508c77fbc455c48d383f8d629bd93496babebef5","signature":"605e683a1f1fd79fa0714eee7c91c2ecac343967b1559eafdea79af91b642173"},"54b7abaab7423e9c952f1aea2eaab68266c85565f21c656c9de31d80a38a7282","040fd3a93bc770d200ecfbd83cab66b22e0c104fdaed29daea5f1ecc05f8dc74",{"version":"dadce1132e3935217c59e21fb2f629905b2a5e4f0a6f65854cef1a9714ef3dd7","signature":"f4209dba9f0d4739fde6fb305d4ec52da611e8221dbcb2603a569e61749f4b7e"},"fb59f620807e4d1dbc5cc95639262f89395c1d20fcab03efc323a221b07bca43","922d4ab256e7db3a63b52e0e4a8f2c7019491b58b0961f297c5b613a2783d970","85b6daaabfe2c275b7013d13df14031bbaf623357e3746bed9ca1afaab46bedf","2ccb0e02fb165ef588673868e5e1b78d7f2b8f22cd463c8e015f9c9fcb8f3eb6","f594f5af79c5a5c75dd9e04e39314d6a193b2a18369d57195ae3675ea25925e2","8b76ee888686669a7bb8b6c5ebeb72e5a7bc0e7263152ff5daa0c486d34d870d","1cad8db035a510050fb929577f148153ef41f0fd2773df0149cbec01556c7050","43b3564d0973387eb4877c5e3a996883331e8c73a2a675aa4a708afcb4f12521","7ef1eabb4e5ec071c4aca406d80abf528ea9a15b968dbc3e275d8453c1924003","ee5d9091cc397c259f406c229cecbb893af5548944ec9b7c3dbf0daa9cced037","cdaa28c764ef022a285afecf6a4b1dc9287843dd2b45c703056e880467257cb6","57a452c40651b548c85a342f28241ee37a84e1967630ed318e515ee21d4c8ee5","e0611572c6aa5500e703c4332eecb1235d618fc91315bd39d4ccedbe3a6fe710","be4f9c6ac98f48266c88db65806e5dc4084170a229c6205159a53f0507c4c666","bb299294f09f4dd9932c8ad94bb16d7d062353d172f72e0a99d7226f3ec5b659","83962d24b7752d8aa205f38ab58ba7fd4464017403e4eb8d2481234248a626d5","260da6fd223535a3d498a1af94f4249a3e67d28e56d7648dcc72e069911cc2c8","d1f0095428cf1de664d9594850b63567b7ca7325fedac6dfd425130041b420a2","6c430c13fabc220d5e2053a81d1c993ba4341c9f461d98270b1ff503c8c48030","0740a1887c4e1c1c71fff7925a3feef85a6ab4d5b2ee4c24fb84357583da3402","a8124c363cdc2f4bbd08a4283e216b1d1fd358ddbdc658afd6fa510ed16740dd","1c6cecc8cf9d39e2501347175b1be54b8dca76fed82b87f9fa0b8b7268cb5b61","cdc15c17185913bb5b56f5f240728e7096ca133d78464876e98b6d38185c6d19","58fc9b0a79e29fa867e347981ecbef6ef52ffacfc848be358cdc41a5d9e133c8","b3bb55f630f4d3113bd13408c49d2d7a2ea1ddf96061cb5952d0bf778d491eba","f10912d31d9010c44d3aa471b34e77be99329c64f69675b00104a21e4cc50470","20e5482e50895255d12953211f19710988ab278b9f342db385f7985d34ec02bf",{"version":"d4bc691857a8aa60b09cebdfda30afb2eb8ab13d7fa8c29bcd93082803223eff","signature":"e847e443a0d4a2648051133817a02200e0b9c4b2f616a992c7ca8325a7c71d24"},"75304d5f2ccf463507df23f4b8066e7e758225a1a36d6ebe83c1b714da6c2ac5","049398af73a373ead19379fd1da4809d18bf6cf7123e44116c1491ad58d5acf0","9d8c851e30a432fadf9709f5b7802b9346d7c4aa3db0f25907669f98178bdd6e","4eaf53474be1f3e9182de786375b87893f47081e491b45321cac3a8427250f5c","79804619db6df7144de589d927d3c861d5e037ee91c9d55590ac14f927e371ac","5f6104030d0571c22ccb732c1ff1ee3f9ce38d54d0157fb895fd99d62222103c","5a64c9b17d19f3d6f73be414c83c4b4a37bb1898fc8552e2d63f71f2f6904724","a17ffb4463a3144d95960125ae3ea541a055bfa1494a908dc8688a21a0a58216","9428fa53b4d4910ec04a42be6e8bc915242800381bf5782013965ae8b4f6e3ae","dc9928627dc636458117a0f2888131ab222a160ed58d4942ebdba75d32beb69b","632cf9f97223c29fa65cf7eb81ba15d2d21a851dff8f5e0921b7e428a837af84","5e789cac338582e7c1522ce9fde6b2c4cfd626989fb5dedbdcbd1fdb7028e82d","1ccf807c1e706cf662a2475227e453da3a57cd9f6c21aaa843d3358d619e78f4","dad7f9496da68b21150b690b1bf00dcb5ddc05f575f082b9f79a0316e3cf0923","356173560bdd68ce8d6f93eb359829b4b93fdc9799369052964544c0c0551303","0ef5075c8c118e4b9f6d6e61164849a3da33cfa8a6d709b9e76b9e187cc502d7","30b18dfd86c195a4e504a9c7cab74e253cc9a81e5150a1e078a60116f8413a7b","eb572a47bd52202d9046b74cd08e11b817a09d7da9d411221c209605b53d50bf","507d41570c8cad3489d272cba504fd33baa7d60903086b2b1ea6a44a32cab870","4f4dad224f28ae73a636eaca502d71b374a4095d5a3f540f66cb56a688e067f1","4139b027a8724e900dc887009ac6df809cc5f6a5fa5f58d2d684a6f6d96ff245","09e6acd94660ad0acf93ee0cce96ba7c540b67bd65ab5b043decb5ad9976c00d","f90eaf7c39bcd43cca5e307991878da4c86947ade4c9c999534c75647e9356cf","5b9a0c2f29da015a2b552e9890b67dd33cbe9fd15c69384b8eb2e65b0be19320","01101a850ef55e575e5c27672a5788b954101ab3ee2009dc5d94820fd9aeda1d","6c26420749d17833efe84a50d61db06fbd4082e497e65ae2e4b82f2d23476279","d6c4e7806d89a5b3fe00eb53b08ae06a8d26226bc9cfbec2758d4db28462d0c4","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",{"version":"cadeb2c96f1c964d7e49c0f17d6805e1b4dee62f0862c49bb178dae6ab277e8e","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"eb64a68249f1ee45e496a23cd0be8fe8c84aecb4c038b86d4abcc765c5ba115e","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"3b0a56d056d81a011e484b9c05d5e430711aaecd561a788bad1d0498aad782c7","impliedFormat":99},{"version":"3354286ef917d22c72e0c830324062f950134d8882e9ea57ad6ade3d8ad943cf","impliedFormat":99},{"version":"3dedc468e9b0ed804c0226482e344bd769417f834988af838d814504af81cba6","impliedFormat":99},{"version":"ac3d263474022e9a14c43f588f485d549641d839b159ecc971978b90f34bdf6b","impliedFormat":99},{"version":"3b89216a7e38a454985ad17bb2ff85792837dc812f2a89fa5f60ad0a2e216fa7","impliedFormat":99},{"version":"10073cdcf56982064c5337787cc59b79586131e1b28c106ede5bff362f912b70","impliedFormat":99},{"version":"82179358c2d9d7347f1602dc9300039a2250e483137b38ebf31d4d2e5519c181","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"091546ac9077cddcd7b9479cc2e0c677238bf13e39eab4b13e75046c3328df93","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"727858fb893b87791876dee5e3cd4c187a721d2de516fd19d3d00dc6e8a894b3","impliedFormat":99},{"version":"5bfaa2ee33e63a1b17b08dbefd7a3c42d1e0f914e52aca5bef679b420bd7a07c","impliedFormat":99},{"version":"7d5c6cc5d537c47c7723a1fe76411b99373eb55c487045dfd076c1956e87389a","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"a86701e56b10a6d1ef9b2ecaeedbab94ed7b957a646cd71fd09d02b323c6d3d7","impliedFormat":99},{"version":"b3f0791a73b6521da68107c5ba1bfed4bc21ff7099b892700fd65670e88ef6ee","impliedFormat":99},{"version":"d0411dddbef50f9ad568ee9d24b108153bcb8f0db1094de6dfbadf02feb3aa70","impliedFormat":99},{"version":"25249ca5fe64ca60d7bfb7fbbf0cb084324853b03a265dbbbc45fb4949de7567","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"31d5fb0aeb0368909fbe8cd9b893c16350aa94d48a2f909fdd393982ceb4814d","affectsGlobalScope":true,"impliedFormat":99},{"version":"90fe5875e2c7519711442683a9489416819c6cec8d395e48ff568e94254533e7","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"72a863bc6c0fc7a6263d8e07279167d86467a3869b5f002b1df6eaf5000ccc7b","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"42f0f7e74d73ae5873ed666373e09a367d62232ca378677094d0dc06020d6e00","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"ebb9b9fa684d70aef64614a59b7582f46f4982139b8b632b911ef98e10c4d117","impliedFormat":99},{"version":"2c34caeb0e7809e93a2baa06872c49707b1c96e229e1061c6b9ee364607f563f","signature":"b39a7180afe792f0975be5e4841a05ccf9365eca0b213207c44829c618b565be"},{"version":"71892ca6a7b905a5ccaa732d003ad552e07a76ec957e2cc9717c0d7458f57de8","signature":"45d8040117fbf4ff6da3ca820329f98fbfb6a53ea90f85d477d069273d3cddee"},{"version":"fb0577e44999cbc18e46e3721e3f11e88bd4326980b051f9c33f87b3f0d88f33","signature":"fa6afb58ab76f7ef115e4983d5aff7658d7810179b54c274e2ca2ce7f5cc3f32"},{"version":"79d4a7d8e85e688d78492c08447bda92c599a5107de9aa9d13c30f89cdec87ad","signature":"480421dd7de43f42733155a567ccbc4dfa6c1732c77040a0d9db71d953818608"},{"version":"1660de86abdd37f18e89eff8e0f600c4cefe87240a64a5cef9f130558475e8c7","signature":"b7c3f2ff0f5325a53be88fcd977b061496e389f44d24d1a03655d5fd874e242a"},"027eaaf0f2bc7bbed683ca261cffef4e79f685136e82589aa2e4aa1e71937327","aafda1691dfce524edfaa12dd55894c5fa997b5c5abe0c18e9adb705f16aea62","0ce71b3841e0e571e05e366d6bdd611af68ee92ea40b04dfed14e0dd606ae670","168d5d85d4353a63262bf6ae8e8cd75b0aeca9242e09e53d09d2e6b9ff8f2cd1",{"version":"11ee59ce5d4c89e0d36107e691fdd0bcc5667afd19cde6ab67593885ee208b2f","signature":"546b209a3383cd5fa26c8cf7f09562fac022df50d11c1404cebac79c8e740afb"},{"version":"f5e9e3d60d20a07c1c9d998c6dd7d3f730503e60a051904fbbec305e47c4e463","signature":"793eb6715b193f0d493b0dcb6d9fce6fcdbfd2a25b8775e4d67ce57b6fc7d961"},{"version":"5b31250fba28c3d8926c5e9eda10c0e16077bee0877378c63bf8e88ca1c6b61f","signature":"577adbd74f1ed18690df6a408fde38fe75ded8c7d2830ab85df8574a2b259b0e"},{"version":"e8507b835a0daa04898ec215b51d8007c22254e01e8bdb3e857e21dc9dfe3d56","signature":"bbaf8153e927a6a1d2ac8fe6acbf70e8ebe045e06448ad6d177292175d2c6a51"},{"version":"c2c0a7372114e80e57301410ee6dc29793427de944026dc5b28e6c2bbbd26a27","signature":"077bcbe5f319d5aaad4cd08b0d19e866e113ea9cce2b23f8fbce68aa9d119153"},{"version":"bc668b916baa263d2c042498cb8870cd1a7a48959fa121c5ba5c823ff0fa0b9f","signature":"6243ebb0468ef522293c85cc4ae6b18778d3857cd473bd07678b64fc0e1fc402"},"3bb648f566e49849b20bbef6a3b737a875ef4e774b33f94f53dfdbd9deac403f","3d8e5168a19d52d77dc69b6785a069ed2f14000e24eb275dd5846fd31e473801","eb76714199edf4f96ba125503ae4114b4d557a55ad0fc85ec5bc30f09310bbaf",{"version":"7a8fb3b4375a625757be88c9ed06d1ac676fab9032f49ce8fd6829f48a2fb590","signature":"23c3a24df508a4adac15b3ad68a90014f2669edb1d83edcb9b87d2c248e69c2c"},"e51177d723e64eb327102e10399d61ed9e89bddcf69260ac0600ca0a39b3f33b",{"version":"0c18b3b8896b41851780e59b0f140edc45822b3b1d419a83e9dae46822eae7d9","signature":"f92af80279b5cf0dd82210e1aeea133e581de5e7acc21afbf6b96da1cc3f2bd7"},"4143a3ab0eafd782c745bb4ce3b5ca2be6ca96cba8d23dc69755dcc348e319c7","6efb415baab05a5830113c471acf3c82cae9b9560036644f937cb34924921dcb","de5e7d2f6e5328b18b5dc3a39f3ca67df73577cee8b5ca8af48759f1264dca01","f24c8d553c4178e3bca9e6988140a5b98064a1f8820b53adc58803b6317c84c7","043dc788a719c8ead6f63a3f38809be150dfab433068fd674cbe491c7b0af2fd","99af15d20e90e9b50cdcfca19a43ae519039ba1c747da2e3f1eb0dff85345180","af62c3ac94d0a249d8c26a6a285b86557617dfb08f8f665cd58505232f74f78c","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"root":[[58,83],[125,129],[235,261],[264,267],[281,770],[818,850]],"options":{"allowSyntheticDefaultImports":true,"alwaysStrict":true,"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"exactOptionalPropertyTypes":false,"module":99,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitReturns":true,"noImplicitThis":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":false,"noUnusedParameters":false,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"strictBindCallApply":true,"strictFunctionTypes":true,"strictNullChecks":true,"strictPropertyInitialization":true,"target":9,"tsBuildInfoFile":"./tsconfig.tsbuildinfo","useDefineForClassFields":true,"verbatimModuleSyntax":true},"referencedMap":[[797,1],[234,2],[800,3],[798,1],[263,4],[262,1],[180,5],[181,5],[182,6],[135,7],[183,8],[184,9],[185,10],[130,1],[133,11],[131,1],[132,1],[186,12],[187,13],[188,14],[189,15],[190,16],[191,17],[192,17],[193,18],[194,19],[195,20],[196,21],[136,1],[134,1],[197,22],[198,23],[199,24],[233,25],[200,26],[201,1],[202,27],[203,28],[204,29],[205,30],[206,31],[207,32],[208,33],[209,34],[210,35],[211,35],[212,36],[213,1],[214,37],[215,38],[217,39],[216,40],[218,41],[219,42],[220,43],[221,44],[222,45],[223,46],[224,47],[225,48],[226,49],[227,50],[228,51],[229,52],[230,53],[137,1],[138,1],[139,1],[177,54],[178,1],[179,1],[231,55],[232,56],[801,57],[807,58],[808,59],[806,1],[771,1],[780,60],[779,61],[802,60],[787,62],[789,63],[788,64],[795,1],[778,65],[772,66],[774,67],[776,68],[775,1],[777,66],[773,1],[799,1],[814,69],[816,70],[815,71],[813,72],[812,1],[803,1],[796,1],[56,1],[57,1],[11,1],[10,1],[2,1],[12,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[3,1],[20,1],[21,1],[4,1],[22,1],[26,1],[23,1],[24,1],[25,1],[27,1],[28,1],[29,1],[5,1],[30,1],[31,1],[32,1],[33,1],[6,1],[37,1],[34,1],[35,1],[36,1],[38,1],[7,1],[39,1],[44,1],[45,1],[40,1],[41,1],[42,1],[43,1],[8,1],[49,1],[46,1],[47,1],[48,1],[50,1],[9,1],[51,1],[52,1],[53,1],[55,1],[54,1],[1,1],[155,73],[165,74],[154,73],[175,75],[146,76],[145,77],[174,2],[168,78],[173,79],[148,80],[162,81],[147,82],[171,83],[143,84],[142,2],[172,85],[144,86],[149,87],[150,1],[153,87],[140,1],[176,88],[166,89],[157,90],[158,91],[160,92],[156,93],[159,94],[169,2],[151,95],[152,96],[161,97],[141,98],[164,89],[163,87],[167,1],[170,99],[783,100],[786,101],[784,100],[782,1],[785,102],[804,103],[794,104],[790,105],[791,62],[810,106],[805,107],[792,108],[809,109],[781,1],[793,110],[817,111],[811,112],[117,113],[86,1],[104,114],[116,115],[115,116],[85,117],[124,118],[87,1],[105,119],[114,120],[91,121],[102,122],[109,123],[106,124],[89,125],[88,126],[101,127],[92,128],[108,129],[110,130],[111,131],[112,131],[113,132],[118,1],[84,1],[119,131],[120,133],[94,134],[95,134],[96,134],[103,135],[107,136],[93,137],[121,138],[122,139],[97,1],[90,140],[98,141],[99,142],[100,143],[123,122],[272,1],[277,1],[270,144],[275,1],[273,144],[271,145],[274,1],[269,1],[280,146],[279,1],[276,1],[268,1],[278,1],[750,1],[751,1],[752,1],[753,1],[364,147],[365,148],[362,149],[756,1],[757,1],[758,1],[759,1],[760,1],[761,1],[762,1],[763,1],[764,1],[765,1],[766,1],[767,1],[754,1],[755,1],[768,1],[769,1],[770,1],[705,150],[706,151],[704,152],[703,1],[663,1],[698,153],[378,154],[664,155],[701,1],[665,1],[666,1],[667,1],[668,1],[671,156],[672,157],[673,158],[674,1],[675,1],[676,1],[677,1],[678,1],[679,1],[670,157],[680,157],[681,1],[687,159],[682,1],[683,1],[684,1],[685,160],[686,1],[702,161],[688,1],[370,162],[689,163],[690,164],[691,165],[692,164],[694,166],[695,164],[818,167],[819,1],[821,168],[820,169],[586,170],[822,171],[550,172],[551,169],[552,173],[553,173],[554,173],[555,173],[556,173],[557,173],[558,173],[559,173],[560,173],[561,173],[562,173],[563,173],[564,173],[565,173],[566,173],[567,173],[568,173],[569,173],[570,173],[571,173],[572,173],[573,173],[574,173],[575,173],[576,173],[577,173],[578,173],[579,173],[580,173],[582,173],[581,173],[583,173],[584,1],[585,174],[549,169],[482,175],[441,176],[480,177],[442,169],[444,178],[445,178],[446,178],[447,178],[448,178],[449,178],[823,178],[450,178],[451,178],[452,178],[453,178],[454,178],[455,179],[824,178],[458,180],[460,181],[461,182],[462,178],[825,178],[463,183],[464,184],[466,178],[465,178],[467,181],[468,178],[469,178],[470,179],[471,178],[472,178],[473,178],[474,178],[475,178],[476,178],[826,178],[478,182],[477,178],[479,178],[456,176],[481,185],[457,186],[440,1],[459,187],[443,188],[525,189],[827,190],[524,191],[486,169],[488,192],[487,193],[489,193],[490,193],[491,193],[492,193],[493,193],[494,193],[495,193],[496,193],[497,193],[498,194],[499,193],[500,193],[501,193],[502,193],[503,193],[504,193],[505,193],[506,193],[507,193],[508,193],[509,193],[510,193],[511,193],[512,193],[513,193],[514,193],[515,193],[516,193],[517,193],[518,193],[519,193],[520,193],[521,193],[522,193],[523,193],[485,195],[483,1],[484,169],[644,196],[623,169],[625,197],[626,197],[627,197],[628,197],[629,197],[630,197],[631,197],[632,197],[633,197],[634,197],[635,197],[636,197],[637,197],[638,197],[639,197],[640,197],[641,197],[642,197],[643,198],[624,169],[438,199],[437,200],[380,201],[828,201],[381,201],[382,201],[383,201],[385,202],[829,201],[830,201],[384,201],[392,203],[389,204],[391,205],[390,206],[386,207],[387,169],[388,206],[393,201],[394,201],[401,208],[395,206],[396,206],[397,1],[399,209],[398,206],[400,169],[402,201],[403,201],[404,210],[405,201],[406,210],[416,211],[407,1],[409,212],[411,213],[408,206],[412,169],[413,214],[414,1],[410,1],[415,215],[417,210],[418,201],[419,210],[426,216],[422,217],[423,218],[421,219],[425,220],[420,169],[424,1],[427,201],[428,201],[429,201],[831,201],[430,201],[431,201],[432,201],[433,201],[435,201],[434,201],[439,221],[369,1],[436,222],[379,169],[646,223],[647,169],[648,224],[649,224],[650,224],[652,225],[653,224],[654,224],[655,224],[657,224],[658,224],[659,224],[660,224],[656,224],[661,226],[651,223],[645,169],[662,227],[832,228],[527,229],[528,169],[529,230],[531,231],[532,231],[533,231],[534,231],[535,231],[536,231],[537,230],[538,231],[539,231],[540,231],[541,231],[542,230],[543,231],[544,231],[545,231],[546,231],[548,232],[530,229],[547,1],[526,169],[588,233],[589,169],[590,234],[591,234],[592,234],[593,234],[594,234],[595,234],[596,234],[597,234],[598,234],[599,234],[600,234],[601,234],[602,234],[603,234],[604,234],[605,234],[606,234],[607,234],[608,234],[609,234],[610,234],[611,234],[612,234],[613,234],[614,234],[615,234],[616,234],[617,234],[618,234],[619,234],[620,234],[622,235],[621,233],[587,169],[377,236],[373,237],[375,238],[374,239],[376,240],[372,241],[693,1],[699,242],[700,243],[696,244],[371,1],[669,1],[697,245],[367,246],[368,247],[366,1],[329,248],[327,249],[328,249],[326,249],[325,1],[723,250],[722,251],[721,252],[714,1],[720,253],[715,254],[716,1],[717,255],[718,256],[719,257],[713,258],[707,1],[712,259],[709,260],[710,1],[708,1],[711,1],[293,261],[833,262],[292,263],[291,264],[290,264],[289,264],[281,158],[297,265],[300,266],[299,267],[298,268],[285,269],[267,270],[834,271],[282,272],[283,270],[284,273],[266,268],[250,274],[249,275],[248,276],[244,277],[246,278],[245,279],[247,280],[264,281],[265,282],[253,283],[252,284],[251,285],[254,286],[258,287],[259,288],[256,289],[257,290],[255,291],[260,288],[261,292],[243,1],[286,264],[296,293],[295,294],[287,295],[288,296],[294,297],[345,1],[351,298],[348,299],[349,300],[352,301],[346,1],[350,1],[347,1],[356,302],[357,303],[354,304],[355,1],[353,1],[343,305],[344,306],[321,307],[340,308],[322,307],[323,309],[341,310],[324,309],[342,311],[320,1],[358,312],[317,313],[319,314],[315,315],[316,316],[318,317],[314,318],[312,1],[313,319],[311,320],[310,1],[307,321],[309,322],[306,323],[305,324],[308,325],[304,1],[303,1],[749,326],[359,327],[361,328],[360,329],[302,330],[363,331],[301,332],[725,311],[736,333],[727,334],[728,334],[729,334],[730,334],[731,334],[732,334],[733,334],[734,334],[735,334],[726,1],[737,335],[724,158],[339,336],[331,337],[332,337],[333,337],[334,337],[335,337],[336,337],[337,337],[338,338],[330,158],[845,339],[843,340],[836,341],[837,341],[838,341],[839,341],[840,341],[841,341],[842,341],[835,1],[844,342],[127,343],[81,344],[83,345],[125,346],[82,1],[126,344],[79,347],[74,348],[72,349],[75,350],[77,351],[78,350],[73,349],[128,352],[65,349],[70,353],[64,354],[71,349],[80,355],[67,356],[66,357],[69,358],[68,359],[847,1],[76,1],[848,1],[849,1],[850,1],[846,1],[242,360],[241,361],[237,362],[238,363],[239,362],[236,364],[240,365],[235,366],[129,367],[60,331],[61,1],[58,1],[63,367],[59,268],[62,368],[740,369],[746,370],[738,158],[741,369],[743,371],[748,372],[744,373],[742,374],[745,369],[747,375],[739,1]],"affectedFilesPendingEmit":[[364,51],[365,51],[362,51],[756,51],[757,51],[758,51],[759,51],[760,51],[761,51],[762,51],[763,51],[764,51],[765,51],[766,51],[767,51],[754,51],[755,51],[768,51],[769,51],[770,51],[705,51],[706,51],[704,51],[703,51],[663,51],[698,51],[378,51],[664,51],[701,51],[665,51],[666,51],[667,51],[668,51],[671,51],[672,51],[673,51],[674,51],[675,51],[676,51],[677,51],[678,51],[679,51],[670,51],[680,51],[681,51],[687,51],[682,51],[683,51],[684,51],[685,51],[686,51],[702,51],[688,51],[370,51],[689,51],[690,51],[691,51],[692,51],[694,51],[695,51],[818,51],[819,51],[821,51],[820,51],[586,51],[822,51],[550,51],[551,51],[552,51],[553,51],[554,51],[555,51],[556,51],[557,51],[558,51],[559,51],[560,51],[561,51],[562,51],[563,51],[564,51],[565,51],[566,51],[567,51],[568,51],[569,51],[570,51],[571,51],[572,51],[573,51],[574,51],[575,51],[576,51],[577,51],[578,51],[579,51],[580,51],[582,51],[581,51],[583,51],[584,51],[585,51],[549,51],[482,51],[441,51],[480,51],[442,51],[444,51],[445,51],[446,51],[447,51],[448,51],[449,51],[823,51],[450,51],[451,51],[452,51],[453,51],[454,51],[455,51],[824,51],[458,51],[460,51],[461,51],[462,51],[825,51],[463,51],[464,51],[466,51],[465,51],[467,51],[468,51],[469,51],[470,51],[471,51],[472,51],[473,51],[474,51],[475,51],[476,51],[826,51],[478,51],[477,51],[479,51],[456,51],[481,51],[457,51],[440,51],[459,51],[443,51],[525,51],[827,51],[524,51],[486,51],[488,51],[487,51],[489,51],[490,51],[491,51],[492,51],[493,51],[494,51],[495,51],[496,51],[497,51],[498,51],[499,51],[500,51],[501,51],[502,51],[503,51],[504,51],[505,51],[506,51],[507,51],[508,51],[509,51],[510,51],[511,51],[512,51],[513,51],[514,51],[515,51],[516,51],[517,51],[518,51],[519,51],[520,51],[521,51],[522,51],[523,51],[485,51],[483,51],[484,51],[644,51],[623,51],[625,51],[626,51],[627,51],[628,51],[629,51],[630,51],[631,51],[632,51],[633,51],[634,51],[635,51],[636,51],[637,51],[638,51],[639,51],[640,51],[641,51],[642,51],[643,51],[624,51],[438,51],[437,51],[380,51],[828,51],[381,51],[382,51],[383,51],[385,51],[829,51],[830,51],[384,51],[392,51],[389,51],[391,51],[390,51],[386,51],[387,51],[388,51],[393,51],[394,51],[401,51],[395,51],[396,51],[397,51],[399,51],[398,51],[400,51],[402,51],[403,51],[404,51],[405,51],[406,51],[416,51],[407,51],[409,51],[411,51],[408,51],[412,51],[413,51],[414,51],[410,51],[415,51],[417,51],[418,51],[419,51],[426,51],[422,51],[423,51],[421,51],[425,51],[420,51],[424,51],[427,51],[428,51],[429,51],[831,51],[430,51],[431,51],[432,51],[433,51],[435,51],[434,51],[439,51],[369,51],[436,51],[379,51],[646,51],[647,51],[648,51],[649,51],[650,51],[652,51],[653,51],[654,51],[655,51],[657,51],[658,51],[659,51],[660,51],[656,51],[661,51],[651,51],[645,51],[662,51],[832,51],[527,51],[528,51],[529,51],[531,51],[532,51],[533,51],[534,51],[535,51],[536,51],[537,51],[538,51],[539,51],[540,51],[541,51],[542,51],[543,51],[544,51],[545,51],[546,51],[548,51],[530,51],[547,51],[526,51],[588,51],[589,51],[590,51],[591,51],[592,51],[593,51],[594,51],[595,51],[596,51],[597,51],[598,51],[599,51],[600,51],[601,51],[602,51],[603,51],[604,51],[605,51],[606,51],[607,51],[608,51],[609,51],[610,51],[611,51],[612,51],[613,51],[614,51],[615,51],[616,51],[617,51],[618,51],[619,51],[620,51],[622,51],[621,51],[587,51],[377,51],[373,51],[375,51],[374,51],[376,51],[372,51],[693,51],[699,51],[700,51],[696,51],[371,51],[669,51],[697,51],[367,51],[368,51],[366,51],[329,51],[327,51],[328,51],[326,51],[325,51],[723,51],[722,51],[721,51],[714,51],[720,51],[715,51],[716,51],[717,51],[718,51],[719,51],[713,51],[707,51],[712,51],[709,51],[710,51],[708,51],[711,51],[293,51],[833,51],[292,51],[291,51],[290,51],[289,51],[281,51],[297,51],[300,51],[299,51],[298,51],[285,51],[267,51],[834,51],[282,51],[283,51],[284,51],[266,51],[250,51],[249,51],[248,51],[244,51],[246,51],[245,51],[247,51],[264,51],[265,51],[253,51],[252,51],[251,51],[254,51],[258,51],[259,51],[256,51],[257,51],[255,51],[260,51],[261,51],[243,51],[286,51],[296,51],[295,51],[287,51],[288,51],[294,51],[345,51],[351,51],[348,51],[349,51],[352,51],[346,51],[350,51],[347,51],[356,51],[357,51],[354,51],[355,51],[353,51],[343,51],[344,51],[321,51],[340,51],[322,51],[323,51],[341,51],[324,51],[342,51],[320,51],[358,51],[317,51],[319,51],[315,51],[316,51],[318,51],[314,51],[312,51],[313,51],[311,51],[310,51],[307,51],[309,51],[306,51],[305,51],[308,51],[304,51],[303,51],[749,51],[359,51],[361,51],[360,51],[302,51],[363,51],[301,51],[725,51],[736,51],[727,51],[728,51],[729,51],[730,51],[731,51],[732,51],[733,51],[734,51],[735,51],[726,51],[737,51],[724,51],[339,51],[331,51],[332,51],[333,51],[334,51],[335,51],[336,51],[337,51],[338,51],[330,51],[845,51],[843,51],[836,51],[837,51],[838,51],[839,51],[840,51],[841,51],[842,51],[835,51],[844,51],[127,51],[81,51],[83,51],[125,51],[82,51],[126,51],[79,51],[74,51],[72,51],[75,51],[77,51],[78,51],[73,51],[128,51],[65,51],[70,51],[64,51],[71,51],[80,51],[67,51],[66,51],[69,51],[68,51],[847,51],[76,51],[848,51],[849,51],[850,51],[846,51],[242,51],[241,51],[237,51],[238,51],[239,51],[236,51],[240,51],[235,51],[129,51],[60,51],[61,51],[58,51],[63,51],[59,51],[62,51],[740,51],[746,51],[738,51],[741,51],[743,51],[748,51],[744,51],[742,51],[745,51],[747,51],[739,51]],"emitSignatures":[58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,125,126,127,128,129,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,264,265,266,267,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850],"version":"5.9.3"} \ No newline at end of file diff --git a/packages/ui/src/features/ai/services/__tests__/operation-executor.test.ts b/packages/ui/src/features/ai/services/__tests__/operation-executor.test.ts index a32573ce..794ff34c 100644 --- a/packages/ui/src/features/ai/services/__tests__/operation-executor.test.ts +++ b/packages/ui/src/features/ai/services/__tests__/operation-executor.test.ts @@ -589,6 +589,16 @@ describe('executeAiOperations — autoOrganize', () => { const action = calls.find((c) => c.type === 'cards/autoOrganizeCard'); expect(action.payload.direction).toBe('vertical'); }); + + // CCL1 — a directional organize must switch to the rectangular edge style so + // the computed dagre routes actually render (else bezier discards them). + it('also switches to the rectangular edge style after organizing', () => { + setCard(makeCard()); + const { dispatch, calls } = makeDispatch(); + executeAiOperations(dispatch as any, [{ op: 'autoOrganize' }]); + const edge = calls.find((c) => c.type === 'ui/setEdgeStyle'); + expect(edge?.payload).toBe('rectangular'); + }); }); describe('executeAiOperations — unknown op', () => { diff --git a/packages/ui/src/features/ai/services/operation-executor.ts b/packages/ui/src/features/ai/services/operation-executor.ts index bf03b653..7dd62be9 100644 --- a/packages/ui/src/features/ai/services/operation-executor.ts +++ b/packages/ui/src/features/ai/services/operation-executor.ts @@ -27,6 +27,7 @@ import { autoOrganizeCard, selectActiveCard, } from '../../../store/slices/cards-slice'; +import { setEdgeStyle } from '../../../store/slices/ui-slice'; import type { SkippedOp, ExecutionResult } from './ai-ops/types'; import type { AppDispatch } from '../../../store'; import type { Card } from '../../../store/slices/cards-slice'; @@ -280,6 +281,7 @@ export function executeAiOperations( case 'autoOrganize': { dispatch(autoOrganizeCard({ direction: 'vertical' })); + dispatch(setEdgeStyle('rectangular')); // CCL1 — render the computed dagre routes executedOps++; break; } @@ -316,6 +318,7 @@ export function executeAiOperations( const hasExplicitOrganize = ops.some((o) => o.op === 'autoOrganize'); if (hasStructuralOps && !hasExplicitOrganize && executedOps > 0) { dispatch(autoOrganizeCard({ direction: 'vertical' })); + dispatch(setEdgeStyle('rectangular')); // CCL1 — render the computed dagre routes } return { diff --git a/packages/ui/src/features/canvas/components/__tests__/connection-rejection-overlay.test.tsx b/packages/ui/src/features/canvas/components/__tests__/connection-rejection-overlay.test.tsx index 948bb6b1..f16a68fb 100644 --- a/packages/ui/src/features/canvas/components/__tests__/connection-rejection-overlay.test.tsx +++ b/packages/ui/src/features/canvas/components/__tests__/connection-rejection-overlay.test.tsx @@ -63,4 +63,35 @@ describe('ConnectionRejectionOverlay', () => { expect(bodies).toHaveLength(1); expect((bodies[0].props as { children?: unknown }).children).toBe("Gateway can't connect directly to MySQL"); }); + + const body = (tree: React.ReactNode) => + [...walk(tree)].find( + (el) => + el.type === 'div' && (el.props as { 'data-testid'?: string })['data-testid'] === 'connection-rejection-tooltip', + )!; + const fo = (tree: React.ReactNode) => [...walk(tree)].find((el) => el.type === 'foreignObject')!; + + // CCL2 — the overlay lives inside the scaled SVG, so it counter-scales by + // 1/zoom to keep a constant on-screen size (it used to shrink when zoomed out). + it('counter-scales by 1/zoom while staying centred', () => { + const tree = render({ rejection: { x: 500, y: 200, message: 'nope' }, zoom: 0.5 }); + const p = fo(tree).props as { x: number; width: number }; + expect(p.width).toBe(480); // 240 / 0.5 + expect(p.x + p.width / 2).toBe(500); // still centred on rejection.x + const style = (body(tree).props as { style: { transform: string; width: number } }).style; + expect(style.transform).toBe('scale(2)'); + expect(style.width).toBe(240); // natural width; the scale fills the foreignObject + }); + + it('clamps the inverse-zoom so a tiny zoom does not blow up unboundedly', () => { + const tree = render({ rejection: { x: 0, y: 0, message: 'nope' }, zoom: 0.01 }); + // invZoom clamped at 1/0.1 = 10 → width 2400, not 24000. + expect((fo(tree).props as { width: number }).width).toBe(2400); + }); + + it('defaults to no scaling when zoom is omitted', () => { + const tree = render({ rejection: { x: 100, y: 100, message: 'nope' } }); + expect((fo(tree).props as { width: number }).width).toBe(240); + expect((body(tree).props as { style: { transform: string } }).style.transform).toBe('scale(1)'); + }); }); diff --git a/packages/ui/src/features/canvas/components/__tests__/controls-help-modal.test.tsx b/packages/ui/src/features/canvas/components/__tests__/controls-help-modal.test.tsx index 1b0f57b9..7be55e98 100644 --- a/packages/ui/src/features/canvas/components/__tests__/controls-help-modal.test.tsx +++ b/packages/ui/src/features/canvas/components/__tests__/controls-help-modal.test.tsx @@ -33,6 +33,9 @@ vi.mock('react', async (importOriginal) => { } return [initial, vi.fn()]; }), + // The component now installs an Escape-key effect; no-op it for the + // function-call (tree-walker) render so it doesn't touch a real fiber. + useEffect: vi.fn(), }; }); @@ -111,6 +114,19 @@ describe('ControlsHelpModal', () => { expect((trigger.props as { title: string }).title).toBe('canvas.controls.shortcutsTitle'); }); + // AX8/AX6 — the icon-only "?" trigger needs an accessible name (the "?" glyph + // alone announces as "question mark") and an aria-expanded reflecting state. + it('trigger carries an aria-label and aria-expanded reflecting open state', () => { + stateMocks.openValue = false; + const closed = findByType(ControlsHelpModal({}), 'button')[0]; + expect((closed.props as { 'aria-label': string })['aria-label']).toBe('canvas.controls.shortcutsTitle'); + expect((closed.props as { 'aria-expanded': boolean })['aria-expanded']).toBe(false); + + stateMocks.openValue = true; + const open = findByType(ControlsHelpModal({}), 'button')[0]; + expect((open.props as { 'aria-expanded': boolean })['aria-expanded']).toBe(true); + }); + it('renders the backdrop + panel when open=true', () => { stateMocks.openValue = true; const tree = ControlsHelpModal({}); @@ -198,4 +214,35 @@ describe('ControlsHelpModal', () => { ); expect(cmd).toHaveLength(1); }); + + it('documents the Shift+A add affordance (CD2)', () => { + stateMocks.openValue = true; + const tree = ControlsHelpModal({}); + const shiftA = findByPredicate( + tree, + (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 'Shift + A', + ); + expect(shiftA).toHaveLength(1); + }); + + it('no longer advertises the dead 1 / 2 view-level keys (IA3)', () => { + stateMocks.openValue = true; + const tree = ControlsHelpModal({}); + const oneOrTwo = findByPredicate( + tree, + (el) => + el.type === 'span' && + ((el.props as { children?: unknown }).children === '1' || + (el.props as { children?: unknown }).children === '2'), + ); + expect(oneOrTwo).toHaveLength(0); + }); + + it('marks the panel as a modal dialog (AX3)', () => { + stateMocks.openValue = true; + const tree = ControlsHelpModal({}); + const dialog = findByPredicate(tree, (el) => (el.props as { role?: string }).role === 'dialog'); + expect(dialog).toHaveLength(1); + expect((dialog[0].props as { 'aria-modal'?: string })['aria-modal']).toBe('true'); + }); }); diff --git a/packages/ui/src/features/canvas/components/__tests__/empty-canvas-overlay.test.tsx b/packages/ui/src/features/canvas/components/__tests__/empty-canvas-overlay.test.tsx index 8ed37627..c2e5e5f8 100644 --- a/packages/ui/src/features/canvas/components/__tests__/empty-canvas-overlay.test.tsx +++ b/packages/ui/src/features/canvas/components/__tests__/empty-canvas-overlay.test.tsx @@ -174,6 +174,15 @@ describe('EmptyCanvasOverlay', () => { expect(onDismiss).toHaveBeenCalledTimes(1); }); + // AX8 — the icon-only dismiss button must carry an accessible name (it renders + // only an glyph, so screen readers would otherwise announce just "button"). + it('the dismiss "X" button has an accessible name (aria-label)', () => { + mocks.templates = []; + const tree = EmptyCanvasOverlay({}); + const buttons = findByType(tree, 'button'); + expect((buttons[0].props as { 'aria-label'?: string })['aria-label']).toBe('canvas.emptyState.dismiss'); + }); + it('renders the quickStart label key', () => { mocks.templates = []; const tree = EmptyCanvasOverlay({}); diff --git a/packages/ui/src/features/canvas/components/__tests__/socket-legend.test.tsx b/packages/ui/src/features/canvas/components/__tests__/socket-legend.test.tsx new file mode 100644 index 00000000..a8073936 --- /dev/null +++ b/packages/ui/src/features/canvas/components/__tests__/socket-legend.test.tsx @@ -0,0 +1,103 @@ +/** + * SocketLegend (CCL6) — decodes the canvas socket shape/colour language. Pure + * component; tree-walked via direct invocation. + */ + +import React from 'react'; +import { describe, it, expect } from 'vitest'; +import { SocketLegend, SocketGlyph } from '../socket-legend'; + +function* walk(node: React.ReactNode): Generator { + if (node == null || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') return; + if (Array.isArray(node)) { + for (const c of node) yield* walk(c as React.ReactNode); + return; + } + const el = node as React.ReactElement; + yield el; + // Invoke function components so their rendered subtree (e.g. SocketGlyph's + // ) is visible to the predicates. + if (typeof el.type === 'function') { + yield* walk((el.type as (p: unknown) => React.ReactNode)(el.props)); + return; + } + const children = (el.props as { children?: React.ReactNode } | undefined)?.children; + if (children != null) yield* walk(children); +} +const all = (tree: React.ReactNode, p: (el: React.ReactElement) => boolean) => [...walk(tree)].filter(p); + +function collectText(node: React.ReactNode): string { + const out: string[] = []; + const visit = (n: React.ReactNode): void => { + if (n == null || typeof n === 'boolean') return; + if (typeof n === 'string' || typeof n === 'number') { + out.push(String(n)); + return; + } + if (Array.isArray(n)) { + n.forEach((c) => visit(c as React.ReactNode)); + return; + } + const el = n as React.ReactElement; + if (typeof el.type === 'function') { + visit((el.type as (p: unknown) => React.ReactNode)(el.props)); + return; + } + visit((el.props as { children?: React.ReactNode } | undefined)?.children); + }; + visit(node); + return out.join(' '); +} +const text = collectText; + +const renderLegend = () => (SocketLegend as React.FC)({}) as React.ReactElement; + +describe('SocketLegend (CCL6)', () => { + it('renders all four socket shapes', () => { + const tree = renderLegend(); + const shapes = all(tree, (el) => el.type === 'svg').map( + (el) => (el.props as { 'data-shape': string })['data-shape'], + ); + expect(new Set(shapes)).toEqual(new Set(['circle', 'ring', 'diamond', 'square'])); + }); + + it('labels each shape with its connection meaning + a title', () => { + const t = text(renderLegend()); + // Real i18n (default locale). + expect(t).toContain('Connection sockets'); + expect(t).toContain('Data & traffic'); + expect(t).toContain('Config (env, secrets)'); + expect(t).toContain('Source repository'); + expect(t).toContain('Domain / DNS'); + }); +}); + +describe('SocketGlyph', () => { + it('renders a filled circle for the circle shape', () => { + const el = (SocketGlyph as unknown as (p: { shape: string; color: string }) => React.ReactElement)({ + shape: 'circle', + color: '#22c55e', + }); + const circle = all(el, (e) => e.type === 'circle')[0]; + expect((circle.props as { fill: string }).fill).toBe('#22c55e'); + }); + + it('renders an outlined (no-fill) ring for the ring shape', () => { + const el = (SocketGlyph as unknown as (p: { shape: string; color: string }) => React.ReactElement)({ + shape: 'ring', + color: '#f59e0b', + }); + const circle = all(el, (e) => e.type === 'circle')[0]; + expect((circle.props as { fill: string }).fill).toBe('none'); + expect((circle.props as { stroke: string }).stroke).toBe('#f59e0b'); + }); + + it('rotates the diamond shape 45deg', () => { + const el = (SocketGlyph as unknown as (p: { shape: string; color: string }) => React.ReactElement)({ + shape: 'diamond', + color: '#8b5cf6', + }); + const rect = all(el, (e) => e.type === 'rect')[0]; + expect((rect.props as { transform?: string }).transform).toContain('rotate(45'); + }); +}); diff --git a/packages/ui/src/features/canvas/components/__tests__/svg-canvas.test.tsx b/packages/ui/src/features/canvas/components/__tests__/svg-canvas.test.tsx index 4baed078..4e63941f 100644 --- a/packages/ui/src/features/canvas/components/__tests__/svg-canvas.test.tsx +++ b/packages/ui/src/features/canvas/components/__tests__/svg-canvas.test.tsx @@ -52,7 +52,7 @@ const hooks = vi.hoisted(() => ({ handleRenameCommit: vi.fn(), handleRenameCancel: vi.fn(), })), - useCanvasSideEffects: vi.fn(), + useCanvasSideEffects: vi.fn(() => ({ overlayDismissed: false, dismissOverlay: vi.fn() })), useGhostMode: vi.fn(() => ({ ghosts: [], handleAcceptGhost: vi.fn(), @@ -142,6 +142,7 @@ const components = vi.hoisted(() => ({ CanvasDeployBanner: vi.fn(() => null), CanvasContent: vi.fn(() => null), SocketHoverTooltip: vi.fn(() => null), + EmptyCanvasOverlay: vi.fn(() => null), })); const dispatchSpy = vi.fn(); @@ -185,6 +186,9 @@ vi.mock('../nodes/_shared/socket-hover-tooltip', () => ({ vi.mock('../deploy-banner', () => ({ CanvasDeployBanner: components.CanvasDeployBanner, })); +vi.mock('../empty-canvas-overlay', () => ({ + EmptyCanvasOverlay: components.EmptyCanvasOverlay, +})); vi.mock('../canvas-renderer/canvas-content', () => ({ CanvasContent: components.CanvasContent, })); diff --git a/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-row.test.tsx b/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-row.test.tsx new file mode 100644 index 00000000..c63cae3b --- /dev/null +++ b/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-row.test.tsx @@ -0,0 +1,92 @@ +/** + * SpotlightRow / SpotlightHeader (CD8) — the presentational pieces of the + * Shift+A spotlight: a row shows the LOCALIZED category label (not the raw + * iceType category id), and a non-interactive section header groups recent vs + * catalog. Both are pure FCs, tree-walked via direct invocation. + */ + +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { SpotlightRow, SpotlightHeader } from '../spotlight'; + +function* walk(node: React.ReactNode): Generator { + if (node == null || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') return; + if (Array.isArray(node)) { + for (const c of node) yield* walk(c as React.ReactNode); + return; + } + const el = node as React.ReactElement; + yield el; + const children = (el.props as { children?: React.ReactNode } | undefined)?.children; + if (children != null) yield* walk(children); +} +const findByPredicate = (tree: React.ReactNode, p: (el: React.ReactElement) => boolean) => [...walk(tree)].filter(p); +const collectText = (tree: React.ReactNode): string => + [...walk(tree)] + .map((el) => (el.props as { children?: unknown }).children) + .filter((c) => typeof c === 'string') + .join(' '); + +const FakeIcon = () => null; + +const makeCmd = (over: Record = {}) => + ({ + type: 'block', + name: 'Postgres', + description: 'Relational DB', + iceType: 'Database.PostgreSQL', + category: 'Database', + origin: { icon: FakeIcon }, + ...over, + }) as unknown as React.ComponentProps['cmd']; + +const renderRow = (props: Partial>) => + (SpotlightRow as unknown as (p: React.ComponentProps) => React.ReactElement)({ + cmd: makeCmd(), + categoryLabel: 'Data', + highlighted: false, + onSelect: vi.fn(), + onHover: vi.fn(), + ...props, + }); + +describe('SpotlightRow (CD8)', () => { + it('renders the passed (localized) categoryLabel, not the raw cmd.category', () => { + const tree = renderRow({ cmd: makeCmd({ category: 'Database' }), categoryLabel: 'Data' }); + const text = collectText(tree); + expect(text).toContain('Data'); + // the raw category id is NOT shown as the category chip + const chip = findByPredicate( + tree, + (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 'Database', + ); + expect(chip).toHaveLength(0); + }); + + it('renders the name + description', () => { + const tree = renderRow({}); + const text = collectText(tree); + expect(text).toContain('Postgres'); + expect(text).toContain('Relational DB'); + }); + + it('fires onSelect on click and onHover on mouse-enter', () => { + const onSelect = vi.fn(); + const onHover = vi.fn(); + const tree = renderRow({ onSelect, onHover }); + (tree.props as { onClick: () => void }).onClick(); + (tree.props as { onMouseEnter: () => void }).onMouseEnter(); + expect(onSelect).toHaveBeenCalledTimes(1); + expect(onHover).toHaveBeenCalledTimes(1); + }); +}); + +describe('SpotlightHeader (CD8)', () => { + it('renders a non-interactive (aria-hidden) section label', () => { + const el = (SpotlightHeader as unknown as (p: { label: string }) => React.ReactElement)({ label: 'Recent' }); + expect(el.type).toBe('li'); + expect((el.props as { 'aria-hidden': string })['aria-hidden']).toBe('true'); + expect((el.props as { children: string }).children).toBe('Recent'); + expect((el.props as { style: { pointerEvents: string } }).style.pointerEvents).toBe('none'); + }); +}); diff --git a/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-spawn.test.ts b/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-spawn.test.ts new file mode 100644 index 00000000..2d745eec --- /dev/null +++ b/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-spawn.test.ts @@ -0,0 +1,72 @@ +/** + * spotlight-spawn — provider resolution (CD1) + fallback node data (CD5). + */ + +import { describe, it, expect, vi } from 'vitest'; +import { resolveSpotlightProvider, buildSpotlightFallbackData } from '../spotlight-spawn'; + +const allow = () => true; +const deny = () => false; + +describe('resolveSpotlightProvider (CD1)', () => { + it('prefers the active deploy provider when the block supports it', () => { + const r = resolveSpotlightProvider('Compute.Container', ['aws', 'gcp'], 'gcp', allow); + expect(r.effectiveProvider).toBe('gcp'); + expect(r.gateBlocked).toBe(false); + }); + + it('falls back to the first listed provider when the deploy provider is unsupported', () => { + const r = resolveSpotlightProvider('Compute.Container', ['aws', 'gcp'], 'azure', allow); + expect(r.effectiveProvider).toBe('aws'); + }); + + it('uses the deploy provider when the block lists none', () => { + const r = resolveSpotlightProvider('Compute.Container', [], 'aws', allow); + expect(r.effectiveProvider).toBe('aws'); + }); + + it('yields undefined provider when there is neither a list nor a deploy provider', () => { + const r = resolveSpotlightProvider('Compute.Container', [], undefined, allow); + expect(r.effectiveProvider).toBeUndefined(); + expect(r.gateBlocked).toBe(false); + }); + + it('reports gateBlocked when the block is not enabled for the resolved provider', () => { + const r = resolveSpotlightProvider('Compute.Container', ['aws'], 'aws', deny); + expect(r.effectiveProvider).toBe('aws'); + expect(r.gateBlocked).toBe(true); + }); + + it('passes the resolved provider (not the deploy provider) to the enablement check', () => { + const isEnabled = vi.fn(() => true); + // deploy provider azure is unsupported → resolves to aws → that is checked. + resolveSpotlightProvider('Compute.Container', ['aws', 'gcp'], 'azure', isEnabled); + expect(isEnabled).toHaveBeenCalledWith('Compute.Container', 'aws'); + }); +}); + +describe('buildSpotlightFallbackData (CD5)', () => { + const cmd = { name: 'My Block', iceType: 'Compute.Container' }; + + it('stamps the resolved effectiveProvider (not the raw deploy provider)', () => { + const data = buildSpotlightFallbackData(cmd, 'aws', false); + expect(data.provider).toBe('aws'); + expect(data.label).toBe('My Block'); + expect(data.iceType).toBe('Compute.Container'); + }); + + it('seeds name (not just label) so the node is not born missing a required name (PE6)', () => { + const data = buildSpotlightFallbackData(cmd, 'aws', false); + expect(data.name).toBe('My Block'); + }); + + it('flags providerUnsupported when gate-blocked (matches the drag path)', () => { + const data = buildSpotlightFallbackData(cmd, 'aws', true); + expect(data.providerUnsupported).toBe(true); + }); + + it('omits providerUnsupported when not gate-blocked', () => { + const data = buildSpotlightFallbackData(cmd, 'aws', false); + expect('providerUnsupported' in data).toBe(false); + }); +}); diff --git a/packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts b/packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts new file mode 100644 index 00000000..c8015c21 --- /dev/null +++ b/packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts @@ -0,0 +1,64 @@ +/** + * Spotlight (Shift+A) spawn helpers — pure logic extracted from `spotlight.tsx` + * so the provider-resolution + fallback-node decisions are unit-testable and + * stay consistent with the drag-drop add path (`use-canvas-drop.ts`). + * + * CD1 — prefer the active deploy provider when the block supports it (the + * spotlight previously always took `providers[0]`, diverging from drag). + * CD5 — carry `gateBlocked` onto the fallback node as `providerUnsupported` and + * stamp the resolved `effectiveProvider` (not the raw deploy provider), so + * both add paths render the same unsupported-warning state. + */ + +import type { ComponentDef } from '../../../palette/types'; + +type Provider = ComponentDef['providers'][number]; + +export interface SpotlightProviderResolution { + /** The provider the block should spawn against. */ + effectiveProvider: Provider | undefined; + /** True when the block's category isn't enabled for `effectiveProvider`. */ + gateBlocked: boolean; +} + +/** + * Resolve which provider a Shift+A spawn should target, and whether that lands + * on a provider that doesn't support the block. + */ +export function resolveSpotlightProvider( + iceType: string, + providers: readonly Provider[], + deployProvider: string | undefined, + isEnabled: (iceType: string, provider: Provider) => boolean, +): SpotlightProviderResolution { + const prefersDeploy = !!deployProvider && (providers as readonly string[]).includes(deployProvider); + const effectiveProvider: Provider | undefined = prefersDeploy + ? (deployProvider as Provider) + : (providers[0] ?? (deployProvider as Provider | undefined)); + const gateBlocked = !!effectiveProvider && !isEnabled(iceType, effectiveProvider); + return { effectiveProvider, gateBlocked }; +} + +/** + * Build the data for the bare fallback resource node spawned when no blueprint + * resolves — mirroring the drag path's `newNodeData` (including the + * `providerUnsupported` flag that drives the warning badge + deploy refusal). + */ +export function buildSpotlightFallbackData( + cmd: { name: string; iceType: string }, + effectiveProvider: Provider | undefined, + gateBlocked: boolean, +): Record { + return { + // PE6 — seed `name` (not just `label`) so the fallback node isn't born with + // a hidden "Name is required" error, matching the blueprint path which + // seeds `data.name` from the block's display name. + name: cmd.name, + label: cmd.name, + iceType: cmd.iceType, + behavior: 'singleton', + folded: false, + provider: effectiveProvider, + ...(gateBlocked ? { providerUnsupported: true } : {}), + }; +} diff --git a/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx b/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx index 59381a3e..11d359f9 100644 --- a/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx +++ b/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx @@ -19,11 +19,13 @@ import { isIceTypeEnabledForProvider } from '@ice/constants'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { rank, type RankableItem } from './fuzzy-match'; +import { resolveSpotlightProvider, buildSpotlightFallbackData } from './spotlight-spawn'; import { getBlueprint, expandBlueprint } from '../../../../config/blocks'; import { useTranslation } from '../../../../i18n'; import { addNodeToCard, expandBlueprintToCard, type CardNode } from '../../../../store/slices/cards-slice'; import { closeSpotlight, pushSpotlightRecent } from '../../../../store/slices/ui-slice'; import { getComponents } from '../../../palette/data/components'; +import { getCategoryMap } from '../../../palette/data/categories'; import type { AppDispatch, RootState } from '../../../../store'; import type { ComponentDef } from '../../../palette/types'; @@ -63,18 +65,25 @@ export const Spotlight: React.FC = () => { })); }, [t]); + // CD8 — show the same localized, palette category labels (not the raw + // iceType category id) so the spotlight reads like the palette. + const categoryMap = useMemo(() => getCategoryMap(t), [t]); + // Order: when no query, surface recently-used at the top, then // everything else in palette order; with a query, fuzzy-rank. - const ranked = useMemo(() => { + // `recentCount` marks the recent/catalog boundary so the list can show + // lightweight section headers (CD8) without affecting keyboard nav (which + // indexes `ranked`, a flat command list with no header rows). + const { ranked, recentCount } = useMemo<{ ranked: SpotlightCommand[]; recentCount: number }>(() => { if (!query.trim()) { const recentSet = new Set(recent); const fromRecent = recent .map((iceType) => commands.find((c) => c.iceType === iceType)) .filter((c): c is SpotlightCommand => !!c); const rest = commands.filter((c) => !recentSet.has(c.iceType)); - return [...fromRecent, ...rest]; + return { ranked: [...fromRecent, ...rest], recentCount: fromRecent.length }; } - return rank(commands, query); + return { ranked: rank(commands, query), recentCount: 0 }; }, [commands, query, recent]); // Reset state when the modal opens. @@ -96,9 +105,14 @@ export const Spotlight: React.FC = () => { const spawn = (cmd: SpotlightCommand): void => { const blockType = cmd.iceType; - const paletteProvider: Provider | undefined = cmd.origin.providers[0]; - const effectiveProvider = paletteProvider ?? deployProvider; - const gateBlocked = !!effectiveProvider && !isIceTypeEnabledForProvider(blockType, effectiveProvider); + // CD1 — prefer the active deploy provider when this block supports it, so + // Shift+A spawns the same blueprint the drag path would (was: always [0]). + const { effectiveProvider, gateBlocked } = resolveSpotlightProvider( + blockType, + cmd.origin.providers, + deployProvider, + isIceTypeEnabledForProvider, + ); const blueprint = gateBlocked ? undefined : getBlueprint(blockType, effectiveProvider); if (blueprint) { @@ -127,20 +141,15 @@ export const Spotlight: React.FC = () => { } else { // Fall through to a bare resource node so the user still gets a // visible placeholder when the blueprint is missing for the active - // provider. Mirrors the palette drop fallback. + // provider. Mirrors the palette drop fallback — CD5: carries + // `providerUnsupported` so the warning badge + deploy validator flag it. const newNode: CardNode = { id: `node-${Date.now()}`, type: 'resource', position: { x: canvasPos.x, y: canvasPos.y }, width: 200, height: 120, - data: { - label: cmd.name, - iceType: blockType, - behavior: 'singleton', - folded: false, - provider: deployProvider, - }, + data: buildSpotlightFallbackData({ name: cmd.name, iceType: blockType }, effectiveProvider, gateBlocked), }; dispatch(addNodeToCard(newNode)); } @@ -248,29 +257,59 @@ export const Spotlight: React.FC = () => { No matches. )} - {ranked.map((cmd, i) => ( - spawn(cmd)} - onHover={() => setHighlightIdx(i)} - /> - ))} + {ranked.map((cmd, i) => { + // CD8 — lightweight section headers split recently-used from the + // full catalog (only when not searching). Header
  • s are not in + // `ranked`, so they don't affect arrow-key navigation. + const showHeaders = !query.trim() && recentCount > 0; + return ( + + {showHeaders && i === 0 && } + {showHeaders && i === recentCount && } + spawn(cmd)} + onHover={() => setHighlightIdx(i)} + /> + + ); + })} ); }; +// CD8 — non-interactive group label (Recent / All blocks). +export const SpotlightHeader: React.FC<{ label: string }> = ({ label }) => ( +
  • +); + interface SpotlightRowProps { cmd: SpotlightCommand; + /** CD8 — localized palette category label (not the raw iceType category id). */ + categoryLabel: string; highlighted: boolean; onSelect: () => void; onHover: () => void; } -const SpotlightRow: React.FC = ({ cmd, highlighted, onSelect, onHover }) => { +export const SpotlightRow: React.FC = ({ cmd, categoryLabel, highlighted, onSelect, onHover }) => { const Icon = cmd.origin.icon; return (
  • = ({ cmd, highlighted, onSelect, )} - {cmd.category} + {categoryLabel}
  • ); diff --git a/packages/ui/src/features/canvas/components/canvas-renderer/canvas-content.tsx b/packages/ui/src/features/canvas/components/canvas-renderer/canvas-content.tsx index 0bcaade6..1d3e9e30 100644 --- a/packages/ui/src/features/canvas/components/canvas-renderer/canvas-content.tsx +++ b/packages/ui/src/features/canvas/components/canvas-renderer/canvas-content.tsx @@ -193,7 +193,7 @@ export const CanvasContent: React.FC = ({ {/* Floating rejection tooltip — shown for ~2.5s after a failed drop (invalid pair, special-rule conflict, or hard validation error). State lives in `useConnectionDrawing`. */} - {connectionRejection && } + {connectionRejection && } {/* User traffic icon + outbound connections to exposed services — extracted to UserTrafficOverlay (rf-canv-15). Both render only diff --git a/packages/ui/src/features/canvas/components/connection-rejection-overlay.tsx b/packages/ui/src/features/canvas/components/connection-rejection-overlay.tsx index 01761615..25d96460 100644 --- a/packages/ui/src/features/canvas/components/connection-rejection-overlay.tsx +++ b/packages/ui/src/features/canvas/components/connection-rejection-overlay.tsx @@ -19,39 +19,50 @@ export interface ConnectionRejection { interface ConnectionRejectionOverlayProps { rejection: ConnectionRejection; + /** CCL2 — current canvas zoom; the overlay lives inside the scaled SVG group, + * so it counter-scales by 1/zoom to stay a constant on-screen size (it used + * to shrink with the canvas, going illegible exactly when zoomed out). */ + zoom?: number; } const TOOLTIP_WIDTH = 240; const TOOLTIP_OFFSET_Y = 14; -export const ConnectionRejectionOverlay: React.FC = ({ rejection }) => ( - - -
    = ({ rejection, zoom = 1 }) => { + // Mirror the SvgConnectionPath inverse-zoom precedent (clamped so a tiny zoom + // doesn't blow the tooltip up unboundedly). + const invZoom = 1 / Math.max(zoom, 0.1); + return ( + + - {rejection.message} -
    -
    -
    -); +
    + {rejection.message} +
    + + + ); +}; ConnectionRejectionOverlay.displayName = 'ConnectionRejectionOverlay'; diff --git a/packages/ui/src/features/canvas/components/context/__tests__/canvas-menu.test.tsx b/packages/ui/src/features/canvas/components/context/__tests__/canvas-menu.test.tsx index 52677ed3..80113402 100644 --- a/packages/ui/src/features/canvas/components/context/__tests__/canvas-menu.test.tsx +++ b/packages/ui/src/features/canvas/components/context/__tests__/canvas-menu.test.tsx @@ -297,26 +297,33 @@ describe('CanvasMenu — auto-organize submenu', () => { const item = findSubItem(tree, 'canvas.contextMenu.layoutVertical')!; (item.props.onClick as () => void)(); expect(mocks.autoOrganizeCard).toHaveBeenCalledWith({ direction: 'vertical', zoom: 1.5 }); + // CCL1 — a directional organize switches to the rectangular edge style so + // the computed dagre routes render. + expect(mocks.setEdgeStyle).toHaveBeenCalledWith('rectangular'); expect(close).toHaveBeenCalled(); expect(dispatch).toHaveBeenCalled(); }); - it('horizontal layout dispatches autoOrganizeCard with direction=horizontal', () => { + it('horizontal layout dispatches autoOrganizeCard with direction=horizontal + rectangular edges', () => { mocks.useStateOverrides = { 0: 'organize' }; const dispatch = vi.fn(); const tree = render(baseProps({ dispatch })); const item = findSubItem(tree, 'canvas.contextMenu.layoutHorizontal')!; (item.props.onClick as () => void)(); expect(mocks.autoOrganizeCard).toHaveBeenCalledWith({ direction: 'horizontal', zoom: 1 }); + expect(mocks.setEdgeStyle).toHaveBeenCalledWith('rectangular'); // CCL1 }); - it('circular layout dispatches autoOrganizeCard with layout=circular', () => { + it('circular layout dispatches autoOrganizeCard with layout=circular and does NOT force rectangular edges', () => { mocks.useStateOverrides = { 0: 'organize' }; const dispatch = vi.fn(); const tree = render(baseProps({ dispatch })); const item = findSubItem(tree, 'canvas.contextMenu.layoutCircular')!; (item.props.onClick as () => void)(); expect(mocks.autoOrganizeCard).toHaveBeenCalledWith({ layout: 'circular', zoom: 1 }); + // CCL1 — circular is a radial layout; orthogonal rectangular routes don't + // apply, so it must NOT switch the edge style (matches the toolbar button). + expect(mocks.setEdgeStyle).not.toHaveBeenCalledWith('rectangular'); }); }); diff --git a/packages/ui/src/features/canvas/components/context/canvas-menu.tsx b/packages/ui/src/features/canvas/components/context/canvas-menu.tsx index a776441b..0531e311 100644 --- a/packages/ui/src/features/canvas/components/context/canvas-menu.tsx +++ b/packages/ui/src/features/canvas/components/context/canvas-menu.tsx @@ -122,6 +122,11 @@ export const CanvasMenu: React.FC = ({ label: t('canvas.contextMenu.layoutVertical'), onClick: () => { dispatch(autoOrganizeCard({ direction: 'vertical', zoom: currentZoom })); + // CCL1 — a directional organize computes orthogonal dagre routes; + // switch to the rectangular edge style so they actually render + // (otherwise bezier ignores the routePoints and cuts through + // blocks). Matches the toolbar's vertical/horizontal buttons. + dispatch(setEdgeStyle('rectangular')); close(); }, }, @@ -129,6 +134,7 @@ export const CanvasMenu: React.FC = ({ label: t('canvas.contextMenu.layoutHorizontal'), onClick: () => { dispatch(autoOrganizeCard({ direction: 'horizontal', zoom: currentZoom })); + dispatch(setEdgeStyle('rectangular')); // CCL1 — render the computed routes close(); }, }, diff --git a/packages/ui/src/features/canvas/components/controls-help-modal.tsx b/packages/ui/src/features/canvas/components/controls-help-modal.tsx index ef7a9999..99c45412 100644 --- a/packages/ui/src/features/canvas/components/controls-help-modal.tsx +++ b/packages/ui/src/features/canvas/components/controls-help-modal.tsx @@ -4,13 +4,25 @@ * Bottom-right "?" button that opens a popover showing all canvas controls. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { SocketLegend } from './socket-legend'; import { useTranslation } from '../../../i18n'; export const ControlsHelpModal: React.FC = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); + // AX3 — a real Escape handler (the header "ESC" affordance implied one but was + // only a click button). Matches the canvas context menu's pattern. + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [open]); + const CONTROL_SECTIONS = [ { title: t('canvas.controls.sectionNavigation'), @@ -32,6 +44,8 @@ export const ControlsHelpModal: React.FC = () => { { title: t('canvas.controls.sectionEditing'), items: [ + // CD2 — the headline add affordance was undocumented here. + { keys: 'Shift + A', action: t('canvas.controls.addBlock') }, { keys: 'Delete / Backspace', action: t('canvas.controls.deleteSelected') }, { keys: 'Cmd + C / X / V', action: t('canvas.controls.copyPaste') }, { keys: 'Double-click label', action: t('canvas.controls.renameGroup') }, @@ -47,8 +61,9 @@ export const ControlsHelpModal: React.FC = () => { { title: t('canvas.controls.sectionView'), items: [ - { keys: '1', action: t('canvas.controls.architectureView') }, - { keys: '2', action: t('canvas.controls.infrastructureView') }, + // The 1/2 view-level keys were removed with the view-level toggle + // ("always Level 2"); altitude is zoom-driven now, so don't advertise + // dead keys (IA3). { keys: 'Ctrl + Shift + D', action: t('canvas.controls.toggleDebug') }, ], }, @@ -66,6 +81,8 @@ export const ControlsHelpModal: React.FC = () => { border: '1px solid var(--ice-border-strong)', }} title={t('canvas.controls.shortcutsTitle')} + aria-label={t('canvas.controls.shortcutsTitle')} + aria-expanded={open} > ? @@ -78,6 +95,9 @@ export const ControlsHelpModal: React.FC = () => { {/* Panel */}
    {
    ))} + + {/* CCL6 — decode the socket shape/colour language for the user. */} +
    + +
    diff --git a/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx b/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx index 558c44a8..bfd1e81d 100644 --- a/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx +++ b/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx @@ -51,6 +51,7 @@ export const EmptyCanvasOverlay: React.FC = ({ onDismis {t('canvas.emptyState.quickStart')} +
    {t('canvas.emptyState.hint')}
    ); diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/card-shell.test.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/card-shell.test.tsx index 83859388..505272c2 100644 --- a/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/card-shell.test.tsx +++ b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/card-shell.test.tsx @@ -26,6 +26,10 @@ const mocks = vi.hoisted(() => ({ hoverValue: false as boolean, setHoverSpy: vi.fn(), getServiceName: vi.fn((_iceType: string, _provider: string) => null as string | null), + // Value returned by the mocked `useContext` (shared by useIsNodeOrphan + + // useNodeValidation). A Set keeps orphan inert; tests can swap in a Map to + // exercise the validation badge (Map has both `.has` and `.get`). + ctxValue: new Set() as Set | Map, })); vi.mock('../../../../../concept-info', () => ({ @@ -48,15 +52,18 @@ vi.mock('react', async (importOriginal) => { return [initial, vi.fn()]; }), useCallback: vi.fn((fn: T, _deps: unknown[]) => fn), - // `CardShell` reads the orphan-nodes context via `useIsNodeOrphan`. - // Because the test invokes the component as a plain function (not - // through a React renderer), the real `useContext` blows up — return - // a fresh empty Set so the orphan branch is inert in these tests. - useContext: vi.fn(() => new Set()), + // `CardShell` reads the orphan + validation contexts via useContext. + // Because the test invokes the component as a plain function (not through a + // React renderer), the real `useContext` blows up — return a controllable + // value (Set by default) so both branches are inert unless a test opts in. + useContext: vi.fn(() => mocks.ctxValue), }; }); import { CardShell } from '../card-shell'; +import { NodeDeployOverlay } from '../node-deploy-overlay'; +import { StatusDot } from '../status-dot'; +import { ValidationBadge } from '../validation-badge'; import type { LucideIcon } from 'lucide-react'; type ReactNodeLike = React.ReactNode; @@ -111,6 +118,7 @@ beforeEach(() => { mocks.ConceptInfoTrigger.mockClear(); mocks.getServiceName.mockReset(); mocks.getServiceName.mockReturnValue(null); + mocks.ctxValue = new Set(); }); describe('CardShell', () => { @@ -233,6 +241,99 @@ describe('CardShell', () => { expect(footerSpans.length).toBeGreaterThan(0); }); + // CNV1 — the in-flight step + failure reason now surface on the node itself. + it('threads deploy_progress to NodeDeployOverlay when deploying', () => { + const progress = { step_label: 'Creating bucket', step_index: 3, step_total: 6 }; + const tree = renderInner({ node: makeNode({ data: { deploy_status: 'deploying', deploy_progress: progress } }) }); + const overlay = findByPredicate(tree, (el) => el.type === NodeDeployOverlay)[0]; + expect(overlay).toBeDefined(); + expect((overlay.props as { deployStatus: string }).deployStatus).toBe('deploying'); + expect((overlay.props as { deployProgress: unknown }).deployProgress).toEqual(progress); + }); + + it('threads deploy_error to NodeDeployOverlay on failure', () => { + const tree = renderInner({ node: makeNode({ data: { deploy_status: 'error', deploy_error: 'quota exceeded' } }) }); + const overlay = findByPredicate(tree, (el) => el.type === NodeDeployOverlay)[0]; + expect((overlay.props as { deployError: string }).deployError).toBe('quota exceeded'); + }); + + // CNV2 — validation issues surface as a corner badge, read via context. + it('renders a ValidationBadge when the node has an error from the validation context', () => { + mocks.ctxValue = new Map([['n1', { severity: 'error', count: 3 }]]); + const tree = renderInner({ node: makeNode({ id: 'n1' }) }); + const badge = findByPredicate(tree, (el) => el.type === ValidationBadge)[0]; + expect(badge).toBeDefined(); + expect((badge.props as { severity: string; count: number }).severity).toBe('error'); + expect((badge.props as { count: number }).count).toBe(3); + }); + + it('does NOT render a ValidationBadge for an info-only issue', () => { + mocks.ctxValue = new Map([['n1', { severity: 'info', count: 1 }]]); + const tree = renderInner({ node: makeNode({ id: 'n1' }) }); + expect(findByPredicate(tree, (el) => el.type === ValidationBadge)).toHaveLength(0); + }); + + it('renders no ValidationBadge when the node has no validation entry', () => { + const tree = renderInner({ node: makeNode({ id: 'n1' }) }); + expect(findByPredicate(tree, (el) => el.type === ValidationBadge)).toHaveLength(0); + }); + + // CNV3 — the footer status dot pulses while a deploy is in flight. + it('pulses the footer StatusDot while deploying, but not when active', () => { + const deployingTree = renderInner({ node: makeNode({ data: { deploy_status: 'deploying' } }) }); + const deployingDot = findByPredicate(deployingTree, (el) => el.type === StatusDot)[0]; + expect((deployingDot.props as { pulse: boolean }).pulse).toBe(true); + + const activeTree = renderInner({ node: makeNode({ data: { deploy_status: 'active' } }) }); + const activeDot = findByPredicate(activeTree, (el) => el.type === StatusDot)[0]; + expect((activeDot.props as { pulse: boolean }).pulse).toBe(false); + }); + + // CNV7/AX5 — the zoomed-out poster status indicator is no longer a colour-only, + // mouse-`title`-only dot: it carries a shape glyph + an AT-reachable aria-label. + describe('poster-view status indicator (lod < 3)', () => { + const findPosterStatus = (tree: React.ReactNode) => + findByPredicate(tree, (el) => el.type === 'span' && (el.props as { role?: string }).role === 'img')[0]; + + it('renders a labelled glyph (not a bare dot) for a deployed node', () => { + const tree = renderInner({ lod: 1, node: makeNode({ data: { deploy_status: 'deployed' } }) }); + const el = findPosterStatus(tree); + expect(el).toBeDefined(); + const props = el.props as { 'aria-label': string; children: string; title: string }; + expect(props['aria-label']).toBe('Deployed'); + expect(props.title).toBe('Deployed'); + expect(props.children).toBe('✓'); + }); + + it('uses distinct glyphs for in-flight vs failed so they differ without colour', () => { + const deploying = findPosterStatus( + renderInner({ lod: 1, node: makeNode({ data: { deploy_status: 'deploying' } }) }), + ); + const failed = findPosterStatus(renderInner({ lod: 1, node: makeNode({ data: { deploy_status: 'error' } }) })); + expect((deploying.props as { children: string }).children).toBe('…'); + expect((failed.props as { children: string }).children).toBe('✕'); + expect((deploying.props as { children: string }).children).not.toBe( + (failed.props as { children: string }).children, + ); + }); + + it('pulses the glyph while work is in flight, but not when terminal', () => { + const deploying = findPosterStatus( + renderInner({ lod: 1, node: makeNode({ data: { deploy_status: 'deploying' } }) }), + ); + const deployed = findPosterStatus( + renderInner({ lod: 1, node: makeNode({ data: { deploy_status: 'deployed' } }) }), + ); + expect((deploying.props as { className?: string }).className).toContain('animate-pulse'); + expect((deployed.props as { className?: string }).className ?? '').not.toContain('animate-pulse'); + }); + + it('renders no status indicator when the node has no deploy_status', () => { + const tree = renderInner({ lod: 1, node: makeNode({ data: {} }) }); + expect(findPosterStatus(tree)).toBeUndefined(); + }); + }); + it('renders the icon prop with size=16 + the accent color', () => { const tree = renderInner({ accentColor: '#abcdef' }); const icons = findByType(tree, FakeIcon); diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/node-deploy-overlay.test.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/node-deploy-overlay.test.tsx new file mode 100644 index 00000000..ebfd7296 --- /dev/null +++ b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/node-deploy-overlay.test.tsx @@ -0,0 +1,47 @@ +/** + * NodeDeployOverlay (CNV1) — invoked as a plain function (tree-walker style) + * so we can assert the returned element shape without a DOM. + */ +import React from 'react'; +import { describe, it, expect } from 'vitest'; +import { NodeDeployOverlay } from '../node-deploy-overlay'; + +type Props = React.ComponentProps; +const render = (props: Props): React.ReactElement | null => + (NodeDeployOverlay as (p: Props) => React.ReactElement | null)(props); + +describe('NodeDeployOverlay', () => { + it('renders the deploy step with index/total when deploying', () => { + const el = render({ + deployStatus: 'deploying', + deployProgress: { step_label: 'Creating bucket', step_index: 2, step_total: 5 }, + }); + expect(el).not.toBeNull(); + expect((el!.props as { 'data-testid': string })['data-testid']).toBe('node-deploy-progress'); + expect((el!.props as { children: string }).children).toBe('Creating bucket (2/5)'); + }); + + it('renders just the step label when index/total are absent', () => { + const el = render({ deployStatus: 'deploying', deployProgress: { step_label: 'Working' } }); + expect((el!.props as { children: string }).children).toBe('Working'); + }); + + it('renders the error line (with full text as title) on failure', () => { + const el = render({ deployStatus: 'error', deployError: 'quota exceeded' }); + expect((el!.props as { 'data-testid': string })['data-testid']).toBe('node-deploy-error'); + expect((el!.props as { title: string }).title).toBe('quota exceeded'); + expect((el!.props as { children: unknown[] }).children).toEqual(['✗ ', 'quota exceeded']); + }); + + it('respects the bottom offset', () => { + const el = render({ deployStatus: 'error', deployError: 'x', bottom: 30 }); + expect((el!.props as { style: { bottom: number } }).style.bottom).toBe(30); + }); + + it('renders nothing when there is no actionable deploy state', () => { + expect(render({ deployStatus: 'active' })).toBeNull(); + expect(render({ deployStatus: 'deploying' })).toBeNull(); // no step_label + expect(render({ deployStatus: 'error' })).toBeNull(); // no error text + expect(render({ deployStatus: '' })).toBeNull(); + }); +}); diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/poster-status.test.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/poster-status.test.tsx new file mode 100644 index 00000000..52509f36 --- /dev/null +++ b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/poster-status.test.tsx @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { posterStatusTone, posterStatusGlyph } from '../poster-status'; +import { STATUS_COLORS } from '../../../../../../config/canvas-constants'; + +describe('posterStatusTone', () => { + it.each([ + ['active', 'good'], + ['running', 'good'], + ['healthy', 'good'], + ['deployed', 'good'], + ['creating', 'in-flight'], + ['updating', 'in-flight'], + ['deploying', 'in-flight'], + ['planning', 'in-flight'], + ['destroying', 'in-flight'], + ['deleting', 'in-flight'], + ['queued', 'in-flight'], + ['pending', 'warn'], + ['warning', 'warn'], + ['drifted', 'warn'], + ['error', 'error'], + ['failed', 'error'], + ['stopped', 'neutral'], + ['inactive', 'neutral'], + ['idle', 'neutral'], + ['skipped', 'neutral'], + ['cancelled', 'neutral'], + ])('buckets %s → %s', (raw, tone) => { + expect(posterStatusTone(raw)).toBe(tone); + }); + + it('falls back to neutral for unknown / empty / nullish statuses', () => { + expect(posterStatusTone('totally-unknown')).toBe('neutral'); + expect(posterStatusTone('')).toBe('neutral'); + expect(posterStatusTone(undefined)).toBe('neutral'); + expect(posterStatusTone(null)).toBe('neutral'); + }); + + it('classifies every STATUS_COLORS key (no node status is left untoned)', () => { + // Every colour the poster can paint must also map to a non-colour glyph, + // otherwise the redundancy gap (CNV7/AX5) reopens for that status. `idle` + // is the documented neutral fallback, so an explicit entry isn't required. + for (const key of Object.keys(STATUS_COLORS)) { + expect(['good', 'in-flight', 'warn', 'error', 'neutral']).toContain(posterStatusTone(key)); + } + }); +}); + +describe('posterStatusGlyph', () => { + it('gives each tone a distinct, non-empty glyph', () => { + const glyphs = ['good', 'in-flight', 'warn', 'error', 'neutral'].map( + (_t, i) => posterStatusGlyph(['active', 'deploying', 'pending', 'error', 'stopped'][i]).glyph, + ); + expect(new Set(glyphs).size).toBe(glyphs.length); + glyphs.forEach((g) => expect(g.length).toBeGreaterThan(0)); + }); + + it('pulses only while work is in flight', () => { + expect(posterStatusGlyph('deploying').pulse).toBe(true); + expect(posterStatusGlyph('queued').pulse).toBe(true); + expect(posterStatusGlyph('deployed').pulse).toBe(false); + expect(posterStatusGlyph('error').pulse).toBe(false); + expect(posterStatusGlyph('idle').pulse).toBe(false); + }); +}); diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/socket-dot.test.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/socket-dot.test.tsx new file mode 100644 index 00000000..80529b2a --- /dev/null +++ b/packages/ui/src/features/canvas/components/nodes/_shared/__tests__/socket-dot.test.tsx @@ -0,0 +1,73 @@ +/** + * SocketDot — focused on the AX4 reduced-motion gate for the pulsing halo. + * + * The halo's pulse is an SVG SMIL ``, which the CSS + * `prefers-reduced-motion` net does NOT cover — so it's gated in JS via + * `prefersReducedMotion()`. These tests drive that branch by stubbing + * `window.matchMedia` and walking the returned element tree for ``. + */ + +import React from 'react'; +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { SocketDot, type SocketDotProps } from '../socket-dot'; + +function* walk(node: React.ReactNode): Generator { + if (node == null || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') return; + if (Array.isArray(node)) { + for (const c of node) yield* walk(c as React.ReactNode); + return; + } + const el = node as React.ReactElement; + yield el; + const children = (el.props as { children?: React.ReactNode } | undefined)?.children; + if (children != null) yield* walk(children); +} + +const render = (props: Partial = {}) => + (SocketDot as unknown as (p: SocketDotProps) => React.ReactElement)({ + socketId: 's1', + nodeId: 'n1', + side: 'right', + role: 'database', + shape: 'circle', + direction: 'out', + label: 'DB out', + cx: 10, + cy: 20, + state: 'snapped', + ...props, + }); + +const hasAnimate = (tree: React.ReactNode) => [...walk(tree)].some((el) => el.type === 'animate'); + +const setReducedMotion = (matches: boolean) => { + vi.stubGlobal('window', { matchMedia: () => ({ matches }) }); +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('SocketDot — reduced-motion halo gate (AX4)', () => { + it('renders the pulsing SMIL for a snapped port when motion is allowed', () => { + setReducedMotion(false); + expect(hasAnimate(render({ state: 'snapped' }))).toBe(true); + }); + + it('renders the pulse for a source-active port when motion is allowed', () => { + setReducedMotion(false); + expect(hasAnimate(render({ state: 'source-active' }))).toBe(true); + }); + + it('omits the when the user prefers reduced motion', () => { + setReducedMotion(true); + expect(hasAnimate(render({ state: 'snapped' }))).toBe(false); + expect(hasAnimate(render({ state: 'source-active' }))).toBe(false); + }); + + it('never animates a resting (idle) port regardless of the preference', () => { + setReducedMotion(false); + // idle has no halo at all, so certainly no . + expect(hasAnimate(render({ state: 'idle' }))).toBe(false); + }); +}); diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/card-shell.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/card-shell.tsx index 20d1e92f..67771662 100644 --- a/packages/ui/src/features/canvas/components/nodes/_shared/card-shell.tsx +++ b/packages/ui/src/features/canvas/components/nodes/_shared/card-shell.tsx @@ -32,10 +32,14 @@ import { getPortsForNode } from '@ice/types'; import React, { useCallback, useState, type ReactNode } from 'react'; import { getNodeDragState } from './connection-drag-context'; import { ConnectionDragGlow } from './connection-drag-glow'; +import { NodeDeployOverlay } from './node-deploy-overlay'; +import { useNodeValidation } from './node-validation-context'; import { useIsNodeOrphan } from './orphan-context'; +import { posterStatusGlyph } from './poster-status'; import { ProviderPill } from './provider-pill'; import { StatusDot } from './status-dot'; import { TypedSockets } from './typed-sockets'; +import { ValidationBadge } from './validation-badge'; import { getBrandIcon } from '../../../../../assets/icons/brand-registry'; import { getServiceName } from '../../../../../assets/icons/service-names'; import { CATEGORY_STYLE, CORNER_RADIUS, STATUS_COLORS } from '../../../../../config/canvas-constants'; @@ -153,6 +157,12 @@ export const CardShell: React.FC = ({ const provider = (data?.provider as string) || ''; const region = (data?.region as string) || ''; const deployStatus = (data?.deploy_status as string) || ''; + // CNV1 — in-flight step + failure reason, surfaced on the node (not just the + // deploy panel). Same data the deploy panel writes onto every node. + const deployProgress = data?.deploy_progress as + | { step_label?: string; step_index?: number; step_total?: number } + | undefined; + const deployError = (data?.deploy_error as string) || ''; // brandOverride wins (e.g. frontend blocks pass `framework` so React / // Next.js / Vue render in the header). Fall through to the iceType @@ -171,6 +181,8 @@ export const CardShell: React.FC = ({ const statusColor = STATUS_COLORS[deployStatus] || STATUS_COLORS.idle; const statusLabel = deployStatus ? deployStatus.charAt(0).toUpperCase() + deployStatus.slice(1) : ''; + // CNV7/AX5 — non-colour, AT-reachable status cue for the poster view. + const posterStatus = posterStatusGlyph(deployStatus); const rawIsSource = connectionDragState === 'source'; const rawIsValidTarget = connectionDragState === 'valid-target'; @@ -199,6 +211,12 @@ export const CardShell: React.FC = ({ // the indicator while no drag is active, so the orphan warning // doesn't compete with the valid/invalid border during a connect. const isOrphan = useIsNodeOrphan(node.id) && connectionDragState === null; + // CNV2 — config/validation issues, surfaced on the node (read via context so + // they needn't thread through every concept-node component). 'info' is not + // badge-worthy; only error/warning render a corner dot. + const validation = useNodeValidation(node.id); + const validationSeverity = validation && validation.severity !== 'info' ? validation.severity : null; + const validationCount = validation?.count ?? 0; // Ports are always rendered (so users can SEE the connection // affordance on a fresh block instead of having to discover it via // hover). Opacity carries the state: faint at idle, full on @@ -363,16 +381,28 @@ export const CardShell: React.FC = ({
    {deployStatus && ( + // CNV7/AX5 — pair the hue with a shape glyph + an `aria-label` + // (not a mouse-only `title`), so deploying / done / failed are + // distinguishable without colour and reachable by AT. + > + {posterStatus.glyph} + )}
    @@ -616,7 +646,32 @@ export const CardShell: React.FC = ({ > {liveConfig || ''} - {deployStatus && } + {deployStatus && ( + + )} + + )} + + {/* CNV1 — live deploy progress / error overlay, raised above the + status footer when one is shown. */} + + + {/* CNV2 — validation badge (config errors/warnings) in the top-right corner. */} + {validationSeverity && ( +
    +
    )} diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/node-deploy-overlay.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/node-deploy-overlay.tsx new file mode 100644 index 00000000..891749d4 --- /dev/null +++ b/packages/ui/src/features/canvas/components/nodes/_shared/node-deploy-overlay.tsx @@ -0,0 +1,75 @@ +/** + * NodeDeployOverlay — live deploy feedback for CardShell-based nodes (CNV1). + * + * The deploy panel writes `deploy_status` / `deploy_progress` / `deploy_error` + * onto every node's data (see `use-deploy-subscription.ts`), but the CardShell + * family only rendered a footer status dot — the in-flight step and the failure + * reason were visible only on the legacy `CompactLod3` path. This component + * surfaces those two missing pieces ON the node, so a developer watching a + * Postgres/backend block during a deploy sees "building (3/6)" and, on failure, + * the inline error — without opening the deploy panel. + * + * Absolute-positioned at the bottom of the (relative) card so it overlays the + * body rather than reflowing layout as status changes. Visual styling mirrors + * the `CompactLod3` overlay verbatim (blue progress / red error, mono font). + */ + +import React from 'react'; +import { FONT_MONO } from './fonts'; + +interface DeployProgress { + step_label?: string; + step_index?: number; + step_total?: number; +} + +interface NodeDeployOverlayProps { + deployStatus: string; + deployProgress?: DeployProgress; + deployError?: string; + /** Distance from the card bottom, in px. CardShell raises it above the + * status footer; the default suits footer-less compact cards. */ + bottom?: number; +} + +function baseStyle(bottom: number): React.CSSProperties { + return { + position: 'absolute', + left: 8, + right: 8, + bottom, + fontSize: 10, + fontFamily: FONT_MONO, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + pointerEvents: 'none', + }; +} + +export const NodeDeployOverlay: React.FC = ({ + deployStatus, + deployProgress, + deployError, + bottom = 4, +}) => { + if (deployStatus === 'deploying' && deployProgress?.step_label) { + const { step_label, step_index, step_total } = deployProgress; + const text = step_index != null && step_total != null ? `${step_label} (${step_index}/${step_total})` : step_label; + return ( +
    + {text} +
    + ); + } + + if (deployStatus === 'error' && deployError) { + return ( +
    + ✗ {deployError} +
    + ); + } + + return null; +}; diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/node-validation-context.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/node-validation-context.tsx new file mode 100644 index 00000000..9224ab93 --- /dev/null +++ b/packages/ui/src/features/canvas/components/nodes/_shared/node-validation-context.tsx @@ -0,0 +1,35 @@ +/** + * Cheap context for "does this node have a validation issue?" (CNV2). + * + * Mirrors `orphan-context`: computing per-node validation via Redux at every + * CardShell render would mean N subscriptions per canvas render. Instead the + * canvas orchestrator already builds a `nodeValidationMap` (issue → per-node + * {severity, count}) once per render; it's broadcast here so CardShell can do + * an O(1) `Map.get()` and render its own validation badge — without threading + * `validationSeverity`/`validationCount` through every concept-node component. + * + * Nodes outside a provider get `undefined` (no badge) — the right behavior for + * the tree-walker unit tests, which invoke nodes as plain functions. + */ + +import { createContext, useContext } from 'react'; + +export interface NodeValidationInfo { + severity: 'error' | 'warning' | 'info'; + count: number; +} + +const NodeValidationContext = createContext>(new Map()); + +export const NodeValidationProvider = NodeValidationContext.Provider; + +export function useNodeValidation(nodeId: string): NodeValidationInfo | undefined { + const ctx = useContext(NodeValidationContext) as unknown; + // Defensive: the node unit tests mock `useContext` to a bare value (a Set) + // shared across every context. Only call `.get` when it's actually a Map so + // those harnesses get `undefined` (no badge) instead of a TypeError. + if (ctx && typeof (ctx as Map).get === 'function') { + return (ctx as Map).get(nodeId); + } + return undefined; +} diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/poster-status.ts b/packages/ui/src/features/canvas/components/nodes/_shared/poster-status.ts new file mode 100644 index 00000000..848fd6d9 --- /dev/null +++ b/packages/ui/src/features/canvas/components/nodes/_shared/poster-status.ts @@ -0,0 +1,80 @@ +/** + * Poster-view status glyph (CNV7 / AX5). + * + * The zoomed-out poster card (CardShell's `lod < 3` branch) surfaced a node's + * deploy/runtime state as a bare coloured dot whose only label was a mouse + * `title`. At the exact zoom where you survey the whole board to find "what + * failed / what's still deploying", that is hue-only and mouse-only: useless to + * colour-blind users and to keyboard/AT users, and it makes deploying vs done vs + * failed indistinguishable without hovering each card. + * + * This buckets the broad node-status vocabulary (the keys of `STATUS_COLORS`) + * into a small tone set and pairs each tone with a NON-COLOUR glyph, so terminal + * / in-flight / failed read differently from shape alone. The colour is kept as + * a redundant cue for those who can see it; the glyph + an `aria-label` carry + * the meaning for everyone else. + * + * Distinct from the IA4 `deployStatusMeta` vocabulary, which normalises the + * deploy-*operation* lifecycle (planning/deploying/success/error/…). Node + * statuses are broader and include runtime/health words (active/running/healthy/ + * drifted/stopped/…), so they get their own bucketing here. + */ + +export type PosterStatusTone = 'good' | 'in-flight' | 'warn' | 'error' | 'neutral'; + +export interface PosterStatusGlyph { + tone: PosterStatusTone; + /** A single shape-distinct glyph, shown *alongside* the colour (not replacing it). */ + glyph: string; + /** Pulse while work is in flight, consistent with the LOD-3 footer dot (CNV3). */ + pulse: boolean; +} + +const TONE: Record = { + // terminal-healthy + active: 'good', + running: 'good', + healthy: 'good', + deployed: 'good', + // work in flight (an operation is running — colour still conveys danger for + // destroying/deleting; the glyph + pulse convey "not finished yet") + creating: 'in-flight', + updating: 'in-flight', + deploying: 'in-flight', + planning: 'in-flight', + destroying: 'in-flight', + deleting: 'in-flight', + queued: 'in-flight', + // needs-attention but not failed + pending: 'warn', + warning: 'warn', + drifted: 'warn', + // failed + error: 'error', + failed: 'error', + // off / didn't-run + stopped: 'neutral', + inactive: 'neutral', + idle: 'neutral', + skipped: 'neutral', + cancelled: 'neutral', +}; + +// Glyphs chosen for universal font coverage and shape-distinctness at ~12px: +// check / ellipsis (working) / bang / cross / middot. +const TONE_GLYPH: Record = { + good: '✓', + 'in-flight': '…', + warn: '!', + error: '✕', + neutral: '·', +}; + +export function posterStatusTone(raw: string | undefined | null): PosterStatusTone { + return (raw && TONE[raw]) || 'neutral'; +} + +export function posterStatusGlyph(raw: string | undefined | null): PosterStatusGlyph { + const tone = posterStatusTone(raw); + return { tone, glyph: TONE_GLYPH[tone], pulse: tone === 'in-flight' }; +} diff --git a/packages/ui/src/features/canvas/components/nodes/_shared/socket-dot.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/socket-dot.tsx index 7889a0d4..0c4eb27d 100644 --- a/packages/ui/src/features/canvas/components/nodes/_shared/socket-dot.tsx +++ b/packages/ui/src/features/canvas/components/nodes/_shared/socket-dot.tsx @@ -31,6 +31,7 @@ import { CATEGORY_COLORS, type ConnectionCategory } from '@ice/constants'; import { ROLE_CATEGORY, type PortDef } from '@ice/types'; import React from 'react'; import { CATEGORY_STYLE } from '../../../../../config/canvas-constants'; +import { prefersReducedMotion } from '../../../../../shared/hooks/use-reduced-motion'; export type DotState = 'idle' | 'compatible' | 'snapped' | 'incompatible' | 'source-active'; @@ -95,6 +96,9 @@ export const SocketDot: React.FC = ({ }) => { const category: ConnectionCategory = ROLE_CATEGORY[role]; const color = socketColor(role, peerStyle); + // AX4 — SMIL `` ignores the CSS `prefers-reduced-motion` net, so the + // pulsing halo is gated here. One-shot read (no hook) keeps SocketDot pure. + const reducedMotion = prefersReducedMotion(); // Drag-aware sizing — compatible ports grow to invite, snapped grows // most + pulses, incompatible shrinks slightly. @@ -153,7 +157,7 @@ export const SocketDot: React.FC = ({ opacity={haloOpacity} pointerEvents="none" > - {(state === 'snapped' || state === 'source-active') && ( + {(state === 'snapped' || state === 'source-active') && !reducedMotion && ( = memo(({ color, label, radius = 3 }) => ( +export const StatusDot: React.FC = memo(({ color, label, radius = 3, pulse = false }) => ( void = () => {}): React.ReactElement => { +const renderCB = (onClick: (e: React.MouseEvent) => void = () => {}, copied?: boolean): React.ReactElement => { const Inner = ( CopyButton as unknown as { - type: (p: { onClick: (e: React.MouseEvent) => void }) => React.ReactElement; + type: (p: { onClick: (e: React.MouseEvent) => void; copied?: boolean }) => React.ReactElement; } ).type; - return Inner({ onClick }); + return Inner({ onClick, copied }); }; describe('CopyButton', () => { @@ -49,4 +49,17 @@ describe('CopyButton', () => { const tree = renderCB(); expect((tree.props as { type: string }).type).toBe('button'); }); + + // OL7 — confirmation state after a successful copy. + it('shows "COPIED" with a green tint when copied=true', () => { + const tree = renderCB(() => {}, true); + expect((tree.props as { children: string }).children).toBe('COPIED'); + expect((tree.props as { style: { color: string } }).style.color).toBe('#22c55e'); + }); + + it('shows "COPY" in the default tint when copied is falsy', () => { + const tree = renderCB(() => {}, false); + expect((tree.props as { children: string }).children).toBe('COPY'); + expect((tree.props as { style: { color: string } }).style.color).toBe('var(--ice-text-tertiary)'); + }); }); diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/index.test.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/index.test.tsx index 45863c17..8baded15 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/index.test.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/index.test.tsx @@ -79,7 +79,7 @@ vi.mock('react', async (importOriginal) => { }; }); -import { SvgLogNode } from '..'; +import { SvgLogNode, formatCopyTimestamp, formatLogCopyLine } from '..'; import type { CanvasNode } from '../../../svg-canvas'; const MockLogContent = mocks.LogContent; @@ -456,7 +456,8 @@ describe('SvgLogNode — fold/copy/wheel handlers', () => { stopPropagation: () => {}, } as unknown as React.MouseEvent); expect(writes).toHaveLength(1); - expect(writes[0]).toBe('12:34:56 [INFO] hello\n12:34:57 [ERROR] bye'); + // OL7 — copy carries the FULL date+time (display column only shows HH:MM:SS). + expect(writes[0]).toBe('2025-04-27 12:34:56 [INFO] hello\n2025-04-27 12:34:57 [ERROR] bye'); } finally { Object.defineProperty(globalThis, 'navigator', { value: original, configurable: true, writable: true }); } @@ -482,7 +483,7 @@ describe('SvgLogNode — fold/copy/wheel handlers', () => { } }); - it('handleCopyLine writes single-line + sets copied + clears after 1s', () => { + it('handleCopyLine writes single-line (full ts) + sets copied on success + clears after 1s', async () => { const writes: string[] = []; const original = (globalThis as unknown as { navigator?: unknown }).navigator; Object.defineProperty(globalThis, 'navigator', { @@ -504,15 +505,23 @@ describe('SvgLogNode — fold/copy/wheel handlers', () => { const onCopyLine = ( lc.props as { onCopyLine: ( - log: { id: string; timestamp: string; level: string; message: string }, + log: { id: string; timestamp: string; tsFull?: string; level: string; message: string }, e: React.MouseEvent, ) => void; } ).onCopyLine; - onCopyLine({ id: 'l1', timestamp: '12:34:56', level: 'info', message: 'hi' }, { - stopPropagation: () => {}, - } as unknown as React.MouseEvent); - expect(writes).toEqual(['12:34:56 [INFO] hi']); + onCopyLine( + { id: 'l1', timestamp: '12:34:56', tsFull: '2025-04-27T12:34:56.000Z', level: 'info', message: 'hi' }, + { + stopPropagation: () => {}, + } as unknown as React.MouseEvent, + ); + // OL7 — copy uses the full timestamp (write is synchronous). + expect(writes).toEqual(['2025-04-27 12:34:56 [INFO] hi']); + // OL8 — the "copied" flash is gated on the clipboard write succeeding, so + // it fires on a microtask, not synchronously. Flush the writeText→then→ + // catch→then chain (several hops). + for (let i = 0; i < 5; i += 1) await Promise.resolve(); // Slot 4 is copiedLine. expect(mocks.state.stateSetters[4]).toHaveBeenCalledWith('l1'); mocks.state.stateSetters[4].mockClear(); @@ -524,6 +533,50 @@ describe('SvgLogNode — fold/copy/wheel handlers', () => { } }); + // OL8 — a failed clipboard write must NOT show the false-positive "copied" flash. + it('handleCopyLine does NOT flash copied when the clipboard write rejects', async () => { + const original = (globalThis as unknown as { navigator?: unknown }).navigator; + Object.defineProperty(globalThis, 'navigator', { + value: { clipboard: { writeText: () => Promise.reject(new Error('denied')) } }, + configurable: true, + writable: true, + }); + try { + const tree = renderLN(); + const lc = findByType(tree, MockLogContent)[0]; + const onCopyLine = (lc.props as { onCopyLine: (log: Record, e: React.MouseEvent) => void }) + .onCopyLine; + onCopyLine({ id: 'l1', timestamp: '12:34:56', level: 'info', message: 'hi' }, { + stopPropagation: () => {}, + } as unknown as React.MouseEvent); + for (let i = 0; i < 5; i += 1) await Promise.resolve(); + expect(mocks.state.stateSetters[4]).not.toHaveBeenCalledWith('l1'); + } finally { + Object.defineProperty(globalThis, 'navigator', { value: original, configurable: true, writable: true }); + } + }); + + // OL8 — when the clipboard API is unavailable, copy is a no-op (no false flash). + it('handleCopyLine is a no-op when navigator.clipboard is unavailable', async () => { + const original = (globalThis as unknown as { navigator?: unknown }).navigator; + Object.defineProperty(globalThis, 'navigator', { value: {}, configurable: true, writable: true }); + try { + const tree = renderLN(); + const lc = findByType(tree, MockLogContent)[0]; + const onCopyLine = (lc.props as { onCopyLine: (log: Record, e: React.MouseEvent) => void }) + .onCopyLine; + expect(() => + onCopyLine({ id: 'l1', timestamp: '12:34:56', level: 'info', message: 'hi' }, { + stopPropagation: () => {}, + } as unknown as React.MouseEvent), + ).not.toThrow(); + for (let i = 0; i < 5; i += 1) await Promise.resolve(); + expect(mocks.state.stateSetters[4]).not.toHaveBeenCalledWith('l1'); + } finally { + Object.defineProperty(globalThis, 'navigator', { value: original, configurable: true, writable: true }); + } + }); + it('onWheel: scroll up sets isAutoScroll=false and clamps offset', () => { mocks.state.logStream = { entries: Array.from({ length: 30 }, (_, i) => ({ @@ -691,3 +744,26 @@ describe('SvgLogNode — FoldedBadge in folded mode', () => { expect((badge.props as { logCount: number }).logCount).toBe(2); }); }); + +// ─── OL7 copy helpers (pure) ──────────────────────────────────────────────── + +describe('formatCopyTimestamp (OL7)', () => { + it('renders the full date + time, dropping the millis/Z', () => { + expect(formatCopyTimestamp('2025-04-27T12:34:56.789Z')).toBe('2025-04-27 12:34:56'); + }); + it('returns empty for missing/garbage input', () => { + expect(formatCopyTimestamp(undefined)).toBe(''); + expect(formatCopyTimestamp('')).toBe(''); + }); +}); + +describe('formatLogCopyLine (OL7)', () => { + it('uses the full timestamp when present', () => { + expect( + formatLogCopyLine({ timestamp: '12:34:56', tsFull: '2025-04-27T12:34:56.000Z', level: 'error', message: 'boom' }), + ).toBe('2025-04-27 12:34:56 [ERROR] boom'); + }); + it('falls back to the display timestamp when no full ts (e.g. placeholder row)', () => { + expect(formatLogCopyLine({ timestamp: '12:34:56', level: 'info', message: 'hi' })).toBe('12:34:56 [INFO] hi'); + }); +}); diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/live-indicator.test.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/live-indicator.test.tsx index 0ca7be1a..1bf9a41b 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/live-indicator.test.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/live-indicator.test.tsx @@ -90,13 +90,27 @@ describe('LiveIndicator', () => { expect((findLabel(tree)!.props as { children: string }).children).toBe('ERROR'); }); - it('grey + IDLE for pre-deploy / no-source / ambiguous / unsupported / idle', () => { - for (const s of ['pre-deploy', 'no-source', 'ambiguous', 'unsupported', 'idle'] as LogStreamStatus[]) { + // OL6 — non-streaming source states stay grey (to match the panel's pillFor) + // but now carry a DISTINCT label instead of all collapsing to "IDLE". + it('grey + distinct label per non-streaming source state', () => { + const cases: Array<[LogStreamStatus, string]> = [ + ['pre-deploy', 'WAITING'], + ['no-source', 'NO SOURCE'], + ['ambiguous', 'CHOOSE SRC'], + ['unsupported', 'UNSUPPORTED'], + ['provider-unsupported', 'UNAVAILABLE'], + ['idle', 'IDLE'], + ]; + const seen = new Set(); + for (const [s, expected] of cases) { const tree = renderLI(s); - const dot = findDot(tree)!; - expect((dot.props as { style: { background: string } }).style.background).toBe('#94a3b8'); - expect((findLabel(tree)!.props as { children: string }).children).toBe('IDLE'); + expect((findDot(tree)!.props as { style: { background: string } }).style.background).toBe('#94a3b8'); + const label = (findLabel(tree)!.props as { children: string }).children; + expect(label).toBe(expected); + seen.add(label); } + // The five source states + idle are mutually distinguishable. + expect(seen.size).toBe(cases.length); }); it('default (unknown status) → grey + IDLE', () => { diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/log-header.test.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/log-header.test.tsx index e49f5f67..f43d2763 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/log-header.test.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/log-header.test.tsx @@ -127,6 +127,15 @@ describe('LogHeader', () => { expect((btn.props as { onClick: () => void }).onClick).toBe(cp); }); + // OL7 — keep the button mounted (and forward `copied`) right after a copy, + // even once the pointer has left, so the confirmation is visible. + it('renders CopyButton when copiedAll even if not hovered, and forwards copied', () => { + const tree = renderLH({ folded: false, isHovered: false, copiedAll: true }); + const btn = findByType(tree, MockCopyButton)[0]; + expect(btn).toBeDefined(); + expect((btn.props as { copied: boolean }).copied).toBe(true); + }); + it('always renders FoldButton', () => { expect(findByType(renderLH({ folded: false }), MockFoldButton)).toHaveLength(1); expect(findByType(renderLH({ folded: true }), MockFoldButton)).toHaveLength(1); diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/copy-button.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/copy-button.tsx index db05dcf0..2c6cd18b 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/copy-button.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/copy-button.tsx @@ -2,9 +2,11 @@ import React, { memo } from 'react'; interface CopyButtonProps { onClick: (e: React.MouseEvent) => void; + /** OL7 — show a transient "COPIED" confirmation after a successful copy. */ + copied?: boolean; } -export const CopyButton: React.FC = memo(({ onClick }) => ( +export const CopyButton: React.FC = memo(({ onClick, copied = false }) => ( )); diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/index.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/index.tsx index b3df0870..92ca94e0 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/index.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/index.tsx @@ -43,6 +43,33 @@ function formatTs(ts: string): string { return dotIdx > 0 ? after.slice(0, dotIdx) : after.slice(0, 8); } +// OL7 — full date+time for COPY (the display column only shows HH:MM:SS, which +// loses the date so a pasted line can't be correlated to the cloud console). +// '2025-04-27T12:34:56.000Z' → '2025-04-27 12:34:56'. +export function formatCopyTimestamp(ts: string | undefined): string { + if (!ts || typeof ts !== 'string') return ''; + return ts.replace('T', ' ').replace(/\.\d+/, '').replace('Z', '').trim(); +} + +// One copy line: full timestamp + level + message (falls back to the display +// timestamp when the full one isn't available, e.g. the placeholder row). +export function formatLogCopyLine(entry: Pick): string { + const ts = entry.tsFull ? formatCopyTimestamp(entry.tsFull) : entry.timestamp; + return `${ts} [${entry.level.toUpperCase()}] ${entry.message}`; +} + +// OL8 — write to the clipboard, resolving to whether it actually succeeded +// (false when the API is unavailable or the write rejects), so callers only +// show "copied" feedback on a real success instead of a false positive. +function writeClipboard(text: string): Promise { + const clip = (globalThis.navigator as Navigator | undefined)?.clipboard; + if (!clip?.writeText) return Promise.resolve(false); + return clip + .writeText(text) + .then(() => true) + .catch(() => false); +} + function placeholderText(status: LogStreamStatus, lastError: string | null): string { switch (status) { case 'pre-deploy': @@ -53,6 +80,9 @@ function placeholderText(status: LogStreamStatus, lastError: string | null): str return t('canvas.logNode.ambiguous'); case 'unsupported': return t('canvas.logNode.unsupported'); + case 'provider-unsupported': + // `lastError` carries the provider id (set by logs-slice on this state). + return t('canvas.logNode.providerUnsupported', { provider: lastError || 'this provider' }); case 'permission-denied': return lastError || t('canvas.logNode.permissionDenied'); case 'error': @@ -107,6 +137,7 @@ export const SvgLogNode: React.FC = memo( entries.map((e) => ({ id: e.insertId, timestamp: formatTs(e.ts), + tsFull: e.ts, level: mapLevel(e.level), service: serviceName, message: e.message, @@ -166,21 +197,28 @@ export const SvgLogNode: React.FC = memo( [logs.length, maxVisibleLogs], ); + // OL7/OL8 — copy the FULL timestamp (not the truncated display value), and + // only flash the "copied" confirmation once the write actually succeeds. const handleCopyAll = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - const text = logs.map((l) => `${l.timestamp} [${l.level.toUpperCase()}] ${l.message}`).join('\n'); - navigator.clipboard.writeText(text).catch(() => {}); + const text = logs.map(formatLogCopyLine).join('\n'); + writeClipboard(text).then((ok) => { + if (!ok) return; + setCopiedLine('all'); + setTimeout(() => setCopiedLine(null), 1000); + }); }, [logs], ); const handleCopyLine = useCallback((log: LogEntry, e: React.MouseEvent) => { e.stopPropagation(); - const text = `${log.timestamp} [${log.level.toUpperCase()}] ${log.message}`; - navigator.clipboard.writeText(text).catch(() => {}); - setCopiedLine(log.id); - setTimeout(() => setCopiedLine(null), 1000); + writeClipboard(formatLogCopyLine(log)).then((ok) => { + if (!ok) return; + setCopiedLine(log.id); + setTimeout(() => setCopiedLine(null), 1000); + }); }, []); // Visible logs (or single placeholder row when there's nothing live yet) @@ -230,6 +268,7 @@ export const SvgLogNode: React.FC = memo( status={status} onToggleFold={handleToggleFold} onCopyAll={handleCopyAll} + copiedAll={copiedLine === 'all'} /> {!folded && ( diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/live-indicator.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/live-indicator.tsx index 66ab98d1..d97724fc 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/live-indicator.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/live-indicator.tsx @@ -28,10 +28,21 @@ function specFor(status: LogStreamStatus): IndicatorSpec { case 'permission-denied': case 'error': return { tone: 'red', label: t('canvas.status.error'), pulse: false }; + // OL6 — these non-streaming states used to collapse to a single grey "IDLE", + // so surveying the board you couldn't tell "waiting on a deploy" from "no + // source connected" from "pick a source" without opening the panel. Give + // each a distinct label (tones kept grey to match the panel's `pillFor`, so + // the canvas dot and the panel pill still agree). case 'pre-deploy': + return { tone: 'grey', label: t('canvas.status.preDeploy'), pulse: false }; case 'no-source': + return { tone: 'grey', label: t('canvas.status.noSource'), pulse: false }; case 'ambiguous': + return { tone: 'grey', label: t('canvas.status.ambiguous'), pulse: false }; case 'unsupported': + return { tone: 'grey', label: t('canvas.status.unsupported'), pulse: false }; + case 'provider-unsupported': + return { tone: 'grey', label: t('canvas.status.providerUnsupported'), pulse: false }; case 'idle': default: return { tone: 'grey', label: t('canvas.status.idle'), pulse: false }; diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/log-header.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/log-header.tsx index 98ee18c7..cf515344 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/log-header.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/log-header.tsx @@ -11,10 +11,12 @@ interface LogHeaderProps { status: LogStreamStatus; onToggleFold: (e: React.MouseEvent) => void; onCopyAll: (e: React.MouseEvent) => void; + /** OL7 — true briefly after a successful copy-all, so the button confirms. */ + copiedAll?: boolean; } export const LogHeader: React.FC = memo( - ({ label, folded, isHovered, status, onToggleFold, onCopyAll }) => ( + ({ label, folded, isHovered, status, onToggleFold, onCopyAll, copiedAll }) => (
    = memo( canvas header and the properties pill never disagree. */} {!folded && } - {/* Copy all button */} - {!folded && isHovered && } + {/* Copy all button — stays visible briefly after a copy to show the + confirmation even if the pointer has left the header. */} + {!folded && (isHovered || copiedAll) && } {/* Fold button */} diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/types.ts b/packages/ui/src/features/canvas/components/nodes/log-node/types.ts index 1df78896..abee49ba 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/types.ts +++ b/packages/ui/src/features/canvas/components/nodes/log-node/types.ts @@ -12,7 +12,11 @@ export interface SvgLogNodeProps { export interface LogEntry { id: string; + /** Truncated HH:MM:SS for the fixed-width display column. */ timestamp: string; + /** OL7 — the full source timestamp, used for copy fidelity (the display + * `timestamp` drops the date so a pasted line can't be correlated). */ + tsFull?: string; level: 'info' | 'warn' | 'error' | 'debug'; service: string; message: string; diff --git a/packages/ui/src/features/canvas/components/nodes/redis-cache/index.tsx b/packages/ui/src/features/canvas/components/nodes/redis-cache/index.tsx index 5a71dabe..4637e6da 100644 --- a/packages/ui/src/features/canvas/components/nodes/redis-cache/index.tsx +++ b/packages/ui/src/features/canvas/components/nodes/redis-cache/index.tsx @@ -20,7 +20,10 @@ export function computeRedisCacheHeight(): number { return DB_HEADER_HEIGHT + DB_PADDING + DB_BODY_HEIGHT + DB_PADDING + CARD_FOOTER_HEIGHT; } -const REDIS_ACCENT = '#ef4444'; +// CNV6 — Redis's brand red, deliberately NOT the system error red (#ef4444, +// used by the validation/deploy-error treatments) so an idle Redis block doesn't +// read as "errored". Brand colour keeps it recognisable without the collision. +const REDIS_ACCENT = '#d82c20'; function formatMemory(raw: unknown): string | null { if (raw == null || raw === '') return null; diff --git a/packages/ui/src/features/canvas/components/socket-legend.tsx b/packages/ui/src/features/canvas/components/socket-legend.tsx new file mode 100644 index 00000000..fa4beec9 --- /dev/null +++ b/packages/ui/src/features/canvas/components/socket-legend.tsx @@ -0,0 +1,54 @@ +/** + * Socket legend (CCL6). + * + * The canvas encodes a port's connection category in BOTH the socket's shape + * and colour (see `ROLE_SHAPE` / `ROLE_CATEGORY` in @ice/types and + * `CATEGORY_COLORS` in @ice/constants), but nothing decoded that visual language + * for the user — first-timers had no way to learn that a diamond means a source + * repo or a square means a domain. This renders that key. + * + * Kept in sync with the real maps by sourcing the colours from `CATEGORY_COLORS` + * and mirroring `ROLE_SHAPE` (square=domain, diamond=repository, ring=config, + * circle=traffic/data — the default). + */ + +import { CATEGORY_COLORS } from '@ice/constants'; +import React from 'react'; +import { t } from '../../../i18n'; + +type Shape = 'circle' | 'ring' | 'diamond' | 'square'; + +const LEGEND: { shape: Shape; color: string; labelKey: string }[] = [ + { shape: 'circle', color: CATEGORY_COLORS.traffic, labelKey: 'canvas.socketLegend.traffic' }, + { shape: 'ring', color: CATEGORY_COLORS.config, labelKey: 'canvas.socketLegend.config' }, + { shape: 'diamond', color: CATEGORY_COLORS.pipeline, labelKey: 'canvas.socketLegend.repository' }, + { shape: 'square', color: CATEGORY_COLORS.dns, labelKey: 'canvas.socketLegend.domain' }, +]; + +/** A 12×12 glyph mirroring the canvas socket shapes. */ +export const SocketGlyph: React.FC<{ shape: Shape; color: string }> = ({ shape, color }) => ( + +); + +export const SocketLegend: React.FC = () => ( +
    +
    + {t('canvas.socketLegend.title')} +
    +
    + {LEGEND.map((row) => ( +
    + + + + {t(row.labelKey)} +
    + ))} +
    +
    +); diff --git a/packages/ui/src/features/canvas/components/svg-canvas.tsx b/packages/ui/src/features/canvas/components/svg-canvas.tsx index 153b6765..9c46ad0f 100644 --- a/packages/ui/src/features/canvas/components/svg-canvas.tsx +++ b/packages/ui/src/features/canvas/components/svg-canvas.tsx @@ -21,6 +21,7 @@ import { ConnectionTooltip } from './connection-tooltip'; import { CanvasContextMenu } from './context/canvas-context-menu'; import { ControlsHelpModal } from './controls-help-modal'; import { CanvasDeployBanner } from './deploy-banner'; +import { EmptyCanvasOverlay } from './empty-canvas-overlay'; // Bespoke-from-day-one nodes with inline editing import { useClipboard } from '../../../shared/hooks/use-clipboard'; import { useExposedServices } from '../../../shared/hooks/use-exposed-services'; @@ -51,6 +52,7 @@ import { useRenameState } from '../hooks/use-rename-state'; import { useRenderCtx } from '../hooks/use-render-ctx'; import { isContainerNode } from '../utils/node-classification'; import { ConnectionDragProvider } from './nodes/_shared/connection-drag-context'; +import { NodeValidationProvider } from './nodes/_shared/node-validation-context'; import { OrphanNodesProvider } from './nodes/_shared/orphan-context'; import { SocketHoverTooltip } from './nodes/_shared/socket-hover-tooltip'; import type { AppDispatch } from '../../../store'; @@ -194,7 +196,7 @@ export const SvgCanvas: React.FC = ({ cardId, paneId, onFocus }) // reset on card change + AI intent. Per blueprint risk #8 the // setOverlayDismissed setter is preserved verbatim despite no // current reader — a future unit will surface the boolean. - useCanvasSideEffects({ + const { overlayDismissed, dismissOverlay } = useCanvasSideEffects({ card, nodes, edges, @@ -439,43 +441,45 @@ export const SvgCanvas: React.FC = ({ cardId, paneId, onFocus }) `./canvas-renderer/canvas-content`. Visual draw order, prop flow, and dep arrays are preserved verbatim. */} - - - + + + + + @@ -485,6 +489,10 @@ export const SvgCanvas: React.FC = ({ cardId, paneId, onFocus }) {/* Socket hover chip — instant styled tooltip on socket dot hover. */} + {/* Empty-canvas quick-start hint — only on a card with zero nodes, + until dismissed (resets per card / on AI intent). */} + {card && nodes.length === 0 && !overlayDismissed && } + {/* Controls help button — bottom-right */} @@ -492,15 +500,18 @@ export const SvgCanvas: React.FC = ({ cardId, paneId, onFocus }) {/* Shift+A spotlight add-block menu + the key listener that opens it. */} - +
    ); }; -const SpotlightMount: React.FC<{ screenToCanvas: (cx: number, cy: number) => { x: number; y: number } }> = ({ - screenToCanvas, -}) => { - useSpotlightShortcut({ screenToCanvas }); +const SpotlightMount: React.FC<{ + screenToCanvas: (cx: number, cy: number) => { x: number; y: number }; + canvasLocked: boolean; +}> = ({ screenToCanvas, canvasLocked }) => { + // CD6 — a locked canvas disables the right-click "Add Block" path; gate the + // Shift+A shortcut the same way so the lock isn't silently defeated. + useSpotlightShortcut({ screenToCanvas, enabled: !canvasLocked }); useGroupShortcut(); return ; }; diff --git a/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-drop.test.tsx b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-drop.test.tsx index 9400aaef..a017858a 100644 --- a/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-drop.test.tsx +++ b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-drop.test.tsx @@ -483,6 +483,9 @@ describe('useCanvasDrop — block drop without blueprint (fallthrough)', () => { expect(a1.payload.data.iceType).toBe('Resource.Unknown'); // Label sourced from application/ice-block-name fallback chain. expect(a1.payload.data.label).toBe('Fallback Label'); + // PE6 — name is seeded too (from label) so the node isn't born missing a + // required name; matches the blueprint path. + expect(a1.payload.data.name).toBe('Fallback Label'); // logBlueprint did NOT fire (no blueprint). expect(mocks.logBlueprintSpy).not.toHaveBeenCalled(); }); diff --git a/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-effects.test.tsx b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-effects.test.tsx index cbfb1bc1..69af0977 100644 --- a/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-effects.test.tsx +++ b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-effects.test.tsx @@ -261,6 +261,36 @@ describe('useCanvasEffects — wheel listener effect', () => { expect(onWheel.mock.calls[0][0]).toBe(fakeEvent); }); + it('skips canvas zoom (OL1) when the wheel target is inside a log node', () => { + const addEventListener = vi.fn(); + const fakeSvg = { + addEventListener, + removeEventListener: vi.fn(), + } as unknown as SVGSVGElement; + const svgRef = { current: fakeSvg } as React.RefObject; + const onWheel = vi.fn(); + const setConnTooltip = vi.fn(); + + renderHook(makeStore(), { + cardId: 'c', + svgRef, + bindCanvas: { onWheel }, + setConnTooltip: setConnTooltip as any, + }); + + const handler = addEventListener.mock.calls[0][1] as (e: WheelEvent) => void; + const preventDefault = vi.fn(); + // target.closest('.svg-log-node') truthy → the log node owns this wheel. + const target = { closest: vi.fn((sel: string) => (sel === '.svg-log-node' ? {} : null)) }; + const fakeEvent = { preventDefault, target } as unknown as WheelEvent; + handler(fakeEvent); + + expect(target.closest).toHaveBeenCalledWith('.svg-log-node'); + expect(preventDefault).not.toHaveBeenCalled(); + expect(onWheel).not.toHaveBeenCalled(); + expect(setConnTooltip).not.toHaveBeenCalled(); + }); + it('cleanup removes the wheel listener', () => { const removeEventListener = vi.fn(); const fakeSvg = { diff --git a/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-side-effects.test.tsx b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-side-effects.test.tsx index e0bc84fd..458a114f 100644 --- a/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-side-effects.test.tsx +++ b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-side-effects.test.tsx @@ -337,10 +337,15 @@ describe('useCanvasSideEffects — auto-organize threshold', () => { expect(dispatch).not.toHaveBeenCalled(); vi.advanceTimersByTime(100); - expect(dispatch).toHaveBeenCalledTimes(1); + // CCL1 — import-organize now also switches to the rectangular edge style so + // the computed dagre routes actually render. + expect(dispatch).toHaveBeenCalledTimes(2); const action = dispatch.mock.calls[0][0] as { type: string; payload: unknown }; expect(action.type).toBe('cards/autoOrganizeCard'); expect(action.payload).toEqual({ zoom: 0.8 }); + const edgeAction = dispatch.mock.calls[1][0] as { type: string; payload: unknown }; + expect(edgeAction.type).toBe('ui/setEdgeStyle'); + expect(edgeAction.payload).toBe('rectangular'); }); it('does NOT dispatch when delta is below the > 10 threshold (5 → 8)', () => { @@ -374,7 +379,7 @@ describe('useCanvasSideEffects — auto-organize threshold', () => { renderHook(baseArgs({ nodes, dispatch: dispatch as unknown as UseCanvasSideEffectsArgs['dispatch'] })); vi.advanceTimersByTime(100); - expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledTimes(2); // CCL1 — organize + setEdgeStyle const action = dispatch.mock.calls[0][0] as { type: string }; expect(action.type).toBe('cards/autoOrganizeCard'); }); @@ -407,7 +412,7 @@ describe('useCanvasSideEffects — auto-organize threshold', () => { expect(mocks.refSlots[0].current).toBe(12); expect(dispatch).not.toHaveBeenCalled(); vi.advanceTimersByTime(100); - expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledTimes(2); // CCL1 — organize + setEdgeStyle }); it('clears the auto-organize timer on cleanup (no double-dispatch on rapid re-render)', () => { @@ -560,13 +565,13 @@ describe('useCanvasSideEffects — overlay-dismiss on AI intent', () => { }); describe('useCanvasSideEffects — return shape', () => { - it('returns void', () => { + it('returns the overlay-dismiss state + dismiss callback', () => { let result: unknown = 'not-set'; const Probe: React.FC = () => { result = useCanvasSideEffects(baseArgs()); return React.createElement('div'); }; renderToString(React.createElement(Probe)); - expect(result).toBeUndefined(); + expect(result).toEqual({ overlayDismissed: false, dismissOverlay: expect.any(Function) }); }); }); diff --git a/packages/ui/src/features/canvas/hooks/__tests__/use-connection-drawing.test.tsx b/packages/ui/src/features/canvas/hooks/__tests__/use-connection-drawing.test.tsx index 45165bb3..ab3d4623 100644 --- a/packages/ui/src/features/canvas/hooks/__tests__/use-connection-drawing.test.tsx +++ b/packages/ui/src/features/canvas/hooks/__tests__/use-connection-drawing.test.tsx @@ -421,6 +421,54 @@ describe('useConnectionDrawing — connectionDragTargets memo', () => { const result = captureHook(store, { effectiveNodes: nodes }); expect(result.connectionDragTargets).toBeNull(); }); + + // CCL7 — a role/category-compatible target that already violates a cardinality + // rule reads as invalid-target DURING the drag (was only rejected at drop). + it('downgrades a compatible target to invalid-target on a log-source cardinality conflict', () => { + const store = makeStore(); + startDrag({ sourceId: 'svc' }); + mocks.canConnectSpy.mockReturnValue(true); // both compatible by role + // n1 already has a log source wired → conflict; n2 is free. + mocks.findExistingLogSourceSpy.mockImplementation((_src: unknown, tgt: { id: string }) => ({ + conflict: tgt.id === 'n1', + })); + const nodes: CanvasNode[] = [makeNode({ id: 'svc' }), makeNode({ id: 'n1' }), makeNode({ id: 'n2' })]; + const result = captureHook(store, { + effectiveNodes: nodes, + card: { id: 'c', name: 'c', nodes, edges: [] } as unknown as Card, + }); + const targets = result.connectionDragTargets!; + expect(targets.get('n1')).toBe('invalid-target'); // cardinality conflict + expect(targets.get('n2')).toBe('valid-target'); // free + }); + + it('downgrades a compatible target to invalid-target on a special-connection cardinality conflict', () => { + const store = makeStore(); + startDrag({ sourceId: 'svc' }); + mocks.canConnectSpy.mockReturnValue(true); + mocks.findExistingSpecialConnectionSpy.mockImplementation((_src: unknown, tgt: { id: string }) => + tgt.id === 'n1' ? { specialType: 'source', conflict: true } : { specialType: null, conflict: false }, + ); + const nodes: CanvasNode[] = [makeNode({ id: 'svc' }), makeNode({ id: 'n1' }), makeNode({ id: 'n2' })]; + const result = captureHook(store, { + effectiveNodes: nodes, + card: { id: 'c', name: 'c', nodes, edges: [] } as unknown as Card, + }); + const targets = result.connectionDragTargets!; + expect(targets.get('n1')).toBe('invalid-target'); + expect(targets.get('n2')).toBe('valid-target'); + }); + + it('does not apply cardinality dimming when no card is provided', () => { + const store = makeStore(); + startDrag({ sourceId: 'svc' }); + mocks.canConnectSpy.mockReturnValue(true); + mocks.findExistingLogSourceSpy.mockReturnValue({ conflict: true }); + const nodes: CanvasNode[] = [makeNode({ id: 'svc' }), makeNode({ id: 'n1' })]; + // No `card` → the conflict check is skipped (can't read edges); stays valid. + const result = captureHook(store, { effectiveNodes: nodes }); + expect(result.connectionDragTargets!.get('n1')).toBe('valid-target'); + }); }); describe('useConnectionDrawing — handleConnectionMove', () => { diff --git a/packages/ui/src/features/canvas/hooks/use-canvas-drop.ts b/packages/ui/src/features/canvas/hooks/use-canvas-drop.ts index 14e96167..398b66cb 100644 --- a/packages/ui/src/features/canvas/hooks/use-canvas-drop.ts +++ b/packages/ui/src/features/canvas/hooks/use-canvas-drop.ts @@ -215,6 +215,9 @@ export function useCanvasDrop(args: UseCanvasDropArgs): UseCanvasDropResult { ? !isIceTypeEnabledForProvider(iceType, deployProvider) : false; const newNodeData = { + // PE6 — seed `name` (not just `label`) so the fallback node isn't born + // with a hidden "Name is required" error, matching the blueprint path. + name: label, label, iceType, behavior: 'singleton', diff --git a/packages/ui/src/features/canvas/hooks/use-canvas-effects.ts b/packages/ui/src/features/canvas/hooks/use-canvas-effects.ts index 5b6d9e30..8b61d59a 100644 --- a/packages/ui/src/features/canvas/hooks/use-canvas-effects.ts +++ b/packages/ui/src/features/canvas/hooks/use-canvas-effects.ts @@ -94,6 +94,15 @@ export function useCanvasEffects(args: UseCanvasEffectsArgs): void { const svg = svgRef.current; if (!svg) return; const handler = (e: WheelEvent) => { + // OL1 — let a log node own wheel events over itself so scrolling its + // buffer doesn't zoom the whole canvas. The node's own onWheel handler + // does the virtual scroll; the canvas just stays out of the way. (React's + // stopPropagation on the node can't stop this native ancestor listener, + // so the guard has to live here.) + const target = e.target as Element | null; + if (target?.closest?.('.svg-log-node')) { + return; + } e.preventDefault(); setConnTooltip(null); diff --git a/packages/ui/src/features/canvas/hooks/use-canvas-side-effects.ts b/packages/ui/src/features/canvas/hooks/use-canvas-side-effects.ts index 810d4b69..d61aa7b3 100644 --- a/packages/ui/src/features/canvas/hooks/use-canvas-side-effects.ts +++ b/packages/ui/src/features/canvas/hooks/use-canvas-side-effects.ts @@ -10,14 +10,14 @@ * `currentCount - prevCount > 10`. The bulk-import branch must NOT * trip on small blueprint drops (container + 1-3 children = 2-4 * nodes). Do NOT change the threshold. - * - **Risk #8**: `setOverlayDismissed(false)` is the empty-canvas - * overlay's reset setter — the getter is destructured-discarded - * (`const [, setOverlayDismissed] = useState(false)`) because the - * orchestrator currently has no consumer for the boolean. Both - * setter writes (the per-card-id reset and the per-AI-intent - * dismiss) are kept verbatim because a future unit will surface - * the value to a child overlay component. Don't "clean up" by - * dropping the setter or by rewriting it as a non-state value. + * - **Risk #8**: `overlayDismissed` gates the empty-canvas overlay + * (`EmptyCanvasOverlay`, mounted by `svg-canvas.tsx`). It resets to + * `false` on card change and is set `true` on AI intent (the user is + * driving the AI command bar, so the hint would be noise) or when the + * overlay's own dismiss button fires `dismissOverlay`. The hook now + * surfaces `{ overlayDismissed, dismissOverlay }` — the "future unit" + * the prior blueprint anticipated. Keep the per-card-id reset and the + * per-AI-intent dismiss writes verbatim. * * Six effects, in source order: * @@ -63,10 +63,11 @@ * rf-canv-22. */ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { logCanvasRender } from '../../../shared/utils/debug-logger'; import { inspectLayout, updateInspectorState, installInspector } from '../../../shared/utils/layout-inspector'; import { autoOrganizeCard, type Card, type CardNode, type CardEdge } from '../../../store/slices/cards-slice'; +import { setEdgeStyle } from '../../../store/slices/ui-slice'; import type { ViewLevel } from '../../../config/visualization-config'; import type { AppDispatch } from '../../../store'; import type { CanvasNode } from '../components/types'; @@ -94,7 +95,14 @@ export interface UseCanvasSideEffectsArgs { dispatch: AppDispatch; } -export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): void { +export interface UseCanvasSideEffectsResult { + /** True when the empty-canvas overlay should be hidden (dismissed or AI-driven). */ + overlayDismissed: boolean; + /** Marks the empty-canvas overlay dismissed (wired to its close button). */ + dismissOverlay: () => void; +} + +export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): UseCanvasSideEffectsResult { const { card, nodes, edges, canvasNodes, effectiveNodes, viewport, lod, viewLevel, aiCurrentIntent, dispatch } = args; // ── Effect 1: install inspector once on mount ────────────────────────── @@ -148,6 +156,10 @@ export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): void { if (currentCount > 0 && (prevCount === 0 || currentCount - prevCount > 10)) { const timer = setTimeout(() => { dispatch(autoOrganizeCard({ zoom: viewport.zoom })); + // CCL1 — the import-time organize is directional (master branch), so it + // computes orthogonal dagre routes; render them via the rectangular edge + // style instead of letting bezier discard them. + dispatch(setEdgeStyle('rectangular')); }, 100); prevNodeCountRef.current = currentCount; return () => clearTimeout(timer); @@ -169,9 +181,10 @@ export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): void { }); }, [canvasNodes.length, edges.length, effectiveNodes.length, viewLevel]); - // ── Overlay-dismiss state (rf-canv risk #8: read-but-never-read-back) ── + // ── Overlay-dismiss state (rf-canv risk #8: now surfaced to the overlay) ── // Dismiss state for the empty canvas overlay - const [, setOverlayDismissed] = useState(false); + const [overlayDismissed, setOverlayDismissed] = useState(false); + const dismissOverlay = useCallback(() => setOverlayDismissed(true), []); // ── Effect 5: reset overlay-dismiss when card changes ────────────────── // Reset when card changes @@ -190,4 +203,6 @@ export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): void { setOverlayDismissed(true); } }, [aiCurrentIntent]); + + return { overlayDismissed, dismissOverlay }; } diff --git a/packages/ui/src/features/canvas/hooks/use-connection-drawing.ts b/packages/ui/src/features/canvas/hooks/use-connection-drawing.ts index cc427261..8582fe29 100644 --- a/packages/ui/src/features/canvas/hooks/use-connection-drawing.ts +++ b/packages/ui/src/features/canvas/hooks/use-connection-drawing.ts @@ -314,6 +314,18 @@ export function useConnectionDrawing(args: UseConnectionDrawingArgs): UseConnect const srcKindForTargets = getBlockKind(srcIceType); + // CCL7 — a target that's otherwise role/category-compatible but already + // violates a cardinality rule (one Source/EnvVars per service; one source + // per log terminal) should read as `invalid-target` DURING the drag, not + // only get rejected at drop. Reuses the same pure conflict checks the drop + // handler runs, so drag-time dimming and drop-time rejection never disagree. + const violatesCardinality = (node: CanvasNode): boolean => { + if (!card) return false; + const special = findExistingSpecialConnection(sourceNode, node, card.edges as CardEdge[], effectiveNodes); + if (special.specialType && special.conflict) return true; + return findExistingLogSource(sourceNode, node, card.edges as CardEdge[]).conflict; + }; + for (const node of effectiveNodes) { if (node.id === drawingConnection.sourceId) continue; const tgtIceType = (node.data?.iceType as string) || ''; @@ -328,7 +340,8 @@ export function useConnectionDrawing(args: UseConnectionDrawingArgs): UseConnect const tgtPorts = getPortsForNode({ id: node.id, type: node.type, data: node.data }); const tgtKind = getBlockKind(tgtIceType); const matching = findMatchingPorts(sourcePort, tgtPorts, srcKindForTargets, tgtKind); - targets.set(node.id, matching.length > 0 ? 'valid-target' : 'invalid-target'); + const ok = matching.length > 0 && !violatesCardinality(node); + targets.set(node.id, ok ? 'valid-target' : 'invalid-target'); continue; } // Legacy body drag (no typed source port) — fall back to the @@ -339,10 +352,10 @@ export function useConnectionDrawing(args: UseConnectionDrawingArgs): UseConnect tgtNode: node, allNodes: effectiveNodes, }); - targets.set(node.id, categoryAllowed ? 'valid-target' : 'invalid-target'); + targets.set(node.id, categoryAllowed && !violatesCardinality(node) ? 'valid-target' : 'invalid-target'); } return targets; - }, [drawingConnection, effectiveNodes]); + }, [drawingConnection, effectiveNodes, card]); /** Start drawing a connection from a port */ const handleConnectionPortDown = useCallback( diff --git a/packages/ui/src/features/cost/components/cost-panel.tsx b/packages/ui/src/features/cost/components/cost-panel.tsx index 6af9e280..dbcbca40 100644 --- a/packages/ui/src/features/cost/components/cost-panel.tsx +++ b/packages/ui/src/features/cost/components/cost-panel.tsx @@ -134,6 +134,12 @@ export const CostPanel: React.FC = () => { {/* ── Total monthly cost hero ──────────────────────────────── */}
    + + {t('cost.estQualifier')} + {formatCostRaw(totalMonthlyCost)} diff --git a/packages/ui/src/features/deploy/components/__tests__/destroy-confirm-modal.test.tsx b/packages/ui/src/features/deploy/components/__tests__/destroy-confirm-modal.test.tsx index 6f79e923..4421fe6d 100644 --- a/packages/ui/src/features/deploy/components/__tests__/destroy-confirm-modal.test.tsx +++ b/packages/ui/src/features/deploy/components/__tests__/destroy-confirm-modal.test.tsx @@ -330,14 +330,13 @@ describe('DestroyConfirmModal — heading branches', () => { expect(collectText(findHeading(tree))).toBe('Destroy all infrastructure?'); }); - it('seeds destroyEverything from useState initial value (resources.length === 0) when resources empty', () => { - // Simulate the first-render path: the source's useState call uses - // `useState(resources.length === 0)`. The mock should observe the - // initial-value argument when seeded; the helper here flips the seed flag. + it('defaults to the safe single-deploy scope even when no resources are tracked (DE3)', () => { + // The source now uses `useState(false)` — "destroy everything" is an + // explicit opt-in, never the default, even with an empty tracked list. (mocks as unknown as { __seedFromInitial: boolean }).__seedFromInitial = true; const tree = renderModal(makeProps({ resources: [] })); - // Initial value is `true` → heading reflects the destroyEverything branch. - expect(collectText(findHeading(tree))).toBe('Destroy all infrastructure?'); + // Initial value is `false` → heading reflects the safe single-deploy branch. + expect(collectText(findHeading(tree))).toBe('Destroy deployment?'); }); it('seeds destroyEverything to false when resources is non-empty (initial value false)', () => { diff --git a/packages/ui/src/features/deploy/components/banners/__tests__/api-error-banner.test.tsx b/packages/ui/src/features/deploy/components/banners/__tests__/api-error-banner.test.tsx index 87de3d74..534d80fd 100644 --- a/packages/ui/src/features/deploy/components/banners/__tests__/api-error-banner.test.tsx +++ b/packages/ui/src/features/deploy/components/banners/__tests__/api-error-banner.test.tsx @@ -157,6 +157,7 @@ type BannerProps = { error: string; results: Array<{ error?: string; api_enable_url?: string }>; onRetryDeploy: () => void; + provider?: string; }; const renderBanner = (props: BannerProps): React.ReactElement => @@ -196,6 +197,32 @@ beforeEach(() => { // ─── Tests ────────────────────────────────────────────────────────────────── +describe('ApiErrorBanner — provider neutrality (DE2)', () => { + it('renders a plain error card (no GCP banners/links) for a non-GCP provider', () => { + // Even if the error would classify as a GCP quota, a non-GCP provider must + // never see the GCP cascade — classification is skipped entirely. + mocks.classifyDeployErrorSpy.mockReturnValue('quota'); + const tree = renderBanner(makeProps({ error: 'quota exceeded', provider: 'aws' })); + const stubs = findByPredicate( + tree, + (el) => (el.props as Record)?.['data-test-id'] === 'quota-stub', + ); + expect(stubs.length).toBe(0); + expect(findLinks(tree).length).toBe(0); + expect(mocks.classifyDeployErrorSpy).not.toHaveBeenCalled(); + }); + + it('keeps the GCP cascade when provider is "gcp"', () => { + mocks.classifyDeployErrorSpy.mockReturnValue('quota'); + const tree = renderBanner(makeProps({ error: 'quota', provider: 'gcp' })); + const stubs = findByPredicate( + tree, + (el) => (el.props as Record)?.['data-test-id'] === 'quota-stub', + ); + expect(stubs.length).toBe(1); + }); +}); + describe('ApiErrorBanner — quota branch', () => { it('renders QuotaErrorBanner when classifyDeployError returns "quota"', () => { mocks.classifyDeployErrorSpy.mockReturnValue('quota'); diff --git a/packages/ui/src/features/deploy/components/banners/api-error-banner.tsx b/packages/ui/src/features/deploy/components/banners/api-error-banner.tsx index 41e812db..90e78c73 100644 --- a/packages/ui/src/features/deploy/components/banners/api-error-banner.tsx +++ b/packages/ui/src/features/deploy/components/banners/api-error-banner.tsx @@ -16,8 +16,30 @@ export const ApiErrorBanner: React.FC<{ error: string; results: Array<{ error?: string; api_enable_url?: string }>; onRetryDeploy: () => void; -}> = ({ error, results, onRetryDeploy }) => { + /** + * Active deploy provider. The quota/billing/RAPT/API sub-banners and every + * link below are GCP-specific (console.cloud.google.com, GCP IAM, the GCP + * orphan-cleanup CTA), so they only render for GCP (DE2). Any other provider + * gets the plain error card — no misleading Google links. `undefined` keeps + * the legacy GCP behaviour so existing callers/tests are unaffected. + */ + provider?: string; +}> = ({ error, results, onRetryDeploy, provider }) => { const { t } = useTranslation(); + + // DE2 — for non-GCP providers, never run the GCP-shaped classification (which + // can mis-fire on a generic "quota exceeded" and even surface the GCP cleanup + // button). Show the raw error in a neutral card instead. + const isGcp = provider === undefined || provider === 'gcp'; + if (!isGcp) { + return ( +
    + + {error} +
    + ); + } + // Collect all unique enable URLs from results and error message, then // classify the error into one of five priority-cascade kinds. Both the // collection loop and the cascade live in `utils/error-classification` diff --git a/packages/ui/src/features/deploy/components/deploy-panel.tsx b/packages/ui/src/features/deploy/components/deploy-panel.tsx index 8eabdd29..2465050c 100644 --- a/packages/ui/src/features/deploy/components/deploy-panel.tsx +++ b/packages/ui/src/features/deploy/components/deploy-panel.tsx @@ -5,7 +5,7 @@ * Flow: Configure → Plan → Review → Deploy → Results */ -import { Rocket } from 'lucide-react'; +import { Rocket, DollarSign } from 'lucide-react'; import React, { useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { ApiErrorBanner } from './banners/api-error-banner'; @@ -34,6 +34,8 @@ import { resetDeploy, appendLog, } from '../../../store/slices/deploy-slice'; +import { parseCostRange } from '../../cost/utils/cost-calculator'; +import { classifyDeployError } from '../utils/error-classification'; import { useDeployActions } from '../hooks/use-deploy-actions'; import { useDeployEffects } from '../hooks/use-deploy-effects'; import { useDestroyAction } from '../hooks/use-destroy-action'; @@ -71,11 +73,12 @@ export const DeployPanel: React.FC = () => { // handleClose live in `useDeployActions`. The hook returns them as an // object with the same dispatch ordering and retry-after-auth re-dispatch // semantics as the inline source. - const { fetchRequirements, handleVerifyRequirement, handlePlan, handleDeploy, handleClose } = useDeployActions({ - activeCard: activeCard ?? null, - deploy, - pendingRetryRef, - }); + const { fetchRequirements, handleVerifyRequirement, handlePlan, handleDeploy, handleClose, handleAuthenticate } = + useDeployActions({ + activeCard: activeCard ?? null, + deploy, + pendingRetryRef, + }); // ─── Side-effects ───────────────────────────────────────────────────── // @@ -108,6 +111,33 @@ export const DeployPanel: React.FC = () => { // Keep gcpNodes alias for backward compat within this component const gcpNodes = providerNodes; + // DE1 — surface remediation + AI-diagnose for partial async failures too, not + // only when a top-level `deploy.error` string exists. The async + // complete→hydrate path leaves `deploy.error` null even when some resources + // failed, so derive a summary from the failed rows and gate on either signal. + const failedResults = deploy.results.filter((r) => !r.success); + const hasFailure = deploy.error != null || failedResults.length > 0; + const effectiveError = + deploy.error ?? + (failedResults.length > 0 + ? `${failedResults.length} resource(s) failed: ${failedResults.map((r) => `${r.type}/${r.name}`).join(', ')}` + : ''); + + // DE7 — a RAPT/reauth failure means the cached OAuth session is no longer + // usable, but the ConfigSection's connection pill (which only re-checks on a + // provider change) keeps showing green "Connected". Flag it so the pill can + // downgrade to a reconnect warning instead of contradicting the error banner. + const authErrorActive = hasFailure && classifyDeployError(effectiveError, deploy.results) === 'rapt'; + + // DF3 — surface an estimated monthly cost at the commit moment. Reuses the + // same per-node estimate + parser the status bar/cost panel use, so the + // number matches. Labeled "est." (see Phase 1 OS5) — it's a design-time + // estimate from list prices, not billed spend. + const estMonthlyCost = (activeCard?.nodes ?? []).reduce( + (sum, n) => sum + parseCostRange((n.data?.estimatedCost as string) || ''), + 0, + ); + const header = (
    + {/* DF3 — estimated monthly cost at the commit moment */} + {estMonthlyCost > 0 && ( +
    +
    + )}
    {/* Previously deployed resources */} @@ -168,7 +212,7 @@ export const DeployPanel: React.FC = () => { )} {/* Authenticating */} - {deploy.status === 'authenticating' && } + {deploy.status === 'authenticating' && handleAuthenticate()} />} {/* Phase 8 — block requirements (DNS, verification, cert, GitHub repo, etc.) */} {(deploy.requirements.length > 0 || deploy.requirementsLoading) && ( @@ -179,17 +223,34 @@ export const DeployPanel: React.FC = () => { /> )} - {/* Plan preview */} - {deploy.plan && } + {/* Plan preview — echo the destination so the plan is self-describing (DF2) */} + {deploy.plan && ( + + )} {/* Pre-deploy security + cost analysis (AI-Native #3) */} {preDeployAnalysis && } - {/* Error */} - {deploy.error && ( + {/* Error — shown for a top-level error OR any failed result row (DE1) */} + {hasFailure && ( <> - - + + )} @@ -214,7 +275,7 @@ export const DeployPanel: React.FC = () => { the prior summary should still be visible. */} {deploy.results.length > 0 && deploy.status !== 'deploying' && deploy.status !== 'destroying' && (
    - +
    )} diff --git a/packages/ui/src/features/deploy/components/destroy-confirm-modal.tsx b/packages/ui/src/features/deploy/components/destroy-confirm-modal.tsx index b9ccd00e..38c330bf 100644 --- a/packages/ui/src/features/deploy/components/destroy-confirm-modal.tsx +++ b/packages/ui/src/features/deploy/components/destroy-confirm-modal.tsx @@ -21,7 +21,11 @@ export const DestroyConfirmModal: React.FC<{ onConfirm: (destroyEverything: boolean) => void; }> = ({ cardName, resources, onCancel, onConfirm }) => { const [typed, setTyped] = React.useState(''); - const [destroyEverything, setDestroyEverything] = React.useState(resources.length === 0); + // DE3 — default to the SAFE single-deploy scope, even when nothing is tracked. + // "Destroy everything" (which walks all history incl. failed/partial deploys + // and deletes every ICE-managed resource it finds) must be an explicit opt-in, + // never the default — the empty-tracked-list case shows guidance to enable it. + const [destroyEverything, setDestroyEverything] = React.useState(false); const canConfirm = typed.trim() === cardName; React.useEffect(() => { diff --git a/packages/ui/src/features/deploy/components/plan-preview.tsx b/packages/ui/src/features/deploy/components/plan-preview.tsx index 3182a782..ddb8e79d 100644 --- a/packages/ui/src/features/deploy/components/plan-preview.tsx +++ b/packages/ui/src/features/deploy/components/plan-preview.tsx @@ -20,7 +20,7 @@ import { useTranslation } from '../../../i18n'; import { cn } from '../../../shared/utils/cn'; import type { DeployPlan } from '../../../store/slices/deploy-slice'; -export const PlanPreview: React.FC<{ plan: DeployPlan }> = ({ plan }) => { +export const PlanPreview: React.FC<{ plan: DeployPlan; destination?: string }> = ({ plan, destination }) => { const { t } = useTranslation(); const creates = Array.isArray(plan.creates) ? plan.creates : []; const updates = Array.isArray(plan.updates) ? plan.updates : []; @@ -31,6 +31,11 @@ export const PlanPreview: React.FC<{ plan: DeployPlan }> = ({ plan }) => { if (total === 0 && skipped.length === 0) { return (
    + {destination && ( +
    + {t('deploy.plan.destination', { target: destination })} +
    + )} {t('deploy.plan.noChanges')}
    ); @@ -42,6 +47,15 @@ export const PlanPreview: React.FC<{ plan: DeployPlan }> = ({ plan }) => { {t('deploy.plan.changes', { total })} + {/* DF2 — make the plan self-describing: what destination it targets. */} + {destination && ( +
    + {t('deploy.plan.destination', { target: destination })} +
    + )}
    {creates.map((r, i) => ( diff --git a/packages/ui/src/features/deploy/components/results-summary.tsx b/packages/ui/src/features/deploy/components/results-summary.tsx index 73120369..804f0f88 100644 --- a/packages/ui/src/features/deploy/components/results-summary.tsx +++ b/packages/ui/src/features/deploy/components/results-summary.tsx @@ -27,6 +27,7 @@ import React from 'react'; import { useTranslation } from '../../../i18n'; import { cn } from '../../../shared/utils/cn'; import { isApiNotEnabledError, extractApiEnableUrl } from '../../../shared/utils/gcp-errors'; +import { classifyError } from '../error-remediation'; import { primaryOutput } from '../output-extractors'; import { openExternalUrl } from '../utils/open-external-url'; import { buildResultsSummaryText, summaryCounts } from '../utils/results-summary-text'; @@ -43,7 +44,14 @@ export const ResultsSummary: React.FC<{ outputs?: Record; duration_ms?: number; }>; -}> = ({ results }) => { + /** + * Active deploy provider. The remediation table's wording + links are + * GCP-shaped, so plain-language guidance is only surfaced when this is + * `'gcp'`. Other providers keep the raw-error rendering until the table + * is made provider-aware (ux-fix-plan Phase 3.3 / DE2). + */ + provider?: string; +}> = ({ results, provider }) => { const { t } = useTranslation(); const { succeeded, failed, totalMs, allOk } = summaryCounts(results); @@ -243,16 +251,45 @@ export const ResultsSummary: React.FC<{
    ); } + // Plain-language remediation (title + explanation + any + // external links). GCP-gated — see the `provider` prop note. + // onClick actions (retry/authenticate) are intentionally not + // rendered here: they need deploy-action callbacks wired in + // Phase 2.4. The raw error + [copy] is always kept below. + const remediation = provider === 'gcp' ? classifyError(r.error) : null; + const remediationLinks = remediation?.actions.filter((a) => a.href) ?? []; return ( -
    - {r.error} - +
    + {remediation && ( +
    +
    {remediation.title}
    +
    {remediation.explanation}
    + {remediationLinks.length > 0 && ( +
    + {remediationLinks.map((a, ai) => ( + + ))} +
    + )} +
    + )} +
    + {r.error} + +
    ); })()} diff --git a/packages/ui/src/features/deploy/components/sections/__tests__/auth-banner.test.tsx b/packages/ui/src/features/deploy/components/sections/__tests__/auth-banner.test.tsx index 81da69a0..51f7fb4f 100644 --- a/packages/ui/src/features/deploy/components/sections/__tests__/auth-banner.test.tsx +++ b/packages/ui/src/features/deploy/components/sections/__tests__/auth-banner.test.tsx @@ -26,10 +26,14 @@ const mocks = vi.hoisted(() => ({ Loader2: vi.fn((props: { className?: string }) => React.createElement('span', { 'data-icon': 'Loader2', className: props.className }, 'Loader2'), ), + ExternalLink: vi.fn((props: { className?: string }) => + React.createElement('span', { 'data-icon': 'ExternalLink', className: props.className }, 'ExternalLink'), + ), })); vi.mock('lucide-react', () => ({ Loader2: mocks.Loader2, + ExternalLink: mocks.ExternalLink, })); // `useTranslation` mock — identity `t(key) => key` for exact-string assertions. @@ -109,7 +113,8 @@ function collectText(tree: React.ReactNode): string { // ─── Helpers ──────────────────────────────────────────────────────────────── -const renderBanner = (): React.ReactElement => (AuthBanner as unknown as () => React.ReactElement)(); +const renderBanner = (props: { onReopen?: () => void } = {}): React.ReactElement => + (AuthBanner as unknown as (p: { onReopen?: () => void }) => React.ReactElement)(props); // ─── Tests ────────────────────────────────────────────────────────────────── @@ -221,3 +226,31 @@ describe('AuthBanner — translation keys (text-only sweep)', () => { expect(new Set(authKeys)).toEqual(new Set(['deploy.auth.connecting', 'deploy.auth.browserPrompt'])); }); }); + +// ─── DF9 — reopen sign-in affordance ──────────────────────────────────────── + +describe('AuthBanner — reopen sign-in (DF9)', () => { + it('does not render the reopen button when no onReopen is supplied', () => { + const tree = renderBanner(); + expect(collectText(tree)).not.toContain('deploy.auth.reopen'); + const buttons = findByPredicate(tree, (el) => el.type === 'button'); + expect(buttons).toHaveLength(0); + }); + + it('renders the noWindow hint + reopen button when onReopen is supplied', () => { + const tree = renderBanner({ onReopen: vi.fn() }); + const text = collectText(tree); + expect(text).toContain('deploy.auth.noWindow'); + expect(text).toContain('deploy.auth.reopen'); + const buttons = findByPredicate(tree, (el) => el.type === 'button'); + expect(buttons).toHaveLength(1); + }); + + it('invokes onReopen when the button is clicked', () => { + const onReopen = vi.fn(); + const tree = renderBanner({ onReopen }); + const button = findByPredicate(tree, (el) => el.type === 'button')[0]; + (button.props as { onClick: () => void }).onClick(); + expect(onReopen).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/ui/src/features/deploy/components/sections/__tests__/config-section.test.tsx b/packages/ui/src/features/deploy/components/sections/__tests__/config-section.test.tsx index 3464b527..e07e4e46 100644 --- a/packages/ui/src/features/deploy/components/sections/__tests__/config-section.test.tsx +++ b/packages/ui/src/features/deploy/components/sections/__tests__/config-section.test.tsx @@ -239,6 +239,7 @@ type SectionProps = { region: string; environment: string; disabled: boolean; + authError?: boolean; projectId?: string; onProviderChange: (v: string) => void; onProjectChange: (v: string) => void; @@ -490,6 +491,33 @@ describe('ConfigSection — connection-status banner', () => { expect(text).not.toContain(' via Service Account'); }); + // DE7 — an active reauth/RAPT failure suppresses the green pill even though + // the cached connection still reads as "connected". + it('authError=true suppresses the green pill and shows the reconnect warning', () => { + mocks.providerConnectedRef.current = true; + mocks.authTypeRef.current = 'oauth'; + const tree = renderSection(makeProps({ provider: 'gcp', authError: true })); + + // No green connected pill… + const emerald = findByPredicate( + tree, + (el) => + typeof (el.props as { className?: string }).className === 'string' && + (el.props as { className: string }).className.includes('text-emerald-500'), + ); + expect(emerald.length).toBe(0); + // …an amber warning instead, with the reauth-needed copy. + const amber = findByPredicate( + tree, + (el) => + typeof (el.props as { className?: string }).className === 'string' && + (el.props as { className: string }).className.includes('text-amber-500'), + ); + expect(amber.length).toBe(1); + expect(mocks.tSpy).toHaveBeenCalledWith('deploy.status.reauthNeeded', { provider: 'Google Cloud' }); + expect(mocks.tSpy).not.toHaveBeenCalledWith('deploy.status.connected', { provider: 'Google Cloud' }); + }); + it('providerConnected=false → renders AlertCircle, not-connected text; no green banner', () => { mocks.providerConnectedRef.current = false; mocks.authTypeRef.current = null; diff --git a/packages/ui/src/features/deploy/components/sections/auth-banner.tsx b/packages/ui/src/features/deploy/components/sections/auth-banner.tsx index 198bebaf..ae290ff1 100644 --- a/packages/ui/src/features/deploy/components/sections/auth-banner.tsx +++ b/packages/ui/src/features/deploy/components/sections/auth-banner.tsx @@ -12,11 +12,16 @@ * conditional stays at the orchestrator's call site so the component's job * stays the banner, not the gate. */ -import { Loader2 } from 'lucide-react'; +import { Loader2, ExternalLink } from 'lucide-react'; import React from 'react'; import { useTranslation } from '../../../../i18n'; -export const AuthBanner: React.FC = () => { +export const AuthBanner: React.FC<{ + /** DF9 — re-launch the provider sign-in flow when the pop-up was blocked, + * dismissed, or never appeared (the state otherwise sits in 'authenticating' + * with no recovery path other than re-clicking Plan/Deploy). */ + onReopen?: () => void; +}> = ({ onReopen }) => { const { t } = useTranslation(); return ( @@ -26,6 +31,19 @@ export const AuthBanner: React.FC = () => { {t('deploy.auth.connecting')}

    {t('deploy.auth.browserPrompt')}

    + {onReopen && ( +
    + {t('deploy.auth.noWindow')} + +
    + )} ); }; diff --git a/packages/ui/src/features/deploy/components/sections/config-section.tsx b/packages/ui/src/features/deploy/components/sections/config-section.tsx index f6f42907..da535b1a 100644 --- a/packages/ui/src/features/deploy/components/sections/config-section.tsx +++ b/packages/ui/src/features/deploy/components/sections/config-section.tsx @@ -12,12 +12,14 @@ * * Extracted in rf-pdpl-18 from `deploy-panel.tsx` lines 829–954. * - * Public-API contract — preserved verbatim: - * - The four underscore-prefixed parameter names (`_environment`, - * `_projectId`, `_onProviderChange`, `_onEnvironmentChange`) reflect - * props that the parent passes through but this component does NOT - * consume; renaming them would break the call site or future parents - * that expect to wire those callbacks. + * Public-API contract: + * - `environment` IS now consumed — rendered as a read-only chip (DF1/EI1) so + * the user can see which environment a deploy targets. It is display-only + * here; the environment is chosen via the env tab bar and synced into the + * deploy slice on panel open (see `useDeployEffects`). + * - The remaining underscore-prefixed params (`_projectId`, `_onProviderChange`, + * `_onEnvironmentChange`) are passed through but NOT consumed; renaming them + * would break the call site or future parents that expect to wire them. * - The `'Not set'` literal fallback in the provider read-only chip is a * visible UI string that test fixtures and E2E specs match on. * - The `id="ice-deploy-input-project"` on the text-input fallback is an @@ -42,6 +44,10 @@ export const ConfigSection: React.FC<{ region: string; environment: string; disabled: boolean; + /** DE7 — an active RAPT/reauth deploy failure means the cached session is no + * longer usable; suppress the green "Connected" pill so it doesn't contradict + * the error banner. */ + authError?: boolean; projectId?: string; onProviderChange: (v: string) => void; onProjectChange: (v: string) => void; @@ -51,8 +57,9 @@ export const ConfigSection: React.FC<{ provider, gcpProject, region, - environment: _environment, + environment, disabled, + authError = false, projectId: _projectId, onProviderChange: _onProviderChange, onProjectChange, @@ -62,6 +69,12 @@ export const ConfigSection: React.FC<{ const { t } = useTranslation(); const regions = PROVIDER_REGIONS[provider] || PROVIDER_REGIONS.gcp!; const projectMeta = PROVIDER_PROJECT_LABELS[provider] || PROVIDER_PROJECT_LABELS.gcp!; + const envLabel = + environment === 'production' + ? t('deploy.config.envProduction') + : environment === 'staging' + ? t('deploy.config.envStaging') + : t('deploy.config.envDevelopment'); const [providerConnected, setProviderConnected] = React.useState(false); const [connectedProjects, setConnectedProjects] = React.useState>([]); const [authType, setAuthType] = React.useState(null); @@ -91,8 +104,10 @@ export const ConfigSection: React.FC<{ return (
    - {/* Connection status */} - {providerConnected && ( + {/* Connection status — DE7: an active reauth/RAPT failure suppresses the + green pill (the cached session is stale), showing a reconnect warning + so the pill never contradicts the error banner. */} + {providerConnected && !authError && (
    @@ -101,16 +116,18 @@ export const ConfigSection: React.FC<{
    )} - {!providerConnected && ( + {(!providerConnected || authError) && (
    - {t('deploy.status.notConnected', { provider: PROVIDER_LABELS[provider] || provider })} + {authError + ? t('deploy.status.reauthNeeded', { provider: PROVIDER_LABELS[provider] || provider }) + : t('deploy.status.notConnected', { provider: PROVIDER_LABELS[provider] || provider })}
    )} -
    +
    {/* Provider — read-only, set in project settings */}
    @@ -119,6 +136,17 @@ export const ConfigSection: React.FC<{
    + {/* Environment — read-only, chosen via the env tab bar (DF1/EI1) */} +
    + +
    + {envLabel} +
    +
    + {/* Project / Account — dropdown if connected projects available, text input otherwise */}
    diff --git a/packages/ui/src/features/deploy/hooks/__tests__/use-deploy-effects.test.tsx b/packages/ui/src/features/deploy/hooks/__tests__/use-deploy-effects.test.tsx index f3b11f21..e4dd5076 100644 --- a/packages/ui/src/features/deploy/hooks/__tests__/use-deploy-effects.test.tsx +++ b/packages/ui/src/features/deploy/hooks/__tests__/use-deploy-effects.test.tsx @@ -936,9 +936,11 @@ describe('useDeployEffects — return shape', () => { expect((result.logEndRef as { current: unknown }).current).toBeNull(); }); - it('registers exactly four effects per render', () => { + it('registers exactly five effects per render', () => { const store = makeStore(); captureHook({ activeCard: ACTIVE_CARD, store }); - expect(mocks.effects).toHaveLength(4); + // 4 original (auto-scroll, auto-detect, requirement-listener, hydrate) + the + // DF1/EI1 env-sync effect appended last. + expect(mocks.effects).toHaveLength(5); }); }); diff --git a/packages/ui/src/features/deploy/hooks/__tests__/use-destroy-action.test.tsx b/packages/ui/src/features/deploy/hooks/__tests__/use-destroy-action.test.tsx index da2b7fd0..5d0e1a6e 100644 --- a/packages/ui/src/features/deploy/hooks/__tests__/use-destroy-action.test.tsx +++ b/packages/ui/src/features/deploy/hooks/__tests__/use-destroy-action.test.tsx @@ -408,6 +408,33 @@ describe('handleDestroyConfirm — destroyAll path error', () => { expect(types).not.toContain('deploy/resetDeploy'); }); + // DE5 — a partial destroy (some deleted, some failed) must NOT fall through + // to resetDeploy (which wiped the failure logs); it surfaces an error + keeps + // the logs/overlay so the failure stays visible. + it('on partial destroy (some failed), surfaces an error and skips the log-wiping cleanup', async () => { + const store = makeStore(); + mockDeployApi.destroyAll.mockResolvedValueOnce({ + success: true, + deleted: [{ id: 'r1' }], + failed: [{ type: 'gcp.sql.instance', name: 'db', error: 'still has dependents' }], + }); + const dispatchSpy = vi.spyOn(store, 'dispatch'); + const { actions } = captureHook({ activeCard: ACTIVE_CARD, store }); + dispatchSpy.mockClear(); + + await actions.handleDestroyConfirm(true); + + const types = dispatchSpy.mock.calls.map((c) => asAction(c[0]).type); + // summary log + failure log + error; NO cleanup (logs preserved). + expect(types).toEqual(['deploy/startDestroying', 'deploy/appendLog', 'deploy/appendLog', 'deploy/deployError']); + expect(types).not.toContain('deploy/resetDeploy'); + expect(types).not.toContain('cards/clearCardDeployOverlay'); + expect(types).not.toContain('deploy/setDeployedResources'); + const errorAction = asAction(dispatchSpy.mock.calls[dispatchSpy.mock.calls.length - 1][0]); + expect(errorAction.type).toBe('deploy/deployError'); + expect(errorAction.payload).toContain('1 resource could not be destroyed'); + }); + it('on success: false && !deleted with no error, falls back to "Destroy failed with no details"', async () => { const store = makeStore(); mockDeployApi.destroyAll.mockResolvedValueOnce({ success: false }); @@ -444,12 +471,14 @@ describe('handleDestroyConfirm — destroyAll path error', () => { ]); }); - it('on success: false but deleted present, falls through to cleanup (the success-or-deleted branch)', async () => { + it('on success: false but deleted present (and no failures), falls through to cleanup (the success-or-deleted branch)', async () => { const store = makeStore(); + // DE5 — with NO failures, a deleted-present result still runs the cleanup. + // (The with-failures case is covered separately above: error + no wipe.) mockDeployApi.destroyAll.mockResolvedValueOnce({ success: false, deleted: [{ id: 'r1' }], - failed: [{ type: 'cloudrun', name: 'svc-a', error: 'partial' }], + failed: [], }); const dispatchSpy = vi.spyOn(store, 'dispatch'); const { actions } = captureHook({ activeCard: ACTIVE_CARD, store }); @@ -458,11 +487,10 @@ describe('handleDestroyConfirm — destroyAll path error', () => { await actions.handleDestroyConfirm(true); const types = dispatchSpy.mock.calls.map((c) => asAction(c[0]).type); - // Important: NO deployError, but full cleanup. + // Important: NO deployError, but full cleanup (one appendLog — the summary). expect(types).toEqual([ 'deploy/startDestroying', 'deploy/appendLog', - 'deploy/appendLog', 'cards/clearCardDeployOverlay', 'deploy/setDeployedResources', 'deploy/resetDeploy', diff --git a/packages/ui/src/features/deploy/hooks/use-deploy-effects.ts b/packages/ui/src/features/deploy/hooks/use-deploy-effects.ts index 473df5fc..99c67d3e 100644 --- a/packages/ui/src/features/deploy/hooks/use-deploy-effects.ts +++ b/packages/ui/src/features/deploy/hooks/use-deploy-effects.ts @@ -38,11 +38,12 @@ */ import { useEffect, useRef } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getApi } from '../../../shared/api/api-adapter'; import { hydrateDeployFromHistory, setDeployedResources, + setEnvironment, setGcpProject, setProvider, setRegion, @@ -50,9 +51,17 @@ import { type DeployState, } from '../../../store/slices/deploy-slice'; import { PROVIDER_REGIONS, detectDominantProvider } from '../utils/provider-regions'; -import type { AppDispatch } from '../../../store'; +import type { AppDispatch, RootState } from '../../../store'; import type { Card } from '../../../store/slices/cards-slice'; +// DF1/EI1 — map an Environment.type (production|staging|development|pr) onto the +// deploy slice's narrower enum. 'pr' (ephemeral preview) deploys as development. +function mapEnvTypeToDeployEnv(type: string | undefined): 'development' | 'staging' | 'production' { + if (type === 'production') return 'production'; + if (type === 'staging') return 'staging'; + return 'development'; +} + export interface UseDeployEffectsArgs { isOpen: boolean; activeCard: Card | null; @@ -69,6 +78,16 @@ export function useDeployEffects(args: UseDeployEffectsArgs): UseDeployEffectsRe const dispatch = useDispatch(); const logEndRef = useRef(null); + // DF1/EI1 — the env the user has selected (env tab bar) for the active + // project. Read here so we can sync it into the deploy slice on panel open. + const projectId = activeCard?.projectId; + const activeEnvType = useSelector((s: RootState) => { + if (!projectId) return undefined; + const envId = s.environments.activeEnvId[projectId]; + if (!envId) return undefined; + return s.environments.byProject[projectId]?.find((e) => e.id === envId)?.type; + }); + // Auto-scroll logs useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); @@ -222,5 +241,17 @@ export function useDeployEffects(args: UseDeployEffectsArgs): UseDeployEffectsRe // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeCard?.id, dispatch]); + // DF1/EI1 — sync the deploy target environment to the selected env tab so the + // panel plans/applies against (and visibly displays) the env the user picked, + // not the slice default ('development'). Covers every panel-open entry point. + // No-op once synced; re-syncs if the user switches env while the panel is open + // (which also invalidates a stale plan via setEnvironment → DF4). Ordered last + // so it doesn't shift the four original effects' positions. + useEffect(() => { + if (!isOpen || !activeEnvType) return; + const mapped = mapEnvTypeToDeployEnv(activeEnvType); + if (mapped !== deploy.environment) dispatch(setEnvironment(mapped)); + }, [isOpen, activeEnvType, deploy.environment, dispatch]); + return { logEndRef }; } diff --git a/packages/ui/src/features/deploy/hooks/use-destroy-action.ts b/packages/ui/src/features/deploy/hooks/use-destroy-action.ts index 36037583..49893dfa 100644 --- a/packages/ui/src/features/deploy/hooks/use-destroy-action.ts +++ b/packages/ui/src/features/deploy/hooks/use-destroy-action.ts @@ -94,9 +94,24 @@ export function useDestroyAction(args: UseDestroyActionArgs): UseDestroyActionRe `Destroyed ${res.deleted?.length || 0} resource${(res.deleted?.length || 0) === 1 ? '' : 's'} across all historical deploys.`, ), ); - for (const f of res.failed || []) { + const destroyFailures = res.failed || []; + for (const f of destroyFailures) { dispatch(appendLog(`Failed to delete ${f.type}/${f.name}: ${f.error}`)); } + // DE5 — a PARTIAL destroy (some deleted, some failed) used to fall + // through to resetDeploy() below, which wipes the log — so the + // just-logged failures vanished and the user saw a clean slate as + // if everything was destroyed. Surface the failures as an error and + // keep the logs + overlay (the failed resources still exist) so the + // user can see what's left and retry, instead of silently wiping. + if (destroyFailures.length > 0) { + dispatch( + deployError( + `${destroyFailures.length} resource${destroyFailures.length === 1 ? '' : 's'} could not be destroyed — see the log for details.`, + ), + ); + return; + } } } else { console.log('[destroy] destroy starting', { diff --git a/packages/ui/src/features/environments/components/__tests__/environment-tab-bar.test.tsx b/packages/ui/src/features/environments/components/__tests__/environment-tab-bar.test.tsx index 26272dcb..d551701f 100644 --- a/packages/ui/src/features/environments/components/__tests__/environment-tab-bar.test.tsx +++ b/packages/ui/src/features/environments/components/__tests__/environment-tab-bar.test.tsx @@ -42,6 +42,9 @@ const mocks = vi.hoisted(() => ({ // Mock the api adapter. apiGetDeployments: vi.fn(), apiGraphLoad: vi.fn(), + // IA6 — react-router useSearchParams (read ?env / write on switch). + searchParams: new URLSearchParams(), + setSearchParams: vi.fn(), // Sub-components. EnvironmentTabItem: vi.fn(() => null), EnvironmentContextMenu: vi.fn(() => null), @@ -116,6 +119,10 @@ vi.mock('react-redux', () => ({ useDispatch: () => mocks.dispatch, })); +vi.mock('react-router-dom', () => ({ + useSearchParams: () => [mocks.searchParams, mocks.setSearchParams], +})); + vi.mock('../../../../i18n', () => ({ useTranslation: () => ({ t: (k: string) => k }), })); @@ -253,6 +260,9 @@ beforeEach(() => { } mocks.apiGetDeployments.mockResolvedValue([]); mocks.apiGraphLoad.mockResolvedValue(null); + // IA6 — reset the URL search-params mock between tests. + mocks.searchParams = new URLSearchParams(); + mocks.setSearchParams.mockReset(); __resetUseState(); vi.stubGlobal('window', { addEventListener: vi.fn(), @@ -489,6 +499,16 @@ describe('EnvironmentTabBar — onSwitch (handleSwitchEnv) flow', () => { }); }); + // IA6 — switching also writes ?env= so the env is shareable/bookmarkable. + it('writes the chosen env to the URL (?env=) on switch', async () => { + const tree = callRender({ projectId: 'proj-1' }); + const item = findAll(tree, (el) => el.type === mocks.EnvironmentTabItem)[0]; + await (item.props.onSwitch as (e: unknown) => Promise)(makeEnv({ id: 'env-2', card_id: 'card-1' })); + expect(mocks.setSearchParams).toHaveBeenCalled(); + const updater = mocks.setSearchParams.mock.calls[0][0] as (p: URLSearchParams) => URLSearchParams; + expect(updater(new URLSearchParams()).get('env')).toBe('env-2'); + }); + it('falls through to api.graph.load when card is not present in Redux', async () => { mocks.apiGraphLoad.mockResolvedValueOnce({ id: 'card-1', @@ -685,6 +705,26 @@ describe('EnvironmentTabBar — deploy-status fetch effect', () => { await new Promise((r) => setTimeout(r, 0)); expect(mocks.apiGetDeployments).toHaveBeenCalled(); }); + + // EI9 — a rejected status fetch is recorded as a distinct 'fetch-error' (not + // silently dropped), so its env dot differs from "never deployed". + it('records a fetch-error for an env whose getDeployments rejects', async () => { + mocks.state.environments.byProject['proj-1'] = [ + makeEnv({ id: 'env-1', card_id: 'c1' }), + makeEnv({ id: 'env-2', card_id: 'c2' }), + ]; + mocks.apiGetDeployments.mockImplementation((cardId: string) => + cardId === 'c1' + ? Promise.resolve([{ status: 'success', deployed_url: 'https://e1' }]) + : Promise.reject(new Error('network')), + ); + callRender({ projectId: 'proj-1' }); + await new Promise((r) => setTimeout(r, 0)); + // slot 3 = envDeployStatus setter + const lastArg = stateMocks.setters[3].mock.calls.at(-1)?.[0] as Record; + expect(lastArg['env-1']).toEqual({ status: 'success', url: 'https://e1' }); + expect(lastArg['env-2']).toEqual({ status: 'fetch-error' }); + }); }); describe('EnvironmentTabBar — auto-switch to production on first load', () => { @@ -708,6 +748,27 @@ describe('EnvironmentTabBar — auto-switch to production on first load', () => }); }); + // IA6 — a ?env= deep-link wins over the production default on first load. + it('honours a ?env= deep-link over the production default', () => { + mocks.searchParams = new URLSearchParams('env=env-2'); + mocks.state.environments.byProject['proj-1'] = [ + makeEnv({ id: 'env-2', name: 'staging', type: 'staging', card_id: 'card-stg' }), + makeEnv({ id: 'env-3', name: 'prod', type: 'production', card_id: 'card-prod' }), + ]; + callRender({ projectId: 'proj-1' }); + expect(mocks.setActiveEnvironment).toHaveBeenCalledWith({ projectId: 'proj-1', envId: 'env-2' }); + expect(mocks.setActiveEnvironment).not.toHaveBeenCalledWith({ projectId: 'proj-1', envId: 'env-3' }); + }); + + it('falls back to production when ?env= points at a missing env', () => { + mocks.searchParams = new URLSearchParams('env=ghost'); + mocks.state.environments.byProject['proj-1'] = [ + makeEnv({ id: 'env-3', name: 'prod', type: 'production', card_id: 'card-prod' }), + ]; + callRender({ projectId: 'proj-1' }); + expect(mocks.setActiveEnvironment).toHaveBeenCalledWith({ projectId: 'proj-1', envId: 'env-3' }); + }); + it('does not auto-switch when there is already an active env', () => { mocks.state.environments.byProject['proj-1'] = [makeEnv({ id: 'env-3', name: 'prod', type: 'production' })]; mocks.state.environments.activeEnvId['proj-1'] = 'env-3'; @@ -768,10 +829,21 @@ describe('EnvironmentTabBar — modal & context-menu rendering with pre-seeded s expect(stateMocks.setters[2]).toHaveBeenCalledWith(null); }); - it('context-menu onDelete dispatches deleteEnvironment + closes the menu', () => { - __resetUseState([false, null, { envId: 'env-2', x: 0, y: 0 }, {}]); - const tree = callRender({ projectId: 'proj-1' }); - const menu = findFirst(tree, (el) => el.type === mocks.EnvironmentContextMenu)!; + it('context-menu onDelete arms first, then deletes + closes on confirm (EI5)', () => { + // First click: arms (sets confirmDeleteEnvId, slot 4); no dispatch, no close. + __resetUseState([false, null, { envId: 'env-2', x: 0, y: 0 }, {}, null]); + let tree = callRender({ projectId: 'proj-1' }); + let menu = findFirst(tree, (el) => el.type === mocks.EnvironmentContextMenu)!; + (menu.props.onDelete as (id: string) => void)('env-2'); + expect(stateMocks.setters[4]).toHaveBeenCalledWith('env-2'); + expect(mocks.deleteEnvironment).not.toHaveBeenCalled(); + + // Second click, with delete already armed for env-2: deletes + closes menu. + mocks.deleteEnvironment.mockClear(); + __resetUseState([false, null, { envId: 'env-2', x: 0, y: 0 }, {}, 'env-2']); + tree = callRender({ projectId: 'proj-1' }); + menu = findFirst(tree, (el) => el.type === mocks.EnvironmentContextMenu)!; + expect((menu.props as { confirmingDelete?: boolean }).confirmingDelete).toBe(true); (menu.props.onDelete as (id: string) => void)('env-2'); expect(stateMocks.setters[2]).toHaveBeenCalledWith(null); expect(mocks.deleteEnvironment).toHaveBeenCalledWith({ diff --git a/packages/ui/src/features/environments/components/__tests__/environment-tab-item.test.tsx b/packages/ui/src/features/environments/components/__tests__/environment-tab-item.test.tsx index 89bd9afe..80e70e0e 100644 --- a/packages/ui/src/features/environments/components/__tests__/environment-tab-item.test.tsx +++ b/packages/ui/src/features/environments/components/__tests__/environment-tab-item.test.tsx @@ -76,6 +76,34 @@ describe('EnvironmentTabItem — rendering', () => { expect(text).toContain('preview-1'); }); + // EI9 — the status dot carries an accessible name and a distinct hollow dot + // for a failed status fetch (so it's not mistaken for "never deployed"). + it('gives the status dot a role=img + aria-label', () => { + const tree = callRender({ + env: makeEnv(), + isActive: false, + deployStatus: { status: 'success' }, + onSwitch: vi.fn(), + onContextMenu: vi.fn(), + }); + const dot = findByPredicate(tree, (el) => el.type === 'span' && (el.props as { role?: string }).role === 'img'); + expect(dot).toBeDefined(); + expect((dot!.props as { 'aria-label': string })['aria-label']).toBe('Deployed'); + }); + + it('renders the distinct hollow dot + "Status unavailable" label for a fetch-error', () => { + const tree = callRender({ + env: makeEnv(), + isActive: false, + deployStatus: { status: 'fetch-error' }, + onSwitch: vi.fn(), + onContextMenu: vi.fn(), + }); + const dot = findByPredicate(tree, (el) => el.type === 'span' && (el.props as { role?: string }).role === 'img'); + expect((dot!.props as { 'aria-label': string })['aria-label']).toBe('Status unavailable'); + expect(mocks.cnSpy.mock.calls.some((args) => args.includes('bg-transparent ring-1 ring-amber-500/70'))).toBe(true); + }); + it('uses the active class when isActive=true', () => { callRender({ env: makeEnv(), diff --git a/packages/ui/src/features/environments/components/__tests__/promote-modal.test.tsx b/packages/ui/src/features/environments/components/__tests__/promote-modal.test.tsx index 29e9cae3..1a873d6c 100644 --- a/packages/ui/src/features/environments/components/__tests__/promote-modal.test.tsx +++ b/packages/ui/src/features/environments/components/__tests__/promote-modal.test.tsx @@ -159,6 +159,12 @@ describe('PromoteModal — no-changes case', () => { expect(collectText(tree)).toContain('t:environments.promote.inSync'); }); + // EI7 — the design-only note only applies when there are changes to promote. + it('omits the design-only note when there are no changes', () => { + const tree = callRender(); + expect(collectText(tree)).not.toContain('t:environments.promote.designOnlyNote'); + }); + it('does not render the Promote button when noChanges', () => { const tree = callRender(); const buttons = findAll(tree, (el) => el.type === 'button'); @@ -205,6 +211,12 @@ describe('PromoteModal — diff case', () => { expect(text).toContain('region, tier'); }); + // EI7 — clarify that promotion updates the design, not live infrastructure. + it('shows the design-only clarification note', () => { + const tree = callRender(); + expect(collectText(tree)).toContain('t:environments.promote.designOnlyNote'); + }); + it('shows the unchangedCount when > 0', () => { const tree = callRender(); const text = collectText(tree); diff --git a/packages/ui/src/features/environments/components/environment-context-menu.tsx b/packages/ui/src/features/environments/components/environment-context-menu.tsx index c1896ded..f9ab13d2 100644 --- a/packages/ui/src/features/environments/components/environment-context-menu.tsx +++ b/packages/ui/src/features/environments/components/environment-context-menu.tsx @@ -24,6 +24,9 @@ export interface EnvironmentContextMenuProps { onRename: (env: Environment) => void; onDelete: (envId: string) => void; onClose: () => void; + /** EI5 — when true, the delete entry is in its "click again to confirm" + * armed state. The two-step state is owned by the parent (the tab bar). */ + confirmingDelete?: boolean; } export const EnvironmentContextMenu: React.FC = ({ @@ -37,6 +40,7 @@ export const EnvironmentContextMenu: React.FC = ({ onRename, onDelete, onClose, + confirmingDelete = false, }) => { const { t } = useTranslation(); const env = environments.find((e) => e.id === envId); @@ -88,10 +92,12 @@ export const EnvironmentContextMenu: React.FC = ({
    )} diff --git a/packages/ui/src/features/environments/components/environment-tab-bar.tsx b/packages/ui/src/features/environments/components/environment-tab-bar.tsx index af6c5910..1ebd80d0 100644 --- a/packages/ui/src/features/environments/components/environment-tab-bar.tsx +++ b/packages/ui/src/features/environments/components/environment-tab-bar.tsx @@ -9,6 +9,7 @@ import { Plus, Loader2, ArrowUpRight, Rocket } from 'lucide-react'; import React, { useEffect, useState, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; +import { useSearchParams } from 'react-router-dom'; import { CreateEnvironmentModal } from './create-environment-modal'; import { EnvironmentContextMenu } from './environment-context-menu'; import { EnvironmentTabItem } from './environment-tab-item'; @@ -34,9 +35,13 @@ interface EnvironmentTabBarProps { export const EnvironmentTabBar: React.FC = ({ projectId, basePath: _basePath }) => { const { t } = useTranslation(); const dispatch = useDispatch(); + // IA6 — the active env lives only in Redux; mirror it in the URL (?env=) so a + // deploy/env view is bookmarkable + shareable and survives a hard reload. + const [searchParams, setSearchParams] = useSearchParams(); const environments = useSelector((s: RootState) => s.environments.byProject[projectId] || []); const activeEnvId = useSelector((s: RootState) => s.environments.activeEnvId[projectId]); const loading = useSelector((s: RootState) => s.environments.loading); + const fetchError = useSelector((s: RootState) => s.environments.fetchError); const activeEnv = environments.find((e) => e.id === activeEnvId); const prodEnv = environments.find((e) => e.type === 'production'); const canPromote = activeEnv && !activeEnv.is_protected && prodEnv; @@ -44,6 +49,10 @@ export const EnvironmentTabBar: React.FC = ({ projectId, const [renameTarget, setRenameTarget] = useState(null); const [contextMenu, setContextMenu] = useState<{ envId: string; x: number; y: number } | null>(null); const [envDeployStatus, setEnvDeployStatus] = useState>({}); + // EI5 — the env id whose delete is "armed" (awaiting a confirming second + // click). Declared LAST so it doesn't shift the indices of the useState calls + // above (the tab-bar unit test addresses state setters positionally). + const [confirmDeleteEnvId, setConfirmDeleteEnvId] = useState(null); // Fetch environments on mount useEffect(() => { @@ -58,18 +67,27 @@ export const EnvironmentTabBar: React.FC = ({ projectId, let cancelled = false; const fetchStatuses = async () => { const api = getApi(); - const results = await Promise.allSettled( + // EI9 — catch INSIDE the map so a failed fetch keeps its env id (a bare + // allSettled reject drops it), letting us mark that env's status as a + // distinct "fetch-error" instead of an ambiguous grey "never deployed" dot. + const results = await Promise.all( environments.map(async (env) => { - const res = await api.deploy.getDeployments(env.card_id); - const deploys = Array.isArray(res) ? res : res?.deployments || []; - return { envId: env.id, deploy: deploys[0] }; + try { + const res = await api.deploy.getDeployments(env.card_id); + const deploys = Array.isArray(res) ? res : res?.deployments || []; + return { envId: env.id, deploy: deploys[0], fetchError: false }; + } catch { + return { envId: env.id, deploy: undefined, fetchError: true }; + } }), ); if (cancelled) return; const statuses: Record = {}; for (const r of results) { - if (r.status === 'fulfilled' && r.value.deploy) { - statuses[r.value.envId] = { status: r.value.deploy.status, url: r.value.deploy.deployed_url }; + if (r.deploy) { + statuses[r.envId] = { status: r.deploy.status, url: r.deploy.deployed_url }; + } else if (r.fetchError) { + statuses[r.envId] = { status: 'fetch-error' }; } } setEnvDeployStatus(statuses); @@ -84,6 +102,16 @@ export const EnvironmentTabBar: React.FC = ({ projectId, const handleSwitchEnv = useCallback( async (env: Environment) => { dispatch(setActiveEnvironment({ projectId, envId: env.id })); + // IA6 — reflect the chosen env in the URL (replace, so switching doesn't + // spam the history stack). + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set('env', env.id); + return next; + }, + { replace: true }, + ); try { // Check if card already loaded in Redux @@ -121,29 +149,41 @@ export const EnvironmentTabBar: React.FC = ({ projectId, console.error('Failed to load environment card:', err); } }, - [projectId, dispatch], + [projectId, dispatch, setSearchParams], ); - // Auto-switch to production on first load + // First-load default: IA6 — honour a `?env=` deep-link when it points at a + // real env, otherwise fall back to production (the prior behaviour). useEffect(() => { if (environments.length > 0 && !activeEnvId) { - const prod = environments.find((e) => e.type === 'production'); - if (prod) handleSwitchEnv(prod); + const urlEnvId = searchParams.get('env'); + const fromUrl = urlEnvId ? environments.find((e) => e.id === urlEnvId) : undefined; + const target = fromUrl ?? environments.find((e) => e.type === 'production'); + if (target) handleSwitchEnv(target); } // eslint-disable-next-line react-hooks/exhaustive-deps -- use .length to avoid re-firing on array reference changes - }, [environments.length, activeEnvId, handleSwitchEnv]); + }, [environments.length, activeEnvId, handleSwitchEnv, searchParams]); const handleContextMenu = useCallback((e: React.MouseEvent, envId: string) => { e.preventDefault(); + setConfirmDeleteEnvId(null); // a freshly-opened menu starts unarmed setContextMenu({ envId, x: e.clientX, y: e.clientY }); }, []); + // EI5 — two-step delete: first click arms (no close), second click deletes. + // The env staying visible on a rejected delete is itself the failure signal; + // a dedicated error toast can follow once the app has a notification surface. const handleDelete = useCallback( (envId: string) => { - setContextMenu(null); - dispatch(deleteEnvironment({ envId, projectId })); + if (confirmDeleteEnvId === envId) { + setContextMenu(null); + setConfirmDeleteEnvId(null); + dispatch(deleteEnvironment({ envId, projectId })); + } else { + setConfirmDeleteEnvId(envId); + } }, - [projectId, dispatch], + [confirmDeleteEnvId, projectId, dispatch], ); const handlePromote = useCallback( @@ -157,15 +197,21 @@ export const EnvironmentTabBar: React.FC = ({ projectId, [environments, dispatch], ); - // Close context menu on click outside + // Close context menu on click outside (also disarms a pending delete confirm) useEffect(() => { if (!contextMenu) return; - const close = () => setContextMenu(null); + const close = () => { + setContextMenu(null); + setConfirmDeleteEnvId(null); + }; window.addEventListener('click', close); return () => window.removeEventListener('click', close); }, [contextMenu]); - if (environments.length === 0 && !loading) return null; + // EI6 — only fully hide the bar when there are genuinely no environments AND + // no load failure. A transient fetch error keeps the bar present with a retry + // (so the Deploy button and tabs don't silently vanish on a flaky network). + if (environments.length === 0 && !loading && !fetchError) return null; return ( <> @@ -178,6 +224,16 @@ export const EnvironmentTabBar: React.FC = ({ projectId, {t('environments.tabBar.loading')}
    + ) : fetchError && environments.length === 0 ? ( +
    + {t('environments.tabBar.loadError')} + +
    ) : ( <> {/* Environment tabs */} @@ -243,6 +299,7 @@ export const EnvironmentTabBar: React.FC = ({ projectId, y={contextMenu.y} environments={environments} prodEnv={prodEnv} + confirmingDelete={confirmDeleteEnvId === contextMenu.envId} onDeploy={(env) => { setContextMenu(null); handleSwitchEnv(env); @@ -251,7 +308,10 @@ export const EnvironmentTabBar: React.FC = ({ projectId, onPromote={handlePromote} onRename={(env) => setRenameTarget(env)} onDelete={handleDelete} - onClose={() => setContextMenu(null)} + onClose={() => { + setContextMenu(null); + setConfirmDeleteEnvId(null); + }} /> )} diff --git a/packages/ui/src/features/environments/components/environment-tab-item.tsx b/packages/ui/src/features/environments/components/environment-tab-item.tsx index fea89c34..774bec92 100644 --- a/packages/ui/src/features/environments/components/environment-tab-item.tsx +++ b/packages/ui/src/features/environments/components/environment-tab-item.tsx @@ -9,8 +9,9 @@ import { GitPullRequest, Lock } from 'lucide-react'; import React from 'react'; +import { t } from '../../../i18n'; import { cn } from '../../../shared/utils/cn'; -import { getDeployStatusDotColor } from '../utils/deploy-status-color'; +import { getDeployStatusDotColor, getDeployStatusLabelKey } from '../utils/deploy-status-color'; import type { Environment } from '../../../store/slices/environments-slice'; export interface EnvironmentDeployStatus { @@ -34,6 +35,9 @@ export const EnvironmentTabItem: React.FC = ({ onContextMenu, }) => { const dotColor = getDeployStatusDotColor(deployStatus); + // EI9 — pair the dot colour with an AT-reachable name (also disambiguates + // "Status unavailable" from "Not deployed", which both used a grey dot). + const dotLabel = t(getDeployStatusLabelKey(deployStatus)); return (
    {/* Diff list */} @@ -113,6 +128,16 @@ export const PromoteModal: React.FC = () => { )}
    + {/* Promotion error (EI3) — keeps the modal open so prod state is unambiguous */} + {promoteError && ( +
    + {promoteError} +
    + )} + {/* Footer */}
    + {open &&
    {advanced.map(renderRow)}
    } +
    + ); +}; diff --git a/packages/ui/src/features/properties/components/properties-panel.tsx b/packages/ui/src/features/properties/components/properties-panel.tsx index 901ee06f..5d548c01 100644 --- a/packages/ui/src/features/properties/components/properties-panel.tsx +++ b/packages/ui/src/features/properties/components/properties-panel.tsx @@ -28,6 +28,9 @@ export const PropertiesPanel: React.FC = () => { const activeCard = useSelector(selectActiveCard); const { selectedNodes, selectedEdges } = useSelector((state: RootState) => state.selection); const validationIssues = useSelector((state: RootState) => state.validation?.issues ?? []); + // PE8 — surface the debounced validation's in-flight state so the panel can + // show a "checking…" cue instead of the inline feedback silently lagging. + const isValidating = useSelector((state: RootState) => state.validation?.isValidating ?? false); // ─── Properties tab state ────────────────────────────────────────────────── // The `propsTab` state lives at the orchestrator level (lifted from the @@ -79,6 +82,7 @@ export const PropertiesPanel: React.FC = () => { propsTab={propsTab} setPropsTab={setPropsTab} validationIssues={validationIssues} + isValidating={isValidating} activeEnvName={activeEnvName} /> ); diff --git a/packages/ui/src/features/properties/components/sections/__tests__/drift.test.tsx b/packages/ui/src/features/properties/components/sections/__tests__/drift.test.tsx index 6246df48..38e5079f 100644 --- a/packages/ui/src/features/properties/components/sections/__tests__/drift.test.tsx +++ b/packages/ui/src/features/properties/components/sections/__tests__/drift.test.tsx @@ -108,6 +108,7 @@ const miniDeploy = createSlice({ name: 'deploy', initialState: { driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, } as MiniDeployState, reducers: { diff --git a/packages/ui/src/features/properties/components/sections/__tests__/monitoring-log-section.test.tsx b/packages/ui/src/features/properties/components/sections/__tests__/monitoring-log-section.test.tsx index 7bf3b221..2649b7ab 100644 --- a/packages/ui/src/features/properties/components/sections/__tests__/monitoring-log-section.test.tsx +++ b/packages/ui/src/features/properties/components/sections/__tests__/monitoring-log-section.test.tsx @@ -70,6 +70,8 @@ vi.mock('../../../../../store/slices/cards-slice', () => ({ // `selectLogStream` looks up by terminalNodeId on the byTerminalNodeId map. vi.mock('../../../../../store/slices/logs-slice', () => ({ selectLogStream: (state: typeof mocks.state, terminalNodeId: string) => state.logs.byTerminalNodeId[terminalNodeId], + retryStream: (payload: { terminalNodeId: string }) => ({ type: 'logs/retryStream', payload }), + clearEntries: (payload: { terminalNodeId: string }) => ({ type: 'logs/clearEntries', payload }), })); import { MonitoringLogSection } from '../monitoring-log-section'; @@ -483,6 +485,104 @@ describe('MonitoringLogSection', () => { expect(findByTestid(tree, 'monitoring-log-error')).toBeUndefined(); }); + it('renders a Retry button on permission-denied that dispatches retryStream (OL5)', () => { + seedLogNode(); + mocks.state.logs.byTerminalNodeId['log-1'] = { + status: 'permission-denied', + mode: 'polling', + source: null, + entries: [], + lastError: 'denied', + }; + const tree = renderSection('log-1'); + const retry = findByTestid(tree, 'monitoring-log-retry'); + expect(retry).toBeDefined(); + (retry!.props as { onClick: () => void }).onClick(); + const dispatched = mocks.dispatchSpy.mock.calls.map((c: unknown[]) => c[0]) as Array<{ + type?: string; + payload?: { terminalNodeId?: string }; + }>; + expect(dispatched.some((a) => a?.type === 'logs/retryStream' && a?.payload?.terminalNodeId === 'log-1')).toBe( + true, + ); + }); + + it('does NOT render the Retry button when streaming', () => { + seedLogNode(); + mocks.state.logs.byTerminalNodeId['log-1'] = { + status: 'streaming', + mode: 'polling', + source: null, + entries: [], + lastError: null, + }; + const tree = renderSection('log-1'); + expect(findByTestid(tree, 'monitoring-log-retry')).toBeUndefined(); + }); + + it('renders a Clear button that dispatches clearEntries when entries exist (OL3)', () => { + seedLogNode(); + mocks.state.logs.byTerminalNodeId['log-1'] = { + status: 'streaming', + mode: 'polling', + source: null, + entries: [{ insertId: 'e1' }], + lastError: null, + } as unknown as (typeof mocks.state.logs.byTerminalNodeId)['log-1']; + const tree = renderSection('log-1'); + const clear = findByTestid(tree, 'monitoring-log-clear'); + expect(clear).toBeDefined(); + (clear!.props as { onClick: () => void }).onClick(); + const dispatched = mocks.dispatchSpy.mock.calls.map((c: unknown[]) => c[0]) as Array<{ + type?: string; + payload?: { terminalNodeId?: string }; + }>; + expect(dispatched.some((a) => a?.type === 'logs/clearEntries' && a?.payload?.terminalNodeId === 'log-1')).toBe( + true, + ); + }); + + it('hides the Clear button when there are no entries', () => { + seedLogNode(); + mocks.state.logs.byTerminalNodeId['log-1'] = { + status: 'streaming', + mode: 'polling', + source: null, + entries: [], + lastError: null, + }; + const tree = renderSection('log-1'); + expect(findByTestid(tree, 'monitoring-log-clear')).toBeUndefined(); + }); + + it('shows the dropped-lines note when droppedCount > 0 (OL4)', () => { + seedLogNode(); + mocks.state.logs.byTerminalNodeId['log-1'] = { + status: 'streaming', + mode: 'polling', + source: null, + entries: [], + lastError: null, + droppedCount: 42, + } as unknown as (typeof mocks.state.logs.byTerminalNodeId)['log-1']; + const tree = renderSection('log-1'); + expect(findByTestid(tree, 'monitoring-log-dropped')).toBeDefined(); + }); + + it('hides the dropped-lines note when nothing has dropped', () => { + seedLogNode(); + mocks.state.logs.byTerminalNodeId['log-1'] = { + status: 'streaming', + mode: 'polling', + source: null, + entries: [], + lastError: null, + droppedCount: 0, + } as unknown as (typeof mocks.state.logs.byTerminalNodeId)['log-1']; + const tree = renderSection('log-1'); + expect(findByTestid(tree, 'monitoring-log-dropped')).toBeUndefined(); + }); + it('does NOT render the error message when lastError is null', () => { seedLogNode(); mocks.state.logs.byTerminalNodeId['log-1'] = { diff --git a/packages/ui/src/features/properties/components/sections/__tests__/node-identity-card.test.tsx b/packages/ui/src/features/properties/components/sections/__tests__/node-identity-card.test.tsx index a3a8af88..9ece3d8b 100644 --- a/packages/ui/src/features/properties/components/sections/__tests__/node-identity-card.test.tsx +++ b/packages/ui/src/features/properties/components/sections/__tests__/node-identity-card.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { describe, it, expect, vi } from 'vitest'; import { NodeIdentityCard } from '../node-identity-card'; +import { ConceptInfoTrigger } from '../../../../concept-info'; import type { CardNode } from '../../../../../store/slices/cards-slice'; import type { ResourceDef } from '../../../hooks/use-resource-map'; @@ -153,6 +154,42 @@ describe('NodeIdentityCard — rendering', () => { ); expect(chips.length).toBe(0); }); + + // PE4 — the concept "i" explainer is now reachable from the panel, not just + // the canvas node. (It self-gates via hasConceptInfo at render time, so it's + // mounted unconditionally; here we assert the wiring + props.) + it('mounts the concept-info explainer trigger with iceType/displayName/provider', () => { + const tree = callRender({ + selectedNode: makeNode(), + iconUrl: '', + label: 'My DB', + iceType: 'Database.PostgreSQL', + provider: 'aws', + resourceDef: { display_name: 'Amazon RDS', properties: [] } as unknown as ResourceDef, + onUpdateName: vi.fn(), + }); + const trigger = findByPredicate(tree, (el) => el.type === ConceptInfoTrigger); + expect(trigger).toBeDefined(); + expect(trigger?.props.iceType).toBe('Database.PostgreSQL'); + expect(trigger?.props.displayName).toBe('Amazon RDS'); + expect(trigger?.props.currentProvider).toBe('aws'); + }); + + it('falls back to label then iceType for the explainer displayName', () => { + const treeLabel = callRender({ + selectedNode: makeNode(), + iconUrl: '', + label: 'Friendly Name', + iceType: 'Compute.Foo', + provider: '', + resourceDef: undefined, + onUpdateName: vi.fn(), + }); + const t1 = findByPredicate(treeLabel, (el) => el.type === ConceptInfoTrigger); + expect(t1?.props.displayName).toBe('Friendly Name'); + // provider empty → currentProvider is undefined (not an empty string) + expect(t1?.props.currentProvider).toBeUndefined(); + }); }); describe('NodeIdentityCard — handlers', () => { diff --git a/packages/ui/src/features/properties/components/sections/__tests__/node-properties-section.test.tsx b/packages/ui/src/features/properties/components/sections/__tests__/node-properties-section.test.tsx index bfb35218..1757882b 100644 --- a/packages/ui/src/features/properties/components/sections/__tests__/node-properties-section.test.tsx +++ b/packages/ui/src/features/properties/components/sections/__tests__/node-properties-section.test.tsx @@ -321,6 +321,7 @@ interface RenderProps { propsTab?: string; setPropsTab?: (id: string) => void; validationIssues?: ReadonlyArray; + isValidating?: boolean; activeEnvName?: string; } @@ -333,6 +334,7 @@ const renderSection = (props: RenderProps): React.ReactElement => { propsTab: props.propsTab ?? 'config', setPropsTab: props.setPropsTab ?? vi.fn(), validationIssues: props.validationIssues ?? [], + isValidating: props.isValidating ?? false, activeEnvName: props.activeEnvName ?? 'production', }) as React.ReactElement; }; @@ -391,6 +393,21 @@ describe('NodePropertiesSection', () => { expect(mocks.dispatchSpy).toHaveBeenCalledWith({ type: 'ui/toggleProperties' }); }); + // PE8 — a "checking…" cue shows in the Config tab while validation debounces. + it('shows a validating cue in the Config tab when isValidating is true', () => { + const node = makeNode('node-1', { iceType: 'Compute.Service', label: 'svc' }); + const card = makeCard({ nodes: [node] }); + const validating = renderSection({ selectedNode: node, activeCard: card, propsTab: 'config', isValidating: true }); + expect(collectText(validating)).toContain('t:canvas.properties.validating'); + }); + + it('hides the validating cue when isValidating is false', () => { + const node = makeNode('node-1', { iceType: 'Compute.Service', label: 'svc' }); + const card = makeCard({ nodes: [node] }); + const idle = renderSection({ selectedNode: node, activeCard: card, propsTab: 'config', isValidating: false }); + expect(collectText(idle)).not.toContain('t:canvas.properties.validating'); + }); + it('renders the node label as the input defaultValue (name field on identity card)', () => { const node = makeNode('node-1', { iceType: 'Compute.Service', name: 'My Service' }); const card = makeCard({ nodes: [node] }); diff --git a/packages/ui/src/features/properties/components/sections/__tests__/properties-tab-bar.test.tsx b/packages/ui/src/features/properties/components/sections/__tests__/properties-tab-bar.test.tsx index 1d0eb6ca..6981b68a 100644 --- a/packages/ui/src/features/properties/components/sections/__tests__/properties-tab-bar.test.tsx +++ b/packages/ui/src/features/properties/components/sections/__tests__/properties-tab-bar.test.tsx @@ -13,7 +13,7 @@ vi.mock('../../../../../shared/utils/cn', () => ({ cn: mocks.cnSpy, })); -import { PropertiesTabBar } from '../properties-tab-bar'; +import { PropertiesTabBar, TabIssueBadge } from '../properties-tab-bar'; import type { VisibleTab } from '../../../utils/build-visible-tabs'; interface ReactElementLike { @@ -128,3 +128,65 @@ describe('PropertiesTabBar', () => { expect(dots.length).toBe(1); }); }); + +// PE2 — a tab's error/warning counts are shown as a badge so they're visible +// from any tab (not trapped in Config). +describe('PropertiesTabBar — issue badges (PE2)', () => { + const tabs: VisibleTab[] = [ + { id: 'config', label: 'Config', show: true }, + { id: 'scaling', label: 'Scaling', show: true }, + ]; + + it('passes the config issue counts to that tab’s badge', () => { + const tree = callRender({ + visibleTabs: tabs, + activeTab: 'scaling', + onSelect: vi.fn(), + issueCounts: { config: { errors: 2, warnings: 1 } }, + }); + // Every tab renders a TabIssueBadge; the config one carries the counts. + const badges = findAllByPredicate(tree, (el) => el.type === TabIssueBadge).map( + (el) => (el.props as { badge?: unknown }).badge, + ); + expect(badges).toContainEqual({ errors: 2, warnings: 1 }); + // the scaling tab has no entry → undefined badge + expect(badges).toContain(undefined); + }); + + it('renders no badge content when issueCounts is omitted', () => { + const tree = callRender({ visibleTabs: tabs, activeTab: 'config', onSelect: vi.fn() }); + const badges = findAllByPredicate(tree, (el) => el.type === TabIssueBadge).map( + (el) => (el.props as { badge?: unknown }).badge, + ); + expect(badges.every((b) => b === undefined)).toBe(true); + }); +}); + +describe('TabIssueBadge (PE2)', () => { + const render = (badge?: { errors: number; warnings: number }) => + (TabIssueBadge as (p: { badge?: { errors: number; warnings: number } }) => unknown)({ badge }); + + it('renders a red error pill with the count + accessible label when errors > 0', () => { + const el = render({ errors: 2, warnings: 3 }) as ReactElementLike; + expect(el.type).toBe('span'); + expect(el.props.children).toBe(2); + expect(el.props.className as string).toContain('text-red-400'); + expect(el.props['aria-label']).toBe('2 blocking'); + }); + + it('renders an amber warning pill when there are warnings but no errors', () => { + const el = render({ errors: 0, warnings: 1 }) as ReactElementLike; + expect(el.type).toBe('span'); + expect(el.props.children).toBe(1); + expect(el.props.className as string).toContain('text-amber-400'); + expect(el.props['aria-label']).toBe('1 warning'); + }); + + it('renders nothing when there are no errors or warnings', () => { + expect(render({ errors: 0, warnings: 0 })).toBeNull(); + }); + + it('renders nothing when no badge is supplied', () => { + expect(render(undefined)).toBeNull(); + }); +}); diff --git a/packages/ui/src/features/properties/components/sections/deploy-tab-body.tsx b/packages/ui/src/features/properties/components/sections/deploy-tab-body.tsx index e80fbe59..b9230f21 100644 --- a/packages/ui/src/features/properties/components/sections/deploy-tab-body.tsx +++ b/packages/ui/src/features/properties/components/sections/deploy-tab-body.tsx @@ -11,12 +11,38 @@ */ import React from 'react'; +import { useSelector } from 'react-redux'; import { useTranslation } from '../../../../i18n'; import { Section } from '../fields'; import { DeployHistory } from './deploy-history'; import { DriftIndicator, DriftCheckButton } from './drift'; +import type { RootState } from '../../../../store'; import type { Card, CardNode } from '../../../../store/slices/cards-slice'; +/** + * OS2 — the current-state dot must reflect evidence, not just the existence of + * a deploy row. Reconciled with the node's drift status: a confirmed-missing + * resource shows amber "Not in deployment" instead of a green claim; otherwise + * it reads "Deployed" (a deploy record exists) rather than a pulsing "Live" + * (which implied a verified-running state ICE never actually checks). + * + * Kept as its own Redux-aware component so `DeployTabBody` stays a pure tree — + * its unit test renders it as a plain function call with no store. + */ +const CurrentStatusRow: React.FC<{ nodeId: string }> = ({ nodeId }) => { + const { t } = useTranslation(); + const driftStatus = useSelector((s: RootState) => s.deploy.driftByNode[nodeId]?.status); + const isMissing = driftStatus === 'missing'; + return ( +
    +
    + + {isMissing ? t('properties.drift.notInDeployment') : t('properties.deploy.deployed')} + +
    + ); +}; + export const DeployTabBody: React.FC<{ selectedNode: CardNode; activeCard: Card; @@ -27,10 +53,7 @@ export const DeployTabBody: React.FC<{
    -
    -
    - {t('properties.deploy.live')} -
    + {!!selectedNode.data?.url && (
    {t('properties.deploy.urlLabel')}
    diff --git a/packages/ui/src/features/properties/components/sections/drift.tsx b/packages/ui/src/features/properties/components/sections/drift.tsx index 49bdec95..8617ed3e 100644 --- a/packages/ui/src/features/properties/components/sections/drift.tsx +++ b/packages/ui/src/features/properties/components/sections/drift.tsx @@ -24,8 +24,38 @@ import { t } from '../../../../i18n'; import { useDriftCheck } from '../../hooks/use-drift-check'; import type { RootState } from '../../../../store'; +// OS4 — a drift result is only as trustworthy as how recently it ran. Render +// the check timestamp under the indicator and flag it stale past the window so +// a green dot can't quietly age into a lie. +const STALE_AFTER_MS = 10 * 60 * 1000; + +function formatChecked(checkedAt: string | null): { label: string; stale: boolean } | null { + if (!checkedAt) return null; + const then = new Date(checkedAt).getTime(); + if (Number.isNaN(then)) return null; + const diffMs = Math.max(0, Date.now() - then); + const stale = diffMs > STALE_AFTER_MS; + const mins = Math.floor(diffMs / 60000); + if (mins < 1) return { label: t('properties.drift.justNow'), stale }; + if (mins < 60) return { label: t('properties.drift.checkedAgo', { ago: `${mins}m` }), stale }; + const hrs = Math.floor(mins / 60); + return { label: t('properties.drift.checkedAgo', { ago: `${hrs}h` }), stale }; +} + +const CheckedFooter: React.FC<{ checkedAt: string | null }> = ({ checkedAt }) => { + const info = formatChecked(checkedAt); + if (!info) return null; + return ( +
    + {info.label} + {info.stale ? ` · ${t('properties.drift.stale')}` : ''} +
    + ); +}; + export const DriftIndicator: React.FC<{ nodeId: string }> = ({ nodeId }) => { const driftInfo = useSelector((s: RootState) => s.deploy.driftByNode[nodeId]); + const driftMeta = useSelector((s: RootState) => s.deploy.driftMeta); const isLoading = useSelector((s: RootState) => s.deploy.driftCheckLoading); if (isLoading) { @@ -39,20 +69,43 @@ export const DriftIndicator: React.FC<{ nodeId: string }> = ({ nodeId }) => { if (!driftInfo) return null; + const checkedAt = driftMeta?.checkedAt ?? null; + + // OS3 — the cloud was never actually queried (no creds / provider has no + // describe path). NEVER present this as a verified "in sync"; show a + // stored-state caveat regardless of the per-node status the fallback produced. + if (driftMeta?.unsupported) { + return ( +
    +
    +
    + {t('properties.drift.unverified')} +
    + +
    + ); + } + if (driftInfo.status === 'in_sync') { return ( -
    -
    - {t('properties.drift.inSync')} +
    +
    +
    + {t('properties.drift.inSync')} +
    +
    ); } if (driftInfo.status === 'missing') { return ( -
    -
    - {t('properties.drift.notInDeployment')} +
    +
    +
    + {t('properties.drift.notInDeployment')} +
    +
    ); } @@ -79,6 +132,23 @@ export const DriftIndicator: React.FC<{ nodeId: string }> = ({ nodeId }) => {
    ))}
    + +
    + ); + } + + // OS7 — 'unknown' / 'extra' used to render as nothing (silent dead-ends). + // Surface them honestly so the user knows the check ran but couldn't classify. + if (driftInfo.status === 'unknown' || driftInfo.status === 'extra') { + return ( +
    +
    +
    + + {driftInfo.status === 'extra' ? t('properties.drift.extra') : t('properties.drift.unknown')} + +
    +
    ); } diff --git a/packages/ui/src/features/properties/components/sections/monitoring-log-section.tsx b/packages/ui/src/features/properties/components/sections/monitoring-log-section.tsx index fd4f675e..3b9f37f6 100644 --- a/packages/ui/src/features/properties/components/sections/monitoring-log-section.tsx +++ b/packages/ui/src/features/properties/components/sections/monitoring-log-section.tsx @@ -25,7 +25,13 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { t } from '../../../../i18n'; import { selectActiveCard, updateCardNodeData, type CardNode } from '../../../../store/slices/cards-slice'; -import { selectLogStream, type LogStreamMode, type LogStreamStatus } from '../../../../store/slices/logs-slice'; +import { + clearEntries, + retryStream, + selectLogStream, + type LogStreamMode, + type LogStreamStatus, +} from '../../../../store/slices/logs-slice'; import type { AppDispatch, RootState } from '../../../../store'; // ─── LT-2 supported source iceTypes ───────────────────────────────────────── @@ -79,6 +85,8 @@ function pillFor(status: LogStreamStatus | undefined): PillSpec { return { tone: 'grey', label: t('canvas.properties.log.pillAmbiguous') }; case 'unsupported': return { tone: 'grey', label: t('canvas.properties.log.pillUnsupported') }; + case 'provider-unsupported': + return { tone: 'grey', label: t('canvas.properties.log.pillProviderUnsupported') }; case 'permission-denied': return { tone: 'red', label: t('canvas.properties.log.pillAccessDenied') }; case 'error': @@ -210,8 +218,28 @@ export function MonitoringLogSection({ nodeId }: Props): React.ReactElement | nu /> {pill.label} + + {/* OL3 — clear the buffer (the clearEntries reducer existed but was + wired to nothing). Shown only when there are entries to clear. */} + {(streamState?.entries?.length ?? 0) > 0 && ( + + )}
    + {/* OL4 — the 200-line buffer drops the oldest entries; say so instead of + silently losing them. */} + {(streamState?.droppedCount ?? 0) > 0 && ( +

    + {t('canvas.properties.log.dropped', { count: streamState!.droppedCount })} +

    + )} + {/* Caveats — rendered verbatim from the resolver. */} {caveats.length > 0 && (
    @@ -230,6 +258,18 @@ export function MonitoringLogSection({ nodeId }: Props): React.ReactElement | nu

    )} + {/* OL5 — recover in-product: re-subscribe after the user fixes the cause + (e.g. grants the IAM role) without reloading the app. */} + {(streamState?.status === 'error' || streamState?.status === 'permission-denied') && ( + + )} + {/* Streaming mode radio */}
    ); diff --git a/packages/ui/src/features/properties/components/sections/node-properties-section.tsx b/packages/ui/src/features/properties/components/sections/node-properties-section.tsx index 3cb470ba..82c71518 100644 --- a/packages/ui/src/features/properties/components/sections/node-properties-section.tsx +++ b/packages/ui/src/features/properties/components/sections/node-properties-section.tsx @@ -51,6 +51,7 @@ * byte-identical. */ +import { Loader2 } from 'lucide-react'; import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useTranslation } from '../../../../i18n'; @@ -172,6 +173,10 @@ export const NodePropertiesSection: React.FC<{ propsTab: string; setPropsTab: (id: string) => void; validationIssues: ReadonlyArray; + /** PE8 — true while the debounced validation run is in flight, so the panel + * can show a "checking…" cue instead of the inline feedback silently lagging + * an edit by ~500ms. */ + isValidating?: boolean; activeEnvName: string; }> = ({ selectedNode, @@ -181,6 +186,7 @@ export const NodePropertiesSection: React.FC<{ propsTab, setPropsTab, validationIssues, + isValidating = false, activeEnvName, }) => { const { t } = useTranslation(); @@ -259,7 +265,12 @@ export const NodePropertiesSection: React.FC<{ Surfaces missing connections, missing required props, and implicit handler choices BEFORE the user clicks deploy. Pure client-side; no network calls. */} - + i.nodeId === selectedNode.id && i.severity !== 'info')} + /> {/* ── Group color picker — ONLY for synthetic decoration groups, i.e. nodes whose `iceType` follows the `Group.*` convention @@ -304,9 +315,28 @@ export const NodePropertiesSection: React.FC<{ setPropsTab(visibleTabs[0].id); } + // PE2 — the per-field config validation lives in propertyIssuesMap; + // surface its error/warning counts as a badge on the Config tab so the + // signal isn't trapped there when the user is on another tab. + let configErrors = 0; + let configWarnings = 0; + propertyIssuesMap?.forEach((issue) => { + if (issue.severity === 'error') configErrors += 1; + else if (issue.severity === 'warning') configWarnings += 1; + }); + const issueCounts = + configErrors > 0 || configWarnings > 0 + ? { config: { errors: configErrors, warnings: configWarnings } } + : undefined; + return ( <> - + {/* ════ DEPLOY TAB ════ */} {activeTab === 'deploy' && hasDeployment && ( @@ -385,6 +415,19 @@ export const NodePropertiesSection: React.FC<{ {/* ════ CONFIG TAB ════ */} {activeTab === 'config' && ( <> + {/* PE8 — a "checking…" cue while the debounced validation run is + in flight, so the inline feedback doesn't appear to silently + lag the user's edit. */} + {isValidating && ( +
    + + {t('canvas.properties.validating')} +
    + )} {/* Validation issues banner */} {selectedNodeId && (() => { diff --git a/packages/ui/src/features/properties/components/sections/properties-tab-bar.tsx b/packages/ui/src/features/properties/components/sections/properties-tab-bar.tsx index 868c9b96..ffd602eb 100644 --- a/packages/ui/src/features/properties/components/sections/properties-tab-bar.tsx +++ b/packages/ui/src/features/properties/components/sections/properties-tab-bar.tsx @@ -9,16 +9,30 @@ */ import React from 'react'; +import { t } from '../../../../i18n'; import { cn } from '../../../../shared/utils/cn'; import type { VisibleTab } from '../../utils/build-visible-tabs'; +export interface TabIssueCount { + errors: number; + warnings: number; +} + export interface PropertiesTabBarProps { visibleTabs: VisibleTab[]; activeTab: string; onSelect: (id: string) => void; + /** PE2 — per-tab error/warning counts, so a tab's problems are visible even + * when the user is on a different tab. Keyed by tab id. */ + issueCounts?: Record; } -export const PropertiesTabBar: React.FC = ({ visibleTabs, activeTab, onSelect }) => { +export const PropertiesTabBar: React.FC = ({ + visibleTabs, + activeTab, + onSelect, + issueCounts, +}) => { if (visibleTabs.length <= 1) return null; return (
    @@ -37,8 +51,43 @@ export const PropertiesTabBar: React.FC = ({ visibleTabs, > {tab.dot &&
    } {tab.label} + ))}
    ); }; + +// PE2 — errors take precedence over warnings; only one pill shows to keep the +// tab compact. The visible number is paired with an AT-reachable label. +export const TabIssueBadge: React.FC<{ badge?: TabIssueCount }> = ({ badge }) => { + if (!badge) return null; + if (badge.errors > 0) { + const label = t('canvas.properties.requirements.blocking', { count: badge.errors }); + return ( + + {badge.errors} + + ); + } + if (badge.warnings > 0) { + const label = t( + badge.warnings === 1 ? 'canvas.properties.requirements.warning' : 'canvas.properties.requirements.warnings', + { count: badge.warnings }, + ); + return ( + + {badge.warnings} + + ); + } + return null; +}; diff --git a/packages/ui/src/features/properties/hooks/__tests__/use-drift-check.test.tsx b/packages/ui/src/features/properties/hooks/__tests__/use-drift-check.test.tsx index 9c2e40fa..9d581883 100644 --- a/packages/ui/src/features/properties/hooks/__tests__/use-drift-check.test.tsx +++ b/packages/ui/src/features/properties/hooks/__tests__/use-drift-check.test.tsx @@ -196,6 +196,7 @@ describe('useDriftCheck (smoke + capture, renderToString)', () => { expect(dispatchedTypes).toEqual([ 'deploy/setDriftCheckLoading', 'deploy/setDriftResults', + 'deploy/setDriftMeta', // OS3/OS4 — carries checkedAt + unsupported 'cards/updateCardNodeData', // n1 -> drifted 'cards/updateCardNodeData', // n2 -> active (in_sync) // n3 is skipped because 'unknown' is not a recognized status diff --git a/packages/ui/src/features/properties/hooks/use-drift-check.ts b/packages/ui/src/features/properties/hooks/use-drift-check.ts index def90ca3..a5584d7a 100644 --- a/packages/ui/src/features/properties/hooks/use-drift-check.ts +++ b/packages/ui/src/features/properties/hooks/use-drift-check.ts @@ -20,7 +20,7 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import axiosInstance from '../../../shared/api/axios-instance'; import { updateCardNodeData } from '../../../store/slices/cards-slice'; -import { setDriftCheckLoading, setDriftResults } from '../../../store/slices/deploy-slice'; +import { setDriftCheckLoading, setDriftMeta, setDriftResults } from '../../../store/slices/deploy-slice'; import type { AppDispatch, RootState } from '../../../store'; /** @@ -63,6 +63,15 @@ export function useDriftCheck(cardId: string, nodes: any[]): { isLoading: boolea const res = await axiosInstance.post('/canvas/deploy/drift-check', { cardId, nodes }); if (res.data?.driftResults) { dispatch(setDriftResults(res.data.driftResults)); + // OS3/OS4: carry the check's authority (`unsupported` = cloud was never + // queried) + timestamp so the indicator can't show a green "in sync" + // for a stored-state guess. Both ride on the same response. + dispatch( + setDriftMeta({ + checkedAt: typeof res.data.checkedAt === 'string' ? res.data.checkedAt : null, + unsupported: res.data.unsupported === true, + }), + ); // Update canvas node statuses to reflect drift applyDriftStatus(res.data.driftResults, dispatch); } diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index ae49e757..cc30c26d 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -112,6 +112,32 @@ "continueWithGithub": "Continue with GitHub" } }, + "a11y": { + "announce": { + "connecting": "Connecting to cloud provider…", + "planning": "Planning deployment…", + "planReady": "Deployment plan ready for review.", + "deploying": "Deploying…", + "deployingCount": "Deploying {{total}} resources…", + "destroying": "Destroying resources…", + "deploySucceeded": "Deployment succeeded.", + "deployFailed": "Deployment failed.", + "deployFailedCount": "Deployment failed — {{failed}} resource(s) failed." + } + }, + "deployStatus": { + "deployed": "Deployed", + "failed": "Failed", + "deploying": "Deploying", + "destroying": "Destroying", + "planning": "Planning", + "planReady": "Plan ready", + "queued": "Queued", + "connecting": "Connecting", + "cancelled": "Cancelled", + "notDeployed": "Not deployed", + "fetchError": "Status unavailable" + }, "canvas": { "title": "Infrastructure Canvas", "emptyState": { @@ -120,7 +146,20 @@ "blankCanvas": "Blank Canvas", "pickTemplate": "Pick a template...", "quickStart": "Quick start", - "more": "More..." + "more": "More...", + "hint": "Press Shift+A or drag a block from the left to begin", + "dismiss": "Dismiss quick start" + }, + "spotlight": { + "recent": "Recent", + "catalog": "All blocks" + }, + "socketLegend": { + "title": "Connection sockets", + "traffic": "Data & traffic", + "config": "Config (env, secrets)", + "repository": "Source repository", + "domain": "Domain / DNS" }, "tooltip": { "connections": "connections", @@ -215,6 +254,7 @@ "noSource": "Connect a compute or database block to start streaming logs.", "ambiguous": "Multiple inbound connections — choose a source in the properties panel.", "unsupported": "This source type doesn't emit Cloud Logging output.", + "providerUnsupported": "Live logs aren't available for {{provider}} yet.", "permissionDenied": "Cloud Logging access denied. Grant roles/logging.viewer to the deploy service account.", "error": "Connection error. Retrying.", "connecting": "Connecting…", @@ -225,6 +265,11 @@ "connecting": "CONNECTING", "error": "ERROR", "idle": "IDLE", + "preDeploy": "WAITING", + "noSource": "NO SOURCE", + "ambiguous": "CHOOSE SRC", + "unsupported": "UNSUPPORTED", + "providerUnsupported": "UNAVAILABLE", "pipelineLive": "Live", "pipelineFailed": "Failed", "pipelineBuilding": "Building", @@ -516,8 +561,11 @@ "actionTypeBlockDescription": "invoke a block on the canvas", "actionTypeHttpLabel": "Send HTTP request", "actionTypeHttpDescription": "hit an external URL", - "customValuePrefix": "Custom: {{value}} {{unit}}" + "customValuePrefix": "Custom: {{value}} {{unit}}", + "required": "Required", + "advanced": "Advanced" }, + "validating": "Checking…", "log": { "sectionTitle": "Logs", "modePolling": "Polling", @@ -525,6 +573,9 @@ "modeTail": "Tail", "modeTailHint": "Sub-second streaming via gRPC.", "sourceOverride": "Source override", + "retry": "Retry", + "clear": "Clear", + "dropped": "{{count}} older line(s) dropped — buffer is capped at 200.", "noSupportedSource": "No supported source connected. Connect a compute or database block to this Log terminal.", "clearOverride": "Clear override", "selectSource": "— Select a source —", @@ -534,6 +585,7 @@ "pillNoSource": "No source", "pillAmbiguous": "Ambiguous source", "pillUnsupported": "Unsupported source", + "pillProviderUnsupported": "Unavailable", "pillAccessDenied": "Access denied", "pillError": "Error", "pillIdle": "Idle" @@ -640,7 +692,8 @@ "connected": "{{provider}} connected", "connectedViaOAuth": "via Google OAuth", "connectedViaServiceAccount": "via Service Account", - "notConnected": "{{provider}} not connected — configure in Cloud Providers settings" + "notConnected": "{{provider}} not connected — configure in Cloud Providers settings", + "reauthNeeded": "{{provider}} sign-in expired — reconnect to deploy" }, "config": { "gcpProjectLabel": "GCP Project", @@ -671,11 +724,14 @@ }, "auth": { "connecting": "Connecting to GCP...", - "browserPrompt": "A browser window will open for Google authentication. Complete the sign-in to continue." + "browserPrompt": "A browser window will open for Google authentication. Complete the sign-in to continue.", + "noWindow": "Didn't see the window? It may have been blocked or closed.", + "reopen": "Reopen sign-in window" }, "plan": { "noChanges": "No changes detected. Infrastructure is up to date.", "changes": "Plan: {{total}} change(s)", + "destination": "Deploying to {{target}}", "skip": "skip", "actionCreate": "create", "actionUpdate": "update", @@ -1016,6 +1072,9 @@ "contextPromote": "Promote to production", "contextRename": "Rename", "contextDelete": "Delete environment", + "contextDeleteConfirm": "Click again to delete", + "loadError": "Couldn't load environments", + "retry": "Retry", "envNamePlaceholder": "Environment name" }, "renameModal": { @@ -1048,7 +1107,8 @@ "promote": "Promote", "to": "to", "identicalMessage": "Both environments are identical. Nothing to promote.", - "changesWillBeApplied": "will be applied to", + "changesWillBeApplied": "will update the design of", + "designOnlyNote": "Promotion updates the design only — you'll still need to deploy {{target}} to apply these changes to live infrastructure.", "change": "change", "changes": "changes", "inSync": "Environments are in sync", @@ -1224,6 +1284,7 @@ "deploy": { "current": "Current", "live": "Live", + "deployed": "Deployed", "urlLabel": "URL", "imageLabel": "Image", "resourceIdLabel": "Resource ID", @@ -1282,7 +1343,13 @@ "change": "change", "changes": "changes", "checkButton": "Check for Drift", - "checkingButton": "Checking..." + "checkingButton": "Checking...", + "unverified": "Couldn't verify against cloud — showing stored state", + "unknown": "Status unknown", + "extra": "Tracked, but not in your design", + "justNow": "Checked just now", + "checkedAgo": "Checked {{ago}} ago", + "stale": "stale" }, "network": { "title": "Network", @@ -1385,6 +1452,7 @@ "contextRename": "Rename", "contextMoveTo": "Move to", "contextMoveRoot": "Root", + "contextOpen": "Open", "contextDelete": "Delete", "subCanvas": "Architecture", "subTable": "Table", @@ -1965,6 +2033,8 @@ }, "cost": { "title": "Cost Estimation", + "estQualifier": "Est.", + "estProvenance": "Estimated from public list prices (early 2026) — not actual billed spend", "empty": "Add resources to your canvas to see cost estimates. Drag blocks from the left sidebar to get started.", "categoryBreakdown": "Cost Breakdown", "timeProjections": "Time Projections", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index 1983f5c2..9ed0b553 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -112,6 +112,32 @@ "continueWithGithub": "使用 GitHub 继续" } }, + "a11y": { + "announce": { + "connecting": "正在连接云服务提供商…", + "planning": "正在规划部署…", + "planReady": "部署计划已就绪,可供查看。", + "deploying": "正在部署…", + "deployingCount": "正在部署 {{total}} 个资源…", + "destroying": "正在销毁资源…", + "deploySucceeded": "部署成功。", + "deployFailed": "部署失败。", + "deployFailedCount": "部署失败 — {{failed}} 个资源失败。" + } + }, + "deployStatus": { + "deployed": "已部署", + "failed": "失败", + "deploying": "部署中", + "destroying": "销毁中", + "planning": "规划中", + "planReady": "计划就绪", + "queued": "排队中", + "connecting": "连接中", + "cancelled": "已取消", + "notDeployed": "未部署", + "fetchError": "状态不可用" + }, "canvas": { "title": "基础设施画布", "emptyState": { @@ -120,7 +146,20 @@ "blankCanvas": "空白画布", "pickTemplate": "选择模板...", "quickStart": "快速开始", - "more": "更多..." + "more": "更多...", + "hint": "按 Shift+A 或从左侧拖入一个区块即可开始", + "dismiss": "关闭快速开始" + }, + "spotlight": { + "recent": "最近使用", + "catalog": "所有区块" + }, + "socketLegend": { + "title": "连接端口", + "traffic": "数据与流量", + "config": "配置(环境变量、密钥)", + "repository": "源代码仓库", + "domain": "域名 / DNS" }, "tooltip": { "connections": "个连接", @@ -215,6 +254,7 @@ "noSource": "连接一个计算或数据库块以开始流式传输日志。", "ambiguous": "存在多个入站连接 — 请在属性面板中选择一个源。", "unsupported": "此源类型不输出 Cloud Logging 日志。", + "providerUnsupported": "暂不支持 {{provider}} 的实时日志。", "permissionDenied": "Cloud Logging 访问被拒绝。请向部署服务账户授予 roles/logging.viewer 权限。", "error": "连接错误。正在重试。", "connecting": "连接中…", @@ -225,6 +265,11 @@ "connecting": "连接中", "error": "错误", "idle": "空闲", + "preDeploy": "待部署", + "noSource": "无来源", + "ambiguous": "选择来源", + "unsupported": "不支持", + "providerUnsupported": "不可用", "pipelineLive": "运行中", "pipelineFailed": "失败", "pipelineBuilding": "构建中", @@ -516,8 +561,11 @@ "actionTypeBlockDescription": "调用画布上的一个块", "actionTypeHttpLabel": "发送 HTTP 请求", "actionTypeHttpDescription": "访问外部 URL", - "customValuePrefix": "自定义:{{value}} {{unit}}" + "customValuePrefix": "自定义:{{value}} {{unit}}", + "required": "必填", + "advanced": "高级" }, + "validating": "检查中…", "log": { "sectionTitle": "日志", "modePolling": "轮询", @@ -525,6 +573,9 @@ "modeTail": "追踪", "modeTailHint": "通过 gRPC 进行亚秒级流式传输。", "sourceOverride": "源覆盖", + "retry": "重试", + "clear": "清除", + "dropped": "已丢弃 {{count}} 条较早的日志 — 缓冲区上限为 200 行。", "noSupportedSource": "未连接受支持的源。请将计算或数据库块连接到此日志终端。", "clearOverride": "清除覆盖", "selectSource": "— 选择源 —", @@ -534,6 +585,7 @@ "pillNoSource": "无源", "pillAmbiguous": "源不明确", "pillUnsupported": "不支持的源", + "pillProviderUnsupported": "不可用", "pillAccessDenied": "访问被拒绝", "pillError": "错误", "pillIdle": "空闲" @@ -640,7 +692,8 @@ "connected": "已连接 {{provider}}", "connectedViaOAuth": "通过 Google OAuth", "connectedViaServiceAccount": "通过服务账号", - "notConnected": "{{provider}} 未连接——请在云服务提供商设置中配置" + "notConnected": "{{provider}} 未连接——请在云服务提供商设置中配置", + "reauthNeeded": "{{provider}} 登录已过期——请重新连接以部署" }, "config": { "gcpProjectLabel": "GCP 项目", @@ -671,11 +724,14 @@ }, "auth": { "connecting": "正在连接 GCP...", - "browserPrompt": "浏览器窗口将打开进行 Google 认证。请完成登录以继续。" + "browserPrompt": "浏览器窗口将打开进行 Google 认证。请完成登录以继续。", + "noWindow": "没有看到窗口?它可能被拦截或已关闭。", + "reopen": "重新打开登录窗口" }, "plan": { "noChanges": "未检测到更改。基础设施已是最新状态。", "changes": "计划:{{total}} 项更改", + "destination": "部署目标:{{target}}", "skip": "跳过", "actionCreate": "创建", "actionUpdate": "更新", @@ -1015,6 +1071,9 @@ "contextPromote": "提升到生产环境", "contextRename": "重命名", "contextDelete": "删除环境", + "contextDeleteConfirm": "再次点击以删除", + "loadError": "无法加载环境", + "retry": "重试", "envNamePlaceholder": "环境名称" }, "renameModal": { @@ -1047,7 +1106,8 @@ "promote": "提升", "to": "到", "identicalMessage": "两个环境完全一致,无需提升。", - "changesWillBeApplied": "将应用到", + "changesWillBeApplied": "将更新其设计:", + "designOnlyNote": "提升仅更新设计——您仍需部署 {{target}} 才能将这些更改应用到实际基础设施。", "change": "项更改", "changes": "项更改", "inSync": "环境已同步", @@ -1223,6 +1283,7 @@ "deploy": { "current": "当前", "live": "运行中", + "deployed": "已部署", "urlLabel": "URL", "imageLabel": "镜像", "resourceIdLabel": "资源 ID", @@ -1281,7 +1342,13 @@ "change": "项更改", "changes": "项更改", "checkButton": "检查偏差", - "checkingButton": "检查中..." + "checkingButton": "检查中...", + "unverified": "无法与云端核对 — 显示本地存储状态", + "unknown": "状态未知", + "extra": "已跟踪,但不在您的设计中", + "justNow": "刚刚检查", + "checkedAgo": "{{ago}}前检查", + "stale": "已过期" }, "network": { "title": "网络", @@ -1384,6 +1451,7 @@ "contextRename": "重命名", "contextMoveTo": "移动到", "contextMoveRoot": "根目录", + "contextOpen": "打开", "contextDelete": "删除", "subCanvas": "架构", "subTable": "表格", @@ -1964,6 +2032,8 @@ }, "cost": { "title": "成本估算", + "estQualifier": "预估", + "estProvenance": "根据公开标价估算(2026 年初)— 非实际账单金额", "empty": "将资源添加到画布以查看成本估算。从左侧边栏拖动组件开始。", "categoryBreakdown": "成本明细", "timeProjections": "时间预测", diff --git a/packages/ui/src/shared/components/__tests__/app-bar.test.tsx b/packages/ui/src/shared/components/__tests__/app-bar.test.tsx index dfd2481f..13b498ee 100644 --- a/packages/ui/src/shared/components/__tests__/app-bar.test.tsx +++ b/packages/ui/src/shared/components/__tests__/app-bar.test.tsx @@ -25,6 +25,7 @@ const mocks = vi.hoisted(() => ({ navigate: vi.fn(), checkGitHubConnection: vi.fn(() => ({ type: 'integrations/checkGitHubConnection' })), checkAnthropicConnection: vi.fn(() => ({ type: 'integrations/checkAnthropicConnection' })), + checkAllProviderConnections: vi.fn(() => ({ type: 'integrations/checkAllProviderConnections' })), startTour: vi.fn(), tours: [ { id: 'canvas-tour', title: 'tour.canvas.title', steps: [] }, @@ -119,6 +120,7 @@ vi.mock('../../../i18n', () => ({ vi.mock('../../../store/slices/integrations-slice', () => ({ checkGitHubConnection: mocks.checkGitHubConnection, checkAnthropicConnection: mocks.checkAnthropicConnection, + checkAllProviderConnections: mocks.checkAllProviderConnections, })); vi.mock('../../utils/cn', () => ({ @@ -129,6 +131,12 @@ vi.mock('../breadcrumbs', () => ({ Breadcrumbs: mocks.Breadcrumbs, })); +// AX2 — the live-region announcer reads the deploy slice; stub it here (this +// suite renders the full tree and its mock store has no deploy slice). +vi.mock('../live-announcer', () => ({ + LiveAnnouncer: () => null, +})); + vi.mock('../ui/tooltip', () => ({ Tooltip: mocks.Tooltip, TooltipTrigger: mocks.TooltipTrigger, diff --git a/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx b/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx index 6808250e..9d33e4a5 100644 --- a/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx +++ b/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx @@ -33,8 +33,9 @@ vi.mock('react-router-dom', () => ({ ), })); -vi.mock('../../hooks/use-resolve-path', () => ({ - useResolvePath: () => mocks.resolved, +vi.mock('../../hooks/use-resolve-path-context', () => ({ + useResolvePathContext: () => mocks.resolved, + TOP_ROUTES: { settings: 'Settings', team: 'Team' }, })); import { Breadcrumbs } from '../breadcrumbs'; @@ -225,13 +226,15 @@ describe('Breadcrumbs — URL fallback', () => { expect(text).not.toContain('Acme'); }); - it('does not render fallback crumbs when resolver is loading', () => { + // IA9 — show URL-derived fallback crumbs even while resolving, so the trail + // doesn't flicker down to just Home on every navigation. + it('renders fallback crumbs from the URL even while the resolver is loading', () => { mocks.pathname = '/folder'; mocks.resolved.loading = true; mocks.resolved.breadcrumbs = []; const tree = renderBC(); const text = collectText(tree); - expect(text).not.toContain('Folder'); + expect(text).toContain('Folder'); }); it('builds path strings prefixed with /', () => { diff --git a/packages/ui/src/shared/components/__tests__/live-announcer.test.tsx b/packages/ui/src/shared/components/__tests__/live-announcer.test.tsx new file mode 100644 index 00000000..875bb718 --- /dev/null +++ b/packages/ui/src/shared/components/__tests__/live-announcer.test.tsx @@ -0,0 +1,73 @@ +/** + * LiveAnnouncer (AX2) — pure status→announcement mapping + the live-region shape. + */ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + state: { deploy: { status: 'success' as string, nodesById: {} } }, +})); + +vi.mock('react', async (orig) => { + const actual = (await orig()) as typeof import('react'); + return { ...actual, useMemo: (fn: () => T) => fn() }; +}); +vi.mock('react-redux', () => ({ + useSelector: (sel: (s: unknown) => unknown) => sel(mocks.state), + shallowEqual: () => false, +})); +vi.mock('../../../i18n', () => ({ useTranslation: () => ({ t: (k: string) => k }) })); +vi.mock('../../../store/slices/deploy-slice', () => ({ deriveRollup: () => ({ total: 0, failed: 0 }) })); + +import { LiveAnnouncer, deployAnnouncement } from '../live-announcer'; + +const t = (k: string, vars?: Record) => (vars ? `${k}:${JSON.stringify(vars)}` : k); + +describe('deployAnnouncement', () => { + const empty = { total: 0, failed: 0 }; + + it('maps each status to its announcement', () => { + expect(deployAnnouncement(t, 'authenticating' as never, empty)).toBe('a11y.announce.connecting'); + expect(deployAnnouncement(t, 'planning' as never, empty)).toBe('a11y.announce.planning'); + expect(deployAnnouncement(t, 'planned' as never, empty)).toBe('a11y.announce.planReady'); + expect(deployAnnouncement(t, 'destroying' as never, empty)).toBe('a11y.announce.destroying'); + expect(deployAnnouncement(t, 'success' as never, empty)).toBe('a11y.announce.deploySucceeded'); + }); + + it('is silent (empty string) when idle so nothing is announced', () => { + expect(deployAnnouncement(t, 'idle' as never, empty)).toBe(''); + }); + + it('includes the resource total while deploying (or a generic message at 0)', () => { + expect(deployAnnouncement(t, 'deploying' as never, { total: 8, failed: 0 })).toBe( + 'a11y.announce.deployingCount:{"total":8}', + ); + expect(deployAnnouncement(t, 'deploying' as never, empty)).toBe('a11y.announce.deploying'); + }); + + it('includes the failed count on error', () => { + expect(deployAnnouncement(t, 'error' as never, { total: 8, failed: 2 })).toBe( + 'a11y.announce.deployFailedCount:{"failed":2}', + ); + expect(deployAnnouncement(t, 'error' as never, empty)).toBe('a11y.announce.deployFailed'); + }); +}); + +describe('LiveAnnouncer component', () => { + it('renders a polite, atomic status live region with the current announcement', () => { + mocks.state.deploy.status = 'success'; + const el = (LiveAnnouncer as () => React.ReactElement)(); + const props = el.props as Record; + expect(props.role).toBe('status'); + expect(props['aria-live']).toBe('polite'); + expect(props['aria-atomic']).toBe('true'); + expect(props['data-testid']).toBe('live-announcer'); + expect(props.children).toBe('a11y.announce.deploySucceeded'); + }); + + it('renders an empty region when idle (nothing to announce)', () => { + mocks.state.deploy.status = 'idle'; + const el = (LiveAnnouncer as () => React.ReactElement)(); + expect((el.props as Record).children).toBe(''); + }); +}); diff --git a/packages/ui/src/shared/components/__tests__/project-toolbar.test.tsx b/packages/ui/src/shared/components/__tests__/project-toolbar.test.tsx index 952444b5..cec70d62 100644 --- a/packages/ui/src/shared/components/__tests__/project-toolbar.test.tsx +++ b/packages/ui/src/shared/components/__tests__/project-toolbar.test.tsx @@ -45,6 +45,7 @@ const mocks = vi.hoisted(() => ({ selectCanRedoValue: false, dispatch: vi.fn(), navigate: vi.fn(), + setSearchParams: vi.fn(), // Action-creator identity spies — return action objects we can assert on. setEdgeStyle: vi.fn((s: unknown) => ({ type: 'ui/setEdgeStyle', payload: s })), setAutoOrganizeStyle: vi.fn((s: unknown) => ({ type: 'ui/setAutoOrganizeStyle', payload: s })), @@ -90,6 +91,7 @@ vi.mock('react-redux', () => ({ vi.mock('react-router-dom', () => ({ useNavigate: () => mocks.navigate, + useSearchParams: () => [new URLSearchParams(), mocks.setSearchParams], })); vi.mock('../../../i18n', () => ({ @@ -217,6 +219,7 @@ beforeEach(() => { mocks.createCard.mockClear(); mocks.fetchEnvironments.mockClear(); mocks.setActiveEnvironment.mockClear(); + mocks.setSearchParams.mockClear(); vi.stubGlobal('window', { innerWidth: 1280, innerHeight: 720, open: vi.fn() }); }); @@ -570,6 +573,10 @@ describe('ProjectToolbar — environment selector', () => { await (select.props.onChange as (id: string) => Promise)('env-1'); expect(mocks.setActiveEnvironment).toHaveBeenCalledWith({ projectId: 'proj-1', envId: 'env-1' }); expect(mocks.setActiveCard).toHaveBeenCalledWith('c-1'); + // IA6 — the switch also writes ?env= so the env is shareable. + expect(mocks.setSearchParams).toHaveBeenCalled(); + const updater = mocks.setSearchParams.mock.calls[0][0] as (p: URLSearchParams) => URLSearchParams; + expect(updater(new URLSearchParams()).get('env')).toBe('env-1'); }); it('falls back to API graph.load when the card has no nodes yet', async () => { diff --git a/packages/ui/src/shared/components/__tests__/status-bar.test.tsx b/packages/ui/src/shared/components/__tests__/status-bar.test.tsx index f285d7d1..348f36e6 100644 --- a/packages/ui/src/shared/components/__tests__/status-bar.test.tsx +++ b/packages/ui/src/shared/components/__tests__/status-bar.test.tsx @@ -565,12 +565,13 @@ describe('StatusBar — IntegrationStatusDots is rendered', () => { describe('StatusBar — DeployStatusIndicator', () => { it('returns null when deploy.status === idle (no deploy section text)', () => { const tree = callRender(); - expect(collectText(tree)).not.toContain('statusBar.connecting'); - expect(collectText(tree)).not.toContain('statusBar.deploying'); - expect(collectText(tree)).not.toContain('statusBar.planning'); - expect(collectText(tree)).not.toContain('statusBar.deployed'); - expect(collectText(tree)).not.toContain('statusBar.deployFailed'); - expect(collectText(tree)).not.toContain('statusBar.planReady'); + // IA4 — labels now come from the canonical deployStatus.* vocabulary. + expect(collectText(tree)).not.toContain('deployStatus.connecting'); + expect(collectText(tree)).not.toContain('deployStatus.deploying'); + expect(collectText(tree)).not.toContain('deployStatus.planning'); + expect(collectText(tree)).not.toContain('deployStatus.deployed'); + expect(collectText(tree)).not.toContain('deployStatus.failed'); + expect(collectText(tree)).not.toContain('deployStatus.planReady'); }); it('renders the authenticating banner', () => { @@ -579,7 +580,7 @@ describe('StatusBar — DeployStatusIndicator', () => { nodesById: {}, }; const tree = callRender(); - expect(collectText(tree)).toContain('statusBar.connecting'); + expect(collectText(tree)).toContain('deployStatus.connecting'); }); it('renders the deploying banner with rollup percentage', () => { @@ -591,32 +592,32 @@ describe('StatusBar — DeployStatusIndicator', () => { mocks.deriveRollupPercentage.mockReturnValue(25); const tree = callRender(); const text = collectText(tree); - expect(text).toContain('statusBar.deploying'); - expect(text).toContain('"pct":25'); + expect(text).toContain('deployStatus.deploying'); + expect(text).toContain('25%'); expect(mocks.deriveRollup).toHaveBeenCalledWith({ a: {} }); }); it('renders the planning banner', () => { (mocks.state as Record).deploy = { status: 'planning', nodesById: {} }; const tree = callRender(); - expect(collectText(tree)).toContain('statusBar.planning'); + expect(collectText(tree)).toContain('deployStatus.planning'); }); it('renders the success banner', () => { (mocks.state as Record).deploy = { status: 'success', nodesById: {} }; const tree = callRender(); - expect(collectText(tree)).toContain('statusBar.deployed'); + expect(collectText(tree)).toContain('deployStatus.deployed'); }); - it('renders the error banner', () => { + it('renders the error banner with the canonical "failed" label (IA4)', () => { (mocks.state as Record).deploy = { status: 'error', nodesById: {} }; const tree = callRender(); - expect(collectText(tree)).toContain('statusBar.deployFailed'); + expect(collectText(tree)).toContain('deployStatus.failed'); }); it('renders the planned banner', () => { (mocks.state as Record).deploy = { status: 'planned', nodesById: {} }; const tree = callRender(); - expect(collectText(tree)).toContain('statusBar.planReady'); + expect(collectText(tree)).toContain('deployStatus.planReady'); }); }); diff --git a/packages/ui/src/shared/components/app-bar.tsx b/packages/ui/src/shared/components/app-bar.tsx index dacc9f8d..0cccfc2b 100644 --- a/packages/ui/src/shared/components/app-bar.tsx +++ b/packages/ui/src/shared/components/app-bar.tsx @@ -32,7 +32,12 @@ import { GitHubConnectModal } from '../../features/integrations/components/githu import { ProviderConnectModal } from '../../features/integrations/components/provider-connect-modal'; import { useTour } from '../../features/tour'; import { useTranslation } from '../../i18n'; -import { checkAnthropicConnection, checkGitHubConnection } from '../../store/slices/integrations-slice'; +import { + checkAllProviderConnections, + checkAnthropicConnection, + checkGitHubConnection, +} from '../../store/slices/integrations-slice'; +import { LiveAnnouncer } from './live-announcer'; import { cn } from '../utils/cn'; import type { RootState, AppDispatch } from '../../store'; @@ -61,8 +66,10 @@ export const AppBar: React.FC = memo(() => { const dispatch = useDispatch(); const navigate = useNavigate(); const githubStatus = useSelector((s: RootState) => s.integrations.integrations.github?.status); - const gcpStatus = useSelector((s: RootState) => s.integrations.integrations.gcp?.status); const anthropicStatus = useSelector((s: RootState) => s.integrations.integrations.anthropic?.status); + // EI2 — per-provider connection map drives the app-bar rings for every cloud. + const integrations = useSelector((s: RootState) => s.integrations.integrations); + const isProviderConnected = (id: string) => integrations[id]?.status === 'connected'; const [showGitHub, setShowGitHub] = useState(false); const [showGcp, setShowGcp] = useState(false); const [showAws, setShowAws] = useState(false); @@ -77,10 +84,13 @@ export const AppBar: React.FC = memo(() => { useEffect(() => { dispatch(checkGitHubConnection()); dispatch(checkAnthropicConnection()); + dispatch(checkAllProviderConnections()); }, [dispatch]); return ( <> + {/* AX2 — one polite live region for deploy lifecycle announcements. */} +
    { src={gcpIcon} onClick={() => setShowGcp(true)} tip={t('common.providers.gcp')} - connected={gcpStatus === 'connected'} + connected={isProviderConnected('gcp')} /> )} {isProviderEnabled('aws') && ( @@ -121,6 +131,7 @@ export const AppBar: React.FC = memo(() => { src={awsIcon} onClick={() => setShowAws(true)} tip={t('common.providers.aws')} + connected={isProviderConnected('aws')} /> )} {isProviderEnabled('azure') && ( @@ -129,6 +140,7 @@ export const AppBar: React.FC = memo(() => { src={azureIcon} onClick={() => setShowAzure(true)} tip={t('common.providers.azure')} + connected={isProviderConnected('azure')} /> )} {isProviderEnabled('alibaba') && ( @@ -137,6 +149,7 @@ export const AppBar: React.FC = memo(() => { src={alibabaIcon} onClick={() => setShowAlibaba(true)} tip={t('common.providers.alibaba')} + connected={isProviderConnected('alibaba')} /> )} {isProviderEnabled('oci') && ( @@ -145,6 +158,7 @@ export const AppBar: React.FC = memo(() => { src={ociIcon} onClick={() => setShowOci(true)} tip={t('common.providers.oci')} + connected={isProviderConnected('oci')} /> )} {isProviderEnabled('digitalocean') && ( @@ -153,6 +167,7 @@ export const AppBar: React.FC = memo(() => { src={digitaloceanIcon} onClick={() => setShowDigitalocean(true)} tip={t('common.providers.digitalocean')} + connected={isProviderConnected('digitalocean')} /> )} {isProviderEnabled('ibm') && ( @@ -161,6 +176,7 @@ export const AppBar: React.FC = memo(() => { src={ibmIcon} onClick={() => setShowIbm(true)} tip={t('common.providers.ibm')} + connected={isProviderConnected('ibm')} /> )} {isProviderEnabled('kubernetes') && ( @@ -169,6 +185,7 @@ export const AppBar: React.FC = memo(() => { src={kubernetesIcon} onClick={() => setShowKubernetes(true)} tip={t('common.providers.kubernetes')} + connected={isProviderConnected('kubernetes')} /> )} diff --git a/packages/ui/src/shared/components/breadcrumbs.tsx b/packages/ui/src/shared/components/breadcrumbs.tsx index 508c3579..83f06d17 100644 --- a/packages/ui/src/shared/components/breadcrumbs.tsx +++ b/packages/ui/src/shared/components/breadcrumbs.tsx @@ -9,21 +9,18 @@ import { ChevronRight, Home } from 'lucide-react'; import React from 'react'; import { useSelector } from 'react-redux'; import { Link, useLocation } from 'react-router-dom'; -import { useResolvePath } from '../hooks/use-resolve-path'; +import { useResolvePathContext, TOP_ROUTES } from '../hooks/use-resolve-path-context'; import type { RootState } from '../../store'; -const TOP_ROUTES: Record = { - settings: 'Settings', - team: 'Team', -}; - export const Breadcrumbs: React.FC = () => { const { pathname } = useLocation(); const segments = pathname.split('/').filter(Boolean); const selectedOrg = useSelector((s: RootState) => s.account?.selectedOrg); const isTopRoute = segments.length === 1 && TOP_ROUTES[segments[0]]; - const resolved = useResolvePath(isTopRoute ? [] : segments); + // IA7 — the resolution (with the same top-route gate) is now shared at the + // shell; read it instead of firing a duplicate set of resolution POSTs. + const resolved = useResolvePathContext(); // Build crumbs from resolved data, or fall back to URL segments const crumbs: { label: string; path: string }[] = []; @@ -32,7 +29,12 @@ export const Breadcrumbs: React.FC = () => { crumbs.push({ label: TOP_ROUTES[segments[0]], path: pathname }); } else if (resolved.breadcrumbs.length > 0) { crumbs.push(...resolved.breadcrumbs); - } else if (!resolved.loading && segments.length > 0) { + } else if (segments.length > 0) { + // IA9 — fall back to URL-derived crumbs even WHILE the resolver is loading. + // Gating this on `!resolved.loading` collapsed the trail to just Home on + // every navigation, flickering the user's sense of location; the URL + // segments already reflect where they are, so show a best-guess trail until + // the resolved labels arrive. // Fallback: build crumbs from URL segments (skip org slug) const start = selectedOrg ? 1 : 0; for (let i = start; i < segments.length; i++) { diff --git a/packages/ui/src/shared/components/live-announcer.tsx b/packages/ui/src/shared/components/live-announcer.tsx new file mode 100644 index 00000000..f0efc343 --- /dev/null +++ b/packages/ui/src/shared/components/live-announcer.tsx @@ -0,0 +1,73 @@ +/** + * LiveAnnouncer — a single polite screen-reader live region (AX2). + * + * The deploy lifecycle was entirely silent to assistive tech: a low-vision user + * triggered a deploy and heard nothing about whether it was running, succeeded, + * or failed. This mounts one visually-hidden `aria-live="polite"` region in the + * shell whose text mirrors the deploy status, so transitions are announced. + * + * Kept deliberately coarse (start / succeeded / failed) rather than per-node so + * the announcements aren't chatty — the text only changes on status + * transitions, which is exactly when a live region should speak. + */ + +import React, { useMemo } from 'react'; +import { useSelector, shallowEqual } from 'react-redux'; +import { useTranslation } from '../../i18n'; +import { deriveRollup, type DeployStatus } from '../../store/slices/deploy-slice'; +import type { RootState } from '../../store'; + +// Visually hidden, but present in the accessibility tree. +const SR_ONLY: React.CSSProperties = { + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clipPath: 'inset(50%)', + whiteSpace: 'nowrap', + border: 0, +}; + +type T = (key: string, vars?: Record) => string; + +/** Pure status → announcement mapping (exported for unit testing). */ +export function deployAnnouncement(t: T, status: DeployStatus, rollup: { total: number; failed: number }): string { + switch (status) { + case 'authenticating': + return t('a11y.announce.connecting'); + case 'planning': + return t('a11y.announce.planning'); + case 'planned': + return t('a11y.announce.planReady'); + case 'deploying': + return rollup.total > 0 + ? t('a11y.announce.deployingCount', { total: rollup.total }) + : t('a11y.announce.deploying'); + case 'destroying': + return t('a11y.announce.destroying'); + case 'success': + return t('a11y.announce.deploySucceeded'); + case 'error': + return rollup.failed > 0 + ? t('a11y.announce.deployFailedCount', { failed: rollup.failed }) + : t('a11y.announce.deployFailed'); + default: + return ''; + } +} + +export const LiveAnnouncer: React.FC = () => { + const { t } = useTranslation(); + const status = useSelector((s: RootState) => s.deploy.status); + const nodesById = useSelector((s: RootState) => s.deploy.nodesById, shallowEqual); + const rollup = useMemo(() => deriveRollup(nodesById), [nodesById]); + const message = deployAnnouncement(t, status, rollup); + + return ( +
    + {message} +
    + ); +}; diff --git a/packages/ui/src/shared/components/project-toolbar.tsx b/packages/ui/src/shared/components/project-toolbar.tsx index ac51b905..dcbae55a 100644 --- a/packages/ui/src/shared/components/project-toolbar.tsx +++ b/packages/ui/src/shared/components/project-toolbar.tsx @@ -22,7 +22,7 @@ import { } from 'lucide-react'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from '../../i18n'; import { IceSelect } from './ui/ice-select'; import { @@ -104,6 +104,7 @@ const TSep: React.FC = () =>
    ; export const ProjectToolbar: React.FC = ({ basePath, activeSubpage }) => { const { t } = useTranslation(); const navigate = useNavigate(); + const [, setSearchParams] = useSearchParams(); const dispatch = useDispatch(); const isCanvasView = activeSubpage === 'canvas'; @@ -151,6 +152,15 @@ export const ProjectToolbar: React.FC = ({ basePath, active const env = environments.find((e) => e.id === envId); if (!env) return; dispatch(setActiveEnvironment({ projectId, envId })); + // IA6 — keep the env in the URL (shareable / survives reload). + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set('env', envId); + return next; + }, + { replace: true }, + ); try { const { store } = await import('../../store'); const state = store.getState(); @@ -175,7 +185,7 @@ export const ProjectToolbar: React.FC = ({ basePath, active console.error('Failed to load environment card:', err); } }, - [projectId, environments, dispatch], + [projectId, environments, dispatch, setSearchParams], ); // ── Navigation ─────────────────────────────────────────────────────────── diff --git a/packages/ui/src/shared/components/status-bar.tsx b/packages/ui/src/shared/components/status-bar.tsx index 440fd4fd..5b58d139 100644 --- a/packages/ui/src/shared/components/status-bar.tsx +++ b/packages/ui/src/shared/components/status-bar.tsx @@ -29,6 +29,7 @@ import { IntegrationStatusDots } from '../../features/integrations'; import { useTranslation } from '../../i18n'; import { selectActiveCard } from '../../store/slices/cards-slice'; import { deriveRollup, deriveRollupPercentage } from '../../store/slices/deploy-slice'; +import { deployStatusMeta } from '../utils/deploy-status'; import { openValidation } from '../../store/slices/ui-slice'; import { useSystemStats } from '../hooks/use-system-stats'; import type { RootState } from '../../store'; @@ -208,50 +209,37 @@ const DeployStatusIndicator: React.FC = () => { const deployProgress = deriveRollupPercentage(rollup); if (deployStatus === 'idle') return null; + const cfg = DEPLOY_INDICATOR[deployStatus]; + if (!cfg) return null; + const Icon = cfg.icon; + // IA4 — canonical label vocabulary, shared with the env + deployments surfaces + // (e.g. `error` now reads "Failed", consistent with the other surfaces). + const label = t(deployStatusMeta(deployStatus).labelKey); return ( <>
    - {deployStatus === 'authenticating' && ( - <> - - {t('statusBar.connecting')} - - )} - {deployStatus === 'deploying' && ( - <> - - {t('statusBar.deploying', { pct: deployProgress })} - - )} - {deployStatus === 'planning' && ( - <> - - {t('statusBar.planning')} - - )} - {deployStatus === 'success' && ( - <> - - {t('statusBar.deployed')} - - )} - {deployStatus === 'error' && ( - <> - - {t('statusBar.deployFailed')} - - )} - {deployStatus === 'planned' && ( - <> - - {t('statusBar.planReady')} - - )} + + + {label} + {deployStatus === 'deploying' ? ` ${deployProgress}%` : ''} +
    ); }; +// Per-status icon + ice-token colour for the status-bar deploy pill. The label +// itself comes from the canonical `deployStatusMeta` (IA4). +const DEPLOY_INDICATOR: Record = { + authenticating: { icon: Loader2, spin: true, color: 'text-ice-yellow' }, + deploying: { icon: Loader2, spin: true, color: 'text-ice-green' }, + destroying: { icon: Loader2, spin: true, color: 'text-ice-red' }, + planning: { icon: Loader2, spin: true, color: 'text-ice-accent' }, + success: { icon: CheckCircle, color: 'text-ice-green' }, + error: { icon: XCircle, color: 'text-ice-red' }, + planned: { icon: Rocket, color: 'text-ice-yellow' }, +}; + const StatusDivider: React.FC = () =>
    ; diff --git a/packages/ui/src/shared/hooks/__tests__/use-reduced-motion.test.tsx b/packages/ui/src/shared/hooks/__tests__/use-reduced-motion.test.tsx index d61e1ed9..84039839 100644 --- a/packages/ui/src/shared/hooks/__tests__/use-reduced-motion.test.tsx +++ b/packages/ui/src/shared/hooks/__tests__/use-reduced-motion.test.tsx @@ -32,7 +32,7 @@ vi.mock('react', async (orig) => { // ─── Imports after mocks ──────────────────────────────────────────────────── -import { useReducedMotion } from '../use-reduced-motion'; +import { useReducedMotion, prefersReducedMotion } from '../use-reduced-motion'; // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -178,3 +178,31 @@ describe('useReducedMotion', () => { }).not.toThrow(); }); }); + +// One-shot, non-hook read used to gate SVG SMIL (AX4) from render-pure code. +describe('prefersReducedMotion', () => { + it('returns true when matchMedia reports the reduce preference', () => { + vi.stubGlobal('window', { matchMedia: () => makeMQL(true) }); + expect(prefersReducedMotion()).toBe(true); + }); + + it('returns false when the user has not opted in', () => { + vi.stubGlobal('window', { matchMedia: () => makeMQL(false) }); + expect(prefersReducedMotion()).toBe(false); + }); + + it('falls back to false when matchMedia is missing (the ?? guard)', () => { + vi.stubGlobal('window', { matchMedia: undefined }); + expect(prefersReducedMotion()).toBe(false); + }); + + it('returns false when window is undefined (SSR-safe)', () => { + const original = (globalThis as any).window; + delete (globalThis as any).window; + try { + expect(prefersReducedMotion()).toBe(false); + } finally { + (globalThis as any).window = original; + } + }); +}); diff --git a/packages/ui/src/shared/hooks/__tests__/use-resolve-path-context.test.tsx b/packages/ui/src/shared/hooks/__tests__/use-resolve-path-context.test.tsx new file mode 100644 index 00000000..b60adc00 --- /dev/null +++ b/packages/ui/src/shared/hooks/__tests__/use-resolve-path-context.test.tsx @@ -0,0 +1,87 @@ +/** + * use-resolve-path-context (IA7) — the shell-level provider that resolves the + * URL path ONCE and shares it, preserving the top-route ([]) optimisation. + */ + +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + pathname: '/', + lastSegments: null as string[] | null, + resolved: { + loading: false, + type: 'root' as 'root' | 'folder' | 'project' | 'notFound', + id: null as string | null, + name: '', + subpage: 'canvas', + breadcrumbs: [] as { label: string; path: string }[], + orgPrefix: '', + }, +})); + +vi.mock('react-router-dom', () => ({ + useLocation: () => ({ pathname: mocks.pathname }), +})); + +vi.mock('../use-resolve-path', () => ({ + useResolvePath: (segments: string[]) => { + mocks.lastSegments = segments; + return mocks.resolved; + }, +})); + +import { ResolvePathProvider, useResolvePathContext, TOP_ROUTES } from '../use-resolve-path-context'; + +beforeEach(() => { + mocks.pathname = '/'; + mocks.lastSegments = null; +}); + +const renderWithProvider = (probe: React.FC) => + renderToString({React.createElement(probe)}); + +describe('ResolvePathProvider — segment resolution', () => { + it('resolves the full URL segments on a normal project path', () => { + mocks.pathname = '/folder-x/proj-y'; + renderWithProvider(() => null); + expect(mocks.lastSegments).toEqual(['folder-x', 'proj-y']); + }); + + it('resolves [] for a top route (no wasted resolution POST)', () => { + for (const route of Object.keys(TOP_ROUTES)) { + mocks.pathname = `/${route}`; + renderWithProvider(() => null); + expect(mocks.lastSegments).toEqual([]); + } + }); + + it('does NOT treat a deeper path that merely starts with a top-route slug as a top route', () => { + mocks.pathname = '/settings/sub'; + renderWithProvider(() => null); + expect(mocks.lastSegments).toEqual(['settings', 'sub']); + }); +}); + +describe('useResolvePathContext', () => { + it('exposes the shared resolved value to consumers', () => { + mocks.resolved = { ...mocks.resolved, type: 'project', id: 'p1', name: 'My Project' }; + let seen: unknown; + const Probe: React.FC = () => { + seen = useResolvePathContext(); + return null; + }; + renderWithProvider(Probe); + expect((seen as { id: string }).id).toBe('p1'); + expect((seen as { name: string }).name).toBe('My Project'); + }); + + it('throws when used without a provider', () => { + const Probe: React.FC = () => { + useResolvePathContext(); + return null; + }; + expect(() => renderToString()).toThrow(/ResolvePathProvider/); + }); +}); diff --git a/packages/ui/src/shared/hooks/use-log-stream.ts b/packages/ui/src/shared/hooks/use-log-stream.ts index e9e2e477..ede448a4 100644 --- a/packages/ui/src/shared/hooks/use-log-stream.ts +++ b/packages/ui/src/shared/hooks/use-log-stream.ts @@ -150,6 +150,11 @@ export function useLogStream(terminalNodeId: string): UseLogStreamReturn { const entries = slot?.entries ?? EMPTY_ENTRIES; const source = slot?.source ?? null; const lastError = slot?.lastError ?? null; + // OL5 — a primitive projection of the slot's retry counter. Bumped by + // `retryStream` (the Retry button); included in the subscribe-effect deps so + // a bump tears down a dead stream and re-subscribes. Extracting the number + // (not the slot object) keeps the dep value-stable across entry appends. + const retryNonce = slot?.retryNonce ?? 0; useEffect(() => { // Wait for both cardId and environmentId — without them the POST @@ -310,7 +315,7 @@ export function useLogStream(terminalNodeId: string): UseLogStreamReturn { // type, the fingerprint changes and the effect re-subscribes with // the fresh candidate list (instead of the backend reading a stale // Prisma row from before the canvas's 2s save debounce fired). - }, [cardId, environmentId, terminalNodeId, mode, sourceNodeIdOverride, candidateFingerprint, dispatch]); + }, [cardId, environmentId, terminalNodeId, mode, sourceNodeIdOverride, candidateFingerprint, retryNonce, dispatch]); return { status, entries, source, lastError }; } diff --git a/packages/ui/src/shared/hooks/use-menu-actions.ts b/packages/ui/src/shared/hooks/use-menu-actions.ts index c286e25e..ef5a5df6 100644 --- a/packages/ui/src/shared/hooks/use-menu-actions.ts +++ b/packages/ui/src/shared/hooks/use-menu-actions.ts @@ -76,16 +76,19 @@ export function useMenuActions() { break; // View Menu + // NOTE: zoom/fit are not yet wired to a shared canvas action — zoom is + // currently wheel-only and "fit" lives on the project toolbar's local + // handler, neither reachable from here. These are intentional no-ops + // until the shared zoom/fit actions land (ux-fix-plan Phase 4.4/4.5). + // Do NOT re-add a "handled by keyboard shortcuts" comment — no such + // shortcut exists. case 'menu:zoomIn': - // Handled by SvgCanvas keyboard shortcuts break; case 'menu:zoomOut': - // Handled by SvgCanvas keyboard shortcuts break; case 'menu:fitToScreen': - // Handled by SvgCanvas keyboard shortcuts break; case 'menu:toggleMinimap': diff --git a/packages/ui/src/shared/hooks/use-reduced-motion.ts b/packages/ui/src/shared/hooks/use-reduced-motion.ts index 08c4b39b..e39791e4 100644 --- a/packages/ui/src/shared/hooks/use-reduced-motion.ts +++ b/packages/ui/src/shared/hooks/use-reduced-motion.ts @@ -1,5 +1,18 @@ import { useState, useEffect } from 'react'; +/** + * One-shot, non-reactive read of the reduced-motion preference. For call sites + * that can't take the hook cleanly — e.g. gating an SVG SMIL `` (which + * `@media (prefers-reduced-motion)` in CSS does NOT cover) from a render-pure + * presentational component. Does not update if the OS setting changes mid-life; + * use {@link useReducedMotion} where live reactivity matters. + */ +export function prefersReducedMotion(): boolean { + return typeof window !== 'undefined' + ? (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false) + : false; +} + export function useReducedMotion(): boolean { const [reduced, setReduced] = useState(() => typeof window !== 'undefined' ? (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false) : false, diff --git a/packages/ui/src/shared/hooks/use-resolve-path-context.tsx b/packages/ui/src/shared/hooks/use-resolve-path-context.tsx new file mode 100644 index 00000000..a795287e --- /dev/null +++ b/packages/ui/src/shared/hooks/use-resolve-path-context.tsx @@ -0,0 +1,46 @@ +/** + * Shared path resolution (IA7). + * + * `useResolvePath` issues a POST per path segment to resolve folder/project IDs. + * Four components used to call it independently — DynamicContent, Breadcrumbs, + * ProjectBrowser, ResourcePalette — so every project-page navigation fired the + * same resolution ~4×. This lifts the resolve to a single shell-level provider; + * the four consumers read the shared result via `useResolvePathContext`. + * + * The provider preserves Breadcrumbs' top-route optimisation (resolve `[]` for + * `/settings` and `/team`, which aren't folders/projects, so no wasted POST). + * `TOP_ROUTES` lives here as the single source of truth so the provider's gate + * and the breadcrumb label can't drift apart. + */ + +import React, { createContext, useContext } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useResolvePath, type ResolvedPath } from './use-resolve-path'; + +/** Top-level routes that are NOT folder/project paths (slug → breadcrumb label). */ +export const TOP_ROUTES: Record = { + settings: 'Settings', + team: 'Team', +}; + +const ResolvePathContext = createContext(null); + +export const ResolvePathProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { pathname } = useLocation(); + const segments = pathname.split('/').filter(Boolean); + const isTopRoute = segments.length === 1 && segments[0] in TOP_ROUTES; + const resolved = useResolvePath(isTopRoute ? [] : segments); + return {children}; +}; + +/** + * Read the shared, shell-level path resolution. Must be used under a + * `ResolvePathProvider` (mounted at the app root). + */ +export function useResolvePathContext(): ResolvedPath { + const ctx = useContext(ResolvePathContext); + if (ctx === null) { + throw new Error('useResolvePathContext must be used within a ResolvePathProvider'); + } + return ctx; +} diff --git a/packages/ui/src/shared/hooks/use-resolve-path.ts b/packages/ui/src/shared/hooks/use-resolve-path.ts index d9106ca0..3951f12c 100644 --- a/packages/ui/src/shared/hooks/use-resolve-path.ts +++ b/packages/ui/src/shared/hooks/use-resolve-path.ts @@ -11,7 +11,7 @@ import axiosInstance from '../api/axios-instance'; import { toSlug } from '../utils/slug'; import type { RootState } from '../../store'; -interface ResolvedPath { +export interface ResolvedPath { loading: boolean; type: 'root' | 'folder' | 'project' | 'notFound'; id: string | null; diff --git a/packages/ui/src/shared/utils/__tests__/deploy-status.test.ts b/packages/ui/src/shared/utils/__tests__/deploy-status.test.ts new file mode 100644 index 00000000..7ddc5331 --- /dev/null +++ b/packages/ui/src/shared/utils/__tests__/deploy-status.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { deployStatusMeta, deployStatusTone } from '../deploy-status'; + +describe('deployStatusMeta — canonical deploy-status vocabulary (IA4)', () => { + it('treats error and failed as the same canonical "failed"', () => { + expect(deployStatusMeta('error').tone).toBe('failed'); + expect(deployStatusMeta('failed').tone).toBe('failed'); + expect(deployStatusMeta('error').labelKey).toBe('deployStatus.failed'); + expect(deployStatusMeta('failed').labelKey).toBe('deployStatus.failed'); + }); + + it('maps success → deployed', () => { + expect(deployStatusMeta('success')).toMatchObject({ + tone: 'success', + labelKey: 'deployStatus.deployed', + dotClass: 'bg-emerald-500', + }); + }); + + it('buckets the in-progress states', () => { + expect(deployStatusTone('deploying')).toBe('in-progress'); + expect(deployStatusTone('destroying')).toBe('in-progress'); + }); + + it('buckets the pending states', () => { + for (const s of ['planning', 'planned', 'queued', 'authenticating']) { + expect(deployStatusTone(s)).toBe('pending'); + } + }); + + it('maps cancelled to its own tone', () => { + expect(deployStatusMeta('cancelled').tone).toBe('cancelled'); + expect(deployStatusMeta('cancelled').labelKey).toBe('deployStatus.cancelled'); + }); + + // EI9 — a failed status fetch is "unknown", visually distinct from idle. + it('maps fetch-error to the unknown tone with a hollow dot + dedicated label', () => { + expect(deployStatusMeta('fetch-error')).toMatchObject({ + tone: 'unknown', + labelKey: 'deployStatus.fetchError', + dotClass: 'bg-transparent ring-1 ring-amber-500/70', + }); + // distinct from the filled grey idle dot + expect(deployStatusMeta('fetch-error').dotClass).not.toBe(deployStatusMeta(undefined).dotClass); + }); + + it('falls back to idle / not-deployed for unknown or missing input', () => { + expect(deployStatusMeta(undefined)).toMatchObject({ + tone: 'idle', + labelKey: 'deployStatus.notDeployed', + dotClass: 'bg-ice-text-3/30', + }); + expect(deployStatusMeta('whatever').tone).toBe('idle'); + expect(deployStatusMeta(null).labelKey).toBe('deployStatus.notDeployed'); + }); + + it('preserves the env-util dot colours it now backs (regression guard)', () => { + expect(deployStatusMeta('success').dotClass).toBe('bg-emerald-500'); + expect(deployStatusMeta('deploying').dotClass).toBe('bg-blue-500 animate-pulse'); + expect(deployStatusMeta('failed').dotClass).toBe('bg-red-500'); + expect(deployStatusMeta('planning').dotClass).toBe('bg-amber-500 animate-pulse'); + expect(deployStatusMeta('queued').dotClass).toBe('bg-amber-500 animate-pulse'); + }); +}); diff --git a/packages/ui/src/shared/utils/deploy-status.ts b/packages/ui/src/shared/utils/deploy-status.ts new file mode 100644 index 00000000..e16dfd18 --- /dev/null +++ b/packages/ui/src/shared/utils/deploy-status.ts @@ -0,0 +1,82 @@ +/** + * Canonical deploy-status vocabulary (IA4). + * + * Deploy status was rendered in three places — the status bar, the Environments + * page, and the Deployments list — each with its OWN switch, labels, and colors + * (e.g. the status bar said "Deploy failed" off `error` while the others said + * "Failed" off `failed`). This maps every raw status string the app produces + * onto one canonical { tone, labelKey } pair (+ a dot class for the tailwind + * surfaces), so the surfaces agree on what a deploy state is called and coloured. + * + * `tone` is the canonical bucket; surfaces that use a different colour system + * (e.g. the status bar's ice-* tokens) can map the tone themselves while still + * sharing the label. + */ + +export type DeployStatusTone = 'success' | 'failed' | 'in-progress' | 'pending' | 'cancelled' | 'idle' | 'unknown'; + +export interface DeployStatusMeta { + tone: DeployStatusTone; + /** Canonical i18n key for the human label. */ + labelKey: string; + /** Tailwind dot class (bg colour + pulse for active/pending) for dot surfaces. */ + dotClass: string; +} + +const TONE_DOT: Record = { + success: 'bg-emerald-500', + failed: 'bg-red-500', + 'in-progress': 'bg-blue-500 animate-pulse', + pending: 'bg-amber-500 animate-pulse', + cancelled: 'bg-ice-text-3/50', + idle: 'bg-ice-text-3/30', + // EI9 — a hollow outlined dot reads as "no data" — visually distinct from the + // *filled* grey idle dot, so "couldn't fetch the status" isn't mistaken for + // "never deployed". + unknown: 'bg-transparent ring-1 ring-amber-500/70', +}; + +export function deployStatusTone(raw: string | undefined | null): DeployStatusTone { + switch (raw) { + case 'success': + return 'success'; + case 'error': + case 'failed': + return 'failed'; + case 'deploying': + case 'destroying': + return 'in-progress'; + case 'planning': + case 'planned': + case 'queued': + case 'authenticating': + return 'pending'; + case 'cancelled': + return 'cancelled'; + // EI9 — the env-tab status fetch failed (network/auth); distinct from idle. + case 'fetch-error': + return 'unknown'; + default: + return 'idle'; + } +} + +const LABEL_KEY: Record = { + success: 'deployStatus.deployed', + error: 'deployStatus.failed', + failed: 'deployStatus.failed', + deploying: 'deployStatus.deploying', + destroying: 'deployStatus.destroying', + planning: 'deployStatus.planning', + planned: 'deployStatus.planReady', + queued: 'deployStatus.queued', + authenticating: 'deployStatus.connecting', + cancelled: 'deployStatus.cancelled', + 'fetch-error': 'deployStatus.fetchError', +}; + +export function deployStatusMeta(raw: string | undefined | null): DeployStatusMeta { + const tone = deployStatusTone(raw); + const labelKey = (raw && LABEL_KEY[raw]) || 'deployStatus.notDeployed'; + return { tone, labelKey, dotClass: TONE_DOT[tone] }; +} diff --git a/packages/ui/src/store/slices/__tests__/logs-slice.test.ts b/packages/ui/src/store/slices/__tests__/logs-slice.test.ts index 93266f9d..8d661b6a 100644 --- a/packages/ui/src/store/slices/__tests__/logs-slice.test.ts +++ b/packages/ui/src/store/slices/__tests__/logs-slice.test.ts @@ -10,6 +10,7 @@ import { describe, it, expect } from 'vitest'; import logsReducer, { appendEntry, resumed, + retryStream, setError, setSource, setStatus, @@ -94,6 +95,23 @@ describe('logs-slice', () => { }); }); + describe('retryStream — OL5 re-subscribe trigger', () => { + it('bumps retryNonce, flips status to connecting, and clears lastError', () => { + let state = init(); + state = logsReducer(state, setError({ terminalNodeId: TID, message: 'Access denied' })); + expect(state.byTerminalNodeId[TID].status).toBe('error'); + expect(state.byTerminalNodeId[TID].retryNonce).toBe(0); + + state = logsReducer(state, retryStream({ terminalNodeId: TID })); + expect(state.byTerminalNodeId[TID].retryNonce).toBe(1); + expect(state.byTerminalNodeId[TID].status).toBe('connecting'); + expect(state.byTerminalNodeId[TID].lastError).toBeNull(); + + state = logsReducer(state, retryStream({ terminalNodeId: TID })); + expect(state.byTerminalNodeId[TID].retryNonce).toBe(2); + }); + }); + describe('appendEntry — connecting → streaming promotion', () => { it('promotes status from connecting to streaming on the first entry, and stays streaming on subsequent entries', () => { let state = init(); @@ -129,6 +147,20 @@ describe('logs-slice', () => { // Oldest dropped: ids 0..49 should be gone, ids 50..249 kept. expect(entries[0].insertId).toBe('id-50'); expect(entries[entries.length - 1].insertId).toBe('id-249'); + // OL4 — the 50 dropped entries are counted (not silently lost). + expect(state.byTerminalNodeId[TID]!.droppedCount).toBe(50); + }); + + it('clearEntries resets droppedCount (OL4)', () => { + let state = init(); + state = logsReducer(state, setSubscription({ terminalNodeId: TID, subscriptionId: 'sub-1', mode: 'polling' })); + for (let i = 0; i < 205; i++) { + state = logsReducer(state, appendEntry({ terminalNodeId: TID, entry: entry(`x-${i}`) })); + } + expect(state.byTerminalNodeId[TID]!.droppedCount).toBe(5); + state = logsReducer(state, { type: 'logs/clearEntries', payload: { terminalNodeId: TID } }); + expect(state.byTerminalNodeId[TID]!.entries.length).toBe(0); + expect(state.byTerminalNodeId[TID]!.droppedCount).toBe(0); }); }); diff --git a/packages/ui/src/store/slices/deploy-slice.ts b/packages/ui/src/store/slices/deploy-slice.ts index a1c5f1a0..544e560f 100644 --- a/packages/ui/src/store/slices/deploy-slice.ts +++ b/packages/ui/src/store/slices/deploy-slice.ts @@ -71,6 +71,7 @@ const initialState: DeployState = { deployedResources: [], driftByNode: {}, driftCheckLoading: false, + driftMeta: { checkedAt: null, unsupported: false }, requirements: [], requirementsLoading: false, diagnosis: { status: 'idle', result: null, error: null }, @@ -127,6 +128,7 @@ const deploySlice = createSlice({ state.deployedResources = []; state.driftByNode = {}; state.driftCheckLoading = false; + state.driftMeta = { checkedAt: null, unsupported: false }; state.requirements = []; state.requirementsLoading = false; state.requirementsFetchedAt = undefined; @@ -169,6 +171,7 @@ export const { setDeployedResources, setDriftCheckLoading, setDriftResults, + setDriftMeta, clearDrift, startRequirementsFetch, setRequirements, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/auth.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/auth.test.ts index aea01f8c..e3bdf6a4 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/auth.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/auth.test.ts @@ -26,6 +26,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/deploy-phases.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/deploy-phases.test.ts index a066275a..47fe269a 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/deploy-phases.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/deploy-phases.test.ts @@ -32,6 +32,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/diagnosis.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/diagnosis.test.ts index db17a1cd..71634adc 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/diagnosis.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/diagnosis.test.ts @@ -26,6 +26,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/hydrate.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/hydrate.test.ts index cae8464d..5a4924dc 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/hydrate.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/hydrate.test.ts @@ -32,6 +32,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/logs-resources-drift.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/logs-resources-drift.test.ts index 4322591f..a5d87f62 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/logs-resources-drift.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/logs-resources-drift.test.ts @@ -26,6 +26,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/outcome.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/outcome.test.ts index 02031cbf..f2fc2c1a 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/outcome.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/outcome.test.ts @@ -34,6 +34,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/panel-config.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/panel-config.test.ts index a72eb5bc..8f98fac0 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/panel-config.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/panel-config.test.ts @@ -28,6 +28,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/planning.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/planning.test.ts index 9197800d..b2131bf1 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/planning.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/planning.test.ts @@ -35,6 +35,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/pre-deploy.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/pre-deploy.test.ts index 8543c5c5..53207099 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/pre-deploy.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/pre-deploy.test.ts @@ -26,6 +26,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/requirements.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/requirements.test.ts index a3b7c3f2..0d38599c 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/requirements.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/requirements.test.ts @@ -26,6 +26,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, diff --git a/packages/ui/src/store/slices/deploy/reducers/__tests__/wire-events.test.ts b/packages/ui/src/store/slices/deploy/reducers/__tests__/wire-events.test.ts index 70b0fb14..2426f7e9 100644 --- a/packages/ui/src/store/slices/deploy/reducers/__tests__/wire-events.test.ts +++ b/packages/ui/src/store/slices/deploy/reducers/__tests__/wire-events.test.ts @@ -35,6 +35,7 @@ function makeState(overrides: Partial = {}): DeployState { history: [], deployedResources: [], driftByNode: {}, + driftMeta: { checkedAt: null, unsupported: false }, driftCheckLoading: false, requirements: [], requirementsLoading: false, @@ -413,4 +414,17 @@ describe('applyDeployCompleteEvent', () => { }); expect(next.status).toBe('error'); }); + + // DF8 — the complete wire event carries no duration; don't fabricate one. + it('does NOT push a fabricated-duration completion log line', () => { + const next = produce(makeState({ status: 'deploying', logs: ['Deploying...'] }), (draft) => { + wireEventsReducers.applyDeployCompleteEvent(draft, { + type: 'deploy/applyDeployCompleteEvent', + payload: completeEvent('success'), + } as PayloadAction); + }); + // No "0.0s" (or any) wire-driven completion line — hydrate owns the real one. + expect(next.logs.some((l) => l.includes('0.0'))).toBe(false); + expect(next.logs).toEqual(['Deploying...']); + }); }); diff --git a/packages/ui/src/store/slices/deploy/reducers/logs-resources-drift.ts b/packages/ui/src/store/slices/deploy/reducers/logs-resources-drift.ts index a500935f..c3553a9e 100644 --- a/packages/ui/src/store/slices/deploy/reducers/logs-resources-drift.ts +++ b/packages/ui/src/store/slices/deploy/reducers/logs-resources-drift.ts @@ -15,7 +15,7 @@ * @see rf-dslice-9 */ -import type { DeployedResource, DeployState, NodeDriftInfo } from '../types'; +import type { DeployedResource, DeployState, DriftMeta, NodeDriftInfo } from '../types'; import type { PayloadAction } from '@reduxjs/toolkit'; export const logsResourcesDriftReducers = { @@ -39,8 +39,14 @@ export const logsResourcesDriftReducers = { } state.driftCheckLoading = false; }, + // Check-level authority/staleness (OS3/OS4) — set alongside setDriftResults + // so the indicator can tell a verified "in sync" from a stored-state guess. + setDriftMeta: (state: DeployState, action: PayloadAction) => { + state.driftMeta = action.payload; + }, clearDrift: (state: DeployState) => { state.driftByNode = {}; state.driftCheckLoading = false; + state.driftMeta = { checkedAt: null, unsupported: false }; }, } as const; diff --git a/packages/ui/src/store/slices/deploy/reducers/panel-config.ts b/packages/ui/src/store/slices/deploy/reducers/panel-config.ts index 30c22f3b..72d393e7 100644 --- a/packages/ui/src/store/slices/deploy/reducers/panel-config.ts +++ b/packages/ui/src/store/slices/deploy/reducers/panel-config.ts @@ -8,7 +8,9 @@ * * - `openDeployPanel` / `closeDeployPanel` — toggle panel visibility. * - `setProvider` / `setGcpProject` / `setRegion` / `setEnvironment` — - * write the matching field directly from the action payload. + * write the matching field directly from the action payload, and + * invalidate any existing plan (DF4): a plan reviewed for one destination + * must NOT be applied against a different provider/project/region/env. * * @see rf-dslice-3 */ @@ -16,6 +18,18 @@ import type { DeployState } from '../types'; import type { PayloadAction } from '@reduxjs/toolkit'; +// DF4 — a plan describes a specific destination. Changing any destination +// field makes the on-screen plan stale, so drop it (and roll a 'planned' +// status back to 'idle') forcing a re-plan before the user can Deploy. No-op +// when there's no plan, so the panel-open auto-detect (which dispatches +// setProvider/setRegion before any plan exists) is unaffected. +function invalidatePlan(state: DeployState) { + if (state.plan) { + state.plan = null; + if (state.status === 'planned') state.status = 'idle'; + } +} + export const panelConfigReducers = { openDeployPanel: (state: DeployState) => { state.isOpen = true; @@ -27,14 +41,18 @@ export const panelConfigReducers = { // Configuration setProvider: (state: DeployState, action: PayloadAction) => { state.provider = action.payload; + invalidatePlan(state); }, setGcpProject: (state: DeployState, action: PayloadAction) => { state.gcpProject = action.payload; + invalidatePlan(state); }, setRegion: (state: DeployState, action: PayloadAction) => { state.region = action.payload; + invalidatePlan(state); }, setEnvironment: (state: DeployState, action: PayloadAction<'development' | 'staging' | 'production'>) => { state.environment = action.payload; + invalidatePlan(state); }, } as const; diff --git a/packages/ui/src/store/slices/deploy/reducers/wire-events.ts b/packages/ui/src/store/slices/deploy/reducers/wire-events.ts index 6ef331fe..5c833293 100644 --- a/packages/ui/src/store/slices/deploy/reducers/wire-events.ts +++ b/packages/ui/src/store/slices/deploy/reducers/wire-events.ts @@ -29,7 +29,6 @@ * @see rf-dslice-7 */ -import { t } from '../../../../i18n'; import type { DeployResourceResult, DeployState } from '../types'; import type { DeployCompleteEvent, DeployNodeProgressEvent, DeployNodeStatusEvent } from '@ice/types'; import type { PayloadAction } from '@reduxjs/toolkit'; @@ -159,6 +158,11 @@ export const wireEventsReducers = { state.status = 'error'; } state.currentDeployCardId = undefined; - state.logs.push(t('deploy.slice.completed', { seconds: '0.0' })); + // DF8 — the `complete` wire event carries no duration, so this reducer used + // to log "Deploy completed in 0.0s" — a fabricated time that contradicted + // the real-duration line the hydrate reducer pushes moments later (from the + // DB record). The status above already flips instantly; the authoritative + // completion log line (with the real duration) comes from `hydrate`, so + // don't emit a fake-duration line here. }, } as const; diff --git a/packages/ui/src/store/slices/deploy/types.ts b/packages/ui/src/store/slices/deploy/types.ts index 2750c1c6..b4b3b5e3 100644 --- a/packages/ui/src/store/slices/deploy/types.ts +++ b/packages/ui/src/store/slices/deploy/types.ts @@ -89,6 +89,20 @@ export interface NodeDriftInfo { changes: DriftChange[]; } +/** + * Check-level drift metadata (NOT per-node). `unsupported` is true when the + * drift service could not actually query the cloud (no credentials, or the + * provider has no describe path) and therefore fell back to a non-authoritative + * stored-state comparison — see `services/deploy/src/services/drift.service.ts`. + * `checkedAt` is the ISO timestamp the check ran. Both come back on the + * drift-check response alongside `driftResults`; the UI must surface them so a + * green "in sync" can't masquerade as a verified cloud query (OS3/OS4). + */ +export interface DriftMeta { + checkedAt: string | null; + unsupported: boolean; +} + export type DeployStatus = | 'idle' | 'authenticating' @@ -210,6 +224,8 @@ export interface DeployState { // Drift detection driftByNode: Record; driftCheckLoading: boolean; + /** Check-level authority/staleness metadata for the last drift check. */ + driftMeta: DriftMeta; // Phase 8 — block requirements (DNS, domain verification, cert issuance, etc.) requirements: ResolvedRequirementState[]; diff --git a/packages/ui/src/store/slices/environments-slice.ts b/packages/ui/src/store/slices/environments-slice.ts index c39f0a34..d3611d0a 100644 --- a/packages/ui/src/store/slices/environments-slice.ts +++ b/packages/ui/src/store/slices/environments-slice.ts @@ -42,18 +42,25 @@ export interface EnvironmentsState { byProject: Record; activeEnvId: Record; // projectId → envId loading: boolean; + /** EI6 — last environments-list fetch failure, so the env bar can show a + * retry affordance instead of vanishing entirely. */ + fetchError: string | null; pendingDiff: EnvironmentDiff | null; pendingPromote: { sourceEnvId: string; targetEnvId: string } | null; promoting: boolean; + /** EI3 — last promotion failure message, surfaced in the promote modal. */ + promoteError: string | null; } const initialState: EnvironmentsState = { byProject: {}, activeEnvId: {}, loading: false, + fetchError: null, pendingDiff: null, pendingPromote: null, promoting: false, + promoteError: null, }; // ─── Thunks ───────────────────────────────────────────────────────────────── @@ -165,15 +172,18 @@ const environmentsSlice = createSlice({ clearPendingDiff(state) { state.pendingDiff = null; state.pendingPromote = null; + state.promoteError = null; }, }, extraReducers: (builder) => { builder .addCase(fetchEnvironments.pending, (state) => { state.loading = true; + state.fetchError = null; }) .addCase(fetchEnvironments.fulfilled, (state, action) => { state.loading = false; + state.fetchError = null; const { projectId, environments } = action.payload; state.byProject[projectId] = environments; // Auto-select production if no active env @@ -182,8 +192,9 @@ const environmentsSlice = createSlice({ if (prod) state.activeEnvId[projectId] = prod.id; } }) - .addCase(fetchEnvironments.rejected, (state) => { + .addCase(fetchEnvironments.rejected, (state, action) => { state.loading = false; + state.fetchError = (action.payload as string) || 'Failed to load environments'; }) .addCase(createEnvironment.fulfilled, (state, action) => { @@ -219,14 +230,18 @@ const environmentsSlice = createSlice({ .addCase(promoteEnvironment.pending, (state) => { state.promoting = true; + state.promoteError = null; }) .addCase(promoteEnvironment.fulfilled, (state) => { state.promoting = false; + state.promoteError = null; state.pendingDiff = null; state.pendingPromote = null; }) - .addCase(promoteEnvironment.rejected, (state) => { + .addCase(promoteEnvironment.rejected, (state, action) => { state.promoting = false; + // Keep the modal open (pendingDiff/pendingPromote intact) and surface why. + state.promoteError = (action.payload as string) || 'Promotion failed'; }); }, }); diff --git a/packages/ui/src/store/slices/integrations-slice.ts b/packages/ui/src/store/slices/integrations-slice.ts index 58baa566..adbe6d4f 100644 --- a/packages/ui/src/store/slices/integrations-slice.ts +++ b/packages/ui/src/store/slices/integrations-slice.ts @@ -5,7 +5,7 @@ * and generic integration status for GCP/AWS/Azure/etc. */ -import { type IntegrationStatus } from '@ice/constants'; +import { ALL_PROVIDERS, isProviderEnabled, type IntegrationStatus } from '@ice/constants'; import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; import { getApi } from '../../shared/api/api-adapter'; @@ -192,6 +192,36 @@ export const disconnectAnthropic = createAsyncThunk('integrations/disconnectAnth await api.provider.disconnect('anthropic'); }); +// ── Cloud providers (EI2) ─────────────────────────────────────────────────── +// The per-provider connect modal manages connection locally; nothing ever +// wrote the result back to Redux, so the app-bar rings + status dots were dead +// (even GCP's wired ring could never light up). This bootstrap thunk checks +// every enabled cloud provider's connection on app load and populates the +// slice, and `setProviderConnection` lets the modal push live updates on +// connect/disconnect without a reload. +export const checkAllProviderConnections = createAsyncThunk( + 'integrations/checkAllProviderConnections', + async (_, { dispatch }) => { + const api = getApi(); + const providers = (ALL_PROVIDERS as readonly string[]).filter((p) => isProviderEnabled(p)); + await Promise.all( + providers.map(async (id) => { + try { + const connected = await api.provider.isConnected(id); + dispatch( + integrationsSlice.actions.setProviderConnection({ + providerId: id, + status: connected ? 'connected' : 'disconnected', + }), + ); + } catch { + // Leave the provider's current status untouched on a check failure. + } + }), + ); + }, +); + export const fetchGitHubRepos = createAsyncThunk( 'integrations/fetchGitHubRepos', async (page: number | undefined, { rejectWithValue }) => { @@ -237,6 +267,12 @@ const integrationsSlice = createSlice({ setDeviceFlow(state, action: PayloadAction) { state.github.deviceFlow = action.payload; }, + // EI2 — set a provider's connection status (cloud providers, from the + // connect modal's connect/disconnect and the bootstrap check thunk). + setProviderConnection(state, action: PayloadAction<{ providerId: string; status: IntegrationStatus }>) { + const existing = state.integrations[action.payload.providerId] || { status: 'disconnected' }; + state.integrations[action.payload.providerId] = { ...existing, status: action.payload.status }; + }, }, extraReducers: (builder) => { builder @@ -338,5 +374,5 @@ const integrationsSlice = createSlice({ }, }); -export const { setDeviceFlow } = integrationsSlice.actions; +export const { setDeviceFlow, setProviderConnection } = integrationsSlice.actions; export default integrationsSlice.reducer; diff --git a/packages/ui/src/store/slices/logs-slice.ts b/packages/ui/src/store/slices/logs-slice.ts index 8cca1ffd..a4a011b2 100644 --- a/packages/ui/src/store/slices/logs-slice.ts +++ b/packages/ui/src/store/slices/logs-slice.ts @@ -31,6 +31,7 @@ export type SourceResolution = | { state: 'pre-deploy'; sourceNodeId: string; iceType: string } | { state: 'ambiguous'; candidates: Array<{ nodeId: string; iceType: string; label?: string }> } | { state: 'unsupported-source'; sourceNodeId: string; iceType: string } + | { state: 'provider-unsupported'; provider: string; sourceNodeId: string; iceType: string } | { state: 'permission-denied'; message: string } | { state: 'none' }; @@ -42,6 +43,7 @@ export type LogStreamStatus = | 'streaming' | 'pre-deploy' | 'unsupported' + | 'provider-unsupported' | 'permission-denied' | 'ambiguous' | 'no-source' @@ -54,6 +56,12 @@ export interface LogStreamState { source: SourceResolution | null; entries: LogEntry[]; lastError: string | null; + /** OL5 — bumped by `retryStream` to force `use-log-stream` to re-subscribe + * (a recovery path for `error`/`permission-denied` that doesn't need a reload). */ + retryNonce: number; + /** OL4 — how many entries have scrolled off the top of the 200-line cap, so + * the UI can say "N older lines dropped" instead of silently losing them. */ + droppedCount: number; } export interface LogsState { @@ -78,6 +86,8 @@ function defaultStreamState(mode: LogStreamMode = 'polling'): LogStreamState { source: null, entries: [], lastError: null, + retryNonce: 0, + droppedCount: 0, }; } @@ -99,6 +109,8 @@ function statusForSource(source: SourceResolution): LogStreamStatus { return 'ambiguous'; case 'unsupported-source': return 'unsupported'; + case 'provider-unsupported': + return 'provider-unsupported'; case 'permission-denied': return 'permission-denied'; case 'none': @@ -151,6 +163,9 @@ const logsSlice = createSlice({ // carries its own `message` and we want to show it as the placeholder. if (source.state === 'permission-denied') { slot.lastError = source.message; + } else if (source.state === 'provider-unsupported') { + // Carry the provider id so the placeholder/pill can name it (OL2). + slot.lastError = source.provider; } else if (slot.lastError && source.state === 'resolved') { slot.lastError = null; } @@ -170,7 +185,9 @@ const logsSlice = createSlice({ slot.entries.push(entry); if (slot.entries.length > MAX_ENTRIES) { - slot.entries.splice(0, slot.entries.length - MAX_ENTRIES); + const overflow = slot.entries.length - MAX_ENTRIES; + slot.entries.splice(0, overflow); + slot.droppedCount += overflow; // OL4 — track what scrolled off, don't drop silently } // First entry promotes the status from `connecting` (set by @@ -184,7 +201,10 @@ const logsSlice = createSlice({ clearEntries(state, action: PayloadAction<{ terminalNodeId: string }>) { const slot = state.byTerminalNodeId[action.payload.terminalNodeId]; - if (slot) slot.entries = []; + if (slot) { + slot.entries = []; + slot.droppedCount = 0; + } }, setMode(state, action: PayloadAction<{ terminalNodeId: string; mode: LogStreamMode }>) { @@ -198,6 +218,20 @@ const logsSlice = createSlice({ slot.lastError = action.payload.message; }, + /** + * OL5 — user-triggered re-subscribe. Bumping `retryNonce` changes a + * `use-log-stream` effect dependency, which tears down the dead stream and + * starts a fresh subscribe (e.g. after the user fixes an IAM grant), with + * no page reload. Optimistically clears the error + flips to `connecting` + * so the UI reflects the retry immediately. + */ + retryStream(state, action: PayloadAction<{ terminalNodeId: string }>) { + const slot = ensureSlot(state, action.payload.terminalNodeId); + slot.retryNonce += 1; + slot.status = 'connecting'; + slot.lastError = null; + }, + /** * `resumed` — frontend handler for the backend `logs:resumed` socket * event, which fires after a transient stream error is recovered @@ -233,6 +267,7 @@ export const { clearEntries, setMode, setError, + retryStream, resumed, teardown, } = logsSlice.actions; diff --git a/packages/web/src/app/__tests__/app.test.tsx b/packages/web/src/app/__tests__/app.test.tsx index eb259bb8..89a459ee 100644 --- a/packages/web/src/app/__tests__/app.test.tsx +++ b/packages/web/src/app/__tests__/app.test.tsx @@ -152,8 +152,10 @@ vi.mock('@ui/shared/hooks/use-menu-actions', () => ({ useMenuActions: () => mocks.menuActionsInvoked(), })); -vi.mock('@ui/shared/hooks/use-resolve-path', () => ({ - useResolvePath: () => mocks.resolvedPath, +vi.mock('@ui/shared/hooks/use-resolve-path-context', () => ({ + // IA7 — provider is a passthrough in tests; consumers read the shared result. + ResolvePathProvider: ({ children }: { children: React.ReactNode }) => children, + useResolvePathContext: () => mocks.resolvedPath, })); vi.mock('@ui/store/slices/account-slice', () => ({ @@ -192,6 +194,10 @@ vi.mock('@/pages/app-settings', () => ({ AppSettings: () =>
    , })); +vi.mock('@ui/features/account/components/team-page', () => ({ + TeamPage: () =>
    , +})); + vi.mock('@/pages/folder-view', () => ({ FolderView: ({ folderId, @@ -334,7 +340,7 @@ describe('App — top-level shell', () => { expect(router).toHaveLength(1); }); - it('registers four routes', () => { + it('registers the settings / templates / team / catch-all routes', () => { const tree = renderApp(); const routes = findByPredicate( tree, @@ -342,14 +348,15 @@ describe('App — top-level shell', () => { typeof (el.props as { ['data-stub']?: string })['data-stub'] === 'string' && (el.props as { ['data-stub']: string })['data-stub'] === 'Route', ); - expect(routes).toHaveLength(3); + expect(routes).toHaveLength(4); const paths = routes.map((r) => (r.props as { ['data-path']: string })['data-path']); - expect(paths).toEqual(['/settings', '/templates', '/*']); + // IA8 — /team is now a real route (was missing, fell through to the 404). + expect(paths).toEqual(['/settings', '/templates', '/team', '/*']); }); it('wraps each route in an ErrorBoundary with a name', () => { const tree = renderApp(); - // ErrorBoundary names: App (root), AppSettings, TemplateGallery, Canvas + // ErrorBoundary names: App (root), AppSettings, TemplateGallery, TeamPage, Canvas const boundaries = findByPredicate( tree, (el) => @@ -357,7 +364,7 @@ describe('App — top-level shell', () => { (el.props as { ['data-stub']: string })['data-stub'] === 'ErrorBoundary', ); const names = boundaries.map((b) => (b.props as { ['data-name']: string })['data-name']); - expect(names).toEqual(expect.arrayContaining(['App', 'AppSettings', 'TemplateGallery', 'Canvas'])); + expect(names).toEqual(expect.arrayContaining(['App', 'AppSettings', 'TemplateGallery', 'TeamPage', 'Canvas'])); }); }); diff --git a/packages/web/src/app/app.tsx b/packages/web/src/app/app.tsx index 4d16dc01..4308ada8 100644 --- a/packages/web/src/app/app.tsx +++ b/packages/web/src/app/app.tsx @@ -26,12 +26,13 @@ import { DebugOverlay } from '@ui/features/debug/components/debug-overlay'; import { useDeploySubscription } from '@ui/features/deploy/hooks/use-deploy-subscription'; import { TourRunner } from '@ui/features/tour/components/tour-runner'; import { useTranslation, LocaleProvider } from '@ui/i18n'; +import { TeamPage } from '@ui/features/account/components/team-page'; import { AppBar } from '@ui/shared/components/app-bar'; import { DevAccentPicker } from '@ui/shared/components/dev-accent-picker'; import { ErrorBoundary } from '@ui/shared/components/error-boundary'; import { MainLayout } from '@ui/shared/components/main-layout'; import { useMenuActions } from '@ui/shared/hooks/use-menu-actions'; -import { useResolvePath } from '@ui/shared/hooks/use-resolve-path'; +import { ResolvePathProvider, useResolvePathContext } from '@ui/shared/hooks/use-resolve-path-context'; import { fetchProfile } from '@ui/store/slices/account-slice'; import { selectActiveCard } from '@ui/store/slices/cards-slice'; import { openDeployPanel } from '@ui/store/slices/deploy-slice'; @@ -92,7 +93,7 @@ const DynamicContent: React.FC = () => { const navigate = useNavigate(); const dispatch = useDispatch(); const segments = pathname.split('/').filter(Boolean); - const resolved = useResolvePath(segments); + const resolved = useResolvePathContext(); // IA7 — shared shell-level resolution const activeCard = useSelector(selectActiveCard); // Cross-tab deploy visibility — subscribe to the active card's deploy @@ -244,43 +245,65 @@ const App: React.FC = () => ( - {/* TourRunner: mounted as a sibling of Routes so the popover/overlay + {/* IA7 — resolve the URL path ONCE at the shell and share it; the four + consumers (DynamicContent, Breadcrumbs, ProjectBrowser, + ResourcePalette) read it via useResolvePathContext instead of each + firing the same resolution POSTs. */} + + {/* TourRunner: mounted as a sibling of Routes so the popover/overlay portals at document.body regardless of which route is active. Inside BrowserRouter (needs useNavigate / useLocation), inside LocaleProvider (popover uses i18n), inside Provider (slice dispatch). See blueprint §2.1. */} - - - -
    - -
    - + + + +
    + +
    + +
    -
    - - } - /> - - - - } - /> - - - - } - /> - + + } + /> + + + + } + /> + {/* IA8 — the /team breadcrumb is advertised (Breadcrumbs TOP_ROUTES) + and TeamPage is built + tested, but the route was never wired, so + /team fell through to the catch-all and 404'd. */} + +
    + +
    + +
    +
    + + } + /> + + + + } + /> + + diff --git a/packages/web/src/pages/project/__tests__/environments.test.tsx b/packages/web/src/pages/project/__tests__/environments.test.tsx index 51b57d8e..9bed4512 100644 --- a/packages/web/src/pages/project/__tests__/environments.test.tsx +++ b/packages/web/src/pages/project/__tests__/environments.test.tsx @@ -510,23 +510,23 @@ describe('ProjectEnvironments — status indicators', () => { expect(dots.length).toBeGreaterThanOrEqual(1); }); - it('renders the status label "Not deployed" when no status', () => { + it('renders the canonical "not deployed" label when no status (IA4)', () => { mocks.environments = [{ id: 'e1', name: 'Env', type: 'staging', card_id: 'c1', is_protected: false }]; const tree = render(); const label = findByPredicate( tree, - (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 'Not deployed', + (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 'deployStatus.notDeployed', ); expect(label.length).toBeGreaterThanOrEqual(1); }); - it('capitalises the status label', () => { + it('uses the canonical deploy-status label (success → deployed, IA4)', () => { mocks.environments = [{ id: 'e1', name: 'Env', type: 'staging', card_id: 'c1', is_protected: false }]; mocks.stateSlots.push({ e1: { status: 'success', url: '', date: '' } }, false, '', 'staging', false); const tree = render(); const label = findByPredicate( tree, - (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 'Success', + (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 'deployStatus.deployed', ); expect(label.length).toBeGreaterThanOrEqual(1); }); diff --git a/packages/web/src/pages/project/deployments.tsx b/packages/web/src/pages/project/deployments.tsx index dc267798..fab2faf3 100644 --- a/packages/web/src/pages/project/deployments.tsx +++ b/packages/web/src/pages/project/deployments.tsx @@ -9,9 +9,21 @@ import { useTranslation } from '@ui/i18n'; import axiosInstance from '@ui/shared/api/axios-instance'; import { cn } from '@ui/shared/utils/cn'; +import { deployStatusMeta, deployStatusTone, type DeployStatusTone } from '@ui/shared/utils/deploy-status'; import { selectActiveCard } from '@ui/store/slices/cards-slice'; import { fetchEventsForNode, type DeploymentEvent, type DeployStep } from '@ui/store/slices/pipeline-slice'; -import { Loader2, CheckCircle, XCircle, Clock, Rocket, GitBranch, Server, ChevronDown, RotateCcw } from 'lucide-react'; +import { + Loader2, + CheckCircle, + XCircle, + Clock, + Rocket, + GitBranch, + Server, + ChevronDown, + RotateCcw, + HelpCircle, +} from 'lucide-react'; import React, { useEffect, useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import type { RootState, AppDispatch } from '@ui/store'; @@ -221,21 +233,27 @@ const InfraDeploymentList: React.FC<{ } }; - const statusIcon = (status: string) => { - switch (status) { - case 'success': - return ; - case 'failed': - return ; - case 'deploying': - return ; - case 'planning': - return ; - case 'cancelled': - return ; - default: - return ; - } + // IA4 — icon keyed off the canonical tone (not the raw status), so this list + // agrees with the status bar + Environments page and an `error` status no + // longer slips through to the neutral default. + const TONE_ICON: Record = { + success: , + failed: , + 'in-progress': , + pending: , + cancelled: , + idle: , + // EI9 — status couldn't be determined (e.g. a failed fetch). + unknown: , + }; + const statusIcon = (status: string) => TONE_ICON[deployStatusTone(status)]; + + // Canonical human label, shared with the other deploy surfaces. Unknown + // backend statuses fall back to the raw value rather than mislabel a historical + // record as "Not deployed". + const statusLabel = (status: string) => { + const meta = deployStatusMeta(status); + return meta.labelKey === 'deployStatus.notDeployed' ? status : t(meta.labelKey); }; // The latest successful deployment (index 0 since sorted desc) — can't roll back to current @@ -262,7 +280,7 @@ const InfraDeploymentList: React.FC<{
    {statusIcon(d.status)}
    - {d.status} + {statusLabel(d.status)} {d.provider} · {d.region} · {d.environment} diff --git a/packages/web/src/pages/project/environments.tsx b/packages/web/src/pages/project/environments.tsx index 3a8d471f..3f4a73b7 100644 --- a/packages/web/src/pages/project/environments.tsx +++ b/packages/web/src/pages/project/environments.tsx @@ -8,6 +8,7 @@ import { useTranslation } from '@ui/i18n'; import { getApi } from '@ui/shared/api/api-adapter'; import { IceSelect } from '@ui/shared/components/ui/ice-select'; import { cn } from '@ui/shared/utils/cn'; +import { deployStatusMeta } from '@ui/shared/utils/deploy-status'; import { setActiveCard, importToActiveCard, createCard } from '@ui/store/slices/cards-slice'; import { openDeployPanel } from '@ui/store/slices/deploy-slice'; import { @@ -117,20 +118,10 @@ export const ProjectEnvironments: React.FC = ({ projec setCreating(false); }; - const statusDot = (envId: string) => { - const s = deployStatus[envId]?.status; - if (s === 'success') return 'bg-emerald-500'; - if (s === 'deploying') return 'bg-blue-500 animate-pulse'; - if (s === 'failed') return 'bg-red-500'; - if (s === 'planning' || s === 'queued') return 'bg-amber-500 animate-pulse'; - return 'bg-ice-text-3/30'; - }; - - const statusLabel = (envId: string) => { - const s = deployStatus[envId]?.status; - if (!s) return 'Not deployed'; - return s.charAt(0).toUpperCase() + s.slice(1); - }; + // IA4 — one canonical vocabulary + colour map, shared with the status bar + // and the Deployments list. + const statusDot = (envId: string) => deployStatusMeta(deployStatus[envId]?.status).dotClass; + const statusLabel = (envId: string) => t(deployStatusMeta(deployStatus[envId]?.status).labelKey); if (loading && environments.length === 0) { return ( diff --git a/packages/web/src/styles/globals.css b/packages/web/src/styles/globals.css index c7bfec3b..64944657 100644 --- a/packages/web/src/styles/globals.css +++ b/packages/web/src/styles/globals.css @@ -389,3 +389,24 @@ .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: var(--ice-border-strong); } } +/* ── Reduced motion (AX4) ───────────────────────────────────────────────────── + * Global safety net: when the OS asks for reduced motion, neutralise every CSS + * animation/transition — including inline-style ones that don't individually + * thread `useReducedMotion` (e.g. the deploy-banner `iceDeployPulse` pulse and + * the `ice-node-entrance` / `ice-edge-entrance` keyframes above). Left + * unlayered + `!important` so it beats inline `style={{ animation: … }}`. + * + * NOTE: SVG SMIL (``) is NOT affected by this media query — those are + * gated in JS at their call sites via `prefersReducedMotion()` (see socket-dot). + */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + diff --git a/services/deploy/src/services/__tests__/drift-service.test.ts b/services/deploy/src/services/__tests__/drift-service.test.ts index f784ff94..c87c5425 100644 --- a/services/deploy/src/services/__tests__/drift-service.test.ts +++ b/services/deploy/src/services/__tests__/drift-service.test.ts @@ -329,8 +329,8 @@ describe('checkDrift — GCP describe path (orgId present)', () => { it('passes provider_id to deployer.describe (or falls back to resource_name when absent)', async () => { findManyMock.mockResolvedValueOnce([ - { node_id: 'a', resource_type: 't', resource_name: 'res-a', provider_id: 'pid-a' }, - { node_id: 'b', resource_type: 't', resource_name: 'res-b', provider_id: null }, + { node_id: 'a', resource_type: 'gcp.t', resource_name: 'res-a', provider_id: 'pid-a' }, + { node_id: 'b', resource_type: 'gcp.t', resource_name: 'res-b', provider_id: null }, ]); const describe = vi.fn().mockResolvedValue({ exists: true, properties: {} }); setupDeployerHappyPath(describe); @@ -344,13 +344,15 @@ describe('checkDrift — GCP describe path (orgId present)', () => { { orgId: 'org-1' }, ); - expect(describe).toHaveBeenNthCalledWith(1, 't', 'res-a', 'pid-a'); + expect(describe).toHaveBeenNthCalledWith(1, 'gcp.t', 'res-a', 'pid-a'); // No provider_id → falls back to resource_name as the third arg. - expect(describe).toHaveBeenNthCalledWith(2, 't', 'res-b', 'res-b'); + expect(describe).toHaveBeenNthCalledWith(2, 'gcp.t', 'res-b', 'res-b'); }); it('falls back to authClient.projectId when scope.project is absent in initialize()', async () => { - findManyMock.mockResolvedValueOnce([{ node_id: 'n1', resource_type: 't', resource_name: 'r', provider_id: 'p' }]); + findManyMock.mockResolvedValueOnce([ + { node_id: 'n1', resource_type: 'gcp.t', resource_name: 'r', provider_id: 'p' }, + ]); const deployer = fakeDeployer({ describe: async () => ({ exists: true, properties: {} }) }); getDecryptedCredentialsMock.mockResolvedValueOnce({ project_id: 'proj-1' }); createDeployerMock.mockResolvedValueOnce(deployer); @@ -368,8 +370,32 @@ describe('checkDrift — GCP describe path (orgId present)', () => { expect(deployer.initialize).toHaveBeenCalledWith(expect.objectContaining({ project: 'fallback-proj' })); }); + it('does NOT build a GCP deployer for a non-GCP deployment, even with an orgId (OS6)', async () => { + // resource_type prefix is 'aws' → no GCP describe path → must not pretend + // to verify. unsupported:true so the UI says "couldn't verify", not green. + findManyMock.mockResolvedValueOnce([ + { node_id: 'n1', resource_type: 'aws.ecs.service', resource_name: 'svc', provider_id: 'arn:...' }, + ]); + // Note: no getDecryptedCredentials queued — the gate must short-circuit + // before any creds fetch (a queued-but-unconsumed Once would leak). + + const result = await checkDrift( + 'card-1', + [{ id: 'n1', type: 'resource', data: { iceType: 'Compute.Container' } }], + { + orgId: 'org-1', + }, + ); + + expect(createDeployerMock).not.toHaveBeenCalled(); + expect(getDecryptedCredentialsMock).not.toHaveBeenCalled(); + expect(result.unsupported).toBe(true); + }); + it('falls back to stored-state path when getDecryptedCredentials returns null', async () => { - findManyMock.mockResolvedValueOnce([{ node_id: 'n1', resource_type: 't', resource_name: 'r', provider_id: 'p' }]); + findManyMock.mockResolvedValueOnce([ + { node_id: 'n1', resource_type: 'gcp.t', resource_name: 'r', provider_id: 'p' }, + ]); getDecryptedCredentialsMock.mockResolvedValueOnce(null); const result = await checkDrift('card-1', [{ id: 'n1', type: 'resource', data: { iceType: 'X' } }], { @@ -382,7 +408,9 @@ describe('checkDrift — GCP describe path (orgId present)', () => { }); it('logs a warning + falls through to stored-state when deployer init throws', async () => { - findManyMock.mockResolvedValueOnce([{ node_id: 'n1', resource_type: 't', resource_name: 'r', provider_id: 'p' }]); + findManyMock.mockResolvedValueOnce([ + { node_id: 'n1', resource_type: 'gcp.t', resource_name: 'r', provider_id: 'p' }, + ]); getDecryptedCredentialsMock.mockResolvedValueOnce({ project_id: 'proj-1' }); createDeployerMock.mockRejectedValueOnce(new Error('init blew up')); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); @@ -398,7 +426,9 @@ describe('checkDrift — GCP describe path (orgId present)', () => { }); it('always cleans up deployer + scoped auth even when describe throws mid-loop', async () => { - findManyMock.mockResolvedValueOnce([{ node_id: 'n1', resource_type: 't', resource_name: 'r', provider_id: 'p' }]); + findManyMock.mockResolvedValueOnce([ + { node_id: 'n1', resource_type: 'gcp.t', resource_name: 'r', provider_id: 'p' }, + ]); const deployer = fakeDeployer({ describe: async () => { throw new Error('describe failed'); @@ -427,7 +457,9 @@ describe('checkDrift — GCP describe path (orgId present)', () => { }); it('swallows errors from deployer.cleanup() and cleanupProviderAuth() in the finally block', async () => { - findManyMock.mockResolvedValueOnce([{ node_id: 'n1', resource_type: 't', resource_name: 'r', provider_id: 'p' }]); + findManyMock.mockResolvedValueOnce([ + { node_id: 'n1', resource_type: 'gcp.t', resource_name: 'r', provider_id: 'p' }, + ]); const deployer = fakeDeployer({ describe: async () => ({ exists: true, properties: {} }), cleanup: vi.fn().mockRejectedValue(new Error('cleanup-fail')), diff --git a/services/deploy/src/services/__tests__/log-stream-source-resolution.test.ts b/services/deploy/src/services/__tests__/log-stream-source-resolution.test.ts index 73660fd5..5b720f8b 100644 --- a/services/deploy/src/services/__tests__/log-stream-source-resolution.test.ts +++ b/services/deploy/src/services/__tests__/log-stream-source-resolution.test.ts @@ -117,6 +117,26 @@ describe('resolveSource — candidateSources path', () => { } }); + it('returns provider-unsupported for a non-GCP deployed resource, without a GCP creds check (OL2)', async () => { + prismaMock.environment.findUnique.mockResolvedValue({ type: 'production', region: 'us-east-1' }); + prismaMock.deployedResourceMapping.findFirst.mockResolvedValue({ + resource_name: 'api-server', + resource_type: 'aws.ecs.service', + provider_id: 'arn:aws:ecs:...', + }); + const result = await resolveSource({ + ...baseArgs, + candidateSources: [{ nodeId: 'src-1', iceType: 'Compute.Container' }], + }); + expect(result.state).toBe('provider-unsupported'); + if (result.state === 'provider-unsupported') { + expect(result.provider).toBe('aws'); + expect(result.sourceNodeId).toBe('src-1'); + } + // Must NOT fall through to the GCP credentials path (the misleading IAM error). + expect(credentialsMock.getDecryptedCredentials).not.toHaveBeenCalled(); + }); + it('returns permission-denied when no credentials are available', async () => { prismaMock.environment.findUnique.mockResolvedValue({ type: 'production', region: 'us-central1' }); prismaMock.deployedResourceMapping.findFirst.mockResolvedValue({ diff --git a/services/deploy/src/services/drift.service.ts b/services/deploy/src/services/drift.service.ts index 7a5f417c..5b390206 100644 --- a/services/deploy/src/services/drift.service.ts +++ b/services/deploy/src/services/drift.service.ts @@ -37,10 +37,22 @@ export async function checkDrift(cardId: string, nodes: any[], options?: { envir return { driftResults: [], checkedAt: new Date().toISOString(), unsupported: false }; } - // If we have an org id, spin up a real deployer so describe calls can hit GCP. - // Without one, we fall back to stored-state comparison which is still better - // than nothing for sanity checking canvas consistency. - const canQueryGcp = Boolean(options?.orgId); + // OS6 — real drift only has a GCP `describe` path for v1. Detect which clouds + // were actually deployed (the `resource_type` prefix ICE writes, e.g. + // 'aws.ecs.service' → 'aws'). Only build the GCP deployer when GCP resources + // are present: otherwise a GCP deployer would `describe`-miss every non-GCP + // resource yet report `unsupported:false`, masquerading as a verified check. + // With no GCP resources we leave the deployer null → `unsupported:true`, which + // the UI surfaces honestly as "couldn't verify against cloud" rather than a + // false green "in sync" (OS3/Phase 1). Mixed GCP+other deployments still query + // GCP for the GCP resources; non-GCP ones fall through to 'unknown'. + const deployedProviders = new Set(mapping.map((m) => String(m.resource_type || '').split('.')[0]).filter(Boolean)); + const hasGcpResources = deployedProviders.has('gcp'); + + // If we have an org id AND GCP resources, spin up a real deployer so describe + // calls can hit GCP. Without one, we fall back to stored-state comparison + // which is still better than nothing for sanity checking canvas consistency. + const canQueryGcp = Boolean(options?.orgId) && hasGcpResources; let deployer: any = null; let driftScopedAuth: any = null; if (canQueryGcp) { diff --git a/services/deploy/src/services/log-stream/source-resolution.ts b/services/deploy/src/services/log-stream/source-resolution.ts index 19544b5e..fa2b4fdc 100644 --- a/services/deploy/src/services/log-stream/source-resolution.ts +++ b/services/deploy/src/services/log-stream/source-resolution.ts @@ -162,6 +162,22 @@ export async function resolveSource(args: SubscribeArgs): Promise; } | { state: 'unsupported-source'; sourceNodeId: string; iceType: string } + // OL2 — the deployed resource is on a cloud whose live logs aren't wired up + // yet (the resolver/streamer are GCP-only for v1). Distinct from + // `unsupported-source` (iceType not supported) and `permission-denied` (GCP + // creds missing) so the client can say "not available for yet". + | { state: 'provider-unsupported'; provider: string; sourceNodeId: string; iceType: string } | { state: 'permission-denied'; message: string } | { state: 'none' };