From 4f630bfac3f56ee2a2bd8514c080a76217f7cff7 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 00:11:40 +0200 Subject: [PATCH 01/39] =?UTF-8?q?ux:=20implement=20fix-plan=20Phases=200?= =?UTF-8?q?=E2=80=935=20(honesty,=20deploy=20safety,=20provider=20neutrali?= =?UTF-8?q?ty,=20canvas=20legibility,=20log=20recovery)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves a large batch of UI/UX weak spots from the canvas / deploy / observability review (see inprogress/ux-review-findings.md, not committed). Phase 0 (dead wiring): mount the empty-canvas onboarding overlay; surface per-resource deploy remediation guidance (GCP-gated); correct the misleading "handled by keyboard shortcuts" menu comments. Phase 1 (honesty): drift-reconcile the hardcoded green "Live"; plumb drift authority (unsupported) + staleness via driftMeta; render unknown/extra drift states; label estimated cost "Est.". Phase 2 (deploy safety): sync + display the deploy target environment; echo the plan destination; invalidate stale plans on config change; show remediation / AI-diagnose on partial async failures; safer destroy default; persist cloud connection status; surface promotion + env-delete failures + env-bar resilience; estimated cost at the commit moment. Phase 3 (provider neutrality): honest non-GCP logs (new provider-unsupported resolution state); provider-aware drift (only query GCP when GCP resources are deployed); GCP-gated deploy error banners. Phase 4 (canvas legibility): deploy progress/error overlay + validation badge + in-flight pulse on CardShell nodes; log-scroll no longer zooms the canvas; Redis accent de-collided from the system error red. Phase 5 (observability): in-product log Retry after fixing an error / permission cause, no reload. Verified: @ice/ui (~10k) + deploy-service (~1.1k) unit suites green; both packages typecheck clean. Not yet verified in the headed app. --- .../components/__tests__/svg-canvas.test.tsx | 6 +- .../components/empty-canvas-overlay.tsx | 1 + .../_shared/__tests__/card-shell.test.tsx | 66 +++++++++++++-- .../__tests__/node-deploy-overlay.test.tsx | 47 +++++++++++ .../components/nodes/_shared/card-shell.tsx | 42 +++++++++- .../nodes/_shared/node-deploy-overlay.tsx | 75 +++++++++++++++++ .../nodes/_shared/node-validation-context.tsx | 35 ++++++++ .../components/nodes/_shared/status-dot.tsx | 7 +- .../components/nodes/log-node/index.tsx | 3 + .../components/nodes/redis-cache/index.tsx | 5 +- .../features/canvas/components/svg-canvas.tsx | 84 ++++++++++--------- .../__tests__/use-canvas-effects.test.tsx | 30 +++++++ .../use-canvas-side-effects.test.tsx | 4 +- .../canvas/hooks/use-canvas-effects.ts | 9 ++ .../canvas/hooks/use-canvas-side-effects.ts | 34 +++++--- .../features/cost/components/cost-panel.tsx | 6 ++ .../__tests__/destroy-confirm-modal.test.tsx | 11 ++- .../__tests__/api-error-banner.test.tsx | 27 ++++++ .../components/banners/api-error-banner.tsx | 24 +++++- .../deploy/components/deploy-panel.tsx | 68 +++++++++++++-- .../components/destroy-confirm-modal.tsx | 6 +- .../deploy/components/plan-preview.tsx | 16 +++- .../deploy/components/results-summary.tsx | 57 ++++++++++--- .../components/sections/config-section.tsx | 35 ++++++-- .../__tests__/use-deploy-effects.test.tsx | 6 +- .../deploy/hooks/use-deploy-effects.ts | 35 +++++++- .../__tests__/environment-tab-bar.test.tsx | 19 ++++- .../components/environment-context-menu.tsx | 10 ++- .../components/environment-tab-bar.tsx | 48 +++++++++-- .../environments/components/promote-modal.tsx | 32 +++++-- .../__tests__/provider-connect-modal.test.tsx | 6 ++ .../components/provider-connect-modal.tsx | 9 ++ .../sections/__tests__/drift.test.tsx | 1 + .../__tests__/monitoring-log-section.test.tsx | 36 ++++++++ .../components/sections/deploy-tab-body.tsx | 31 ++++++- .../properties/components/sections/drift.tsx | 82 ++++++++++++++++-- .../sections/monitoring-log-section.tsx | 21 ++++- .../hooks/__tests__/use-drift-check.test.tsx | 1 + .../properties/hooks/use-drift-check.ts | 11 ++- packages/ui/src/i18n/en.json | 21 ++++- packages/ui/src/i18n/zh.json | 21 ++++- .../components/__tests__/app-bar.test.tsx | 2 + packages/ui/src/shared/components/app-bar.tsx | 20 ++++- .../ui/src/shared/hooks/use-log-stream.ts | 7 +- .../ui/src/shared/hooks/use-menu-actions.ts | 9 +- .../store/slices/__tests__/logs-slice.test.ts | 18 ++++ packages/ui/src/store/slices/deploy-slice.ts | 3 + .../deploy/reducers/__tests__/auth.test.ts | 1 + .../reducers/__tests__/deploy-phases.test.ts | 1 + .../reducers/__tests__/diagnosis.test.ts | 1 + .../deploy/reducers/__tests__/hydrate.test.ts | 1 + .../__tests__/logs-resources-drift.test.ts | 1 + .../deploy/reducers/__tests__/outcome.test.ts | 1 + .../reducers/__tests__/panel-config.test.ts | 1 + .../reducers/__tests__/planning.test.ts | 1 + .../reducers/__tests__/pre-deploy.test.ts | 1 + .../reducers/__tests__/requirements.test.ts | 1 + .../reducers/__tests__/wire-events.test.ts | 1 + .../deploy/reducers/logs-resources-drift.ts | 8 +- .../slices/deploy/reducers/panel-config.ts | 20 ++++- packages/ui/src/store/slices/deploy/types.ts | 16 ++++ .../ui/src/store/slices/environments-slice.ts | 19 ++++- .../ui/src/store/slices/integrations-slice.ts | 40 ++++++++- packages/ui/src/store/slices/logs-slice.ts | 26 ++++++ .../services/__tests__/drift-service.test.ts | 50 +++++++++-- .../log-stream-source-resolution.test.ts | 20 +++++ services/deploy/src/services/drift.service.ts | 20 ++++- .../services/log-stream/source-resolution.ts | 16 ++++ .../deploy/src/services/log-stream/types.ts | 5 ++ 69 files changed, 1236 insertions(+), 162 deletions(-) create mode 100644 packages/ui/src/features/canvas/components/nodes/_shared/__tests__/node-deploy-overlay.test.tsx create mode 100644 packages/ui/src/features/canvas/components/nodes/_shared/node-deploy-overlay.tsx create mode 100644 packages/ui/src/features/canvas/components/nodes/_shared/node-validation-context.tsx 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/empty-canvas-overlay.tsx b/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx index 558c44a8..cbc8a830 100644 --- a/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx +++ b/packages/ui/src/features/canvas/components/empty-canvas-overlay.tsx @@ -79,6 +79,7 @@ export const EmptyCanvasOverlay: React.FC = ({ onDismis {t('canvas.emptyState.more')} +
{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..df265d5e 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,54 @@ 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); + }); + 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/card-shell.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/card-shell.tsx index 20d1e92f..1a1a8ced 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,13 @@ 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 { 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 +156,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 @@ -199,6 +208,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 @@ -616,7 +631,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/status-dot.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/status-dot.tsx index d5c44642..3d4635cd 100644 --- a/packages/ui/src/features/canvas/components/nodes/_shared/status-dot.tsx +++ b/packages/ui/src/features/canvas/components/nodes/_shared/status-dot.tsx @@ -4,11 +4,16 @@ interface StatusDotProps { color: string; label?: string; radius?: number; + /** CNV3 — pulse the dot while work is in flight (e.g. deploying) so it reads + * differently from a static "active". Uses Tailwind's `animate-pulse` (a real, + * defined keyframe) gated on `motion-safe` so it respects reduced-motion. */ + pulse?: boolean; } -export const StatusDot: React.FC = memo(({ color, label, radius = 3 }) => ( +export const StatusDot: React.FC = memo(({ color, label, radius = 3, pulse = false }) => ( = ({ 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 */} 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..73b478a5 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 @@ -560,13 +560,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/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..2e96231e 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,7 +63,7 @@ * 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'; @@ -94,7 +94,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 ────────────────────────── @@ -169,9 +176,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 +198,6 @@ export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): void { setOverlayDismissed(true); } }, [aiCurrentIntent]); + + return { overlayDismissed, dismissOverlay }; } 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..5b7640ad 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,7 @@ import { resetDeploy, appendLog, } from '../../../store/slices/deploy-slice'; +import { parseCostRange } from '../../cost/utils/cost-calculator'; import { useDeployActions } from '../hooks/use-deploy-actions'; import { useDeployEffects } from '../hooks/use-deploy-effects'; import { useDestroyAction } from '../hooks/use-destroy-action'; @@ -108,6 +109,27 @@ 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(', ')}` + : ''); + + // 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 */} @@ -179,17 +214,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 +266,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/config-section.tsx b/packages/ui/src/features/deploy/components/sections/config-section.tsx index f6f42907..d1031d23 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 @@ -51,7 +53,7 @@ export const ConfigSection: React.FC<{ provider, gcpProject, region, - environment: _environment, + environment, disabled, projectId: _projectId, onProviderChange: _onProviderChange, @@ -62,6 +64,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); @@ -110,7 +118,7 @@ export const ConfigSection: React.FC<{
)} -
+
{/* Provider — read-only, set in project settings */}
@@ -119,6 +127,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/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/environments/components/__tests__/environment-tab-bar.test.tsx b/packages/ui/src/features/environments/components/__tests__/environment-tab-bar.test.tsx index 26272dcb..f8b54f28 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 @@ -768,10 +768,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/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..5fa85779 100644 --- a/packages/ui/src/features/environments/components/environment-tab-bar.tsx +++ b/packages/ui/src/features/environments/components/environment-tab-bar.tsx @@ -37,6 +37,7 @@ export const EnvironmentTabBar: React.FC = ({ projectId, 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 +45,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(() => { @@ -135,15 +140,24 @@ export const EnvironmentTabBar: React.FC = ({ projectId, 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 +171,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 +198,16 @@ export const EnvironmentTabBar: React.FC = ({ projectId, {t('environments.tabBar.loading')}
+ ) : fetchError && environments.length === 0 ? ( +
+ {t('environments.tabBar.loadError')} + +
) : ( <> {/* Environment tabs */} @@ -243,6 +273,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 +282,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/promote-modal.tsx b/packages/ui/src/features/environments/components/promote-modal.tsx index 0961eb99..f70f8485 100644 --- a/packages/ui/src/features/environments/components/promote-modal.tsx +++ b/packages/ui/src/features/environments/components/promote-modal.tsx @@ -16,6 +16,7 @@ export const PromoteModal: React.FC = () => { const pendingDiff = useSelector((s: RootState) => s.environments.pendingDiff); const pendingPromote = useSelector((s: RootState) => s.environments.pendingPromote); const promoting = useSelector((s: RootState) => s.environments.promoting); + const promoteError = useSelector((s: RootState) => s.environments.promoteError); const allEnvs = useSelector((s: RootState) => s.environments.byProject); if (!pendingDiff || !pendingPromote) return null; @@ -38,13 +39,20 @@ export const PromoteModal: React.FC = () => { const noChanges = totalChanges === 0; const handlePromote = async () => { - await dispatch( - promoteEnvironment({ - sourceEnvId: pendingPromote.sourceEnvId, - targetEnvId: pendingPromote.targetEnvId, - }), - ); - if (projectId) dispatch(fetchEnvironments(projectId)); + try { + // EI3 — unwrap so a rejected promotion does NOT fall through to a refetch + // that masks the failure. On success the slice clears pendingDiff (modal + // closes); on failure it sets promoteError (rendered below, modal stays). + await dispatch( + promoteEnvironment({ + sourceEnvId: pendingPromote.sourceEnvId, + targetEnvId: pendingPromote.targetEnvId, + }), + ).unwrap(); + if (projectId) dispatch(fetchEnvironments(projectId)); + } catch { + // Surfaced via state.promoteError; keep the modal open so the user sees it. + } }; const handleClose = () => dispatch(clearPendingDiff()); @@ -113,6 +121,16 @@ export const PromoteModal: React.FC = () => { )}
+ {/* Promotion error (EI3) — keeps the modal open so prod state is unambiguous */} + {promoteError && ( +
+ {promoteError} +
+ )} + {/* Footer */}
+ )} + {/* Streaming mode radio */}
{/* Caveats — rendered verbatim from the resolver. */} diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index 4a2c9d9a..f82dba3a 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -528,6 +528,7 @@ "modeTailHint": "Sub-second streaming via gRPC.", "sourceOverride": "Source override", "retry": "Retry", + "clear": "Clear", "noSupportedSource": "No supported source connected. Connect a compute or database block to this Log terminal.", "clearOverride": "Clear override", "selectSource": "— Select a source —", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index f45df51c..a4e4e8b6 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -528,6 +528,7 @@ "modeTailHint": "通过 gRPC 进行亚秒级流式传输。", "sourceOverride": "源覆盖", "retry": "重试", + "clear": "清除", "noSupportedSource": "未连接受支持的源。请将计算或数据库块连接到此日志终端。", "clearOverride": "清除覆盖", "selectSource": "— 选择源 —", From c03dfc0a40642980caec6c129768ea7ef6484508 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 00:24:20 +0200 Subject: [PATCH 03/39] ux(observability): surface dropped log lines instead of silently capping (OL4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 200-line log buffer splices off the oldest entries with no signal. Track a droppedCount in the logs slice (reset by clearEntries) and show "N older lines dropped — buffer is capped at 200" in the Monitoring.Log section, so the cap is honest. (Backward "load earlier" paging still needs a server-side query.) --- .../__tests__/monitoring-log-section.test.tsx | 28 +++++++++++++++++++ .../sections/monitoring-log-section.tsx | 8 ++++++ packages/ui/src/i18n/en.json | 1 + packages/ui/src/i18n/zh.json | 1 + .../store/slices/__tests__/logs-slice.test.ts | 14 ++++++++++ packages/ui/src/store/slices/logs-slice.ts | 13 +++++++-- 6 files changed, 63 insertions(+), 2 deletions(-) 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 296a8241..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 @@ -555,6 +555,34 @@ describe('MonitoringLogSection', () => { 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/monitoring-log-section.tsx b/packages/ui/src/features/properties/components/sections/monitoring-log-section.tsx index c8ecd6e2..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 @@ -232,6 +232,14 @@ export function MonitoringLogSection({ nodeId }: Props): React.ReactElement | nu )}
+ {/* 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 && (
diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index f82dba3a..783061f0 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -529,6 +529,7 @@ "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 —", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index a4e4e8b6..4a28ffe3 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -529,6 +529,7 @@ "sourceOverride": "源覆盖", "retry": "重试", "clear": "清除", + "dropped": "已丢弃 {{count}} 条较早的日志 — 缓冲区上限为 200 行。", "noSupportedSource": "未连接受支持的源。请将计算或数据库块连接到此日志终端。", "clearOverride": "清除覆盖", "selectSource": "— 选择源 —", 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 a73f9a96..8d661b6a 100644 --- a/packages/ui/src/store/slices/__tests__/logs-slice.test.ts +++ b/packages/ui/src/store/slices/__tests__/logs-slice.test.ts @@ -147,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/logs-slice.ts b/packages/ui/src/store/slices/logs-slice.ts index c1aaf21c..a4a011b2 100644 --- a/packages/ui/src/store/slices/logs-slice.ts +++ b/packages/ui/src/store/slices/logs-slice.ts @@ -59,6 +59,9 @@ export interface LogStreamState { /** 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 { @@ -84,6 +87,7 @@ function defaultStreamState(mode: LogStreamMode = 'polling'): LogStreamState { entries: [], lastError: null, retryNonce: 0, + droppedCount: 0, }; } @@ -181,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 @@ -195,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 }>) { From ed212e9ea237da94eac63a1deda854efac85939c Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 00:36:44 +0200 Subject: [PATCH 04/39] ux(a11y): add a polite live region for deploy lifecycle announcements (AX2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deploy lifecycle was silent to assistive tech. Mount one visually-hidden aria-live="polite" region (LiveAnnouncer) in the AppBar that mirrors the deploy status — connecting / planning / deploying N resources / succeeded / failed — so screen-reader users hear transitions. Coarse by design (status transitions only) to avoid chatter. Pure status→text mapping is unit-tested. --- packages/ui/src/i18n/en.json | 13 ++++ packages/ui/src/i18n/zh.json | 13 ++++ .../components/__tests__/app-bar.test.tsx | 6 ++ .../__tests__/live-announcer.test.tsx | 73 +++++++++++++++++++ packages/ui/src/shared/components/app-bar.tsx | 3 + .../src/shared/components/live-announcer.tsx | 73 +++++++++++++++++++ 6 files changed, 181 insertions(+) create mode 100644 packages/ui/src/shared/components/__tests__/live-announcer.test.tsx create mode 100644 packages/ui/src/shared/components/live-announcer.tsx diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index 783061f0..ad81b1d6 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -112,6 +112,19 @@ "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." + } + }, "canvas": { "title": "Infrastructure Canvas", "emptyState": { diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index 4a28ffe3..50983257 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -112,6 +112,19 @@ "continueWithGithub": "使用 GitHub 继续" } }, + "a11y": { + "announce": { + "connecting": "正在连接云服务提供商…", + "planning": "正在规划部署…", + "planReady": "部署计划已就绪,可供查看。", + "deploying": "正在部署…", + "deployingCount": "正在部署 {{total}} 个资源…", + "destroying": "正在销毁资源…", + "deploySucceeded": "部署成功。", + "deployFailed": "部署失败。", + "deployFailedCount": "部署失败 — {{failed}} 个资源失败。" + } + }, "canvas": { "title": "基础设施画布", "emptyState": { 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 57de11b8..13b498ee 100644 --- a/packages/ui/src/shared/components/__tests__/app-bar.test.tsx +++ b/packages/ui/src/shared/components/__tests__/app-bar.test.tsx @@ -131,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__/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/app-bar.tsx b/packages/ui/src/shared/components/app-bar.tsx index 09dc22b8..0cccfc2b 100644 --- a/packages/ui/src/shared/components/app-bar.tsx +++ b/packages/ui/src/shared/components/app-bar.tsx @@ -37,6 +37,7 @@ import { checkAnthropicConnection, checkGitHubConnection, } from '../../store/slices/integrations-slice'; +import { LiveAnnouncer } from './live-announcer'; import { cn } from '../utils/cn'; import type { RootState, AppDispatch } from '../../store'; @@ -88,6 +89,8 @@ export const AppBar: React.FC = memo(() => { return ( <> + {/* AX2 — one polite live region for deploy lifecycle announcements. */} +
) => 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} +
+ ); +}; From 11977a9507964b0ec3ab03cd1b39e1369edbd698 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 00:40:06 +0200 Subject: [PATCH 05/39] =?UTF-8?q?ux(a11y):=20fix=20the=20controls-help=20m?= =?UTF-8?q?odal=20=E2=80=94=20document=20Shift+A,=20drop=20dead=20keys,=20?= =?UTF-8?q?dialog=20semantics=20(AX6/CD2/AX3/IA3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CD2: document the headline "Shift + A" add affordance (it was undocumented). - IA3: stop advertising the dead "1"/"2" view-level keys (the view-level toggle was removed — altitude is zoom-driven now). - AX3: give the panel role="dialog" + aria-modal + aria-label, and a REAL Escape-key handler (the header "ESC" affordance previously only clicked). --- .../__tests__/controls-help-modal.test.tsx | 34 +++++++++++++++++++ .../canvas/components/controls-help-modal.tsx | 23 +++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) 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..29e02dfe 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(), }; }); @@ -198,4 +201,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/controls-help-modal.tsx b/packages/ui/src/features/canvas/components/controls-help-modal.tsx index ef7a9999..432d4d3d 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,24 @@ * Bottom-right "?" button that opens a popover showing all canvas controls. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; 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 +43,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 +60,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') }, ], }, @@ -78,6 +92,9 @@ export const ControlsHelpModal: React.FC = () => { {/* Panel */}
Date: Thu, 18 Jun 2026 00:49:12 +0200 Subject: [PATCH 06/39] ux(ia): add a canonical deploy-status vocabulary and adopt it on the Environments page (IA4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deploy status was rendered with three divergent switches (e.g. the status bar said "Deploy failed" off `error` while the env page / deployments list said "Failed" off `failed`). Introduce one canonical `deployStatusMeta(raw)` → { tone, labelKey, dotClass } in shared/utils that normalises every raw status (error≡failed, success→"Deployed", etc.). The env-dot colour util now delegates to it (outputs unchanged) and the Environments page uses it for labels + dots. Status-bar + Deployments-list adoption to follow. --- .../environments/utils/deploy-status-color.ts | 20 ++--- packages/ui/src/i18n/en.json | 12 +++ packages/ui/src/i18n/zh.json | 12 +++ .../utils/__tests__/deploy-status.test.ts | 53 +++++++++++++ packages/ui/src/shared/utils/deploy-status.ts | 74 +++++++++++++++++++ .../project/__tests__/environments.test.tsx | 8 +- .../web/src/pages/project/environments.tsx | 19 ++--- 7 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 packages/ui/src/shared/utils/__tests__/deploy-status.test.ts create mode 100644 packages/ui/src/shared/utils/deploy-status.ts diff --git a/packages/ui/src/features/environments/utils/deploy-status-color.ts b/packages/ui/src/features/environments/utils/deploy-status-color.ts index 04429763..f14c45b6 100644 --- a/packages/ui/src/features/environments/utils/deploy-status-color.ts +++ b/packages/ui/src/features/environments/utils/deploy-status-color.ts @@ -1,25 +1,17 @@ /** * Deploy-status → CSS class mapping for environment status dots. * - * Extracted from `components/environment-tab-bar.tsx` during rf-etabs-1. - * Keeps the chained ternary verbatim in classification logic, only swaps - * the inline expression for a named pure function. Returns the Tailwind - * class string for the dot color/animation; default falls back to a muted - * grey/30 when no deploy status is available. + * Now a thin wrapper over the canonical `deployStatusMeta` (IA4) so the env dots + * share one vocabulary + colour map with the status bar and Deployments list. + * Output is unchanged for the statuses this previously handled. */ +import { deployStatusMeta } from '../../../shared/utils/deploy-status'; + export interface DeployStatusInput { status?: string; } export function getDeployStatusDotColor(deployStatus: DeployStatusInput | undefined): string { - return deployStatus?.status === 'success' - ? 'bg-emerald-500' - : deployStatus?.status === 'deploying' - ? 'bg-blue-500 animate-pulse' - : deployStatus?.status === 'failed' - ? 'bg-red-500' - : deployStatus?.status === 'planning' || deployStatus?.status === 'queued' - ? 'bg-amber-500 animate-pulse' - : 'bg-ice-text-3/30'; + return deployStatusMeta(deployStatus?.status).dotClass; } diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index ad81b1d6..42bacc58 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -125,6 +125,18 @@ "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" + }, "canvas": { "title": "Infrastructure Canvas", "emptyState": { diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index 50983257..f6e87859 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -125,6 +125,18 @@ "deployFailedCount": "部署失败 — {{failed}} 个资源失败。" } }, + "deployStatus": { + "deployed": "已部署", + "failed": "失败", + "deploying": "部署中", + "destroying": "销毁中", + "planning": "规划中", + "planReady": "计划就绪", + "queued": "排队中", + "connecting": "连接中", + "cancelled": "已取消", + "notDeployed": "未部署" + }, "canvas": { "title": "基础设施画布", "emptyState": { 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..cd5d453c --- /dev/null +++ b/packages/ui/src/shared/utils/__tests__/deploy-status.test.ts @@ -0,0 +1,53 @@ +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'); + }); + + 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..3fa10719 --- /dev/null +++ b/packages/ui/src/shared/utils/deploy-status.ts @@ -0,0 +1,74 @@ +/** + * 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'; + +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', +}; + +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'; + 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', +}; + +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/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/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 ( From 475d5a3f2a3b25e7529660dec1ca3798d289aa6a Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 08:27:41 +0200 Subject: [PATCH 07/39] ux(ia): adopt the canonical deploy-status vocabulary in the status bar + Deployments list (IA4) Finishes IA4 so all three deploy-status surfaces share one vocabulary: - status-bar DeployStatusIndicator: label now comes from deployStatusMeta (e.g. 'error' reads "Failed", matching the env + deployments surfaces); icon/colour kept as a small data-driven per-status map. - Deployments list: statusIcon is keyed off the canonical tone, so an 'error' status no longer slips through to the neutral default icon, and the row label uses the shared labels (falling back to the raw value for unknown backend statuses rather than mislabelling history as "Not deployed"). --- .../components/__tests__/status-bar.test.tsx | 29 ++++----- .../ui/src/shared/components/status-bar.tsx | 60 ++++++++----------- .../web/src/pages/project/deployments.tsx | 37 +++++++----- 3 files changed, 60 insertions(+), 66 deletions(-) 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/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/web/src/pages/project/deployments.tsx b/packages/web/src/pages/project/deployments.tsx index dc267798..ccd5ec31 100644 --- a/packages/web/src/pages/project/deployments.tsx +++ b/packages/web/src/pages/project/deployments.tsx @@ -9,6 +9,7 @@ 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'; @@ -221,21 +222,25 @@ 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: , + }; + 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 +267,7 @@ const InfraDeploymentList: React.FC<{
{statusIcon(d.status)}
- {d.status} + {statusLabel(d.status)} {d.provider} · {d.region} · {d.environment} From 6b476226d2dbd5efe2e2cc8b386814f2f92d687d Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 08:33:22 +0200 Subject: [PATCH 08/39] ux(a11y): give the zoomed-out poster status a shape glyph + aria-label (CNV7, AX5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the poster view (CardShell's lod<3 branch) a node's deploy/runtime state was a bare coloured dot whose only label was a mouse `title` — the dominant render across the whole zoom-out band. That is hue-only and mouse-only: a colour-blind or keyboard/AT user can't tell deploying from done from failed, and nobody can without hovering each card. - New pure helper `poster-status.ts` buckets the broad node-status vocabulary (the STATUS_COLORS keys: active/running/deploying/drifted/error/…) into five tones and pairs each with a NON-COLOUR glyph (✓ / … / ! / ✕ / ·); in-flight tones pulse (motion-safe), consistent with the LOD-3 footer dot (CNV3). - The poster dot now renders that glyph with `role=img` + `aria-label` (the status word) instead of a mouse-only `title`, so the state is reachable by AT and distinguishable by shape. Colour is kept as a redundant cue. Unit-tested the helper (every STATUS_COLORS key is toned; glyphs distinct; pulse only in-flight) and the poster branch (labelled glyph, distinct in-flight vs failed, pulse gating, absent when no status). --- .../_shared/__tests__/card-shell.test.tsx | 45 +++++++++++ .../_shared/__tests__/poster-status.test.tsx | 65 +++++++++++++++ .../components/nodes/_shared/card-shell.tsx | 29 +++++-- .../components/nodes/_shared/poster-status.ts | 80 +++++++++++++++++++ 4 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 packages/ui/src/features/canvas/components/nodes/_shared/__tests__/poster-status.test.tsx create mode 100644 packages/ui/src/features/canvas/components/nodes/_shared/poster-status.ts 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 df265d5e..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 @@ -289,6 +289,51 @@ describe('CardShell', () => { 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__/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/card-shell.tsx b/packages/ui/src/features/canvas/components/nodes/_shared/card-shell.tsx index 1a1a8ced..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 @@ -35,6 +35,7 @@ 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'; @@ -180,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'; @@ -378,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} + )}
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' }; +} From b2611a77707dddf935569675855100df512a7609 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 08:38:06 +0200 Subject: [PATCH 09/39] ux(a11y): honour prefers-reduced-motion consistently (AX4) Several persistent animations ignored the OS reduced-motion setting because they were applied via inline style / SMIL rather than threading the existing useReducedMotion hook, and globals.css had no media-query safety net. - globals.css: add the standard `@media (prefers-reduced-motion: reduce)` net (unlayered + !important so it beats inline `style={{ animation }}`), neutralising the deploy-banner `iceDeployPulse` pulse and the `ice-node-entrance` / `ice-edge-entrance` keyframes in one place. - SVG SMIL is not covered by that media query, so the socket-dot pulsing halo is gated in JS via a new non-hook `prefersReducedMotion()` read (keeps the render-pure SocketDot hook-free). Tested: prefersReducedMotion (true/false/missing-matchMedia/SSR) and the socket-dot halo gate (animate present when allowed, absent when reduced). --- .../_shared/__tests__/socket-dot.test.tsx | 73 +++++++++++++++++++ .../components/nodes/_shared/socket-dot.tsx | 6 +- .../__tests__/use-reduced-motion.test.tsx | 30 +++++++- .../ui/src/shared/hooks/use-reduced-motion.ts | 13 ++++ packages/web/src/styles/globals.css | 21 ++++++ 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/features/canvas/components/nodes/_shared/__tests__/socket-dot.test.tsx 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/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 && ( { // ─── 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/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/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; + } +} + From 82385d63c801b752da516c967259937c7e0998cf Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 08:42:04 +0200 Subject: [PATCH 10/39] ux(a11y): accessible names for the icon-only canvas chrome buttons (AX8, AX7) The empty-canvas quick-start dismiss () and the controls-help "?" trigger are icon/glyph-only, so screen readers announced just "button" / "question mark". - empty-canvas dismiss: aria-label (new `canvas.emptyState.dismiss` key, en+zh). - controls-help "?" trigger: aria-label + aria-expanded reflecting the popover state (AX6 follow-up). Note on AX7: globals.css already ships a global `:focus-visible` ring, and these native 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 cbc8a830..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')}
); From eb6e7ff0567837afad747a4c819b3fbca2aafd5f Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 08:54:11 +0200 Subject: [PATCH 13/39] ux(properties): reveal advanced-tier props via a disclosure (PE5) Advanced-tier props were filtered out of the panel with no escape hatch, so a user wanting to pin a subnet/VPC CIDR or attach a specific TLS cert had no UI path and had to accept the deployer's inference. PropertyFields now splits advanced-tier props into a collapsed "Advanced" disclosure (new `canvas.properties.fields.advanced`, en+zh) that reveals them on demand, keeping the default view clean. State lives in a small child `AdvancedDisclosure` (so the orchestrator stays hook-free) and rows render lazily only when expanded. Tested: PropertyFields routes advanced props into the disclosure (and none when there are none); AdvancedDisclosure shows a count + toggle, hides rows when collapsed, reveals them when expanded, and flips state on click. --- .../__tests__/advanced-disclosure.test.tsx | 107 ++++++++++++++++++ .../__tests__/render-property-field.test.tsx | 32 +++++- .../fields/render-property-field.tsx | 101 +++++++++++------ packages/ui/src/i18n/en.json | 3 +- packages/ui/src/i18n/zh.json | 3 +- 5 files changed, 210 insertions(+), 36 deletions(-) create mode 100644 packages/ui/src/features/properties/components/fields/__tests__/advanced-disclosure.test.tsx diff --git a/packages/ui/src/features/properties/components/fields/__tests__/advanced-disclosure.test.tsx b/packages/ui/src/features/properties/components/fields/__tests__/advanced-disclosure.test.tsx new file mode 100644 index 00000000..487b2717 --- /dev/null +++ b/packages/ui/src/features/properties/components/fields/__tests__/advanced-disclosure.test.tsx @@ -0,0 +1,107 @@ +/** + * AdvancedDisclosure (PE5) — the collapsed "Advanced" section that reveals + * advanced-tier props on demand. Tree-walked as a plain function with a + * controllable `useState` so both collapsed and expanded states are covered. + */ + +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + openValue: false as boolean, + setOpenSpy: vi.fn(), +})); + +vi.mock('react', async (orig) => { + const actual = await orig(); + // AdvancedDisclosure's only state is the open/closed boolean. The source uses + // `React.useState`, so patch the default export too (not just the named one). + const useState = vi.fn(() => [mocks.openValue, mocks.setOpenSpy]); + return { + ...actual, + useState, + default: { ...(actual as unknown as { default: object }).default, useState }, + }; +}); + +import { AdvancedDisclosure, type HighLevelProperty } from '../render-property-field'; + +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 mkProp = (name: string): HighLevelProperty => ({ + name, + label: name, + type: 'string', + required: false, + description: '', + tier: 'advanced', +}); + +const renderRow = (prop: HighLevelProperty) => + React.createElement('div', { key: prop.name, 'data-prop-key': prop.name }); + +const render = () => + ( + AdvancedDisclosure as unknown as (p: { + advanced: HighLevelProperty[]; + renderRow: typeof renderRow; + }) => React.ReactElement + )({ advanced: [mkProp('a'), mkProp('b')], renderRow }); + +const rows = (tree: React.ReactNode) => + findByPredicate( + tree, + (el) => el.type === 'div' && typeof (el.props as { 'data-prop-key'?: string })['data-prop-key'] === 'string', + ); + +const button = (tree: React.ReactNode) => findByPredicate(tree, (el) => el.type === 'button')[0]; + +beforeEach(() => { + mocks.openValue = false; + mocks.setOpenSpy.mockClear(); +}); + +describe('AdvancedDisclosure (PE5)', () => { + it('shows the toggle with a count but no rows when collapsed', () => { + mocks.openValue = false; + const tree = render(); + const btn = button(tree); + expect((btn.props as { 'aria-expanded': boolean })['aria-expanded']).toBe(false); + // The "2" count badge is present... + expect( + findByPredicate(tree, (el) => el.type === 'span' && (el.props as { children?: unknown }).children === 2), + ).toHaveLength(1); + // ...but the advanced rows are not rendered while collapsed. + expect(rows(tree)).toHaveLength(0); + }); + + it('reveals the advanced rows when expanded', () => { + mocks.openValue = true; + const tree = render(); + expect((button(tree).props as { 'aria-expanded': boolean })['aria-expanded']).toBe(true); + expect(rows(tree).map((el) => (el.props as { 'data-prop-key': string })['data-prop-key'])).toEqual(['a', 'b']); + }); + + it('toggles open state when the button is clicked', () => { + mocks.openValue = false; + const tree = render(); + (button(tree).props as { onClick: () => void }).onClick(); + expect(mocks.setOpenSpy).toHaveBeenCalledTimes(1); + // Called with an updater fn that flips the previous value. + const updater = mocks.setOpenSpy.mock.calls[0][0] as (o: boolean) => boolean; + expect(updater(false)).toBe(true); + expect(updater(true)).toBe(false); + }); +}); diff --git a/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx b/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx index 584ecfec..943d5355 100644 --- a/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx +++ b/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx @@ -419,7 +419,7 @@ describe('PropertyFields', () => { expect(sections).toHaveLength(0); }); - it('filters out tier === "advanced" properties from visible list', () => { + it('keeps tier === "advanced" properties out of the main visible Section', () => { const properties = [ mkProp({ name: 'shown1', tier: 'essential' }), mkProp({ name: 'hidden', tier: 'advanced' }), @@ -430,6 +430,8 @@ describe('PropertyFields', () => { nodeData: {}, onFieldChange: vi.fn(), }) as React.ReactElement; + // The advanced row lives inside the (unexecuted) AdvancedDisclosure child, + // so the directly-rendered rows are still only the visible ones. const wrappers = findByPredicate( tree, (el) => el.type === 'div' && typeof (el.props as { 'data-prop-key'?: string })['data-prop-key'] === 'string', @@ -438,6 +440,34 @@ describe('PropertyFields', () => { expect(keys).toEqual(['shown1', 'shown2']); }); + // PE5 — advanced props are no longer dropped; they get a collapsed disclosure. + it('routes advanced-tier props into an AdvancedDisclosure', () => { + const properties = [ + mkProp({ name: 'shown', tier: 'essential' }), + mkProp({ name: 'adv1', tier: 'advanced' }), + mkProp({ name: 'adv2', tier: 'advanced' }), + ]; + const tree = (PropertyFields as React.FC[0]>)({ + properties, + nodeData: {}, + onFieldChange: vi.fn(), + }) as React.ReactElement; + const disclosure = findByDisplayName(tree, 'AdvancedDisclosure'); + expect(disclosure).toHaveLength(1); + const advancedNames = (disclosure[0].props as { advanced: HighLevelProperty[] }).advanced.map((p) => p.name); + expect(advancedNames).toEqual(['adv1', 'adv2']); + }); + + it('renders no AdvancedDisclosure when there are no advanced props', () => { + const properties = [mkProp({ name: 'shown', tier: 'essential' })]; + const tree = (PropertyFields as React.FC[0]>)({ + properties, + nodeData: {}, + onFieldChange: vi.fn(), + }) as React.ReactElement; + expect(findByDisplayName(tree, 'AdvancedDisclosure')).toHaveLength(0); + }); + it('treats no-tier properties as visible (essential default)', () => { const properties = [mkProp({ name: 'untiered' })]; const tree = (PropertyFields as React.FC[0]>)({ diff --git a/packages/ui/src/features/properties/components/fields/render-property-field.tsx b/packages/ui/src/features/properties/components/fields/render-property-field.tsx index 8c47447a..a00750af 100644 --- a/packages/ui/src/features/properties/components/fields/render-property-field.tsx +++ b/packages/ui/src/features/properties/components/fields/render-property-field.tsx @@ -21,16 +21,17 @@ * `'custom'` and `prop.customInput` is configured. * * `PropertyFields` — orchestrator. Provider-filters `optionDetails` by the - * node's cloud provider, hides `tier === 'advanced'` properties (essential - * + detailed only — advanced is intentionally hidden), iterates with - * `renderPropertyField`, and surfaces `propertyIssues` as inline error / - * amber messages. Wraps each field with `data-prop-key={prop.name}` - * (E2E selector — preserved verbatim). + * node's cloud provider, renders essential + detailed props in the main + * Section and routes `tier === 'advanced'` props into a collapsed + * `AdvancedDisclosure` (PE5), iterates with `renderPropertyField`, and + * surfaces `propertyIssues` as inline error / amber messages. Wraps each + * field with `data-prop-key={prop.name}` (E2E selector — preserved verbatim). * * Extracted from `properties-panel.tsx` lines 66–124 (types) + 286–415 * (`renderPropertyField`) + 417–468 (`PropertyFields`) in rf-props-9. */ +import { ChevronRight } from 'lucide-react'; import React from 'react'; import { Section, @@ -324,16 +325,8 @@ export const PropertyFields: React.FC<{ }> = ({ properties, nodeData, onFieldChange, propertyIssues, selfNodeId }) => { const provider = ((nodeData.provider as string) || '').toLowerCase(); - // Filter optionDetails by the node's cloud provider - const filterByProvider = (prop: HighLevelProperty): HighLevelProperty => { - if (!prop.optionDetails || !provider) return prop; - const filtered = prop.optionDetails.filter((o) => !o.provider || o.provider === provider); - return filtered.length > 0 ? { ...prop, optionDetails: filtered } : prop; - }; - - // Show all properties (essential + detailed); advanced tier is intentionally hidden. - // Then apply visibleWhen gates so conditional fields stay hidden until their - // gating field has the right value (e.g. cron-expression input only when + // Apply visibleWhen gates so conditional fields stay hidden until their gating + // field has the right value (e.g. cron-expression input only when // frequency === 'Custom'). const matchesVisibleWhen = (p: HighLevelProperty): boolean => { if (!p.visibleWhen) return true; @@ -345,27 +338,69 @@ export const PropertyFields: React.FC<{ const visible = properties .filter((p) => !p.tier || p.tier === 'essential' || p.tier === 'detailed') .filter(matchesVisibleWhen); + // PE5 — advanced-tier props used to be dropped with no escape hatch. Keep the + // default view clean but make them reachable via a collapsed disclosure. + const advanced = properties.filter((p) => p.tier === 'advanced').filter(matchesVisibleWhen); + + const renderRow = (prop: HighLevelProperty) => ( +
+ {renderPropertyField( + filterOptionDetailsByProvider(prop, provider), + nodeData[prop.name], + onFieldChange, + nodeData, + selfNodeId, + )} + {propertyIssues?.has(prop.name) && ( +
+ {propertyIssues.get(prop.name)!.message} +
+ )} +
+ ); return ( <> - {visible.length > 0 && ( -
- {visible.map((prop) => ( -
- {renderPropertyField(filterByProvider(prop), nodeData[prop.name], onFieldChange, nodeData, selfNodeId)} - {propertyIssues?.has(prop.name) && ( -
- {propertyIssues.get(prop.name)!.message} -
- )} -
- ))} -
- )} + {visible.length > 0 &&
{visible.map(renderRow)}
} + {advanced.length > 0 && } ); }; + +/** Filter a prop's `optionDetails` down to the node's cloud provider. */ +function filterOptionDetailsByProvider(prop: HighLevelProperty, provider: string): HighLevelProperty { + if (!prop.optionDetails || !provider) return prop; + const filtered = prop.optionDetails.filter((o) => !o.provider || o.provider === provider); + return filtered.length > 0 ? { ...prop, optionDetails: filtered } : prop; +} + +/** + * PE5 — collapsed "Advanced" disclosure. Holds its own open/closed state (so the + * orchestrator stays hook-free) and renders the advanced rows lazily, only when + * expanded, via the parent's `renderRow`. + */ +export const AdvancedDisclosure: React.FC<{ + advanced: HighLevelProperty[]; + renderRow: (prop: HighLevelProperty) => React.ReactNode; +}> = ({ advanced, renderRow }) => { + const [open, setOpen] = React.useState(false); + return ( +
+ + {open &&
{advanced.map(renderRow)}
} +
+ ); +}; diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index c5784fe0..c717bc2b 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -545,7 +545,8 @@ "actionTypeHttpLabel": "Send HTTP request", "actionTypeHttpDescription": "hit an external URL", "customValuePrefix": "Custom: {{value}} {{unit}}", - "required": "Required" + "required": "Required", + "advanced": "Advanced" }, "log": { "sectionTitle": "Logs", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index f76f5bb9..87e5a65b 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -545,7 +545,8 @@ "actionTypeHttpLabel": "发送 HTTP 请求", "actionTypeHttpDescription": "访问外部 URL", "customValuePrefix": "自定义:{{value}} {{unit}}", - "required": "必填" + "required": "必填", + "advanced": "高级" }, "log": { "sectionTitle": "日志", From 34a5731a3c45c8e871d9d016a4be22953f1ff45e Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 08:59:50 +0200 Subject: [PATCH 14/39] ux(properties): surface per-field validation as a tab badge (PE2) Per-field config validation rendered only inside the Config tab, so a user on Scaling/Domain/Source/Connections got no in-panel signal that the node was invalid (the global validation panel is off by default). PropertiesTabBar now shows an error/warning count pill on a tab (errors take precedence; one pill, AT-labelled via the existing requirements.* strings). node-properties-section derives the Config tab's counts from propertyIssuesMap (which IS the per-field config validation), so the badge is visible from any tab. No new i18n. Tested: TabIssueBadge renders the red error pill / amber warning pill / nothing across counts; PropertiesTabBar threads the config counts to the right tab. --- .../__tests__/properties-tab-bar.test.tsx | 64 ++++++++++++++++++- .../sections/node-properties-section.tsx | 21 +++++- .../sections/properties-tab-bar.tsx | 51 ++++++++++++++- 3 files changed, 133 insertions(+), 3 deletions(-) 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/node-properties-section.tsx b/packages/ui/src/features/properties/components/sections/node-properties-section.tsx index 3cb470ba..2cbb00d2 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 @@ -304,9 +304,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 && ( 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; +}; From 30d0c24064f7393ca4fd301d0bf564ff05cd3f13 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:06:04 +0200 Subject: [PATCH 15/39] ux(palette): keyword + category search so goal-based queries resolve (CD3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block palette only substring-matched a block's localized name + description, so goal terms a newcomer types ('api', 'cron', 'cdn', 'pubsub', 'cache', 'auth') dead-ended — even though the adjacent template search matches tags + category. Same word, two different behaviours. - ComponentDef gains an optional `keywords` field; a centralized `GOAL_KEYWORDS` map (keyed by iceType, looked up in `def()` so the 25 call sites are untouched) seeds synonyms for the common goal terms. - New pure `componentMatchesQuery` ORs name/description/tooltip + the localized category label + keywords, mirroring `searchTemplates`. The palette filter delegates to it (now also reactive to locale via categoryMap). Tested: keyword wiring (every GOAL_KEYWORDS key is a real iceType; def attaches/ omits correctly) and the matcher (name/desc/tooltip/category/keyword hits, empty-query, no-match; 'cron' finds CronJob not Postgres). --- .../palette/__tests__/components-data.test.ts | 80 ++++++++++++++++++- .../__tests__/resource-palette.test.tsx | 4 +- .../palette/components/resource-palette.tsx | 12 +-- .../src/features/palette/data/components.ts | 55 +++++++++++++ packages/ui/src/features/palette/types.ts | 4 + 5 files changed, 147 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/features/palette/__tests__/components-data.test.ts b/packages/ui/src/features/palette/__tests__/components-data.test.ts index 2badd0ed..c3187e20 100644 --- a/packages/ui/src/features/palette/__tests__/components-data.test.ts +++ b/packages/ui/src/features/palette/__tests__/components-data.test.ts @@ -36,7 +36,8 @@ import { import { describe, it, expect } from 'vitest'; void Folder; -import { getComponents, blockKey, def } from '../data/components'; +import { getComponents, blockKey, def, GOAL_KEYWORDS, componentMatchesQuery } from '../data/components'; +import type { ComponentDef } from '../types'; const t = (k: string) => k; const COMPONENTS = getComponents(t); @@ -336,3 +337,80 @@ describe('COMPONENTS — fallback names', () => { expect(container?.tooltip).toBe('blocks.computeContainer.tooltip'); }); }); + +// ─── CD3 — goal keywords + search matching ──────────────────────────────────── + +describe('GOAL_KEYWORDS wiring', () => { + it('def() attaches keywords from GOAL_KEYWORDS for a known iceType', () => { + const cron = def(t, 'Compute.CronJob', Server, ['aws'], 'Scheduler'); + expect(cron.keywords).toEqual(GOAL_KEYWORDS['Compute.CronJob']); + expect(cron.keywords).toContain('cron'); + expect(cron.keywords).toContain('schedule'); + }); + + it('def() omits keywords entirely for an iceType with no entry', () => { + const unknown = def(t, 'Compute.X', Server, ['aws'], 'Compute'); + expect(unknown.keywords).toBeUndefined(); + }); + + it('getComponents() surfaces the keywords on real blocks', () => { + const redis = COMPONENTS.find((c) => c.type === 'Database.Redis'); + expect(redis?.keywords).toContain('cache'); + const fn = COMPONENTS.find((c) => c.type === 'Compute.ServerlessFunction'); + expect(fn?.keywords).toContain('api'); + }); + + it('every GOAL_KEYWORDS key is a real iceType in the inventory', () => { + const realTypes = new Set(COMPONENTS.map((c) => c.type)); + for (const type of Object.keys(GOAL_KEYWORDS)) { + expect(realTypes.has(type)).toBe(true); + } + }); +}); + +describe('componentMatchesQuery (CD3)', () => { + const mk = (over: Partial): ComponentDef => ({ + type: 'Compute.X', + name: 'Widget', + description: 'does things', + tooltip: 'a widget', + icon: () => null, + providers: ['aws'], + category: 'Compute', + ...over, + }); + + it('matches everything for an empty / whitespace query', () => { + expect(componentMatchesQuery(mk({}), '')).toBe(true); + expect(componentMatchesQuery(mk({}), ' ')).toBe(true); + }); + + it('matches on name and description (the original behaviour)', () => { + expect(componentMatchesQuery(mk({ name: 'PostgreSQL' }), 'postgres')).toBe(true); + expect(componentMatchesQuery(mk({ description: 'relational store' }), 'relational')).toBe(true); + }); + + it('matches on the tooltip', () => { + expect(componentMatchesQuery(mk({ tooltip: 'managed cache' }), 'cache')).toBe(true); + }); + + it('matches on the supplied category label', () => { + expect(componentMatchesQuery(mk({ name: 'Postgres' }), 'database', 'Database')).toBe(true); + }); + + it('matches on goal keywords', () => { + expect(componentMatchesQuery(mk({ keywords: ['cron', 'schedule'] }), 'cron')).toBe(true); + expect(componentMatchesQuery(mk({ keywords: ['api', 'gateway'] }), 'api')).toBe(true); + }); + + it('returns false when nothing matches', () => { + expect(componentMatchesQuery(mk({ name: 'Widget', description: 'does things' }), 'zzz')).toBe(false); + }); + + it('"cron" finds the CronJob block in the real inventory but not Postgres', () => { + const cron = COMPONENTS.find((c) => c.type === 'Compute.CronJob')!; + const pg = COMPONENTS.find((c) => c.type === 'Database.PostgreSQL')!; + expect(componentMatchesQuery(cron, 'cron')).toBe(true); + expect(componentMatchesQuery(pg, 'cron')).toBe(false); + }); +}); diff --git a/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx b/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx index 144bcbd8..2e795496 100644 --- a/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx +++ b/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx @@ -231,7 +231,9 @@ vi.mock('../data/categories', () => ({ ]), })); -vi.mock('../data/components', () => ({ +vi.mock('../data/components', async (importOriginal) => ({ + // Keep the real `componentMatchesQuery` (CD3) — only the inventory is stubbed. + ...(await importOriginal()), getComponents: () => [ { type: 'Compute.A', diff --git a/packages/ui/src/features/palette/components/resource-palette.tsx b/packages/ui/src/features/palette/components/resource-palette.tsx index b24fefeb..502320af 100644 --- a/packages/ui/src/features/palette/components/resource-palette.tsx +++ b/packages/ui/src/features/palette/components/resource-palette.tsx @@ -31,7 +31,7 @@ import { useResolvePath } from '../../../shared/hooks/use-resolve-path'; import { ProjectBrowser } from '../../project-browser'; import { TemplateCategoriesPanel } from '../../templates/components/template-categories-panel'; import { getCategoryMap, CATEGORY_ORDER } from '../data/categories'; -import { getComponents } from '../data/components'; +import { getComponents, componentMatchesQuery } from '../data/components'; import { PALETTE_STYLES, loadCollapsed, saveCollapsed } from '../data/providers'; import { BlocksSection } from '../sections/blocks-section'; import type { CategoryDef, ComponentDef, Provider, ResourcePaletteProps } from '../types'; @@ -115,14 +115,14 @@ export const ResourcePalette: React.FC = ({ ); if (effectiveProviders.length === 0) return false; - const matchesSearch = - !localSearch.trim() || - c.name.toLowerCase().includes(localSearch.toLowerCase()) || - c.description.toLowerCase().includes(localSearch.toLowerCase()); + // CD3 — match name/description/tooltip + the localized category label + + // goal/synonym keywords, mirroring the richer template search. Lets + // "database", "api", "cron", "cache" etc. resolve to the right block. + const matchesSearch = componentMatchesQuery(c, localSearch, categoryMap.get(c.category)?.label ?? ''); const matchesProvider = selectedProvider === 'all' || effectiveProviders.includes(selectedProvider as Provider); return matchesSearch && matchesProvider; }), - [components, localSearch, selectedProvider], + [components, localSearch, selectedProvider, categoryMap], ); // Providers with at least one concept whose (category × provider) gate diff --git a/packages/ui/src/features/palette/data/components.ts b/packages/ui/src/features/palette/data/components.ts index 24afe07d..afc17acb 100644 --- a/packages/ui/src/features/palette/data/components.ts +++ b/packages/ui/src/features/palette/data/components.ts @@ -59,6 +59,59 @@ export function blockKey(type: string): string { } /** Helper: builds a ComponentDef from i18n keys with inline fallbacks for newly-added concept iceTypes. */ +/** + * CD3 — goal/synonym keywords per iceType, ORed into palette search so the + * "I know the outcome, not the block name" terms resolve. Keyed by iceType, + * looked up in `def()` so the 25 call sites stay untouched. Provider-agnostic + * and intentionally NOT localized (these are mostly technical English terms a + * developer types regardless of UI language; the localized name/description/ + * category label already participate in the match). + */ +export const GOAL_KEYWORDS: Record = { + 'AI.LLMGateway': ['ai', 'llm', 'gpt', 'openai', 'model', 'inference', 'chat', 'completion'], + 'AI.PrivateAIService': ['ai', 'ml', 'private', 'model', 'inference', 'serving'], + 'AI.VectorDB': ['vector', 'embedding', 'embeddings', 'rag', 'similarity', 'ai', 'semantic'], + 'Compute.Container': ['container', 'docker', 'service', 'api', 'backend', 'microservice', 'pod', 'app'], + 'Compute.CronJob': ['cron', 'schedule', 'scheduled', 'job', 'task', 'timer', 'batch', 'recurring'], + 'Compute.ServerlessFunction': ['serverless', 'lambda', 'function', 'faas', 'api', 'endpoint', 'handler'], + 'Compute.SSRSite': ['ssr', 'frontend', 'website', 'web', 'nextjs', 'react', 'app'], + 'Compute.StaticSite': ['static', 'cdn', 'website', 'web', 'frontend', 'spa', 'jamstack', 'html'], + 'Compute.Worker': ['worker', 'background', 'job', 'consumer', 'processor', 'async'], + 'Config.Environment': ['env', 'environment', 'config', 'variables', 'settings'], + 'Database.MongoDB': ['database', 'db', 'nosql', 'document', 'mongo'], + 'Database.MySQL': ['database', 'db', 'sql', 'relational', 'mysql', 'mariadb'], + 'Database.PostgreSQL': ['database', 'db', 'sql', 'relational', 'postgres', 'postgresql', 'rds'], + 'Database.Redis': ['cache', 'redis', 'in-memory', 'kv', 'key-value', 'database'], + 'Messaging.Email': ['email', 'mail', 'smtp', 'notification', 'ses', 'sendgrid'], + 'Messaging.EventStream': ['event', 'events', 'stream', 'streaming', 'kafka', 'pubsub', 'kinesis'], + 'Messaging.Queue': ['queue', 'pubsub', 'message', 'sqs', 'topic', 'async', 'job'], + 'Monitoring.Log': ['log', 'logs', 'logging', 'observability', 'monitoring', 'metrics', 'trace'], + 'Network.CustomDomain': ['domain', 'dns', 'url', 'hostname', 'tls', 'ssl', 'cert', 'certificate'], + 'Network.Gateway': ['api', 'gateway', 'rest', 'endpoint', 'ingress', 'proxy', 'load balancer', 'loadbalancer', 'lb'], + 'Network.PrivateNetwork': ['network', 'vpc', 'subnet', 'private', 'vnet'], + 'Security.Secret': ['secret', 'secrets', 'auth', 'credential', 'credentials', 'key', 'vault', 'password'], + 'Source.Repository': ['repo', 'repository', 'git', 'github', 'source', 'code'], + 'Storage.Bucket': ['storage', 'bucket', 's3', 'object', 'blob', 'file', 'files'], +}; + +/** + * CD3 — does a component match a palette search query? ORs the localized + * name/description/tooltip, the (caller-supplied, localized) category label, and + * the goal keywords — mirroring the richer template search. An empty/blank query + * matches everything. + */ +export function componentMatchesQuery(c: ComponentDef, query: string, categoryLabel = ''): boolean { + const q = query.trim().toLowerCase(); + if (!q) return true; + return ( + c.name.toLowerCase().includes(q) || + c.description.toLowerCase().includes(q) || + c.tooltip.toLowerCase().includes(q) || + categoryLabel.toLowerCase().includes(q) || + (c.keywords?.some((kw) => kw.toLowerCase().includes(q)) ?? false) + ); +} + export function def( t: Translator, type: string, @@ -73,6 +126,7 @@ export function def( // If the i18n key is missing, `t()` returns the key string verbatim — // detect that and use the fallback. const missing = i18nName === `blocks.${k}.name`; + const keywords = GOAL_KEYWORDS[type]; return { type, name: missing && fallback ? fallback.name : i18nName, @@ -82,6 +136,7 @@ export function def( providers, category, ...(runtimes ? { runtimes } : {}), + ...(keywords ? { keywords } : {}), }; } diff --git a/packages/ui/src/features/palette/types.ts b/packages/ui/src/features/palette/types.ts index 641c2a2a..a356823c 100644 --- a/packages/ui/src/features/palette/types.ts +++ b/packages/ui/src/features/palette/types.ts @@ -46,6 +46,10 @@ export interface ComponentDef { providers: ('aws' | 'gcp' | 'azure' | 'kubernetes' | 'alibaba' | 'oci' | 'digitalocean' | 'ibm')[]; category: string; runtimes?: RuntimeOption[]; + /** CD3 — goal/synonym terms ORed into palette search so "api", "cron", + * "cdn", "pubsub", "cache", "auth" etc. find the right block even though the + * word isn't in its name/description. Provider-agnostic; not localized. */ + keywords?: string[]; } /** Public props for the `ResourcePalette` orchestrator. Each section is From 52c25c734990a40ae750d4f070ebf69870c2fa43 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:11:22 +0200 Subject: [PATCH 16/39] ux(canvas): make Shift+A spotlight match the drag add-path (CD1, CD5, CD6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Shift+A spotlight diverged from the drag-drop add path in three ways: - CD1 — it always took providers[0]; now it prefers the active deploy provider when the block supports it (so it spawns the same blueprint drag would). - CD5 — its missing-blueprint fallback never set providerUnsupported (so an unsupported block looked valid until deploy rejected it) and hardcoded the deploy provider; now it carries providerUnsupported + the resolved effectiveProvider, matching use-canvas-drop. - CD6 — a locked canvas disables the right-click "Add Block" path but Shift+A ignored the lock; SpotlightMount now passes enabled: !canvasLocked to the hook (which already supported it). CD1/CD5 logic extracted to a pure spotlight-spawn helper and unit-tested (provider preference, gate-block, fallback providerUnsupported flag). --- .../__tests__/spotlight-spawn.test.ts | 67 +++++++++++++++++++ .../components/add-menu/spotlight-spawn.ts | 60 +++++++++++++++++ .../canvas/components/add-menu/spotlight.tsx | 23 ++++--- .../features/canvas/components/svg-canvas.tsx | 13 ++-- 4 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-spawn.test.ts create mode 100644 packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts 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..e3c947e9 --- /dev/null +++ b/packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-spawn.test.ts @@ -0,0 +1,67 @@ +/** + * 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('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..5a9144ac --- /dev/null +++ b/packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts @@ -0,0 +1,60 @@ +/** + * 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 { + 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..66e0aa4f 100644 --- a/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx +++ b/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx @@ -19,6 +19,7 @@ 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'; @@ -96,9 +97,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 +133,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)); } diff --git a/packages/ui/src/features/canvas/components/svg-canvas.tsx b/packages/ui/src/features/canvas/components/svg-canvas.tsx index 7ca611d5..9c46ad0f 100644 --- a/packages/ui/src/features/canvas/components/svg-canvas.tsx +++ b/packages/ui/src/features/canvas/components/svg-canvas.tsx @@ -500,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 ; }; From 0c3f914ad91ef902fd624954471d8588f93aeea0 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:20:51 +0200 Subject: [PATCH 17/39] ux(properties): de-dup the name field + anchor hidden-prop issues (PE7, PE9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two properties-panel long-tail lows: - PE7 — the schema `name` prop is tier:'essential' so it rendered in the Config tab alongside the identity card's name input — Name was editable twice in one panel. Drop the duplicate schema `name` field; the identity card owns it. - PE9 — inline per-field error messages only rendered for the `visible` list, so an issue on an advanced-tier or visibleWhen-hidden prop had no field to point at (banner-only). Now any prop with a live validation issue is always shown in the main Section, so every inline message has an anchor. Tested: name field dropped; an advanced prop with an issue is promoted out of the disclosure into the Section; a gated-hidden prop with an issue is shown. --- .../__tests__/render-property-field.test.tsx | 50 +++++++++++++++++++ .../fields/render-property-field.tsx | 21 +++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx b/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx index 943d5355..eb9c164b 100644 --- a/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx +++ b/packages/ui/src/features/properties/components/fields/__tests__/render-property-field.test.tsx @@ -468,6 +468,56 @@ describe('PropertyFields', () => { expect(findByDisplayName(tree, 'AdvancedDisclosure')).toHaveLength(0); }); + const propKeys = (tree: React.ReactNode) => + findByPredicate( + tree, + (el) => el.type === 'div' && typeof (el.props as { 'data-prop-key'?: string })['data-prop-key'] === 'string', + ).map((el) => (el.props as { 'data-prop-key': string })['data-prop-key']); + + // PE7 — the schema `name` field is dropped (the identity card owns it). + it('drops the duplicate schema "name" field from the Config tab', () => { + const properties = [mkProp({ name: 'name', tier: 'essential' }), mkProp({ name: 'size', tier: 'essential' })]; + const tree = (PropertyFields as React.FC[0]>)({ + properties, + nodeData: {}, + onFieldChange: vi.fn(), + }) as React.ReactElement; + expect(propKeys(tree)).toEqual(['size']); + }); + + // PE9 — a prop with a live validation issue is always shown so its inline + // message has an anchor, even if it's advanced-tier or visibleWhen-hidden. + it('promotes an advanced prop WITH a validation issue into the main Section', () => { + const properties = [mkProp({ name: 'shown', tier: 'essential' }), mkProp({ name: 'adv', tier: 'advanced' })]; + const propertyIssues = new Map([['adv', { severity: 'error', message: 'bad' }]]); + const tree = (PropertyFields as React.FC[0]>)({ + properties, + nodeData: {}, + onFieldChange: vi.fn(), + propertyIssues, + }) as React.ReactElement; + // 'adv' is now a directly-rendered row (its inline message has an anchor)… + expect(propKeys(tree)).toContain('adv'); + // …and the disclosure is gone since its only advanced prop was promoted. + expect(findByDisplayName(tree, 'AdvancedDisclosure')).toHaveLength(0); + }); + + it('anchors a visibleWhen-hidden prop that has a validation issue', () => { + const properties = [ + mkProp({ name: 'frequency', type: 'select', options: ['Hourly', 'Custom'] }), + mkProp({ name: 'schedule', visibleWhen: { field: 'frequency', equals: 'Custom' } }), + ]; + const propertyIssues = new Map([['schedule', { severity: 'error', message: 'bad cron' }]]); + // frequency != 'Custom' → 'schedule' is gated-hidden, but its issue forces it visible. + const tree = (PropertyFields as React.FC[0]>)({ + properties, + nodeData: { frequency: 'Hourly' }, + onFieldChange: vi.fn(), + propertyIssues, + }) as React.ReactElement; + expect(propKeys(tree)).toContain('schedule'); + }); + it('treats no-tier properties as visible (essential default)', () => { const properties = [mkProp({ name: 'untiered' })]; const tree = (PropertyFields as React.FC[0]>)({ diff --git a/packages/ui/src/features/properties/components/fields/render-property-field.tsx b/packages/ui/src/features/properties/components/fields/render-property-field.tsx index a00750af..318d8ff2 100644 --- a/packages/ui/src/features/properties/components/fields/render-property-field.tsx +++ b/packages/ui/src/features/properties/components/fields/render-property-field.tsx @@ -335,12 +335,21 @@ export const PropertyFields: React.FC<{ if (Array.isArray(target)) return target.some((v) => v === current); return current === target; }; - const visible = properties - .filter((p) => !p.tier || p.tier === 'essential' || p.tier === 'detailed') - .filter(matchesVisibleWhen); - // PE5 — advanced-tier props used to be dropped with no escape hatch. Keep the - // default view clean but make them reachable via a collapsed disclosure. - const advanced = properties.filter((p) => p.tier === 'advanced').filter(matchesVisibleWhen); + const isVisibleTier = (p: HighLevelProperty): boolean => !p.tier || p.tier === 'essential' || p.tier === 'detailed'; + const hasIssue = (p: HighLevelProperty): boolean => propertyIssues?.has(p.name) ?? false; + + // PE7 — the identity card is the dedicated `name` editor; drop the duplicate + // schema `name` field so Name isn't editable twice in the same panel. + const editable = properties.filter((p) => p.name !== 'name'); + + // PE9 — a prop that currently has a validation issue is ALWAYS shown in the + // main Section, so its inline message has a visible anchor even if it's + // advanced-tier or hidden by a `visibleWhen` gate (otherwise the issue only + // appears in the node banner with no field to point at). + const visible = editable.filter((p) => (isVisibleTier(p) && matchesVisibleWhen(p)) || hasIssue(p)); + // PE5 — advanced-tier props (without a live issue) stay reachable via a + // collapsed disclosure instead of being dropped entirely. + const advanced = editable.filter((p) => p.tier === 'advanced' && matchesVisibleWhen(p) && !hasIssue(p)); const renderRow = (prop: HighLevelProperty) => (
From 4d7753de404e6bf7d4b77fd15ae0054706d24139 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:22:29 +0200 Subject: [PATCH 18/39] ux(canvas): seed data.name on the fallback add-paths (PE6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blueprint add-path seeds data.name from the block's display name, but the no-blueprint fallback (drag drop + Shift+A spotlight) set only data.label — so those nodes were born with a hidden "Name is required" validation error that the identity card masks (it shows data.name || data.label). Seed data.name from the label on both fallback paths, matching the blueprint path. Tested on both (drag fallback + buildSpotlightFallbackData). --- .../components/add-menu/__tests__/spotlight-spawn.test.ts | 5 +++++ .../features/canvas/components/add-menu/spotlight-spawn.ts | 4 ++++ .../features/canvas/hooks/__tests__/use-canvas-drop.test.tsx | 3 +++ packages/ui/src/features/canvas/hooks/use-canvas-drop.ts | 3 +++ 4 files changed, 15 insertions(+) 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 index e3c947e9..2d745eec 100644 --- 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 @@ -55,6 +55,11 @@ describe('buildSpotlightFallbackData (CD5)', () => { 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); 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 index 5a9144ac..c8015c21 100644 --- a/packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts +++ b/packages/ui/src/features/canvas/components/add-menu/spotlight-spawn.ts @@ -50,6 +50,10 @@ export function buildSpotlightFallbackData( 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', 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/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', From 181e5b28c1a4cd47e92e3664faf8c9c16f7c056e Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:32:49 +0200 Subject: [PATCH 19/39] ux(deploy): stop logging a fabricated '0.0s' completion time (DF8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The async `complete` wire event carries no duration, but the reducer logged "Deploy completed in 0.0s" — a fake time that contradicted the real-duration line the hydrate reducer pushes moments later from the DB record. The status flips instantly from the event regardless, so drop the fabricated line and let hydrate own the authoritative completion log line. Tested: applyDeployCompleteEvent no longer appends any '0.0s' line. --- .../deploy/reducers/__tests__/wire-events.test.ts | 13 +++++++++++++ .../src/store/slices/deploy/reducers/wire-events.ts | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) 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 67378b21..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 @@ -414,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/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; From 4a5d727d290785931884fbea297cb66ce719f55b Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:41:11 +0200 Subject: [PATCH 20/39] ux(observability): log copy fidelity + honest copy feedback (OL7, OL8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two log-node copy issues: - OL7 — copied lines used the truncated HH:MM:SS display timestamp, dropping the date so a pasted line couldn't be correlated to the cloud console. Copy now carries the full date+time (new pure formatCopyTimestamp/formatLogCopyLine). Copy-all also gained a transient 'COPIED' confirmation (the CopyButton stays briefly visible after a copy). - OL8 — clipboard writes set the 'copied' flash unconditionally, so a rejected or unavailable-clipboard write showed a false-positive confirmation. The flash is now gated on the write actually succeeding (writeClipboard → Promise; handles a missing API and rejection). Tested: full-ts copy lines; copied flash fires only on success, not on reject / missing clipboard; CopyButton COPIED state; LogHeader copiedAll passthrough. (Deferred, noted in plan: resource-label inclusion in copy + a distinct 'notice' log colour — narrower OL7 sub-points.) --- .../log-node/__tests__/copy-button.test.tsx | 19 +++- .../nodes/log-node/__tests__/index.test.tsx | 92 +++++++++++++++++-- .../log-node/__tests__/log-header.test.tsx | 9 ++ .../components/nodes/log-node/copy-button.tsx | 10 +- .../components/nodes/log-node/index.tsx | 48 ++++++++-- .../components/nodes/log-node/log-header.tsx | 9 +- .../canvas/components/nodes/log-node/types.ts | 4 + 7 files changed, 167 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/copy-button.test.tsx b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/copy-button.test.tsx index 68309a71..0bfd5f58 100644 --- a/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/copy-button.test.tsx +++ b/packages/ui/src/features/canvas/components/nodes/log-node/__tests__/copy-button.test.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { describe, it, expect, vi } from 'vitest'; import { CopyButton } from '../copy-button'; -const renderCB = (onClick: (e: React.MouseEvent) => 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__/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 6b47cf78..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': @@ -110,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, @@ -169,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) @@ -233,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/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; From 0adf912220b80fe45380c1955f2d1df0a32e2772 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:43:30 +0200 Subject: [PATCH 21/39] ux(observability): differentiate the log node's non-streaming states (OL6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The canvas log node's LiveIndicator collapsed five non-streaming states (pre-deploy / no-source / ambiguous / unsupported / provider-unsupported) into a single grey 'IDLE' dot — so surveying the board you couldn't tell 'waiting on a deploy' from 'no source connected' from 'pick a source' without opening the properties panel, which already differentiates them via pillFor. specFor now returns a distinct short label per state (WAITING / NO SOURCE / CHOOSE SRC / UNSUPPORTED / UNAVAILABLE), keeping the grey tone so the canvas dot and the panel pill still agree. New canvas.status.* keys (en+zh). Tested: each state shows its own label, all grey, all mutually distinct. --- .../__tests__/live-indicator.test.tsx | 24 +++++++++++++++---- .../nodes/log-node/live-indicator.tsx | 11 +++++++++ packages/ui/src/i18n/en.json | 5 ++++ packages/ui/src/i18n/zh.json | 5 ++++ 4 files changed, 40 insertions(+), 5 deletions(-) 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/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/i18n/en.json b/packages/ui/src/i18n/en.json index c717bc2b..3d556c54 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -253,6 +253,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", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index 87e5a65b..a4510b1b 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -253,6 +253,11 @@ "connecting": "连接中", "error": "错误", "idle": "空闲", + "preDeploy": "待部署", + "noSource": "无来源", + "ambiguous": "选择来源", + "unsupported": "不支持", + "providerUnsupported": "不可用", "pipelineLive": "运行中", "pipelineFailed": "失败", "pipelineBuilding": "构建中", From 7bd083a7104e948d609dab8b8ba4748fdb1a1bb0 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:45:24 +0200 Subject: [PATCH 22/39] ux(environments): clarify promotion updates design, not live infra (EI7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The promote modal read "N changes will be applied to production" — which sounds like a live deployment, but promoteEnvironment only copies the design (cards/ nodes) into the target env; the user still has to deploy that env to change running infrastructure. Reword the count line ("...will update the design of ") and add an amber clarification note: promotion updates the design only — deploy the target to apply it to live infrastructure. New `designOnlyNote` key (en+zh). Tested: the note shows in the diff case and is omitted when there are no changes. --- .../components/__tests__/promote-modal.test.tsx | 12 ++++++++++++ .../environments/components/promote-modal.tsx | 7 +++++++ packages/ui/src/i18n/en.json | 3 ++- packages/ui/src/i18n/zh.json | 3 ++- 4 files changed, 23 insertions(+), 2 deletions(-) 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/promote-modal.tsx b/packages/ui/src/features/environments/components/promote-modal.tsx index f70f8485..a60442e2 100644 --- a/packages/ui/src/features/environments/components/promote-modal.tsx +++ b/packages/ui/src/features/environments/components/promote-modal.tsx @@ -77,6 +77,13 @@ export const PromoteModal: React.FC = () => { ? t('environments.promote.identicalMessage') : `${totalChanges} ${totalChanges !== 1 ? t('environments.promote.changes') : t('environments.promote.change')} ${t('environments.promote.changesWillBeApplied')} ${targetName}.`}

+ {/* EI7 — make clear promotion updates the DESIGN, not live infra; the + old "will be applied to " copy read as a live deployment. */} + {!noChanges && ( +

+ {t('environments.promote.designOnlyNote', { target: targetName })} +

+ )}
{/* Diff list */} diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index 3d556c54..b8692201 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -1091,7 +1091,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", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index a4510b1b..4afb847a 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -1090,7 +1090,8 @@ "promote": "提升", "to": "到", "identicalMessage": "两个环境完全一致,无需提升。", - "changesWillBeApplied": "将应用到", + "changesWillBeApplied": "将更新其设计:", + "designOnlyNote": "提升仅更新设计——您仍需部署 {{target}} 才能将这些更改应用到实际基础设施。", "change": "项更改", "changes": "项更改", "inSync": "环境已同步", From 7c12890171903871503b68eedbf463482799faa0 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 09:52:18 +0200 Subject: [PATCH 23/39] ux(environments): distinguish a failed status fetch from never-deployed (EI9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The env-tab deploy-status fetch used Promise.allSettled but only recorded fulfilled results, silently dropping rejected (network/auth) fetches — so a fetch failure and a never-deployed env both showed the same grey idle dot. - The fetch now catches inside the map (preserving the env id) and records a distinct 'fetch-error' status for failures. - Canonical deploy-status (IA4) gains an 'unknown' tone → a hollow amber-ringed dot (visually distinct from the filled grey idle) + a 'Status unavailable' label. deployments.tsx TONE_ICON gets the matching entry. - The env-tab dot now also carries role=img + aria-label (the status was colour-only), pairing colour with an AT name. Tested: fetch-error → unknown tone/hollow dot/label; the fetch loop records fetch-error for a rejected env; the dot exposes its accessible name. --- .../__tests__/environment-tab-bar.test.tsx | 20 +++++++++++++ .../__tests__/environment-tab-item.test.tsx | 28 +++++++++++++++++++ .../components/environment-tab-bar.tsx | 21 ++++++++++---- .../components/environment-tab-item.tsx | 13 +++++++-- .../environments/utils/deploy-status-color.ts | 6 ++++ packages/ui/src/i18n/en.json | 3 +- packages/ui/src/i18n/zh.json | 3 +- .../utils/__tests__/deploy-status.test.ts | 11 ++++++++ packages/ui/src/shared/utils/deploy-status.ts | 10 ++++++- .../web/src/pages/project/deployments.tsx | 15 +++++++++- 10 files changed, 118 insertions(+), 12 deletions(-) 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 f8b54f28..aef96396 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 @@ -685,6 +685,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', () => { 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/environment-tab-bar.tsx b/packages/ui/src/features/environments/components/environment-tab-bar.tsx index 5fa85779..438543db 100644 --- a/packages/ui/src/features/environments/components/environment-tab-bar.tsx +++ b/packages/ui/src/features/environments/components/environment-tab-bar.tsx @@ -63,18 +63,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); 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 --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index 0bf8ab49..859c165f 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -713,7 +713,9 @@ }, "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.", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index c7d70cf5..81320c4b 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -713,7 +713,9 @@ }, "auth": { "connecting": "正在连接 GCP...", - "browserPrompt": "浏览器窗口将打开进行 Google 认证。请完成登录以继续。" + "browserPrompt": "浏览器窗口将打开进行 Google 认证。请完成登录以继续。", + "noWindow": "没有看到窗口?它可能被拦截或已关闭。", + "reopen": "重新打开登录窗口" }, "plan": { "noChanges": "未检测到更改。基础设施已是最新状态。", From 808bce87a2b696f7991fb86292e3515bff80953b Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 10:13:59 +0200 Subject: [PATCH 28/39] ux(canvas): spotlight section headers + localized category labels (CD8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Shift+A spotlight rendered a flat list with no recent-vs-catalog structure and showed each row's raw iceType category id instead of the localized, palette category label — so the same word read differently in the palette vs the spotlight. - Map each row's category through getCategoryMap(t), matching the palette's labels (SpotlightRow now takes a categoryLabel prop). - Add lightweight 'Recent' / 'All blocks' section headers when not searching (new canvas.spotlight.* keys, en+zh). Headers are non-interactive aria-hidden
  • s outside the ranked command list, so arrow-key nav is unaffected. Tested: row shows the localized label not the raw id; header is aria-hidden + non-interactive; row select/hover wiring intact. --- .../add-menu/__tests__/spotlight-row.test.tsx | 92 +++++++++++++++++++ .../canvas/components/add-menu/spotlight.tsx | 66 ++++++++++--- packages/ui/src/i18n/en.json | 4 + packages/ui/src/i18n/zh.json | 4 + 4 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 packages/ui/src/features/canvas/components/add-menu/__tests__/spotlight-row.test.tsx 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/spotlight.tsx b/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx index 66e0aa4f..11d359f9 100644 --- a/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx +++ b/packages/ui/src/features/canvas/components/add-menu/spotlight.tsx @@ -25,6 +25,7 @@ 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'; @@ -64,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. @@ -249,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/i18n/en.json b/packages/ui/src/i18n/en.json index 859c165f..e2f5f489 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -150,6 +150,10 @@ "hint": "Press Shift+A or drag a block from the left to begin", "dismiss": "Dismiss quick start" }, + "spotlight": { + "recent": "Recent", + "catalog": "All blocks" + }, "tooltip": { "connections": "connections", "protocol": "Protocol", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index 81320c4b..b2427fa3 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -150,6 +150,10 @@ "hint": "按 Shift+A 或从左侧拖入一个区块即可开始", "dismiss": "关闭快速开始" }, + "spotlight": { + "recent": "最近使用", + "catalog": "所有区块" + }, "tooltip": { "connections": "个连接", "protocol": "协议", From a7feb56a7e5134fbd8f5305bbca193460fa016bd Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:16:22 +0200 Subject: [PATCH 29/39] ux(nav): keep breadcrumbs from flickering to Home during resolution (IA9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The URL-fallback breadcrumb branch was gated on !resolved.loading, so on every folder/project navigation the trail collapsed to just the Home icon until the resolver returned — flickering the user's sense of location. The URL segments already reflect where they are, so render a best-guess trail immediately and let the resolved labels replace it when they arrive. Tested: fallback crumbs now render from the URL even while loading=true. --- .../src/shared/components/__tests__/breadcrumbs.test.tsx | 6 ++++-- packages/ui/src/shared/components/breadcrumbs.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx b/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx index 6808250e..fa5f3c1e 100644 --- a/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx +++ b/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx @@ -225,13 +225,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/breadcrumbs.tsx b/packages/ui/src/shared/components/breadcrumbs.tsx index 508c3579..c2c8e72b 100644 --- a/packages/ui/src/shared/components/breadcrumbs.tsx +++ b/packages/ui/src/shared/components/breadcrumbs.tsx @@ -32,7 +32,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++) { From 6808b3e7e32fa371219396e307f79ae0cb8baf0d Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:19:13 +0200 Subject: [PATCH 30/39] ux(nav): wire the advertised /team route (IA8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The breadcrumbs advertise a /team top route and TeamPage is built + tested, but the route was never registered in app.tsx — so /team fell through to the catch-all DynamicContent and 404'd. Add the route (same AppBar + scroll wrapper as /settings, since TeamPage is a self-contained account page). Tested: the route list now includes /team; TeamPage wrapped in its ErrorBoundary. --- packages/web/src/app/__tests__/app.test.tsx | 15 ++++++++++----- packages/web/src/app/app.tsx | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/web/src/app/__tests__/app.test.tsx b/packages/web/src/app/__tests__/app.test.tsx index eb259bb8..95090e34 100644 --- a/packages/web/src/app/__tests__/app.test.tsx +++ b/packages/web/src/app/__tests__/app.test.tsx @@ -192,6 +192,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 +338,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 +346,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 +362,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..5eeb1f25 100644 --- a/packages/web/src/app/app.tsx +++ b/packages/web/src/app/app.tsx @@ -26,6 +26,7 @@ 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'; @@ -272,6 +273,22 @@ const App: React.FC = () => ( } /> + {/* 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. */} + +
    + +
    + +
    +
    + + } + /> Date: Thu, 18 Jun 2026 12:25:03 +0200 Subject: [PATCH 31/39] ux(canvas): render dagre routes from all directional organize paths (CCL1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toolbar's vertical/horizontal organize buttons set the rectangular edge style so the orthogonal dagre routes they compute actually render; the other directional organize entry points didn't — so they tidied positions but bezier edges still cut through blocks (the computed routePoints were silently discarded). Switch to the rectangular edge style after the directional (master-branch) organize at the remaining sites: the right-click context-menu, the import-time auto-organize, and the AI operation-executor. Scoped deliberately: - node-menu's per-container organize is excluded — applyEdgeRoutes is master-branch-only, so per-container computes no routePoints to render. - circular layouts are excluded — they're radial; orthogonal rectangular routes don't apply (matches the toolbar's circular button, which also doesn't switch). Tested: each directional path now also dispatches setEdgeStyle('rectangular'); circular does not. --- .../ai/services/__tests__/operation-executor.test.ts | 10 ++++++++++ .../ui/src/features/ai/services/operation-executor.ts | 3 +++ .../components/context/__tests__/canvas-menu.test.tsx | 11 +++++++++-- .../canvas/components/context/canvas-menu.tsx | 6 ++++++ .../hooks/__tests__/use-canvas-side-effects.test.tsx | 11 ++++++++--- .../features/canvas/hooks/use-canvas-side-effects.ts | 5 +++++ 6 files changed, 41 insertions(+), 5 deletions(-) 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/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/hooks/__tests__/use-canvas-side-effects.test.tsx b/packages/ui/src/features/canvas/hooks/__tests__/use-canvas-side-effects.test.tsx index 73b478a5..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)', () => { 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 2e96231e..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 @@ -67,6 +67,7 @@ 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'; @@ -155,6 +156,10 @@ export function useCanvasSideEffects(args: UseCanvasSideEffectsArgs): UseCanvasS 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); From babe2185bfb924c79f73d9bab8c1e72637c3ac97 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:28:39 +0200 Subject: [PATCH 32/39] ux(canvas): reflect cardinality limits in drag-time target highlighting (CCL7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cardinality rules (one Source/EnvVars per service; one source per log terminal) were only enforced at drop time — so a service that already has a repo wired still highlighted as a valid drop target during the drag, and the user only learned it was full when the wire snapped back on release. Fold the same pure conflict checks (findExistingSpecialConnection / findExistingLogSource) the drop handler uses into the connectionDragTargets memo, so an already-served target now dims as invalid-target during the drag. Schema-driven (reuses the existing rule helpers, no new state); card.edges added to the memo deps so it re-evaluates as wiring changes. Tested: a compatible target with a log-source or special-connection conflict is classified invalid-target; a free one stays valid; no card → check skipped. --- .../__tests__/use-connection-drawing.test.tsx | 48 +++++++++++++++++++ .../canvas/hooks/use-connection-drawing.ts | 19 ++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) 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-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( From a6b372481e0c344dc961fc0596b0199af139e66f Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:32:17 +0200 Subject: [PATCH 33/39] ux(nav): reach project subpages from the tree context menu (IA5) The onNavigateSubpage handler was wired through TreeItem but never bound to any clickable element, so the project subpages (Architecture / Table / Deployments / Activity) were only reachable from the toolbar tabs tucked under the canvas. Add an 'Open' submenu to the tree context menu (project nodes only) that invokes onNavigateSubpage for each subpage. New projectBrowser.contextOpen key (en+zh). Tested: a subpage item calls onNavigateSubpage with the right id; the submenu is absent on folder nodes. --- .../components/__tests__/tree-item.test.tsx | 17 +++++++++ .../project-browser/components/tree-item.tsx | 37 +++++++++++++++++++ packages/ui/src/i18n/en.json | 1 + packages/ui/src/i18n/zh.json | 1 + 4 files changed, 56 insertions(+) diff --git a/packages/ui/src/features/project-browser/components/__tests__/tree-item.test.tsx b/packages/ui/src/features/project-browser/components/__tests__/tree-item.test.tsx index ca737feb..f66e4d3d 100644 --- a/packages/ui/src/features/project-browser/components/__tests__/tree-item.test.tsx +++ b/packages/ui/src/features/project-browser/components/__tests__/tree-item.test.tsx @@ -377,6 +377,23 @@ describe('TreeItem — click handlers', () => { expect(onDelete).toHaveBeenCalledWith('p'); }); + // IA5 — the Open submenu lets the user jump to a project subpage from the tree. + it('calls onNavigateSubpage with the chosen subpage on a project node', () => { + const onNavigateSubpage = vi.fn(); + const node = makeNode({ id: 'p', type: 'project' }); + const tree = callTreeItem(makeProps({ node, onNavigateSubpage })); + const item = findByPredicate(tree, (el) => collectText(el) === 't:projectBrowser.subDeployments'); + expect(item).toBeDefined(); + (item?.props.onClick as () => void)?.(); + expect(onNavigateSubpage).toHaveBeenCalledWith(node, 'deployments'); + }); + + it('does not render the subpage Open submenu for a folder node', () => { + const tree = callTreeItem(makeProps({ node: makeNode({ id: 'f', type: 'folder' }) })); + const item = findByPredicate(tree, (el) => collectText(el) === 't:projectBrowser.subDeployments'); + expect(item).toBeUndefined(); + }); + it('rename menu item flips renameValue + isRenaming state', () => { const tree = callTreeItem(makeProps({ node: makeNode({ name: 'Original Name' }) })); const item = findByPredicate(tree, (el) => collectText(el) === 't:projectBrowser.contextRename'); diff --git a/packages/ui/src/features/project-browser/components/tree-item.tsx b/packages/ui/src/features/project-browser/components/tree-item.tsx index 753f17de..f7edd48d 100644 --- a/packages/ui/src/features/project-browser/components/tree-item.tsx +++ b/packages/ui/src/features/project-browser/components/tree-item.tsx @@ -31,6 +31,16 @@ import React, { useEffect, useRef, useState, memo } from 'react'; import { useTranslation } from '../../../i18n'; import type { ProjectNode } from '../types/project-node'; +// IA5 — project subpages, mirroring the toolbar's SUB_PAGES, so they're reachable +// from the tree context menu (the onNavigateSubpage handler existed but was never +// bound to a clickable affordance). +const PROJECT_SUBPAGES: { id: string; i18nKey: string }[] = [ + { id: 'canvas', i18nKey: 'projectBrowser.subCanvas' }, + { id: 'table', i18nKey: 'projectBrowser.subTable' }, + { id: 'deployments', i18nKey: 'projectBrowser.subDeployments' }, + { id: 'activity', i18nKey: 'projectBrowser.subActivity' }, +]; + export interface TreeItemProps { node: ProjectNode; level: number; @@ -274,6 +284,33 @@ export const TreeItem = memo( )} + {/* IA5 — open a specific project subpage (Canvas / Table / + Deployments / Activity) from the tree, not just the toolbar. */} + {!isFolder && ( + + + + + + {PROJECT_SUBPAGES.map((page) => ( + onNavigateSubpage(node, page.id)} + className="flex items-center gap-2 px-3 py-1.5 text-ice-md text-ice-text-2 rounded cursor-pointer outline-none hover:bg-ice-active" + > + {t(page.i18nKey)} + + ))} + + + + )} onDelete(node.id)} diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json index e2f5f489..b258327a 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -1445,6 +1445,7 @@ "contextRename": "Rename", "contextMoveTo": "Move to", "contextMoveRoot": "Root", + "contextOpen": "Open", "contextDelete": "Delete", "subCanvas": "Architecture", "subTable": "Table", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index b2427fa3..730b966f 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -1444,6 +1444,7 @@ "contextRename": "重命名", "contextMoveTo": "移动到", "contextMoveRoot": "根目录", + "contextOpen": "打开", "contextDelete": "删除", "subCanvas": "架构", "subTable": "表格", From 0d86ca4043f3252893b03f87e4a9b02977e802be Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:36:10 +0200 Subject: [PATCH 34/39] ux(canvas): keep the connection-rejection tooltip legible when zoomed out (CCL2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rejection tooltip lives inside the scaled SVG group, so it shrank with the canvas — going illegible exactly when a user dragging a long wire most needs to read why the drop was rejected. Counter-scale the tooltip by 1/zoom (mirroring the SvgConnectionPath inverse-zoom precedent, clamped at 1/0.1 so a tiny zoom can't blow it up): the foreignObject is sized at TOOLTIP_WIDTH/zoom and the body counter-scaled to fill it, keeping a constant on-screen size while staying centred on the rejection point. Chose the in-place inverse-zoom over moving to a screen-space overlay (lower risk, no coordinate-conversion). Tested at zoom 0.5 / clamp / default. (Constant on-screen size worth an eyeball with the headed app.) --- .../connection-rejection-overlay.test.tsx | 31 +++++++++ .../canvas-renderer/canvas-content.tsx | 2 +- .../connection-rejection-overlay.tsx | 67 +++++++++++-------- 3 files changed, 71 insertions(+), 29 deletions(-) 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/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'; From 3c960749eaa08439dde243c3d70e22b4ead02172 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:40:34 +0200 Subject: [PATCH 35/39] ux(canvas): add a socket legend to decode the connection language (CCL6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The canvas encodes a port's connection category in BOTH the socket's shape and colour (ROLE_SHAPE / CATEGORY_COLORS), but nothing decoded it — a first-timer had no way to learn that a diamond is a source repo or a square is a domain. Add a SocketLegend (circle=data/traffic, ring=config, diamond=source repo, square=domain/DNS) and mount it as a section in the controls-help modal. Colours sourced from CATEGORY_COLORS and shapes mirror ROLE_SHAPE so the key can't drift from the real socket rendering. New canvas.socketLegend.* keys (en+zh). Tested: all four shapes render with the right fill/stroke/rotation + labels. (Whether it lands the discoverability win is worth a headed eyeball.) --- .../__tests__/socket-legend.test.tsx | 103 ++++++++++++++++++ .../canvas/components/controls-help-modal.tsx | 6 + .../canvas/components/socket-legend.tsx | 54 +++++++++ packages/ui/src/i18n/en.json | 7 ++ packages/ui/src/i18n/zh.json | 7 ++ 5 files changed, 177 insertions(+) create mode 100644 packages/ui/src/features/canvas/components/__tests__/socket-legend.test.tsx create mode 100644 packages/ui/src/features/canvas/components/socket-legend.tsx 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/controls-help-modal.tsx b/packages/ui/src/features/canvas/components/controls-help-modal.tsx index c29a2f49..99c45412 100644 --- a/packages/ui/src/features/canvas/components/controls-help-modal.tsx +++ b/packages/ui/src/features/canvas/components/controls-help-modal.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useState } from 'react'; +import { SocketLegend } from './socket-legend'; import { useTranslation } from '../../../i18n'; export const ControlsHelpModal: React.FC = () => { @@ -144,6 +145,11 @@ export const ControlsHelpModal: React.FC = () => {
    ))} + + {/* CCL6 — decode the socket shape/colour language for the user. */} +
    + +
    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/i18n/en.json b/packages/ui/src/i18n/en.json index b258327a..cc30c26d 100644 --- a/packages/ui/src/i18n/en.json +++ b/packages/ui/src/i18n/en.json @@ -154,6 +154,13 @@ "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", "protocol": "Protocol", diff --git a/packages/ui/src/i18n/zh.json b/packages/ui/src/i18n/zh.json index 730b966f..9ed0b553 100644 --- a/packages/ui/src/i18n/zh.json +++ b/packages/ui/src/i18n/zh.json @@ -154,6 +154,13 @@ "recent": "最近使用", "catalog": "所有区块" }, + "socketLegend": { + "title": "连接端口", + "traffic": "数据与流量", + "config": "配置(环境变量、密钥)", + "repository": "源代码仓库", + "domain": "域名 / DNS" + }, "tooltip": { "connections": "个连接", "protocol": "协议", From f12fda24b16140aeccc91ac5484f176d386a7c37 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 12:50:03 +0200 Subject: [PATCH 36/39] ux(nav): make the active environment a shareable deep-link (IA6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The active env lived only in Redux — never the URL — so a deploy/env view couldn't be bookmarked or shared, and a hard reload always reset to production. - Write: both env switchers (the EnvironmentTabBar tabs and the toolbar env dropdown) now set ?env= (replace, so switching doesn't spam history). - Read: the first-load default honours a ?env= deep-link when it points at a real env, otherwise falls back to production (the prior behaviour). Tested: switching writes ?env; a ?env deep-link wins over the production default on load; a stale/missing ?env falls back to production. (Follow-up: deployments/activity could read ?env to warn on a Redux↔URL mismatch. End-to-end reload-restore worth a headed eyeball.) --- .../__tests__/environment-tab-bar.test.tsx | 41 +++++++++++++++++++ .../components/environment-tab-bar.tsx | 27 +++++++++--- .../__tests__/project-toolbar.test.tsx | 7 ++++ .../src/shared/components/project-toolbar.tsx | 14 ++++++- 4 files changed, 82 insertions(+), 7 deletions(-) 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 aef96396..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', @@ -728,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'; 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 438543db..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,6 +35,9 @@ 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); @@ -98,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 @@ -135,17 +149,20 @@ 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(); 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/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 ─────────────────────────────────────────────────────────── From d083a278e879e8043c008c78607c32b578b5c0d6 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 13:10:22 +0200 Subject: [PATCH 37/39] =?UTF-8?q?perf(nav):=20resolve=20the=20URL=20path?= =?UTF-8?q?=20once=20at=20the=20shell,=20not=204=C3=97=20per=20page=20(IA7?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DynamicContent, Breadcrumbs, ProjectBrowser and ResourcePalette each called useResolvePath independently, so every project-page navigation fired the same folder/project resolution (a POST per path segment) ~4× concurrently. Lift the resolve to a single shell-level ResolvePathProvider (mounted in app.tsx, inside BrowserRouter); the four consumers now read the shared result via useResolvePathContext. The provider preserves Breadcrumbs' top-route optimisation (resolve [] for /settings and /team), with TOP_ROUTES moved into the context module as the single source so the gate and the breadcrumb label can't drift. Tested: provider resolves full segments on project paths, [] on top routes, and not on deeper paths that merely start with a top-route slug; the hook throws without a provider; all four consumers' tests rewired to the shared hook. (End-to-end request de-dup worth confirming in the headed app's network tab.) --- .../__tests__/resource-palette.test.tsx | 4 +- .../palette/components/resource-palette.tsx | 7 +- .../__tests__/project-browser.test.tsx | 4 +- .../components/project-browser.tsx | 9 +- .../components/__tests__/breadcrumbs.test.tsx | 5 +- .../ui/src/shared/components/breadcrumbs.tsx | 11 +- .../use-resolve-path-context.test.tsx | 87 +++++++++++++++ .../shared/hooks/use-resolve-path-context.tsx | 46 ++++++++ .../ui/src/shared/hooks/use-resolve-path.ts | 2 +- packages/web/src/app/__tests__/app.test.tsx | 6 +- packages/web/src/app/app.tsx | 100 ++++++++++-------- 11 files changed, 209 insertions(+), 72 deletions(-) create mode 100644 packages/ui/src/shared/hooks/__tests__/use-resolve-path-context.test.tsx create mode 100644 packages/ui/src/shared/hooks/use-resolve-path-context.tsx diff --git a/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx b/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx index 2e795496..98e35bd6 100644 --- a/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx +++ b/packages/ui/src/features/palette/__tests__/resource-palette.test.tsx @@ -120,8 +120,8 @@ vi.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: mocks.pathname }), })); -vi.mock('../../../shared/hooks/use-resolve-path', () => ({ - useResolvePath: () => mocks.resolved, +vi.mock('../../../shared/hooks/use-resolve-path-context', () => ({ + useResolvePathContext: () => mocks.resolved, })); vi.mock('../../../shared/api/axios-instance', () => ({ diff --git a/packages/ui/src/features/palette/components/resource-palette.tsx b/packages/ui/src/features/palette/components/resource-palette.tsx index 502320af..18268c4b 100644 --- a/packages/ui/src/features/palette/components/resource-palette.tsx +++ b/packages/ui/src/features/palette/components/resource-palette.tsx @@ -27,7 +27,7 @@ import { useTranslation } from '../../../i18n'; import axiosInstance from '../../../shared/api/axios-instance'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../../shared/components/ui/resizable'; import { TooltipProvider } from '../../../shared/components/ui/tooltip'; -import { useResolvePath } from '../../../shared/hooks/use-resolve-path'; +import { useResolvePathContext } from '../../../shared/hooks/use-resolve-path-context'; import { ProjectBrowser } from '../../project-browser'; import { TemplateCategoriesPanel } from '../../templates/components/template-categories-panel'; import { getCategoryMap, CATEGORY_ORDER } from '../data/categories'; @@ -54,9 +54,8 @@ export const ResourcePalette: React.FC = ({ // Localized concept blocks — same locale-reactivity contract. const components = useMemo(() => getComponents(t), [t]); - // Get the current project's locked provider (if set) - const segments = pathname.split('/').filter(Boolean); - const resolved = useResolvePath(segments); + // Get the current project's locked provider (if set) — IA7: shared resolution. + const resolved = useResolvePathContext(); const [projectProvider, setProjectProvider] = useState(null); useEffect(() => { diff --git a/packages/ui/src/features/project-browser/components/__tests__/project-browser.test.tsx b/packages/ui/src/features/project-browser/components/__tests__/project-browser.test.tsx index 1043f51e..09c0774a 100644 --- a/packages/ui/src/features/project-browser/components/__tests__/project-browser.test.tsx +++ b/packages/ui/src/features/project-browser/components/__tests__/project-browser.test.tsx @@ -86,8 +86,8 @@ vi.mock('../../../../i18n', () => ({ useTranslation: () => ({ t: (k: string) => k }), })); -vi.mock('../../../../shared/hooks/use-resolve-path', () => ({ - useResolvePath: () => mocks.resolved, +vi.mock('../../../../shared/hooks/use-resolve-path-context', () => ({ + useResolvePathContext: () => mocks.resolved, })); vi.mock('../../hooks/use-project-browser-data', () => ({ diff --git a/packages/ui/src/features/project-browser/components/project-browser.tsx b/packages/ui/src/features/project-browser/components/project-browser.tsx index 88340928..bde89110 100644 --- a/packages/ui/src/features/project-browser/components/project-browser.tsx +++ b/packages/ui/src/features/project-browser/components/project-browser.tsx @@ -12,11 +12,11 @@ import { Folder, FolderOpen, Loader2, FolderPlus, FilePlus } from 'lucide-react'; import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { TreeItem } from './tree-item'; import { useTranslation } from '../../../i18n'; import { PanelHeader, PanelHeaderAction } from '../../../shared/components/ui/panel-header'; -import { useResolvePath } from '../../../shared/hooks/use-resolve-path'; +import { useResolvePathContext } from '../../../shared/hooks/use-resolve-path-context'; import { createEmptyProjectAndNavigate } from '../../wizard/utils/create-empty-project'; import { useProjectBrowserActions } from '../hooks/use-project-browser-actions'; import { useProjectBrowserData } from '../hooks/use-project-browser-data'; @@ -25,11 +25,10 @@ import type { RootState } from '../../../store'; export function ProjectBrowser() { const { t } = useTranslation(); const navigate = useNavigate(); - const { pathname } = useLocation(); const selectedOrg = useSelector((s: RootState) => s.account?.selectedOrg); - // Resolve which node is active from the current URL - const resolved = useResolvePath(pathname.split('/').filter(Boolean)); + // Resolve which node is active from the current URL — IA7: shared resolution. + const resolved = useResolvePathContext(); const activeNodeId = resolved.id; const activeSubpage = resolved.type === 'project' ? resolved.subpage || 'canvas' : null; diff --git a/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx b/packages/ui/src/shared/components/__tests__/breadcrumbs.test.tsx index fa5f3c1e..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'; diff --git a/packages/ui/src/shared/components/breadcrumbs.tsx b/packages/ui/src/shared/components/breadcrumbs.tsx index c2c8e72b..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 }[] = []; 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-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/web/src/app/__tests__/app.test.tsx b/packages/web/src/app/__tests__/app.test.tsx index 95090e34..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', () => ({ diff --git a/packages/web/src/app/app.tsx b/packages/web/src/app/app.tsx index 5eeb1f25..4308ada8 100644 --- a/packages/web/src/app/app.tsx +++ b/packages/web/src/app/app.tsx @@ -32,7 +32,7 @@ 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'; @@ -93,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 @@ -245,59 +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) + + } + /> + + + + } + /> + {/* 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. */} - -
    - -
    - + +
    + +
    + +
    -
    - - } - /> - - - - } - /> - + + } + /> + + + + } + /> + + From d1c58a6f87d7d97914cc63c1bf3ab46ebc30a034 Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 13:14:13 +0200 Subject: [PATCH 38/39] ux(properties): generalize design-requirements via a validation fallback (PE3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DesignRequirements hard-coded rules for only Database.PostgreSQL and Network.PrivateNetwork, so for every other block it rendered nothing — a user wiring a compute/storage/queue block got the same blank panel a fully-configured block gets. A truly general 'missing expected connection' rule would need a per-block connection-expectation schema that doesn't exist (PortDef has no required flag), so generalizing that is a product/schema decision, not a UX fix. Instead, fall back to surfacing the validator's own node issues when no bespoke rule applies: every INVALID block now gets a 'what's missing' summary at the top of the panel (visible on any tab, unlike the config-tab-only banner). Schema-respecting — it reads the validator output, no new per-block hardcoding; bespoke rules still win. Tested: fallback maps node issues (error/warning) when no rule applies; is suppressed when a bespoke rule fires; emits nothing for a valid non-bespoke block. --- .../__tests__/design-requirements.test.tsx | 43 +++++++++++++++++++ .../components/design-requirements.tsx | 38 +++++++++++++++- .../sections/node-properties-section.tsx | 7 ++- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/features/properties/components/__tests__/design-requirements.test.tsx b/packages/ui/src/features/properties/components/__tests__/design-requirements.test.tsx index 6a2acc5e..2840a986 100644 --- a/packages/ui/src/features/properties/components/__tests__/design-requirements.test.tsx +++ b/packages/ui/src/features/properties/components/__tests__/design-requirements.test.tsx @@ -182,6 +182,49 @@ describe('collectDesignRequirements — non-iceType nodes', () => { }); }); +// ─── PE3 — generalized validation-issue fallback ────────────────────────────── + +describe('collectDesignRequirements — validation fallback (PE3)', () => { + const node = makeNode({ id: 'n', data: { iceType: 'Compute.WebServer' } }); + + it('surfaces the node validation issues when no bespoke rule applies', () => { + const out = collectDesignRequirements( + node, + [node], + [], + [ + { id: 'i1', severity: 'error', message: '"Name" is required', suggestion: 'Set a name' }, + { id: 'i2', severity: 'warning', message: 'No region set' }, + ], + ); + expect(out).toHaveLength(2); + expect(out[0]).toMatchObject({ + id: 'validation-i1', + level: 'error', + title: '"Name" is required', + hint: 'Set a name', + }); + expect(out[1]).toMatchObject({ id: 'validation-i2', level: 'warning', title: 'No region set' }); + }); + + it('does NOT apply the fallback when a bespoke rule already produced guidance', () => { + const pg = makeNode({ id: 'pg', data: { iceType: 'Database.PostgreSQL' } }); + // Postgres with no service connection → bespoke rule fires; fallback suppressed. + const out = collectDesignRequirements(pg, [pg], [], [{ id: 'x', severity: 'error', message: 'should not appear' }]); + expect(out.some((r) => r.id === 'validation-x')).toBe(false); + expect(out.some((r) => r.id === 'pg-no-service')).toBe(true); + }); + + it('emits nothing for a valid non-bespoke block (no issues, no rule)', () => { + expect(collectDesignRequirements(node, [node], [], [])).toEqual([]); + }); + + it('maps a non-warning severity to the error level', () => { + const out = collectDesignRequirements(node, [node], [], [{ id: 'i', severity: 'error', message: 'boom' }]); + expect(out[0].level).toBe('error'); + }); +}); + // ─── Component coverage ─────────────────────────────────────────────────── interface ReactElementLike { diff --git a/packages/ui/src/features/properties/components/design-requirements.tsx b/packages/ui/src/features/properties/components/design-requirements.tsx index bd4fa1ca..481c4e0c 100644 --- a/packages/ui/src/features/properties/components/design-requirements.tsx +++ b/packages/ui/src/features/properties/components/design-requirements.tsx @@ -37,14 +37,28 @@ export interface DesignRequirement { // ─── Rule set (Postgres + PrivateNetwork) ─────────────────────────────────── +/** Minimal validator-issue shape the generalized fallback consumes (PE3). */ +export interface DesignFallbackIssue { + id: string; + severity: string; + message: string; + suggestion?: string; +} + /** * Compute pure list of requirement findings for a node. Pure function — * easy to unit-test and to extend per iceType. + * + * PE3 — bespoke rules exist for only a couple of block types; for every other + * block, fall back to surfacing the validator's own node issues so the + * top-of-panel "what's missing" guidance is never empty for an INVALID block + * (the bespoke rules still win when they apply). */ export function collectDesignRequirements( node: CardNode, allNodes: readonly CardNode[], edges: readonly CardEdge[], + nodeIssues: readonly DesignFallbackIssue[] = [], ): DesignRequirement[] { const iceType = (node.data?.iceType as string) || ''; const out: DesignRequirement[] = []; @@ -111,6 +125,20 @@ export function collectDesignRequirements( } } + // PE3 — generalized fallback: when no bespoke rule produced guidance, surface + // the validator's node issues so an invalid block still gets a "what's + // missing" summary at the top of the panel (regardless of which tab is open). + if (out.length === 0 && nodeIssues.length > 0) { + for (const issue of nodeIssues) { + out.push({ + id: `validation-${issue.id}`, + level: issue.severity === 'warning' ? 'warning' : 'error', + title: issue.message, + hint: issue.suggestion, + }); + } + } + return out; } @@ -146,8 +174,14 @@ export const DesignRequirements: React.FC<{ node: CardNode; allNodes: readonly CardNode[]; edges: readonly CardEdge[]; -}> = ({ node, allNodes, edges }) => { - const requirements = React.useMemo(() => collectDesignRequirements(node, allNodes, edges), [node, allNodes, edges]); + /** PE3 — this node's non-info validator issues, used as a fallback when no + * bespoke design rule applies to the block. */ + nodeIssues?: readonly DesignFallbackIssue[]; +}> = ({ node, allNodes, edges, nodeIssues }) => { + const requirements = React.useMemo( + () => collectDesignRequirements(node, allNodes, edges, nodeIssues ?? []), + [node, allNodes, edges, nodeIssues], + ); // Auto-collapse when all-green so power users aren't bothered. Errors // expand by default (they block deploy); warnings + info collapse. 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 43b8d527..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 @@ -265,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 From ed19c2ad86eed310762ef5f5f1aa65422035a6ca Mon Sep 17 00:00:00 2001 From: Julia Kafarska Date: Thu, 18 Jun 2026 19:41:51 +0200 Subject: [PATCH 39/39] R1 --- .github/workflows/ci.yml | 6 ++++++ docs/architecture/desktop.md | 3 +++ package.json | 3 ++- .../src/deploy/providers/__tests__/live/_live-helpers.ts | 7 ++++--- .../src/deploy/providers/__tests__/live/_live-types.ts | 2 +- .../core/src/deploy/providers/alibaba/alibaba-deployer.ts | 2 ++ packages/core/src/deploy/providers/alibaba/sdk-loader.ts | 4 +++- packages/core/src/deploy/providers/aws/types.ts | 5 +++++ .../core/src/deploy/providers/azure/handlers/subnet.ts | 2 +- .../deploy/providers/digitalocean/digitalocean-deployer.ts | 2 ++ .../deploy/providers/ibm/handlers/databases-instance.ts | 2 +- packages/core/src/deploy/providers/ibm/ibm-deployer.ts | 2 ++ packages/core/src/deploy/providers/ibm/sdk-loader.ts | 4 +++- .../core/src/deploy/providers/kubernetes/handlers/_crd.ts | 2 +- packages/core/src/deploy/providers/kubernetes/namespace.ts | 2 +- packages/core/src/deploy/providers/oci/oci-deployer.ts | 2 ++ packages/core/src/deploy/providers/oci/sdk-loader.ts | 4 +++- packages/core/tsconfig.tsbuildinfo | 2 +- 18 files changed, 43 insertions(+), 13 deletions(-) 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