Skip to content

chore(deps)(deps): bump zod from 3.25.76 to 4.4.3#15

Closed
dependabot[bot] wants to merge 35 commits into
mainfrom
dependabot/npm_and_yarn/zod-4.4.3
Closed

chore(deps)(deps): bump zod from 3.25.76 to 4.4.3#15
dependabot[bot] wants to merge 35 commits into
mainfrom
dependabot/npm_and_yarn/zod-4.4.3

Conversation

@dependabot
Copy link
Copy Markdown

@dependabot dependabot Bot commented on behalf of github May 19, 2026

Bumps zod from 3.25.76 to 4.4.3.

Release notes

Sourced from zod's releases.

v4.4.3

Commits:

  • 4c2fa95ce3f3390fbc522324e406b4e9e89b88f9 docs: use Zernio primary wordmark for gold sponsor logo
  • 2aeec83eb135e3a83756e973ef44845fc5a455d2 docs: prune lapsed gold sponsors and rebalance logo sizing
  • 7391be88ac1ee5cd02057f5ccc012a1f5df4efd0 docs: prune lapsed silver/bronze sponsors and add active ones
  • 2c703322a21b4e2b12f33f49ea8430c451a68b4f docs: normalize bronze sponsor logos to github avatar pattern
  • 9195250cab0e7950efe39c3926d6c203b4b0a170 docs: remove Mintlify from bronze sponsors (churned)
  • b8dffe9e62f17e6571e6249d05cc5102b54d94e4 docs: remove Numeric and Speakeasy (2+ missed monthly cycles)
  • 1cab69383fcdeae2a366d5e2a2fc4d8fc765d168 fix(v4): restore catch handling for absent object keys (#5937) (#5939)
  • c2be4f819064eed62c7c350a2d399b5faecd15f8 fix(v4): generalize optin/fallback to transform; restore preprocess on absent keys (#5941)
  • f3c9ec03ba7a28ae72d25cc295f38674bee0f559 4.4.3
  • 1fb56a5c18c27102dbc92260a4007c7732a0ccca docs: document release procedure in AGENTS.md

v4.4.2

Commits:

  • 0c62df0ea19fd05abdf90473e9eef7eea530fab2 Clean up docs navigation and stale labels (#5901)
  • 20cc794895cc8604fe0c87d83a5d1c3f89fad0ac chore: add security policy and refresh tooling deps
  • 6fbe07b0177efdd1bf1c0b05160e70d7a0702337 fix(docs): heading anchor links now include the hash so it doesnt scoll all the way up, follows navbar logic (#5791)
  • 4bbed1b1c73eca4ce9e59b1189ed236aa6c8b5bd Tighten discriminated union option typing
  • bbac3e567e7fccfaaf7cdc97f1ce30c295e2c908 Update PR guidance for agents
  • cf0dc942a32805c292fff59ade20a7ace980735a Merge remote-tracking branch 'origin/main' into fix-discriminated-union-key-constraint
  • 292c894a5fd2aa42e527900b83d8d7a3009a709c docs: add Zernio gold sponsor
  • 1fc9f311c28dcf80d0bb5a36b177086cbc3d8eca docs: document codec inversion
  • 1373c85da9aeff704a9762d27bc58699618aefb7 docs: remove AI disclosure guidance
  • e20d02b473c08e3a4e557bc610b1b5fac079b649 chore: ignore triage notes
  • e58ea4d91b1dfe8194b73508203213cbc7e9c936 docs: test Zod Mini tab code heights
  • 905761a5d127e8d5dd2ebb3bc88c75cb0b8149ff docs: document preprocess input type narrowing
  • bf64bac850d4dee2b7dde7e64909d5d796d32043 chore: tighten test guidance in AGENTS.md
  • 8ec4e73f4c4693b6361ad591be40fb41eb8a9f95 chore: update play.ts scratch
  • 02c2baf7d0d615872fa4528a8020603b71211702 Make z.preprocess defer optionality to inner schema (#5929)
  • 88015df8e25c44fb5385eb3ef28935119cd5edea fix(docs): drop deprecated baseUrl from tsconfig
  • c59d4474e3b4cad1b323462186cf607178ce8267 4.4.2

v4.4.1

Commits:

  • 481f7be4238c83ed58183f921b2646f340a91c6a ci: gate release publishing on full test workflow
  • 95ccab423aec720b2523c3a64cdc7e3204537cc7 test(v3): restore optional undefined expectations
  • cede2c63739a5823d6aa5093d291e9a111da943d fix(v4): reject tuple holes before required defaults (#5900)
  • edd0bf0f5ada4a8dc581c259407d7bbad0a71ea7 release: 4.4.1
  • 180d83d1dbe6a59260710cc8637a3dea2281ee56 docs: remove Jazz featured sponsor

v4.4.0

4.4.0

This is a minor release with a wide set of correctness and soundness fixes. Some fixes intentionally make Zod stricter, so code that depended on previously accepted invalid or ambiguous inputs may need small updates.

Potentially breaking bug fixes

... (truncated)

Commits
  • 1fb56a5 docs: document release procedure in AGENTS.md
  • f3c9ec0 4.4.3
  • c2be4f8 fix(v4): generalize optin/fallback to transform; restore preprocess on absent...
  • 1cab693 fix(v4): restore catch handling for absent object keys (#5937) (#5939)
  • b8dffe9 docs: remove Numeric and Speakeasy (2+ missed monthly cycles)
  • 9195250 docs: remove Mintlify from bronze sponsors (churned)
  • 2c70332 docs: normalize bronze sponsor logos to github avatar pattern
  • 7391be8 docs: prune lapsed silver/bronze sponsors and add active ones
  • 2aeec83 docs: prune lapsed gold sponsors and rebalance logo sizing
  • 4c2fa95 docs: use Zernio primary wordmark for gold sponsor logo
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for zod since your current version.


Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

…le shared package

Security (15/15):
- Remove default JWT/encryption key fallbacks, throw on missing env vars
- Fix Stripe/GitHub webhook HMAC with raw body parsing
- Command injection prevention (shell:false + allowlist)
- OAuth redirect via URL fragment, Socket.IO JWT auth
- Google token audience validation, IDOR fix, refresh token rotation
- Replace crypto-js with Node.js native AES-256-GCM
- Block password login for OAuth-only accounts

Backend Services (16/16):
- Fix billing service imports and passport-jwt → requireAuth migration
- Refresh token rotation with reuse detection, type claim validation
- Add requireProjectAccess to deploy status/history routes
- Graceful shutdown, ESM import fix, rate limiter keyed by userId
- Consolidate getProfile and requireProjectAccess queries
- Build cache via hardlinks/rsync, credential cleanup in finally blocks
- Scoped Helmet CSP, CORS origins parsed once, AiAuditLog user/org fields

Frontend (18/18):
- ErrorBoundary wrapping app root and canvas
- Fix persistMessages race condition, stale closures, serial fetches
- Cap deploy history, deduplicate fetchProfile, fix onboarding error handling
- Wire invite code to Redux, fix GCP region strings in templates
- Memoize AppBar, fix logout token, add signup accessibility
- Remove console.logs, check JWT expiry in ProtectedRoute
- Remove unused deps (@xyflow/react, react-hook-form, zod)

Core Engine (12/18):
- Implement AWS (27 types) and Azure (26 types) deployer type maps
- Design-only provider warnings for Alibaba/DigitalOcean/Kubernetes
- Fix Messaging.Topic mapping (dataflow → pubsub)
- Complete load balancer (full resource chain), API gateway, cloud functions source
- Fix Vertex AI type detection, delete 4 duplicate deployer files
- Implement desktop deploy:destroy and deploy:getStatus handlers

Database (8/8):
- Add indexes: CanvasDeployment, CanvasProject, DeployJob, DeploymentRule, WebhookDelivery
- Add User relations to CanvasDeployment and AiConversation (cascade delete)
- WebhookDelivery TTL cleanup cron (7-day retention)

Infrastructure (15/16):
- Fix CI workflow (correct paths, Node 22), create gateway Dockerfile
- Add .env.example, .nvmrc, SECURITY.md, ci.yml (typecheck + tests + build + audit)
- Fix docker-compose (env_file, restart policy, remove hardcoded secrets)
- Fix gateway tsconfig (NodeNext), add packageManager field
- Delete unused root playwright.config.ts

Developer Experience (10/10):
- Add typecheck script to all 20 packages, align TypeScript to ^5.6.3
- Add root build/test:unit/test:build scripts, root tsconfig with project references
- Add prisma postinstall, replace jest with vitest in core
- Move GCP SDKs to deps, fix UI React peerDeps

Refactoring (8/8):
- Migrate ALL shared UI to @ice/ui (features, store, shared, hooks, utils, config, assets)
- Web reduced to thin shell (app routing, pages, styles)
- Vite @ alias points to ui/src, Tailwind scans ui package
- Delete 200+ duplicate files, fix 30+ broken import paths
- Export all feature modules from ui barrel, add missing barrel exports

Package rename:
- @lightcloud/web → @ice/web
- @ice-saas/* → @ice/*
- @ice-engine/core → @ice/core

Testing:
- 23 unit tests (vitest): crypto, auth, build validation, card translator
- 32 e2e tests (Playwright): security, backend services, frontend
- Vite build check in CI catches import resolution errors
… handlers

Architecture change: the desktop app now runs the same Express gateway and
services as the web app. The renderer loads from an embedded HTTP server
instead of using custom IPC handlers. Zero code duplication between web
and desktop.

Desktop (Electron):
- Main process starts embedded gateway on port 15173 with SQLite + in-memory queue
- Renderer loads web app from localhost (same HTTP adapter, same UI)
- Preload reduced from 221 lines to 22 lines (just menu actions + fullscreen state)
- Deleted ipc-handlers.ts (1814 lines), deploy-handler.ts (938 lines),
github-service.ts (282 lines), old renderer app
- Traffic lights: proper macOS positioning with reactive fullscreen padding
- AppBar shared between web and desktop (extracted to @ice/ui)
- Window dragging via -webkit-app-region on header

Database:
- Created schema.sqlite.prisma for desktop (SQLite, no PostgreSQL needed)
- Zero schema changes needed — Prisma handles both providers identically

Deploy service:
- InMemoryQueue fallback when Redis unavailable (ICE_DESKTOP=true)
- Queue service dynamically imports BullMQ/ioredis only when Redis is configured

Gateway desktop mode (ICE_DESKTOP=true):
- Serves web app static files from packages/web/dist
- Auth bypass with auto-seeded local user (no login screen)
- setDesktopUser() sets userId/orgId on requireAuth middleware

Build & scripts:
- pnpm dev:desktop — starts gateway + web Vite + Electron in parallel
- pnpm dist:desktop:mac/win/linux — electron-builder packaging
- electron-builder.yml for macOS DMG, Windows NSIS, Linux AppImage
- Vite proxy target configurable via PORT env var
- electron postinstall enabled in pnpm.onlyBuiltDependencies

Fixes:
- Fixed 23 @/ alias imports in ui package (converted to relative paths)
- Fixed 2 self-referencing @ice/ui imports in ui package
- Fixed preload CJS output format (Electron sandbox requires CommonJS)
- Added ./src/* wildcard to @ice/ui package exports
  - Canvas: group node nesting, reparenting, context menu enhancements,
    container expansion, z-index depth ordering, LOD rendering
  - Properties panel: drift detection indicators, group color picker
  - Deploy: drift detection service, rollback validation, service
  deployments
  - New activity feed page with infra/service/AI event timeline
  - Environment tab bar: activity navigation tab
  - Auth: fix refresh token unique constraint (add jti nonce)
  - Gateway: raise rate limit to 1000/min in dev/test environments
  - E2E tests: fix onboarding redirect, canvas fixture creates project,
    reuse global-setup token, remove demo data assumptions, remove
    view-levels tests, fix smoke login wait, fix multi-tab card count
  - Lint fixes across all modified files
julia-kafarska and others added 5 commits April 7, 2026 23:34
* rf-esp-1: extract SQLite types + converters from embedded-schema-provider

Pulls the inline SQLite registry interfaces and the two private
`convert_*` methods of `EmbeddedSchemaProvider` into:
  - schema/embedded/sqlite-types.ts (interfaces + to_sqlite_query helper)
  - schema/embedded/converters.ts (convert_resource_to_schema, convert_property)

Class methods now delegate to the standalone functions (registry passed
as first arg). Conversion is byte-identical: nulls -> undefined for
docs_url and validation sub-fields, null description -> empty string,
nested_properties recurses. 14 tests pin the behaviour.

embedded-schema-provider.ts: 591 -> 402 LOC.

* rf-esp-2: extract query operations from embedded-schema-provider

Pulls 11 query methods (get_schema, has_schema, query, get_categories,
get_providers, get_implementation, get_native_type, get_property_schema,
get_required_properties, get_computed_properties, get_stats) into
schema/embedded/queries.ts as standalone functions taking the registry
+ a QueryCache holder for the two cached methods.

Class fields `cached_stats` and `cached_providers` are unified into a
single `query_cache: QueryCache` slot (initialised by `make_query_cache`).
Behaviour preserved verbatim: null registry returns InternalError /
[]/undefined, lazy cache for providers + stats, missing terraform/pulumi
sources default to 0 in stats. 24 new tests pin the helpers.

embedded-schema-provider.ts: 402 -> 335 LOC.

* rf-esp-3: extract graph-query operations from embedded-schema-provider

Pulls get_dependencies, get_dependents, and get_equivalents into
schema/embedded/graph-queries.ts. Each helper takes the registry as its
first arg; behaviour preserved: null registry -> InternalError, max_depth
forwarded to registry, results mapped through convert_resource_to_schema.

The default `max_depth = 10` stays on the orchestrator class so the
public API still exposes the default. 7 tests pin the new helpers.

embedded-schema-provider.ts: 335 -> 324 LOC.

* rf-esp-4: extract events + initialization from embedded-schema-provider

Pulls the on/off/emit_event trio into schema/embedded/events.ts (operating
on a passed-in EventListenerMap) and the dynamic schemas/db import +
project-vs-bundled DB resolution into schema/embedded/initialization.ts.

The dynamic-import path moves from `'../schemas/db'` to
`'../../schemas/db'` since the new file is one directory deeper. The
TS2834 baseline noise simply moves with the import — total count
unchanged at 29 (verified post-extraction).

initialize_registry returns null gracefully when the schemas package
or its `get_schema_registry` export is absent. resolve_db_path tests
chdir into a tmpdir rather than mocking fs (`fs.existsSync` is
non-configurable under Vitest ESM). 14 new tests for the helpers.

embedded-schema-provider.ts: 324 -> 284 LOC. File 1 series complete.

* rf-rval-1: extract type-checker and validation types from resource-validator

Moves the public Validation* types into resource-validator-types.ts so
helper modules can import them without dragging in the orchestrator
class. The shim file `resource-validator.ts` re-exports every type from
the new sibling file -> public API unchanged.

Pulls `validate_type` and `get_type_name` (private methods of
ResourceValidator) into validation/type-checker.ts as standalone pure
functions. The 'object'/'map' shared check, NaN-as-number-mismatch, and
'any' fall-through behaviour are preserved verbatim. 26 tests cover
every branch including unknown-type fall-through.

resource-validator.ts: 543 -> 374 LOC.

* rf-rval-2: extract constraint validation from resource-validator

Pulls `validate_constraints` (private method on ResourceValidator) into
schema/validation/constraints.ts with five named helpers:
  check_enum, check_pattern, check_numeric_range, check_string_length,
  check_array_length, plus a `validate_constraints` orchestrator.

Behaviour preserved verbatim: invalid regex strings silently swallowed,
inclusive boundaries on min/max and min_length/max_length, canonical
issue order (enum, pattern, range, length). Each helper independently
testable. 25 new tests pin every branch including issue ordering.

resource-validator.ts: 374 -> 258 LOC.

* rf-rval-3: extract property-validator and error-conversion

Pulls the recursive `validate_property` walker into
schema/validation/property-validator.ts and the
`to_validation_error` mapper into schema/validation/error-conversion.ts.

The class methods on ResourceValidator now delegate, including the
public `to_validation_error`, `is_valid`, and `validate_property_value`.
The recursive walker is byte-identical (required-missing, type-mismatch,
constraint, and nested object/array branches preserved); 17 new tests
pin its behaviour, plus 5 for the error converter.

resource-validator.ts: 258 -> 157 LOC. File 2 series complete.

* rf-cload-1: extract example-file content + paths from customization-loader

Pulls the four `_example.<ext>.disabled` content blocks (provider JSON,
override YAML, custom resource YAML, relationships YAML) out of the
inline `create_example_files` private method into named constants in
schema/customization/example-files.ts. The CustomizationPaths type and
subdir constants move to schema/customization/paths.ts.

Customization-loader.ts re-exports CustomizationPaths from the new
location so external consumers' import paths are unchanged. Behaviour
preserved: each file only created when missing, content byte-identical.
9 new tests pin both the constants and the directory write-out.

customization-loader.ts: 521 -> 410 LOC.

* rf-cload-2: extract per-file validators from customization-loader

Pulls the four `validate_*_file` private methods (provider JSON, override
YAML, custom resource YAML, relationships YAML) into
schema/customization/file-validators.ts as standalone async functions,
each returning `FileValidationResult`. The CustomizationError and
ValidationWarning types move to that file too — the orchestrator
re-exports them so external consumers (schema/index.ts) keep their
import paths.

Behaviour preserved verbatim: "Invalid JSON: ..." / "Invalid YAML: ..."
prefixes on parse failures, 1-indexed relationship error messages,
provider resources without properties emit warnings (not errors). 16
new tests drive each helper end-to-end via tmp file writes.

customization-loader.ts: 410 -> 261 LOC.

* rf-cload-3: extract directory scanner + base-db resolver

Pulls the `scan_directory` private method into
schema/customization/scanner.ts (along with the CustomizationFile type
that the scanner produces) and the `get_base_db_path` standalone
function into schema/customization/base-db.ts. The orchestrator
re-exports CustomizationFile + delegates `get_base_db_path` so external
imports are unaffected.

Behaviour preserved verbatim including the pre-existing buggy
`require.resolve('@ice-engine/schemas/data/ice-schemas.db')` eagerness
(noted in the test file's docstring; not in scope to fix here). 7 new
tests for the scanner driving real fs reads via tmp dirs.

customization-loader.ts: 261 -> 204 LOC. File 3 series complete.

* state: add three learnings from P3 cohort 3 (rf-esp / rf-rval / rf-cload)

- ts2834-baseline-error-moves-with-the-import: extracting a TS2834
  baseline import into a deeper subdirectory relocates the error,
  doesn't introduce one.
- fs-existssync-is-non-configurable-under-vitest-esm: vi.spyOn fails
  on node:fs namespace imports; use chdir into tmpdir instead.
- one-source-of-truth-for-types-in-shim-refactors: when decomposing
  a class file, public types live in a sibling <name>-types.ts to
  avoid cycles between the shim and helper modules.

* rf-pimp-1: extract state parsing helpers from pulumi state-importer

Pulls six pure helpers out of state-importer.ts into a new
parsing.ts module:

  - get_deployment           — read PulumiDeployment from either state shape
  - get_stack_info           — derive {stack,project} from checkpoint or URN
  - extract_name_from_urn    — last-segment fallback when parse_urn fails
  - is_secret_value          — sentinel-UUID check for Pulumi secret wrappers
  - unwrap_secret            — ciphertext > plaintext > value resolver
  - create_empty_metadata    — unknown-sentinel PulumiImportMetadata for errors

state-importer.ts: 564 -> 481 LOC.

22 new tests in parsing.test.ts pin the byte-identical behaviour
(secret sentinel UUID, both state shapes, URN fallback path,
empty-metadata defaults). All 79 existing pulumi/terraform tests
still pass; TS2834 baseline (29) unchanged.

* rf-pimp-2: extract resource conversion from pulumi state-importer

Pulls the resource-shape conversion out of state-importer.ts into a
new resource-conversion.ts module:

  - process_properties — recursive property walker with secret unwrap
  - import_resource    — PulumiResource -> PulumiImportedResource

Both are pure (warnings array is mutated like before).  Behaviour
preserved verbatim:
  - URN parse + extract_name_from_urn fallback for name
  - outputs > inputs precedence (with NO_OUTPUTS warning fallback)
  - dependencies = explicit ++ parent (parent appended last, in order)
  - additional_secret_outputs mirrored verbatim
  - protect/external default false; id passes through

state-importer.ts: 481 -> 387 LOC.

18 new tests pin the secret-mask path, the warning emission path, the
parent-last dependency append, and the protect/external defaults.
86 total importer tests pass; TS2834 baseline (29) unchanged.

* rf-pimp-3: extract graph conversion from pulumi state-importer

Pulls the two graph-emit functions out of state-importer.ts into a
new graph-conversion.ts module:

  - import_result_to_graph — PulumiImportResult -> MutableGraph
  - import_pulumi_to_graph — file-path -> MutableGraph wrapper

state-importer.ts re-exports both via barrel-style 'export {...} from'
to preserve the public surface — index.ts imports state-importer and
the existing 79 importer tests bind directly to it.

state-importer.ts: 387 -> 278 LOC (564 LOC at series start).

Behaviour preserved verbatim:
  - graph-level labels (source/version/stack/project)
  - per-node labels (provider, pulumi_type, optional protected/external)
  - per-node provenance annotations (imported_from, pulumi_urn)
  - resource.id lifted to node properties.id
  - self-dependency and missing-target edges skipped
  - target_graph merge mode preserves nodes only (edges already dropped)

12 new tests cover the node properties/labels/annotations, the edge
filters (self-loop + missing target), and the graph metadata.  98
total importer tests pass; TS2834 baseline (29) unchanged.

* rf-timp-1: extract sensitive-attribute masking + empty metadata

Pulls three pure helpers out of state-importer.ts into a new
sensitive.ts module:

  - mask_sensitive_attributes — top-level walker over sensitive paths
  - mask_path                 — recursive leaf-mask mutator
  - create_empty_metadata     — unknown-sentinel ImportMetadata for errors

state-importer.ts: 547 -> 514 LOC.

15 new tests pin the path-tokenisation behaviour:
  - top-level / dotted / bracket-array path forms
  - non-object intermediates abort the walk (no error)
  - missing leaves and null intermediates are no-ops
  - empty path / empty-input early return

All 79 existing pulumi/terraform tests still pass; TS2834 baseline
(29) unchanged.

* rf-timp-2: extract resource conversion + dependency inference

Pulls three functions out of state-importer.ts into a new
resource-conversion.ts module:

  - import_resource_instance — (resource, instance) -> ImportedResource
  - infer_dependencies       — id/arn-driven dep inference post-pass
  - scan_for_references      — recursive walker over property tree

state-importer.ts: 496 -> 368 LOC.

Behaviour preserved verbatim:
  - address = [module.]type.name[index_key], JSON-encoded index_key
  - ICE name = name_prefix + name + (_index_key when present)
  - sensitive_attributes path masking + SENSITIVE_MASKED warning
  - explicit instance.dependencies pass-through
  - id_lookup indexed by both 'id' AND 'arn' property values
  - Set-seeded dedup union of explicit + inferred deps
  - dependencies[] mutated in place per resource

22 new tests cover address building (incl. module/index_key paths),
the masking warning emission, the include_sensitive=true bypass, and
all three sub-cases of scan_for_references (string/array/object/null).
65 total terraform tests pass; TS2834 baseline (29) unchanged.

* rf-timp-3: extract graph conversion from terraform state-importer

Pulls the two graph-emit functions out of state-importer.ts into a
new graph-conversion.ts module:

  - import_result_to_graph    — TerraformImportResult -> MutableGraph
  - import_terraform_to_graph — file-path -> MutableGraph wrapper

state-importer.ts re-exports both via barrel-style 'export {...} from'
to preserve the public surface — index.ts imports state-importer.

state-importer.ts: 368 -> 268 LOC (547 LOC at series start).

Behaviour preserved verbatim:
  - graph-level labels (source/version/lineage)
  - per-node labels (provider, terraform_type, optional module)
  - per-node provenance annotations (imported_from, terraform_address)
  - all edges tagged 'inferred: true' regardless of origin
  - missing-target edges silently skipped
  - target_graph merge mode preserves nodes only

10 new tests cover the node properties/labels/annotations, the
optional module label, and the missing-target edge filter. 75 total
terraform importer tests pass; TS2834 baseline (29) unchanged.

* rf-aimp-1: extract ARN/tag helpers from aws-importer

Pulls four pure helpers out of aws-importer.ts into a new
arn-helpers.ts module:

  - extract_name_from_arn   — trailing /-or-:-separated name
  - extract_account_from_arn — 5th segment, '' when malformed
  - extract_region_from_arn  — 4th segment, 'global' default for IAM/CF
  - parse_tags               — Tags-array OR tags-object normalisation

aws-importer.ts: 533 -> 495 LOC.

Behaviour preserved verbatim:
  - 6-segment ARN check, segment-5+ join for resource portion
  - resource fallback when split-on-/-or-: leaves empty trailing
  - global default for empty region slot (IAM, CloudFront)
  - Tags array preferred over tags object when both exist
  - String() coercion for non-string Key/Value pairs

22 new tests cover all four helpers across well-formed ARNs,
malformed inputs, IAM-style global resources, and both tag formats.
TS2834 baseline (29) unchanged.

* rf-aimp-2: extract AWS SDK init from aws-importer

Pulls the dynamic-import wrapper functions out of aws-importer.ts into
a new sdk-init.ts module:

  - AWSSdk interface  — STS / ResourceExplorer / ConfigService bundle
  - init_aws_sdk      — dynamic-import client-sts/-resource-explorer-2/
                        -config-service, optional fromIni({profile})
  - get_account_id    — STS GetCallerIdentity wrapper, 'unknown' on err

aws-importer.ts: 495 -> 444 LOC.

Behaviour preserved verbatim — including the load-bearing
'Function("m", "return import(m)")' pattern that prevents bundlers
from transpiling the dynamic import to a static require (which would
break the optional-dep guarantee for users who never use AWS).

5 new tests cover the friendly install-the-sdk error path and the
'unknown' fallback in get_account_id.  TS2834 baseline (29) unchanged.

* rf-aimp-3: extract resource discovery from aws-importer

Pulls the two paginated AWS-API discovery loops out of aws-importer.ts
into a new discovery.ts module:

  - map_resource_explorer_hit       — pure: hit -> AWSResource
  - map_config_result               — pure: JSON-string -> AWSResource|null
  - discover_with_resource_explorer — paginated SearchCommand wrapper
  - discover_with_config            — paginated SelectResourceConfig wrapper

Two pure mappers were extracted alongside the discover_*() loops to
make the response-shape -> AWSResource conversion testable without
needing to stub the dynamic @aws-sdk/client-* imports (the
'Function("m", "return import(m)")' indirection bypasses any
Vitest module registry).

aws-importer.ts: 438 -> 358 LOC.

Behaviour preserved verbatim:
  - Resource Explorer: QueryString='*', MaxResults=100, NextToken pagination
  - Config: SelectResourceConfigCommand SQL DSL (LIKE '%')
  - region default 'global' for both paths
  - resourceId preferred over ARN-derived name (Config only)
  - JSON.parse failures silently skipped (Config only)

8 new tests cover the pure mappers across well-formed / partial /
malformed inputs, plus the SDK-not-installed failure path on each
discover_*() entrypoint.  TS2834 baseline (29) unchanged.

* rf-aimp-4: extract graph conversion + relationship inference

Pulls the two graph functions out of aws-importer.ts into a new
graph-conversion.ts module:

  - aws_result_to_graph  — AWSImportResult -> MutableGraph
  - infer_relationships  — ARN-driven dep inference post-pass

aws-importer.ts re-exports aws_result_to_graph via barrel-style
'export {...} from' to preserve the public surface — index.ts and
the importers index both bind to it.  Local consumer
import_aws_to_graph aliases the local import as aws_result_to_graph_impl
to avoid same-name collision with the re-export.

aws-importer.ts: 346 -> 250 LOC (533 LOC at series start).

Behaviour preserved verbatim:
  - graph-level labels (source, account_id)
  - per-node labels: provider, aws_type, account_id, region, ...tags
    (tags spread last — tags WIN on key collision with canonical labels)
  - per-node provenance: imported_from, aws_arn, aws_account
  - depends_on edges with inferred:true + source:aws labels
  - self-dependency and missing-target edges silently skipped
  - infer_relationships REPLACES dependencies (not unions) — load-bearing
  - ARN matching gated by 'arn:aws:' prefix and arn_set membership

19 new tests cover the canonical-label-vs-tag collision, the
self-dep/missing-target edge filters, dedup of repeated references,
own-ARN exclusion, and the dependency-replacement (not -union)
contract.  148 total importer tests pass; TS2834 baseline (29)
unchanged.

* state: add two learnings from P3 cohort 4 (rf-aimp series)

dynamic-import-indirection-blocks-test-mocks
  The Function('m', 'return import(m)') pattern in aws-importer
  bypasses Vitest's module registry — vi.mock can't stub the AWS
  SDK calls. Workaround: extract pure mappers and test those.

same-name-local-import-and-reexport-collision
  When an extracted function is both consumed locally AND re-exported
  for the public surface, alias the local import to make the two
  roles explicit (X_impl vs X).

* rf-tfexp-1: extract terraform types module

Verbatim port of TerraformExportOptions / RequiredProvider /
TerraformResource / TerraformLifecycle / TerraformConfig /
TerraformBlock / TerraformProviderConfig / TerraformVariable /
TerraformOutput / TerraformExportResult shapes from
terraform-exporter.ts (pre-extraction L20-160) into a dedicated
types module. Public surface preserved — re-exported from the
orchestrator in the slim-down unit.

* rf-tfexp-2: extract terraform sanitize_name helper

Verbatim port of the private sanitizeName method from
terraform-exporter.ts (pre-extraction L420-428) into a pure
helper module. The Terraform identifier rules differ from
Pulumi's (underscore prefix vs r- prefix; preserves _ in
identifiers); kept separate to avoid coupling.

Tests: 10 cases covering alphanumeric pass-through, dot/slash/
space substitution, leading-digit prefix, unicode replacement.

* rf-tfexp-3: extract terraform fallback_type_mapping helper

Verbatim port of the private fallbackTypeMapping method from
terraform-exporter.ts (pre-extraction L335-362). Provider-prefix
table preserved exactly; gcp/aws/azure branch ordering preserved
(a gcp.* type always hits the gcp branch even if provider token
is non-gcp). Documented the pre-extraction quirks where the aws
and azure branches hard-code the prefix regardless of provider
token.

Tests: 17 cases covering each branch + provider mapping table.

* rf-tfexp-4: extract terraform value-transform helpers

Verbatim port of mapProperties / transformValue / formatDependencies
from terraform-exporter.ts (pre-extraction L367-418). Three pure
transformations with no class-state dependency.

Key differences vs Pulumi's value-transform: keys are preserved
AS-IS (Terraform uses snake_case natively); transform_value does
not rename nested keys. format_dependencies emits # placeholders
(pre-extraction had a TODO comment about lookup; preserved).

Tests: 23 cases covering null/undefined normalisation, _-prefix
filtering at top level only, recursive nested transforms, array
handling, dependency placeholder formatting.

* rf-tfexp-5: extract terraform HCL formatter helpers

Verbatim port of formatHCLValue / toHCL / toJSON from
terraform-exporter.ts (pre-extraction L433-545). Pure functions;
no class state.

The HCL output format is byte-identical to the pre-extraction
class methods. Particularly load-bearing:
 - String escape order (backslash first, then quote)
 - null/undefined property values SKIPPED (not emitted as null)
 - depends_on block omitted when empty
 - HCL object syntax 'key = value' (not JSON-style 'key: value')
 - Trailing blank line after each section

Tests: 37 cases covering all value types + full to_hcl snapshot
regression guard.

* rf-tfexp-6: extract terraform converter helpers

Verbatim port of buildDependencyMap / nodeToResource /
exportGraph from terraform-exporter.ts (pre-extraction L189-330).
The class state previously held by the orchestrator
(schema_provider) is now passed as the first argument to each
helper.

Pre-extraction quirks preserved:
 - depends_on edge filter (other relationships ignored)
 - node.properties || {} defensive default
 - unmapped_types deduped via Set; warnings NOT deduped
 - format-selection branch checks 'json' literal; everything
   else (including undefined) emits HCL

* rf-tfexp-7: slim terraform-exporter orchestrator (558 -> 102 LOC)

The class is now a thin orchestration shell:
 - constructor instantiates schema_provider
 - initialize() lazy-initialises the schema provider
 - exportGraph() delegates to ./terraform/converter.export_graph

Public API unchanged — TerraformExporter, create_terraform_exporter,
and the eleven exported types all keep their pre-extraction shape.
External consumers (export/index.ts) continue importing through
the orchestrator path; the new terraform/* modules are internal.

* rf-pmap-1: extract pulumi type-mapper data tables

Verbatim port of PROVIDER_MAP (~24 entries) and TYPE_MAP
(~280 entries) from importers/pulumi/type-mapper.ts
(pre-extraction L94-413). The TYPE_MAP entries are the
SOURCE OF TRUTH for ICE iceType names — external consumers
depend on the exact dotted-form values; preserved verbatim.

Size exception: 376 LOC justified by data-only nature
(cf. /docs/refactoring-patterns.md 'Data-heavy shim split').

Tests: 18 cases pinning provider count, key existence, ICE
type format, dedup behaviour for collapsed mappings (e.g.
aws:s3/bucket:Bucket and aws:s3/bucketV2:BucketV2 both
mapping to aws.s3.bucket).

* rf-pmap-2: extract pulumi type-mapper URN/type parsers

Verbatim port of parse_urn and parse_type from
importers/pulumi/type-mapper.ts (pre-extraction L19-86).
Pure string parsers; no data-table dependency.

Tests: 19 cases covering URN parts validation, special
type handling (pulumi:pulumi:Stack, pulumi:providers:*),
standard format vs alternative format priority order,
malformed input fallback to empty object.

* rf-pmap-3: extract pulumi type-mapper mapping helpers

Verbatim port of get_ice_type, get_ice_provider,
get_provider_from_type, is_type_supported,
get_supported_types, get_supported_ice_types,
get_name_from_urn, is_provider_resource, is_stack_resource
from importers/pulumi/type-mapper.ts (pre-extraction
L422-527). Plus the private to_snake_case helper
(pre-extraction L500-505).

Pre-extraction quirks preserved:
 - get_ice_type three-stage fallback (TYPE_MAP -> synth -> lowercase dotted)
 - get_ice_provider three-stage fallback (URN -> type -> simple-name -> 'unknown')
 - is_type_supported strict TYPE_MAP membership (synthesised paths excluded)
 - get_supported_ice_types deduped via Set
 - to_snake_case strips leading underscore from ([A-Z]) capture

Tests: 21 cases covering all eight helpers + fallback paths.

* rf-pmap-4: slim pulumi type-mapper orchestrator (527 -> 42 LOC)

The file is now a pure re-export shim. All eleven exported
functions plus the (implicit) data-table re-exports keep
their pre-extraction shapes. External consumers
(state-importer.ts, parsing.ts, resource-conversion.ts,
index.ts) continue importing through this shim path
without changes.

* rf-galg-1: extract graph topological-sort + cycle detection

Verbatim port of topological_sort, reverse_topological_sort,
has_cycle, find_cycles, find_cycle_in_subgraph from
graph/algorithms.ts (pre-extraction L18-220). Grouped together
because topological_sort uses find_cycle_in_subgraph (private)
for error reporting; has_cycle is a one-line wrapper around
topological_sort.

Pre-extraction quirks documented:
 - topological_sort double-counts in-degree decrement (iterates
   outgoing edges + ALL edges with target === current)
 - find_cycles returns DUPLICATE cycles when nodes participate
   in multiple cycles (no dedup)
 - find_cycle_in_subgraph returns first cycle OR up to 5 nodes
   from input on failure (best-effort error fallback)

Tests: 16 cases covering linear chains, cycles, disconnected
nodes, non-depends-on edge filtering. Plus shared fixtures.ts
with make_graph helper for the rest of the rf-galg series.

* rf-galg-2: extract graph path-finding helpers

Verbatim port of find_all_paths and find_shortest_path from
graph/algorithms.ts (pre-extraction L229-297). Independent
DFS/BFS path finding; no dependency on topo/cycle/components.

Pre-extraction quirks documented:
 - find_all_paths uses recursive DFS with visited-set cycle
   avoidance + max_paths cap (default 100)
 - find_shortest_path uses Array.shift BFS (O(n) per shift,
   not O(1)); preserved verbatim
 - find_shortest_path returns null on no path, [start] when
   start === end

Tests: 12 cases covering direct edges, diamonds, cycle
avoidance, max_paths cap, null vs empty array.

* rf-galg-3: extract connected components helpers

Verbatim port of find_connected_components and
find_strongly_connected_components from graph/algorithms.ts
(pre-extraction L307-402). Two distinct algorithms:
 - find_connected_components: BFS, treats edges as UNDIRECTED
   (uses both incoming and outgoing edges)
 - find_strongly_connected_components: Tarjan's, treats edges
   as DIRECTED; filters single-node SCCs (length > 1 only)

Pre-extraction quirks documented:
 - find_connected_components tolerates duplicate enqueue (the
   visited.has check is at top of inner loop, not at enqueue)
 - find_strongly_connected_components excludes singleton SCCs
   (intentional — singleton means trivially-strongly-connected,
   so excluding gives meaningful SCCs only)
 - Recursive Tarjan can hit JS stack limits on deep graphs

Tests: 12 cases covering empty graphs, disconnected nodes,
diamonds, mutual cycles, multi-cycle graphs.

* rf-galg-4: extract dependency analysis + graph metrics

Verbatim port of get_execution_layers, get_critical_path,
calculate_metrics, GraphMetrics interface from
graph/algorithms.ts (pre-extraction L412-586). Depends on
helpers from topo-cycle and components modules.

Pre-extraction quirks documented:
 - get_execution_layers uses iterative layer-peel; on cycle
   produces empty layer and breaks (silent ceasing)
 - get_critical_path KNOWN BUG: distance update walks
   incoming-edge predecessors, but topo order processes leaves
   first for depends_on graphs, so the source-distance lookup
   reads -Infinity and the chain never propagates. Effectively
   returns just the start (no-deps) node. Preserved verbatim
   — fixing this changes public behaviour, out-of-scope for
   refactor.
 - calculate_metrics density formula: e/(n*(n-1)); guards
   max_edges > 0 to avoid div-by-zero on empty graphs

Tests: 15 cases covering empty graphs, parallelisable layers,
cycle detection, density edge cases, degree statistics. Two
critical-path tests assert length >= 1 (preserving the
pre-extraction quirk).

* rf-galg-5: slim graph algorithms orchestrator (586 -> 51 LOC)

The file is now a pure re-export shim. All eleven exported
functions plus the GraphMetrics type keep their pre-extraction
shapes. External consumers (graph/index.ts,
plan/plan-engine.ts, graph/validator/validators.ts) continue
importing through this shim path without changes.

* rf-pmap rf-galg: append learnings on critical-path quirk + data-table LOC exception

* rf-cmove: split use-container-move.ts (564 → 189 LOC orchestrator)

Decomposes the rf-canv-25b useContainerMove hook into pure runners
under hooks/container-move/. Behavior preserved verbatim — both
handleNodeMove and handleToggleFold delegate to the new sub-modules.

  - container-move/types.ts (25) — PositionUpdate / SizeUpdate
  - container-move/ancestor-expansion.ts (275) — walkAncestorsAndExpand,
    expandAncestorOnce (shared by both handlers, parameterized over
    sibling-bounds reading strategy)
  - container-move/clamp.ts (114) — clampDraggedNodeToParent +
    detectExitingGroupId (tri-state setter directive)
  - container-move/move-runner.ts (127) — runNodeMove pure runner
  - container-move/toggle-fold-runner.ts (187) — resolveToggleFoldDecision
    + runUnfoldExpansion pair

Tests: 27 baseline (use-container-move.test.tsx) + 68 new sub-module
tests = 95 total passing. Coverage on container-move/: 100% statements
/ 100% branches / 100% functions / 100% lines (types.ts excluded as
runtime-empty).

Per blueprint risk #2 — DO NOT consolidate the four ancestor-expansion
sites with rf-canv-4's expandToFitChildren; they have subtly different
rules. The split preserves the per-handler bbox-reading strategy via
the siblingPosLookup + siblingBoundsOverride params on
walkAncestorsAndExpand.

* rf-vval: split validators.ts (524 → 68 LOC orchestrator)

Decomposes the built-in validators into domain-grouped modules under
graph/validator/validators/. Public API and behavior preserved verbatim
— the orchestrator file becomes a thin re-export shim plus the two
factory functions.

  - validators/structure.ts (163) — Cycle, Reference, Naming,
    Connectivity (graph topology + names, no schema dep)
  - validators/schema.ts (195) — Type, Property (schema-provider deps)
  - validators/security.ts (138) — SensitiveData, BestPractices
  - validators.ts (68) — re-exports + create_builtin_validators +
    create_configured_validator factories

Tests: 3 baseline (core.test.ts Graph Validator suite) + 70 new
sub-module tests = 73 total passing. Coverage on
validators/{structure,schema,security}.ts + validators.ts: 100%
statements / 100% branches / 100% functions / 100% lines.

The public exports from packages/core/src/index.ts (CycleValidator,
ReferenceValidator, ..., create_builtin_validators,
create_configured_validator) remain wired through the orchestrator
shim, so external consumers and the dist barrel don't change.

* rf-ierr: split import-errors.ts (507 → 30 LOC orchestrator)

Decomposes the import error classification system into per-cloud
modules under errors/import-errors/. Behavior + every error message
string preserved verbatim (user-facing strings are stable).

  - import-errors/types.ts (117) — ImportErrorCode enum,
    ImportErrorAction, ImportError, ImportWarning, ImportErrorActionType
  - import-errors/gcp.ts (145) — classifyGCPError
  - import-errors/aws.ts (144) — classifyAWSError
  - import-errors/azure.ts (119) — classifyAzureError
  - import-errors.ts (30) — re-export shim, public API unchanged

Tests: 0 baseline + 87 new sub-module + shim tests = 87 total. Coverage
on import-errors/{types,gcp,aws,azure}.ts: 100% statements / 100%
branches / 100% functions / 100% lines. The shim file shows 0% in v8
coverage (re-only-export quirk; the named imports through it are
exercised by the dedicated shim test file).

External consumers (aws-importer.ts, azure-importer.ts,
gcp/services/asset-inventory.ts, errors/index.ts barrel) keep their
original `from '../../errors/import-errors.js'` import paths.

Full core suite: 2472 tests still pass.

* rf-cmove: append learning on tri-state setter directive pattern

* Phase 3 complete: all 18 files in 500-600 LOC band refactored

64 commits across 6 cohorts. 9756 → 3441 LOC orchestrators (-65%).
+1356 tests added. 3 pre-existing bugs surfaced and documented
(preserved verbatim per refactor discipline).

Remaining files over 500 LOC are documented exceptions only:
- Generated (resource-types.ts)
- Data-heavy (high-level-resources, scale-presets-data, cloud-blocks-data,
  themes, ast/types)
- Refactored orchestrators with residual cohesion (deploy.service,
  svg-canvas, system-prompt)

The actionable refactor queue is empty.

* rf-deploy2-1: extract planDeployment + fallbackPlan to plan-deployment.ts

Pulls planDeployment (~85 LOC) and its module-private fallbackPlan
(~44 LOC) out of deploy.service.ts into a dedicated module. The
orchestrator re-exports planDeployment so the namespace import in
routes/canvas-deploy.ts and the export * in services/deploy/src/index.ts
keep resolving unchanged.

deploy.service.ts: 1572 → 1449 LOC.
Adds plan-deployment.test.ts covering happy path + translator-throw fallback.

* rf-deploy2-2: extract applyDeployment to apply-deployment.ts

Pulls applyDeployment (~573 LOC) — the 5-phase translate/auth/deploy
pipeline — out of deploy.service.ts into a dedicated module. The
orchestrator re-exports applyDeployment so the namespace import in
routes/canvas-deploy.ts and the named import in queue.service.ts
continue to resolve unchanged.

deploy.service.ts: 1449 → 894 LOC.
apply-deployment.ts: 600 LOC (slightly over 500-line ceiling; flagged
for follow-up internal split per brief).

* rf-deploy2-3: extract destroyAllForCard to destroy-all-for-card.ts

Pulls destroyAllForCard (~242 LOC) — the 'nuke' path that destroys every
ICE-managed resource for a card across all historical deployments — out
of deploy.service.ts into a dedicated module. The orchestrator re-exports
destroyAllForCard so the namespace import in routes/canvas-deploy.ts
keeps resolving unchanged.

deploy.service.ts: 894 → 644 LOC.
destroy-all-for-card.ts: 281 LOC.

* rf-deploy2-4: extract destroyDeployment to destroy-deployment.ts

Pulls destroyDeployment (~338 LOC) — the latest-apply-baseline destroy
path with reverse-order delete loop and pdl-10 per-resource node_status
emit — out of deploy.service.ts into a dedicated module. The orchestrator
re-exports destroyDeployment so the namespace import in
routes/canvas-deploy.ts keeps resolving unchanged.

deploy.service.ts: 644 → 311 LOC.
destroy-deployment.ts: 364 LOC.

* rf-deploy2-5: extract rollbackDeployment to rollback-deployment.ts

Pulls rollbackDeployment (~187 LOC) — the rebuild-from-target-deployment
diff/deploy path — out of deploy.service.ts into a dedicated module.
The orchestrator re-exports rollbackDeployment so the namespace import
in routes/canvas-deploy.ts keeps resolving unchanged.

deploy.service.ts: 311 → 129 LOC.
rollback-deployment.ts: 210 LOC.

* rf-deploy2-6: orchestrator slim-down — drop unused imports + docstring

Trims now-unused imports from deploy.service.ts (acquireDeployLock,
DeployLockError, providerService, getResourceMap, etc.) and updates the
top-of-file docstring to describe the file's new role: a thin re-export
shim plus 5 small DB-only helpers and the snapshot-persister side-effect
init. Body is unchanged behaviorally.

deploy.service.ts: 129 → 107 LOC (1572 → 107 across the rf-deploy2 series,
93% reduction). Public API surface preserved: every symbol exported
pre-refactor still resolves through the orchestrator (planDeployment,
applyDeployment, destroyAllForCard, destroyDeployment, rollbackDeployment,
getDeploymentStatus, getDeployedResources, getDeploymentHistory,
requestDeployCancel, getCurrentDeploySnapshot, DeployProgressSnapshot,
getNodeDeploymentOverlay, checkDrift).

* rf-deploy2-2 housekeeping: split apply-deployment.ts under 500 LOC

apply-deployment.ts landed at 600 LOC after rf-deploy2-2 (just over the
project's 500 LOC ceiling). This follow-up extracts five mechanically-
separable helpers into apply-pipeline-helpers.ts, bringing the orchestrator
under the ceiling without changing the apply pipeline's behavior:

- logSourceRepoDiagnostics: pre-deploy canvas-shape diagnostic log lines
- ensureAutoDeployRules: best-effort Source.Repository → Compute auto-rule writer
- logDiffForDebugging: console-only desired-vs-current node dump
- normalizeIdempotentResultErrors: NOT_FOUND/ALREADY_EXISTS rewrites
- persistResourceMappings: post-deploy mapping-table writes (in-place mutation)

apply-deployment.ts: 600 → 493 LOC.
apply-pipeline-helpers.ts: 220 LOC.
Adds 11 tests covering the three helper shapes that benefit most from
isolated test coverage.

* rf-deploy2: append awk-line-range learning from rf-deploy2-2

* rf-hlres-1: extract HighLevel* type interfaces to high-level-resources/types.ts

Pulls the 5 type interfaces (ProviderImplementation, HighLevelResource,
OptionDetail, HighLevelProperty, HighLevelCategory) plus the NodeBehavior
re-export out of the orchestrator. The shim now re-exports each name
verbatim, preserving the public API for both packages/core/src/resources/index.ts
and packages/core/src/index.ts.

Smoke test pins:
- the types module is loadable as a namespace
- structurally-typed values for each of the 5 interfaces compile
- the shim re-exports the 5 type names + NodeBehavior
- HIGH_LEVEL_CATEGORIES still has the 7 canonical category ids in order

Baseline typecheck preserved at 29 TS2834 errors (all in unrelated barrel
files); no new errors in touched paths.

* rf-hlres-2: extract compute category to high-level-resources/categories/compute.ts

Cuts the 'compute' object literal (~1779 LOC) verbatim out of
HIGH_LEVEL_CATEGORIES and re-imports it as a const. The orchestrator
shrinks from 6354 to 4577 LOC.

Size exception documented in the file header — the file is dominated by
data (per-resource property catalogues, instance-size pickers, provider
implementations) and would only fragment further without improving
readability.

Smoke test pins the 13 canonical resource ids in order plus the shape
of frontend-app and backend-api. Baseline typecheck preserved at 29
TS2834 errors.

* rf-hlres-3: extract database category to high-level-resources/categories/database.ts

Cuts the 'database' object literal (~2167 LOC) verbatim out of
HIGH_LEVEL_CATEGORIES. The orchestrator shrinks from 4577 to 2411 LOC.

Size exception documented in the file header — same shape as compute.

Smoke test pins the 13 canonical database resource ids in order
(postgres-db through search-engine) plus the shape of postgres-db
and redis-cache. Baseline typecheck preserved at 29 TS2834 errors.

* rf-hlres-4: extract storage category to high-level-resources/categories/storage.ts

Cuts the 'storage' object literal (~449 LOC) verbatim out of
HIGH_LEVEL_CATEGORIES. The orchestrator shrinks from 2411 to 1964 LOC.

Smoke test pins the 5 canonical storage resource ids in order
(object-storage, oss, oci-object-storage, do-spaces, file-storage).
Baseline typecheck preserved at 29 TS2834 errors.

* rf-hlres-5: extract networking category to high-level-resources/categories/networking.ts

Cuts the 'networking' object literal (~559 LOC) verbatim out of
HIGH_LEVEL_CATEGORIES. The orchestrator shrinks from 1964 to 1407 LOC.

Smoke test pins the 7 canonical networking resource ids in order
(public-endpoint, vpc-network, subnet, load-balancer, cdn, api-gateway,
dns-zone). Baseline typecheck preserved at 29 TS2834 errors.

* rf-hlres-6: extract messaging category to high-level-resources/categories/messaging.ts

Cuts the 'messaging' object literal (~842 LOC) verbatim out of
HIGH_LEVEL_CATEGORIES. The orchestrator shrinks from 1407 to 567 LOC.

Smoke test pins the 7 canonical messaging resource ids in order
(message-queue, event-bus, rabbitmq, cloud-pubsub, service-bus,
email-service, event-stream) and that message-queue carries the deep
optionDetails arrays used by the AWS / GCP / Azure queue-type picker.
Baseline typecheck preserved at 29 TS2834 errors.

* rf-hlres-7: extract security + monitoring categories (combined unit)

Cuts the last two object literals (security ~185 LOC, monitoring ~185 LOC)
verbatim out of HIGH_LEVEL_CATEGORIES. The orchestrator shrinks from 567
to 200 LOC — 7 categories now imported, no inline literals remain in
HIGH_LEVEL_CATEGORIES.

Both files are well under the 500 LOC ceiling; split out for symmetry
with the rest of the categories sub-tree.

Smoke tests pin the canonical resource ids in order: security
(secret-store, ssl-certificate, service-account) and monitoring
(log-group, alert, dashboard). Baseline typecheck preserved at 29
TS2834 errors.

* rf-hlres-8: extract helpers + HIGH_LEVEL_CATEGORIES to high-level-resources/helpers.ts

Moves the 7 helper functions (getAllHighLevelResources, palette projection,
filterResourcesByProvider, getBehaviorLabel/Color, getGCPCloudAssetTypes,
cloudAssetToHighLevelType) plus the PULUMI_TO_CLOUD_ASSET map and the
HIGH_LEVEL_CATEGORIES assembly into a new helpers module.

helpers.ts owns the runtime origin of HIGH_LEVEL_CATEGORIES because the
cloud-asset helpers iterate over it — making the helpers the runtime owner
avoids a 'helpers → orchestrator → helpers' cycle. The orchestrator now
re-exports each runtime symbol verbatim so external consumers
(packages/core/src/index.ts, packages/core/src/resources/index.ts,
schema-bridge, property-rules, gcp asset-inventory, type-mapper) keep
the same import surface.

Exhaustive tests pin: HIGH_LEVEL_CATEGORIES order, getAll flatMap +
ordering, palette projection (ice_type / display_name / category /
behavior / providers / implementations / properties), provider filter
+ 'all' bypass + unknown provider returns empty, behavior label/color
delegation, GCP asset-types deduplication, and cloudAsset→HL reverse
lookup including null on unknown.

Orchestrator file: 202 → 37 LOC. Helpers: 195 LOC. Baseline typecheck
preserved at 29 TS2834 errors. Full @ice/core suite (2515 tests) passes.

* rf-hlres-9: orchestrator slim-down — finalize the high-level-resources shim

Polishes the orchestrator after rf-hlres-8 left it as a 37-LOC re-export
shim. Drops the in-progress wording from the docstring, merges the two
`export type {}` blocks into one, and adds section dividers so future
readers can spot the type re-exports vs. the runtime re-exports at a
glance.

Final file size: 47 LOC. Net delta on the rf-hlres series:
  - high-level-resources.ts: 6434 → 47 LOC (a ~99.3% shrink)
  - new files: types.ts, helpers.ts, 7 categories/<name>.ts
  - public API surface unchanged (every name still resolves on the shim)

Repo-wide test run after the slim-down: 8144 tests pass. Baseline
@ice/core typecheck preserved at 29 TS2834 errors (all unrelated).

* rf-hlres: append learning on helpers-owns-assembled-array pattern + awk per-splice tactics

* rf-svgcv2-1: extract pan/zoom transform group as CanvasContent

The inner <g transform=...> body — grid, selection frame, both connection
layers, clipPaths, nodes layer, connection-drawing preview, user-traffic
overlay, and ghost overlay — moves into canvas-renderer/canvas-content.tsx.
Visual draw order, prop flow, and gating logic preserved verbatim.

svg-canvas: 570 → 490 LOC.
canvas-content: 236 LOC (new).

Tests: 10 passing — direct-FC tree-walker matching mocks by reference.
Typecheck clean.

* rf-svgcv2-2: extract SVG mouse-event routing as useCanvasMouseRouting

The four inline arrow handlers on the <svg> element — onMouseDown
(connection-port classList sniff + bindCanvas fall-through), onMouseMove
(drawingConnection branch + bindCanvas fall-through), onMouseUp (same
shape), and onMouseLeave (tooltip dismiss + bindCanvas) — collapse into
a hook that returns the spread-able handler bundle.

Behavior preserved verbatim: classList sniff stays orchestrator-side (per
RISK #5), tooltip dismissal still fires on every onMouseDown/onMouseLeave,
onAuxClick / onContextMenu pass through bindCanvas references unchanged.

svg-canvas: 490 → 473 LOC.
use-canvas-mouse-routing: 114 LOC (new).

Tests: 12 passing — covers all four routing seams + identity-pass
through for AuxClick/ContextMenu. Typecheck clean.

* rf-svgcv2-3: extract selection-dispatch + snapToGrid wiring as useCanvasInteractionsBindings

The orchestrator's useCanvasInteractions call carried three inline
arrow-function callbacks (onSelect / onToggleSelect / onBoxSelect)
plus the snapToGrid → gridSize ternary. Both groups are now bundled in
a thin wrapper hook that calls useCanvasInteractions internally.

Behavior preserved verbatim — the same dispatch sequences fire for each
selection-change branch, and snapToGrid still toggles between GRID_SIZE
and 0. The wrapper drops the snapToGrid prop from the inner args object
so useCanvasInteractions sees only its own keys.

svg-canvas: 473 → 457 LOC.
use-canvas-interactions-bindings: 69 LOC (new).

Tests: 8 passing — covers each dispatch callback, the gridSize ternary,
and the snapToGrid prop-drop. Typecheck clean. Full canvas-tests suite:
207/207 passing.

* rf-svgcv2-4: extract RenderCtx assembly as useRenderCtx hook

The eighteen-field renderCtx object the per-node renderer dispatch
consumes — sortedNodes, lod, pipelineNodeStatus, drag/exit highlight
ids, the seven event-handler callbacks, etc. — moves into a
useRenderCtx hook. The hook also binds getConnectedPipelineStatuses
to (card, pipelineNodeStatus) so renderers call it as a single-arg
function (preserving rf-canv2-5's surface).

Behavior preserved verbatim: the returned ctx contains every field
the original literal had, with the same refs. card and zoom move
into the hook's args bundle; svg-canvas no longer imports
getConnectedPipelineStatuses or the RenderCtx type.

svg-canvas: 457 → 453 LOC.
use-render-ctx: 105 LOC (new).

Tests: 5 passing. Bonus: rf-svgcv2-3 test JSX-form fix (Provider needs
explicit children prop in TS strict — convert createElement(Provider,
{store}, child) to JSX form). Typecheck clean.

* rf-spr2-1: split system prompt into composable section builders

The 350-line static prose body of buildSystemPrompt's template literal
moves into seven section-builder functions in system-prompt-sections.ts:
  - buildHeaderPrompt(dominantProvider)
  - buildIntentRoutingPrompt() — WHEN TO ACT vs WHEN TO ASK
  - buildOperationsPrompt(availableBlockTypes) — STRICT BLOCK REGISTRY
  - buildPropertyPrefillPrompt() — PROPERTY PRE-FILL RULES
  - buildOptimizationGuidelinesPrompt() — improve security/cost/HA/...
  - buildCanvasContextPrompt(nodes/edges/selected/schema) — Current Canvas
  - buildContainerNetworkingPrompt(connectionPrompt) — VPC + WIRING

The orchestrator concatenates them via the + operator. Output is
byte-identical to the pre-rf-spr2 prompt — verified by a new snapshot
test (3 fixtures: bare canvas, cloud-architect skill, question intent
with deployment context). Pure helpers (formatNodesSummary,
detectDominantProvider, buildCloudArchitectPrompt) stay in
system-prompt.ts.

system-prompt.ts: 516 → 203 LOC.
system-prompt-sections.ts: 420 LOC (new, prose-heavy — header documents
the data-table exception).

Tests: 33 passing (30 existing + 3 new snapshot fixtures).
Typecheck clean.

* rf-asttyp-1: split AST types by category, keep ast/types.ts as re-export shim

The 581-LOC types.ts split into four sub-files by AST node category:
  - types/base.ts        — AstNode + AstNodeKind union (65 LOC)
  - types/expressions.ts — Expression + literals + access + splat +
                            for-comprehension types (253 LOC)
  - types/blocks.ts      — Block + Attribute + NestedBlock (44 LOC)
  - types/statements.ts  — Program + Statement union + every top-level
                            block + LifecycleConfig + ValidationRule +
                            TypeExpression (268 LOC)

ast/types.ts becomes a 70-LOC type-only re-export shim. Every existing
consumer (the parent ast.ts shim, ast-helpers.test.ts, plus the broader
parser/visit_ast machinery) keeps importing from '../ast/types.js'
unchanged.

Tests: 15 type-shape tests added in __tests__/types-shim.test.ts —
covers every kind discriminator + the optional-field shapes for
ResourceBlock/LifecycleConfig/TypeExpression. Full parser test suite:
297/297 passing. Typecheck: no new errors (existing 29 TS2834 baseline
errors unaffected per the ts2834-baseline learning).

* rf-svgcv2-4 followup: fix RenderCtx → Record cast in test (TS2352)

Two-step cast (as unknown as Record) to satisfy strict overlap check —
RenderCtx has no index signature so the direct cast fails.

* rf-svgcv2 + rf-spr2-1 + rf-asttyp-1: capture three new learnings

- byte-identity-snapshot-must-be-captured-pre-refactor-not-post (rf-spr2-1)
- svg-canvas-orchestrator-loc-budget-flexible-with-renderctx-bundling (rf-svgcv2-4)
- render-ctx-record-cast-needs-double-as-unknown-as-record (rf-svgcv2-4)

* rf-spdat: split scale-presets-data 1482 LOC into 7 category files + 46-LOC orchestrator

Extract SCALE_PRESETS dict by domain category (compute, database, storage,
networking, messaging, security, monitoring) under scale-presets-data/.
Orchestrator becomes a 46-LOC re-export shim that spreads each per-category
record into the SCALE_PRESETS dict — order preserved.

Per-category sizes (LOC): compute 387, database 500, messaging 265,
networking 166, storage 142, security 30, monitoring 30. All categories
data-only (size-exception applies).

Smoke test (scale-presets-data.test.ts): 14 tests pinning category counts,
no cross-category collisions, assembled-dict equals union, byte-identical
sample lookups for compute/database/monitoring tiers.

* rf-cbdat: split cloud-blocks-data 1009 LOC into 9 category files + 126-LOC orchestrator

Extract the 16 BlockTemplate entries by their declared `category:` field
under cloud-blocks-data/ (frontend, backend, compute, data, storage,
networking, messaging, observability, security). Orchestrator assembles
BLOCK_TEMPLATES in the documented original order and derives BLOCK_CATEGORIES
(palette grouping) by category-field filter.

Per-category sizes (LOC): backend 210, data 192, networking 128, messaging
106, security 106, frontend 79, storage 72, compute 70, observability 65.

Smoke test (cloud-blocks-data.test.ts): 17 tests pinning per-category
counts, no cross-category template-name collisions, byte-stable assembled
ordering vs. the original file, BLOCK_CATEGORIES references the assembled
list (filter, not clone).

* rf-thmdat: split themes 590 LOC into 3 4-theme groups + 30-LOC orchestrator

Extract the 12 ColorTheme entries into 3 group files (4 themes each) by
the original ordering: group-1 default/retro/cupcake/valentine (warm),
group-2 synthwave/coffee/luxury/aqua (bold), group-3 forest/sage/dracula/
night (nature/dark). Orchestrator becomes a 30-LOC re-export shim that
spreads the three groups into the public T array.

Per-group sizes: 200/201/201 LOC — each well within the 200-500 ceiling.

Smoke test (themes-data.test.ts): 12 tests pinning per-group counts and
ordering, no cross-group id collisions, byte-stable assembled T array,
reference-equality preserved through the spread (orchestrator FC depends
on T[i] identity for the shallow click-handler closures).

* Final round complete: all exceptions decomposed (excluding generated files)

8 exception files split: deploy.service, high-level-resources, svg-canvas,
system-prompt, ast/types, scale-presets-data, cloud-blocks-data, themes.

12,754 → 1,082 LOC across orchestrators (-91%). 28 commits, +180 tests.

Files over 500 LOC remaining: only the generated resource-types.ts
(excluded per user) and 4 high-level-resources category sub-modules
(pure resource-definition data with SIZE EXCEPTION headers; further
fragmentation would split related resources without maintainability
gain).

* bugfix-2: lazy require.resolve in get_base_db_path

The pre-fix code eagerly evaluated
`require.resolve('@ice-engine/schemas/data/ice-schemas.db')` while
constructing the candidate array, so the function threw before
reaching the existsSync loop in any environment where that package
isn't installed (test envs, fresh checkouts, this monorepo's dev
setup). Wrap each candidate in a thunk and the require.resolve
fallback in its own try/catch so a missing package degrades to
"skip this candidate" instead of crashing the whole call.

Tests:
- Confirm get_base_db_path() does not throw without the package.
- Confirm it returns a string ending in ice-schemas.db.
- Confirm it returns the dev path when staged at the resolved location
  (priority over the missing-package fallback).

Discovered during rf-cload-3 (extraction of `get_base_db_path` from
`customization-loader.ts`); preserved verbatim then per the
verbatim-preservation rule for refactors. Now fixed as a
behaviour-change PR per the rule that pre-existing bugs documented
in learnings get separate fix tickets.

* bugfix-3: get_critical_path now propagates distance through chain

Pre-fix the distance update walked `get_incoming_edges` and read
the *source's* distance, but `topological_sort` emits leaves first
for `depends_on` graphs — so source nodes are processed AFTER the
current node and their distance is always -Infinity at lookup
time. The chain never propagated past the start node and the
function returned `[start]` for any DAG.

Fix: walk `get_outgoing_edges` (the current node's dependencies)
and read the *target's* distance. Targets are processed earlier
in topo order (leaves first), so the lookup is always populated.

Tests:
- 3-node chain a→b→c → returns [c, b, a] (was [c]).
- 4-node chain a→b→c→d → returns [d, c, b, a].
- Diamond DAG (a→b, a→c, b→d, c→d) → 3-node longest path.
- Isolated node → returns [solo].
- Empty graph → [].
- Cyclic graph → [].
- Disconnected components → returns longest among them.
- Two pre-existing softened tests (`expect(path.length)
  .toBeGreaterThanOrEqual(1)`) removed; replaced by strict
  assertions that pin the corrected length and node order.

Path orientation: leaf-first → root-last, matching the natural
"start → end" reading of a critical path. For chain a→b→c the
"start" is c (no deps) and the "end" is a (most-dependent).

Discovered during rf-galg-4 (extraction from `graph/algorithms.ts`);
preserved verbatim then per the verbatim-preservation rule for
refactors. Now fixed as a behaviour-change PR per the rule that
pre-existing bugs documented in learnings get separate fix
tickets.

* bugfix-4: detectJsFramework now reads pnpm/yarn lockfile signals

Pre-fix `filesToCheck` had only 6 entries — `package.json`,
`Dockerfile`, `requirements.txt`, `go.mod`, `pom.xml`, `Cargo.toml`.
The detector iterated these and pushed found names into
`detectedFiles`, but the JS-ecosystem branch in `detectJsFramework`
checked `detectedFiles.includes('pnpm-lock.yaml')` /
`yarn.lock` — neither of which was in `filesToCheck`. So the
package-manager guess always fell through to npm regardless of
which lockfile the repo actually had.

Fix: add the three JS lockfiles to `filesToCheck` so they show up
in `detectedFiles` when present. Three extra Contents-API GETs per
detection call; cheap given GitHub's rate limits and one-shot
nature of this path.

Tests:
- pnpm-lock.yaml present → packageManager='pnpm',
  installCommand='pnpm install --frozen-lockfile'.
- yarn.lock present → packageManager='yarn',
  installCommand='yarn install --frozen-lockfile'.
- package-lock.json present → packageManager='npm' (default fall-
  through preserved; ladder has no explicit branch for it).
- No lockfile → packageManager='npm' (still works).
- pnpm + yarn both present → pnpm wins (ladder priority pinned).
- Removed the test that pinned the broken behavior ("package.json
  defaults to npm package manager (lock files are not in
  detectedFiles list)") — replaced by the corrected
  pnpm/yarn/no-lockfile triplet.

Bundles the SHA fixup for the bugfix-3 `_Fixed:_` line in
state/learnings.md.

Discovered during rf-pipe-6 (extraction of `framework-detection.ts`
from `pipeline.service.ts`); preserved verbatim then per the
verbatim-preservation rule for refactors. Now fixed as a
behaviour-change PR per the rule that pre-existing bugs documented
in learnings get separate fix tickets.

* learnings: append bugfix-2/3/4 sweep gotcha (Fixed-line audit trail)

Capture the rule of thumb that emerged from the bugfix sweep: only
the rf-galg-4 quirk had a dedicated learning anchor; the other two
bugs were documented inline in their SUT header comments. The
brief assumed every bug had its own anchor for the `_Fixed:_`
audit trail; reality is sparser and the implementer should pick
between inline-doc and learning-anchor based on whether the
deferred fix is a real future ticket.

* progress: archive entry for completed LOC discipline initiative

Multi-day refactor work (2026-04-29 → 2026-05-02) marked complete in
the Archive section of progress.md. Cross-references to phase totals,
bug fixes, and the new /docs/refactoring-patterns.md doc.

Cumulative: ~470 commits, ~7500 new tests, 73 files refactored,
4 latent bugs fixed, 1 generated file excluded, 4 data-leaf
exceptions remaining (documented).

Codebase decomposition is complete. Pausing here.

* progress: housekeep In flight, move pdl + rf-* detail trail to Archive

Reduces progress.md from 322 → 216 lines. In flight section now contains
just the 6 deferred follow-ups (pdl-11, rollupPercentage, nodesById warm-
seed, dead snapshot fields, data.status fallback, 3 rf-0c dedups). All
pdl-1..10 and rf-deploy/rf-props/rf-canv/rf-pdpl/rf-ctrans/rf-cards/
rf-fbh/rf-parse subsections moved verbatim under Archive so anchor/commit
references remain searchable.

* decisions: 2026-05-02 merge-story entry for refactoring branch

Document the choice to merge the 509-commit refactoring branch as a
single PR rather than splitting per phase. Rationale and alternatives
captured in the decision entry; consequences note that deferred follow-
ups (pdl-11 etc.) ship as separate PRs after the merge.

* fix(test): cast source_node_id read in apply-pipeline-helpers test

The helper mutates result.resources[i].source_node_id at runtime via
res: any. The test fixture's inline literal infers a strict type
without source_node_id, so the post-call assertion fails TS2834.
Add { source_node_id?: string } cast at the two assertion sites to
match the runtime mutation contract.

* state: archive pre-compaction snapshot of learnings.md (2026-Q2)

Verbatim copy of learnings.md before the Q2-2026 compaction pass.
Per CLAUDE.md schedule: cluster duplicates once per quarter, archive
the pre-compaction file as state/archive/learnings-YYYY-Qn.md, then
write the compacted version back. The compacted version lands in the
next commit.

204 anchors / 1676 lines preserved here for reference.

* state: compact learnings.md (Q2-2026 pass)

Cluster duplicates and trim redundancy after the parallel-deploy +
LOC-discipline initiatives. 204 → 113 anchors (-44%), 1676 → 780
lines (-53%). All 25 must-preserve anchors retained verbatim (the 24
referenced from /docs/refactoring-patterns.md plus the read-state-first
anchor cited in decisions.md and CLAUDE.md). Both _Promoted to:_ and
_Fixed:_ trailers preserved verbatim.

Representative cluster merges (canonical ← folded):
- ux-log-terminal-pitfalls ← 5 ux-log-* siblings
- ux-deploy-real-cloud-pitfalls ← 3 ux-deploy-* siblings
- pdl-7-wire-contract-trims-downstream-ui ← 3 pdl-7 follow-ups
- pdl-10-destroy-snapshot-and-dedup-traps ← 3 pdl-10 follow-ups
- ux-pdl-smoke-test-pitfalls ← 3 ux-pdl-* siblings
- inline-classification-duplications-are-not-actually-duplicates
  ← 4 sibling rf-canv anchors about inverted tie-breaks
- test-helper-defaults-traps-coalesce-and-spread ← 3 helper-default traps
- brief-numerics-are-approximate-source-is-canonical ← absorbed 3 brief-vs-source variants

Pre-compaction snapshot archived in the prior commit at
state/archive/learnings-2026-Q2.md.

* pdl-11: default node.data.provider to active deploy provider on drop

Closes the deferred follow-up from the parallel-deploy initiative
recorded in state/progress.md In flight. Palette drops without an
explicit `application/ice-block-provider` key (or with the 'all'
sentinel) now fall back to `state.deploy.provider`, so the deploy
panel doesn't filter newly-dropped blocks as "skipped — non-<provider>"
when the toolbar provider is set.

Three drop branches:
- Group drop: unchanged (groups have no provider).
- Block drop: paletteProvider 'all' or unset → effectiveProvider
  becomes deployProvider; threaded into both getBlueprint and
  expandBlueprint. Explicit palette providers (aws/azure) still win.
- Resource drop: newNodeData.provider is set to deployProvider.

logBlueprint still records the *palette* provider (analytics tracks
user intent, not the post-fallback value).

Tests: 21 → 23 in use-canvas-drop.test.tsx (+ 2 pdl-11 cases:
fallback-when-palette-omits, palette-wins-over-deploy-provider). One
existing test assertion updated from `provider: 'all'` to `provider:
'gcp'` to match the new behavior. Resource-drop case asserts
`data.provider === 'gcp'` (default deploy provider in the test store).

* deploy: extract deriveRollupPercentage helper from 3 inline copies

Closes pdl-5 critic findings #2 and #4 (deferred to follow-up). The
cap-at-99 progress formula was inlined identically in three places:
  - features/deploy/components/deploy-in-flight-panel.tsx:50
  - features/canvas/components/deploy-banner.tsx:94
  - shared/components/status-bar.tsx:216

Extracted to deploy/derive.ts next to deriveRollup as a pure projection
from DeployRollup → 0..100 percentage. Re-exported from deploy-slice
alongside deriveRollup / orderNodesForPanel so existing import paths
keep resolving.

Tests: 5 new dedicated cases for deriveRollupPercentage (empty / full /
cap-at-99 boundary / rounding / defensive zero-total). The
deploy-in-flight-panel.test mock switched to importOriginal +
selective override so deriveRollupPercentage runs real (it's a pure
projection from the rollup the test already controls via
mocks.deriveRollup) — saves maintaining a parallel mock that has to
mirror the cap formula.

19 → 19 derive tests (+5), 53 → 53 deploy-in-flight tests (refactor
preserves behavior), 0 typecheck regressions.

* deploy: warm-seed nodesById from snapshot in Phase 2 (pdl-5 #7)

Closes pdl-5 critic finding #7 (deferred to follow-up). When a tab
joins mid-deploy, Phase 2 of useDeploySubscription pulls the persisted
snapshot from the gateway and was previously only mirroring the
overlay onto the canvas blocks. The deploy panel's per-row list,
which derives from `nodesById`, stayed empty until Phase 2.5's replay
loop completed — leaving a brief "Preparing…" sentinel window even
though the per-block overlays already showed the right colors.

The warm-seed dispatches synthetic `node_status` events (and
`node_progress` when the snapshot carries a step descriptor) for each
node in `snapshot.nodeStatuses`, so the panel's per-row list renders
immediately.

Synthetic events use seq=0 so any live event (or replayed event from
Phase 2.5) with seq>0 dedup-wins on the same node, overwriting the
warm-seed entry's empty resource_name / resource_type / action with
the authoritative wire values.

New helper: overlayToWireStatus inverts mapWireStatusToOverlay
(snapshot stores the post-mapping overlay string; warm-seed walks it
back to a wire DeployNodeStatus). Returns null for overlay strings
that don't round-trip — e.g. 'destroying' / 'gone' from pre-pdl-10
destroy paths — so warm-seed skips them and Phase 2.5 fills them in
from the event tape.

Tests: 14 → 17 in use-deploy-subscription.test.ts (+3 for
overlayToWireStatus: full inverse map, null fallback for unknown
strings, round-trip through mapWireStatusToOverlay).

* deploy: drop DeployProgressSnapshot dead fields

The frontend stopped reading snapshot.progress, snapshot.currentResource,
and snapshot.currentStep when pdl-5 rewired the deploy panel + canvas
banner to derive every in-flight signal from `nodesById` (populated by
the typed node_status / node_progress wire). The server-side writers
have been writing dead state ever since.

Removed:
- progress: number, currentResource?, currentStep? from
  DeployProgressSnapshot interface (deploy-locks.ts).
- progress: 0 seed in startDeploySnapshot.
- progress: 100 stamp in finishDeploySnapshot.
- updateDeploySnapshot helper (no remaining callers after this drop).
- progress / currentResource writes in scheduler-callbacks (the totals
  count bump is preserved — callers still read it after the deploy
  returns).
- progress: 0 in canvas-deploy.ts /current/:cardId fallback.

The Json column on the Prisma `canvas_deployment.snapshot` field is
schemaless, so removing keys from the TS type doesn't migrate. Old DB
rows still carry these fields; new rows won't. The shared-modules
registry entry…
Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.4.3.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](colinhacks/zod@v3.25.76...v4.4.3)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.4.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels May 19, 2026
julia-kafarska added a commit that referenced this pull request May 20, 2026
Findings #15: when production already existed the function
returned the existing row unconditionally, regardless of whether
the caller's `existingCardId` matched. A stale callsite that
thought it was seeding a fresh env with its own card got the OLD
env back unchanged — silently — and trusted the response.

Loud failure is the safer mode: when `existingCardId` is provided
and the existing env's `card_id` differs, throw with both card_ids
in the message so the caller can diagnose. Match-or-no-arg keeps
the idempotent fast path (no transaction).

57 tests pass; added a positive matches-existing test and a
negative mismatch-throws test.
@dependabot @github
Copy link
Copy Markdown
Author

dependabot Bot commented on behalf of github May 20, 2026

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot dependabot Bot deleted the dependabot/npm_and_yarn/zod-4.4.3 branch May 20, 2026 22:42
julia-kafarska added a commit that referenced this pull request May 25, 2026
* fix(canvas): correct snap target + drag-snap stickiness for Custom Domain rows

Two bugs in connection drag:
1. dragCompatibility positions used getPortAnchorPoint (schema's
   side-distribution math) instead of getSocketCanvasPosition, so the
   snap target Y on Custom Domain row ports drifted progressively
   from the visible dot (~50px on row 0, ~0px on the last row).
2. snap was computed from currentPoint, which itself is set to the
   snapped port's position. Distance-to-self stayed at 0 so the snap
   locked onto the first port that ever won. Added cursorPoint to
   DrawingConnectionState and compute snap from it; currentPoint
   remains the visible (snapped or cursor) wire endpoint.

* refactor(secret-store): schema-driven 1->N deploy expansion

Properties + deploy model rebuilt around the canonical schema:

- Properties: name -> "Store name"; secrets list -> new `secret_bindings`
  field with two inputs per row (env-var key ← upstream ref); auto_rotate
  dropped (was inert + wrong-scoped). Bindings explained: the block does
  NOT hold secret values — values live in the cloud secret manager.
- Schema: add optional `iceType` and `deployExpansion` to
  HighLevelResource. Secret Store declares
  `deployExpansion: { partitionBy: 'bindings', nameFrom: { field: 'ref',
  fallback: 'key' }, labelFrom: 'key', tagPerEntry: ... }`.
- Lookup: getHighLevelResourceByIceType helper with a cached index.
- Generic pass: new deploy/passes/deploy-expansion.ts emits one cloud
  resource per partition entry, dedup'd within/across blocks, forwarding
  provider-shaped properties verbatim. Knows nothing about secrets.
- Translator: the previous `if (iceType === 'Security.Secret')` branch
  is replaced with `if (schemaResource?.deployExpansion)` — cardinal
  rule, no iceType hardcoded in cross-cutting code.

Adding AWS Secrets Manager or Azure Key Vault requires only an extractor
+ handler for the provider's resource type. Adding a new expanding block
requires only a `deployExpansion` declaration on its schema entry.

* refactor(canvas-renderer): drop hardcoded iceType branches via SPECIAL_NODE_RENDERERS table

Audit item #11 — cardinal rule violation. The dispatcher had three
hardcoded `if (iceType === 'X')` branches for Custom Domain, Reroute,
and Private Network, each wiring a bespoke component with its own
prop set + innerKey formula.

Consolidated into a single declarative `SPECIAL_NODE_RENDERERS` table
keyed by iceType, with factory entries that own their own component
AND innerKey formula. The dispatcher iterates this table generically
— no iceType-specific code paths remain. Adding a new bespoke renderer
extends the table; the dispatcher stays unchanged.

Dispatch order preserved by construction: the special table is
consulted BEFORE the container check so PrivateNetwork (a container
we render with a custom header) and Reroute (which the classifier
calls a container despite being a pass-through dot) hit their
bespoke factories first.

Tests: locked-in entries list + per-entry contract (element + innerKey)
+ innerKey-changes-on-relevant-data assertions for Custom Domain
(routes count) and PrivateNetwork (ingress mode).

* refactor(canvas-path): drop hardcoded iceType in socket-position via BESPOKE_SOCKET_POSITIONS table

Audit item #12 — cardinal rule violation. `getSocketCanvasPosition`
had an `if (iceType === 'Network.CustomDomain' && socketId.startsWith
('domain-out-'))` branch that resolved the bespoke row-Y for per-route
ports.

Consolidated into a `BESPOKE_SOCKET_POSITIONS` table keyed by iceType,
with resolver entries returning `Point | null` (null = fall through to
the standard layout). The dispatcher iterates this table generically
— no iceType branches in the resolver function. New bespoke layouts
register here; dispatch stays unchanged.

Tests: locked-in entries list, per-resolver contract (returns null on
miss), and dispatcher behaviour (bespoke hit, fall-through, dangling
socket id).

* refactor(properties): schema-drive tab visibility + deployment-target skip

Audit item #13 — cardinal rule violation. The tab builder and the
deployment-target card both branched on hardcoded iceType strings:

  - build-visible-tabs.ts:41-46 — config tab visible for 4 iceTypes
  - build-visible-tabs.ts:53    — domain tab visible for 2 iceTypes
  - build-visible-tabs.ts:56    — source tab visible for Source.Repository
  - node-properties-section.tsx:166 — deployment target hidden for 2 iceTypes

Consolidated into a single declarative table
`BLOCK_PROPERTY_PANEL_CONFIGS` keyed by iceType, with per-block
`forceTabs` and `skipDeploymentTarget` flags. Both the builder and the
panel iterate this table generically — no iceType branches remain.
Adding a bespoke panel experience adds an entry; both call sites pick
it up.

Per-tab SECTION rendering (CustomDomainPanel, EnvVarsEditor, etc.)
inside the panel body is still iceType-conditioned — covered by audit
item #14 in the next commit, which extends this same config table.

* refactor(properties): schema-drive per-tab section dispatch via SECTION_COMPONENTS

Audit item #14 — cardinal rule violation. The panel body had six
hardcoded `iceType === 'X'` branches choosing which bespoke section
component to render inside which tab:

  - domain tab: PublicEndpointDomainSection / CustomDomainPanel
  - config tab: EnvVarsEditor / CustomDomainPanel / PrivateNetworkPanel
                / MonitoringLogSection
  - source tab: SourceRepositorySection
  - config tab fallback: SourceRepositorySection when no source tab

Extended BLOCK_PROPERTY_PANEL_CONFIGS with a `sections: Record<TabId,
SectionId[]>` field. Each iceType declares which sections render under
which tabs; the new SECTION_COMPONENTS factory map renders them.
`renderSectionsForTab(iceType, tab, ctx)` is the generic dispatcher
the JSX calls — no iceType branches remain.

Dropped the dead `visibleTabs.length <= 1 && iceType === 'Source.
Repository'` config-tab fallback: with the source tab now always forced
for that block via forceTabs, the fallback could never fire.

Tests: per-iceType set + section-id-tab validity check.

* refactor(canvas-sizing): schema-drive bespoke node sizing via BESPOKE_NODE_SIZING table

Audit item #16 — cardinal rule violation. computeNodeSizes had three
hardcoded iceType checks driving the width/height/fold dispatch:

  - isCustomDomain = iceType === 'Network.CustomDomain'
  - isPrivateNetwork = isPrivateNetworkIce(iceType)
  - isCronJob = iceType === 'Compute.CronJob'

Plus 4 nested ternaries threading those flags through width, height,
expandedHeight, and visualHeight.

Consolidated into BESPOKE_NODE_SIZING — a Record<iceType,
BespokeSizingEntry> where each entry owns its width function, height
function, and an `alwaysExpanded` flag that opts the block out of
folding (so dynamic content like Custom Domain route slots can't
collapse to a pill).

The dispatcher does a single table lookup, falls through to the
compact-node helpers when no bespoke entry exists, and respects
`alwaysExpanded` uniformly. No iceType branches remain.

Tests: locked-in entries list + alwaysExpanded invariant.

* refactor(deploy/edge-classifier): schema-drive isolation + standalone classification

Audit items #5 + #6 — cardinal rule violations. Three hardcoded
iceType checks in cross-cutting classifier code:

  - edge-classifier.ts:65  — `parent.data?.iceType === 'Network.PrivateNetwork'`
                             in the ancestor walk
  - edge-classifier.ts:90  — guard: `iceType !== 'Network.CustomDomain'`
  - edge-classifier.ts:93  — `parent.iceType !== 'Network.PrivateNetwork'`
                             in the standalone-mode check

Introduced `BLOCK_DEPLOY_CLASSIFIERS` — a per-iceType flag table with
two flags:

  - `isolatesNetworkContext`: this iceType is a network-isolation
    container (services nested inside should be internal-only)
  - `metadataOnlyWhenStandalone`: this iceType has two deploy modes
    based on parent context (metadata-only standalone vs. deployable
    when nested in an isolation container)

Renamed predicates to match the generic shape:
  - `hasPrivateNetworkAncestor` -> `hasNetworkIsolatingAncestor`
  - `isCustomDomainStandalone`  -> `isStandaloneMetadataOnly`

Old names kept as `@deprecated` aliases so external callers and tests
don't break. Card-translator call sites switched to the new names.
Adding a new isolation container or a new standalone/nested block
adds a table entry; classifier code stays unchanged.

* refactor(deploy/passes): schema-drive public-ingress detection + domain propagation

Audit items #7 + #8 — cardinal rule violations in two passes:

  - pass-1-5-endpoint-wiring.ts:107-114 — inline `isEndpointIceType`
    branched on Network.PublicEndpoint AND Network.CustomDomain (with
    nested-in-PrivateNetwork check) to identify ingress endpoints
  - pass-1-45-domain-propagation.ts:55-58 — hardcoded srcIce / dstIce
    === 'Network.CustomDomain' to route domain propagation

Extended BLOCK_DEPLOY_CLASSIFIERS with two new flags:

  - `publicIngressMode`: 'always' (PublicEndpoint) or
    'when-nested-in-isolated-network' (CustomDomain — only counts as
    ingress when nested inside an isolatesNetworkContext container)
  - `isDomainPropagator`: true for CustomDomain — generic name so any
    future domain-source block can flow through the same pass

Added generic `isPublicIngressNode(node, allNodes)` predicate to
edge-classifier that reads the flag table. Refactored pass-1-5 to
call it; pass-1-45 reads `isDomainPropagator` directly. No iceType
strings remain in either pass.

Tests: 3 new flag assertions + 5 new isPublicIngressNode behaviour
cases (always-mode, standalone CD, nested-in-isolated-network, nested-
in-non-isolation, plain compute).

* refactor(deploy/security-rules): schema-drive iceType classifiers via SECURITY_ROLES table

Audit item #15 — cardinal rule violation. The pre-deploy security
scanner had 9+ tiny iceType-comparing classifier functions
(`isDatabase`, `isStorage`, `isGateway`, `isService`, `isAuth`,
`isSecret`, `isMonitoring`, `isVpc`, `isSubnet`, `isPrivateNetwork`,
`isVpcLike`), each inlining its own `iceType === 'X'` check.

Consolidated into a schema-shaped role table:

  - `SECURITY_ROLES_BY_ICE_TYPE`: per-iceType role list
  - `SECURITY_ROLES_BY_PREFIX`: category-prefix inheritance
    (Database./Compute./Monitoring.* automatically pick up their role
    without per-iceType table edits)
  - `hasSecurityRole(iceType, role)`: the single lookup function the
    classifier readers call

Classifier functions remain as thin one-line role readers; rule
evaluation code is unchanged. New blocks that need a security role
add an entry to the table.

Split the previous `isVpcLike` into two distinct roles:
`isolatesNestedChildren` (VPC + Subnet + PrivateNetwork — used by the
ancestor check) and `topLevelNetworkBoundary` (VPC + PrivateNetwork
only — used by Rule 6, where a Subnet at the canvas root doesn't
isolate anything). The original code conflated these into a single
overloaded helper.

* refactor(classifiers): unify connection-rules + propagation-rules iceType classifiers via shared @ice/constants table

Audit items #9 + #10 — cardinal rule violations across two packages.
Both `@ice/types/connection-rules/predicates.ts` and
`@ice/core/compute/propagation-rules.ts` had ~15 identical-ish
classifier functions (isBackend, isFrontend, isDatabase, isCache,
isStorage, isQueue, isSecrets, isCustomDomain, …), each duplicating
the same regex + prefix + exact-match bodies. The propagation-rules
copy carried a comment apologising for the duplication ("Minimal
copies of the classifiers from @ice/types/connection-rules. Kept
local to avoid cross-package moduleResolution conflicts.").

Introduced `@ice/constants/block-classifiers.ts` as the single source
of truth — a three-tier role table:

  - `BLOCK_ROLES_BY_ICE_TYPE`: exact iceType -> roles
  - `BLOCK_ROLES_BY_PREFIX`:   category prefix -> role (Compute.*,
                               Database.*, Storage.*, Messaging.*,
                               Monitoring.*, Log.*)
  - `BLOCK_ROLES_BY_REGEX`:    legacy provider-specific iceTypes
                               (PostgreSQL/Redis/Bucket/Worker/…)
                               authored under varied namespaces

`hasBlockRole(t, role)` queries all three tiers. Both packages import
it — `predicates.ts` and `propagation-rules.ts` predicate bodies are
now one-line lookups, no iceType strings remain in classifier code.

Adding a new role/iceType binding edits ONE table; both connection-
rules and propagation-rules pick it up automatically.

Tests: full equivalence preserved (4664 tests pass) + new
block-classifiers test suite covering exact / prefix / regex /
composite / negative cases + table integrity.

* refactor(deploy): dedup SERVICE_BACKEND_ICE_TYPES via shared serviceBackend role

Audit items #17 + #18 — duplicated static set in two cross-cutting
locations:

  - edge-classifier.ts:34-40 — exported SERVICE_BACKEND_ICE_TYPES_FOR_INGRESS
  - pass-1-5-endpoint-wiring.ts:188-194 — local SERVICE_BACKEND_ICE_TYPES

Both held the same 5 iceTypes (Compute.Container/BackendAPI/SSRSite/
Worker/ServerlessFunction). Two copies, two sources of drift.

Added a new `serviceBackend` role to the shared classifier table in
@ice/constants. The 5 iceTypes register the role via
BLOCK_ROLES_BY_ICE_TYPE — Compute.StaticSite is intentionally
excluded (compiles to backendBucket via Firebase Hosting, not a NEG).

- edge-classifier exports SERVICE_BACKEND_ICE_TYPES_FOR_INGRESS as a
  thin materialisation derived from the role table (kept for callers
  that want `.has(t)` membership).
- pass-1-5-endpoint-wiring.ts now uses `hasBlockRole(t, 'serviceBackend')`
  directly; its inline duplicate set is gone.

New iceTypes register the role in ONE table; both consumers pick it up.

* refactor(translator): drop hardcoded iceType + provider branches via type-map + override table

Audit items #1 + #2 — two critical violations in the otherwise-
provider-agnostic translator:

  - card-translator.ts:227 — `ice_type === 'Network.CustomDomain'
    ? 'gcp.compute.globalForwardingRule' : type_map[ice_type]`
    (iceType AND GCP-specific in one cross-cutting line)
  - card-translator.ts:313-322 — `if (gcp_type === 'gcp.run.service')
    ... else if (gcp_type === 'aws.ecs.service') ... else if
    (gcp_type === 'azure.containerapp.containerApp') ...` cascade
    applying provider-specific internal-mode mutations inline

#1: Added Network.CustomDomain to each provider's type-map (mirrors
PublicEndpoint — the nested CD acts as the network's gateway and
compiles to the same ingress chain on every provider; standalone CDs
are filtered earlier by isStandaloneMetadataOnly). Translator now does
a plain `type_map[ice_type]` lookup — no iceType branches.

#2: Introduced `internal-ingress-overrides.ts` — a per-provider
mutator table keyed by resolved resource type:

  - gcp.run.service                      -> ingress=internal-and-cloud-load-balancing, allow_unauthenticated=false
  - aws.ecs.service                      -> assign_public_ip=false, internal=true
  - azure.containerapp.containerApp      -> ingress_external=false

Translator calls `applyInternalIngressOverride(resource_type, props)`
generically. The `SERVICE_BACKEND_ICE_TYPES_FOR_INGRESS.has(t)` set
membership is replaced with `hasBlockRole(t, 'serviceBackend')` per
the earlier dedup commit. No provider strings or iceType strings
appear in the translator's body.

New providers add an override entry; the translator stays unchanged.

Tests: existing type-map entry counts bumped by 1 + new CustomDomain
mapping assertions per provider + new internal-ingress-overrides
suite (table contents + per-provider behaviour + no-op fallthrough).

* refactor(deploy): drop hardcoded Compute.StaticSite in storage extractor + endpoint pass

Audit items #3 + #4 — last two cardinal-rule violations:

  - pass-1-5-endpoint-wiring.ts:204 — `if (be.targetIceType === 'Compute.StaticSite')`
    skipped LB wiring for static-site backends (GCP-Firebase-Hosting-specific behaviour)
  - extractors/network.ts:25 — `iceType === 'Compute.StaticSite'` flipped a Storage.Bucket
    extractor's `public_access` + `website_hosting` when the bucket backs a static site

Two schema-shaped declarations replace them, each scoped to its
natural layer:

  #3 -> `self-serving-resources.ts`: a new `SELF_SERVING_PUBLIC_RESOURCES`
       set keyed by RESOLVED PROVIDER RESOURCE TYPE (gcp.firebase.hosting
       today; future: aws.amplify.app, azure.staticwebapps.staticSite).
       The endpoint-wiring pass reads `targetGraphNode.type` and calls
       `isSelfServingPublicResource(...)` — no iceType strings.

  #4 -> New `publicWebsiteSource` role on the shared
       `BLOCK_ROLES_BY_ICE_TYPE` table. Compute.StaticSite registers
       this role; the storage extractor reads it via
       `hasBlockRole(iceType, 'publicWebsiteSource')`. Adding a future
       static-site-style block (Compute.JamstackSite, …) adds one
       table entry — extractor stays unchanged.

Tests:
  - existing pass-1-5 fixtures updated to mark static-site graph nodes
    with `type: 'gcp.firebase.hosting'` (the resolved type the new
    check reads); behavioural assertions unchanged
  - new self-serving-resources test suite covering registered +
    negative cases

With this commit, every audit item in the schema-driven-refactor
punch list is shipped.

* refactor(deploy/aws): modularise AWSDeployer to mirror gcp/ shape

Phase 0 of the AWS deploy buildout. The previous 496-LOC monolithic
aws-deployer.ts is replaced with the same dispatch shape the GCP
deployer uses:

  providers/aws/
    aws-deployer.ts   — thin dispatcher + HANDLER_REGISTRY map
    types.ts          — AWSHandlerContext + AWSResourceHandler
    sdk-loader.ts     — load_aws_sdk + initialize_aws_clients + destroy
    index.ts          — barrel
    handlers/
      ec2.ts          — migrated from aws-deployer.ts (no behaviour change)
      s3.ts           — migrated (account-id suffix arrives in commit #8)
      lambda.ts       — migrated (S3-ref only; auto-build in commit #28)

The old `providers/aws-deployer.ts` becomes a re-export shim so the
existing import paths in `providers/index.ts` and the test suite keep
resolving without edits.

Cardinal-rule schema-driven: HANDLER_REGISTRY is the single declarative
fact for "which handler runs for which resource type". The dispatcher
iterates it generically; no `if (type === 'aws.X')` branches. Adding
the next ~17 AWS services is now register-an-entry + drop-a-file.

Behaviour preserved verbatim — all 64 existing AWS deployer tests pass
unchanged (one minor wording fix in the dispatcher to match the
original "Unsupported resource type for creation/update/deletion"
phrasing the test suite pins).

* feat(deploy/aws): extractors for compute (ecs.service, lambda.function, events.rule)

Phase 1 commit 1/5 — first AWS extractor module.

  extractors/aws/compute.ts (new):
    - extract_ecs_service_properties   ← Compute.Container, Compute.BackendAPI,
                                          Compute.SSRSite, Compute.Worker
    - extract_lambda_function_properties ← Compute.ServerlessFunction
    - extract_events_rule_properties    ← Compute.CronJob

  extractors/dispatch.ts:
    Register the 3 extractors under their resolved aws.* resource types.
    Adds the first AWS section to PROPERTY_EXTRACTORS.

Provider parity notes (extractor only — handlers come later):
  - ECS multi-port uses the shared parse_exposed_ports() so the canvas
    contract matches what Cloud Run sees today.
  - Lambda accepts the nested code.{s3Bucket,s3Key} shape AND falls
    through to the flat s3_bucket / s3_key fields for back-compat with
    the existing Lambda test harness. Auto-build from Source.Repository
    lands in commit #28.
  - EventBridge cron is the 6-field format (not unix 5-field). The
    named "daily"/"hourly"/"weekly"/"monthly" presets that GCP Cloud
    Scheduler accepts are normalised to the AWS expression so cards
    stay provider-portable.

Tests: 24 new assertions covering defaults, passthrough, exposed_ports
parsing, code-source shape variants, cron preset normalisation. The
dispatch table test gets a new shape — counts GCP entries (27)
separately from AWS (>=3, will grow with commits #3#6), accepts
aws.* keys in the {provider}.{service}.{kind} regex.

* feat(deploy/aws): extractors for database (rds, dynamodb, elasticache, docdb)

Phase 1 commit 2/5.

  extractors/aws/database.ts (new):
    - extract_rds_db_instance_properties      ← Database.PostgreSQL, MySQL
    - extract_dynamodb_table_properties       ← Database.DynamoDB
    - extract_elasticache_cluster_properties  ← Database.Redis
    - extract_docdb_cluster_properties        ← Database.MongoDB

  extractors/dispatch.ts: register all 4 under aws.* resource types.

Provider parity:
  - RDS engine + version inferred from iceType + runtime string, same
    rule the GCP Cloud SQL extractor uses -> cards stay portable.
  - master_user_password defaults to '' so the handler fails loudly
    rather than provisioning RDS / DocDB with no real credential.
  - ElastiCache exposes ELASTICACHE_REDIS_SIZE_MAP for the canvas
    M-series enum (M1 -> cache.t3.micro, M5 -> cache.m5.xlarge ×2 for HA
    parity with GCP STANDARD_HA).
  - DynamoDB defaults to PAY_PER_REQUEST (AWS-recommended for new
    workloads); PROVISIONED branch emits RCU/WCU only when set.

15 new test assertions across the four resources covering engine
detection, version extraction, size-enum translation, billing-mode
branching, and password defaults.

* feat(deploy/aws): extractors for network (s3, apigateway, cloudfront, elbv2)

Phase 1 commit 3/5.

  extractors/aws/network.ts (new):
    - extract_s3_bucket_properties             ← Storage.Bucket / Storage.ObjectStorage / Compute.StaticSite
    - extract_api_gateway_rest_api_properties  ← Network.Gateway
    - extract_cloudfront_distribution_properties ← Network.PublicEndpoint / Network.CustomDomain
    - extract_elbv2_load_balancer_properties   ← Network.LoadBalancer

  extractors/dispatch.ts: register all 4 under aws.* resource types.

Cross-provider parity:
  - S3 reads the publicWebsiteSource role from the shared block-
    classifier table; same flip-policy the GCP cloud_storage extractor
    uses for Compute.StaticSite. Plain Storage.Bucket stays private.
  - CloudFront defaults to HTTPS + auto-cert + PriceClass_100 (most-
    common cost-aware preset). Cert provisioning in us-east-1 is the
    handler's job in commit #19.
  - ELBv2 defaults to internet-facing ALB on HTTPS:443; flips to
    `internal` scheme when `internal: true` (parity with the
    INTERNAL_INGRESS_OVERRIDES table semantics).

11 new tests + dispatch table assertion updated (the previous
"aws.s3.bucket is intentionally absent" assertion is replaced with
a generic "aws.unknown.thing" check now that S3 has landed).

* feat(deploy/aws): extractors for ancillary (sqs, sns, cognito, secrets, cw-logs)

Phase 1 commit 4/5.

  extractors/aws/ancillary.ts (new):
    - extract_sqs_queue_properties                ← Messaging.Queue
    - extract_sns_topic_properties                ← Messaging.Topic / CloudPubSub
    - extract_cognito_user_pool_properties        ← Security.Identity
    - extract_secrets_manager_secret_properties   ← Security.Secret
    - extract_cloudwatch_log_group_properties     ← Monitoring.Log

  extractors/dispatch.ts: register all 5 under aws.* resource types.

Notable:
  - SQS content_based_deduplication is only emitted on FIFO queues
    (AWS rejects the field on standard SQS).
  - Secrets Manager extractor forwards data.secrets as `bindings` —
    same shape the schema-declared deploy-expansion pass already uses
    for GCP Secret Manager. Adding AWS doesn't require translator
    changes; the same expansion branch fires.
  - Cognito reads both signInProviders (canvas camelCase) and
    sign_in_providers (snake) so projects authored with either work.

14 new test assertions.

* feat(deploy/aws): extractors for AI/analytics (opensearch, bedrock, sagemaker, redshift)

Phase 1 commit 5/5 — last extractor module. Every aws.* resource type
in AWS_TYPE_MAP now has a registered property extractor.

  extractors/aws/ai.ts (new):
    - extract_opensearch_domain_properties    ← AI.VectorDB
    - extract_bedrock_endpoint_properties     ← AI.LLMGateway
    - extract_sagemaker_endpoint_properties   ← AI.ModelServing
    - extract_redshift_cluster_properties     ← Analytics.DataWarehouse

  extractors/dispatch.ts: register all 4 under aws.* resource types.

Notable defaults:
  - OpenSearch starts cost-conscious: single t3.small.search node with
    encryption-at-rest + node-to-node encryption on. Production users
    flip dedicated_master_enabled + bump instance_count ≥ 3.
  - Bedrock defaults to on-demand Claude 3 Haiku (zero provisioned
    model units -> handler emits no resource). Provisioned throughput
    fires only when model_units > 0.
  - SageMaker defaults to a real-time ml.t2.medium endpoint.
  - Redshift defaults to a single-node dc2.large with the no-default-
    password invariant the RDS + DocDB extractors share.

12 new test assertions. With this commit Phase 1 is complete:
PROPERTY_EXTRACTORS table now has 27 GCP + 20 AWS entries.

* feat(deploy/aws): shared infra — STS account-id resolver + IAM ensure-role helper

Phase 2 commit 1 — the shared helpers later handlers depend on.

  providers/aws/account.ts (new):
    - create_account_id_resolver(region): memoised STS GetCallerIdentity
      caller. First call hits STS, subsequent calls return cached value.
      Concurrent first-calls coalesce into one STS request. Throws a
      clear "install @aws-sdk/client-sts" message when SDK is absent.

  providers/aws/iam-roles.ts (new):
    - ensureManagedRole(region, roleName, trustPolicyJson, managedPolicyArn):
      idempotent GetRole -> CreateRole-on-NoSuchEntity -> AttachRolePolicy
      pattern. Returns the role ARN. Tolerates already-attached
      policies (AlreadyExists swallowed; any other error fatal).
    - ensureEcsTaskExecutionRole(region): convenience wrapper for the
      standard Fargate execution role (consumed by the ECS handler in
      commit #23).

  providers/aws/types.ts:
    AWSHandlerContext gains `ensure_account_id: AccountIdResolver` —
    handlers `await ctx.ensure_account_id()` to get the cached id.

  providers/aws/aws-deployer.ts:
    initialize() wires the resolver into the context. A pre-init stub
    throws "called before initialize()" if a handler tries to use it
    out of band.

  providers/aws/index.ts: re-export the new helpers.

Tests: 9 new — memoisation, concurrent-call coalescing, missing-SDK
error path, missing-Account-field error path, ensureManagedRole
happy-path + create-on-miss + IAM-SDK-missing path.

* feat(deploy/aws): s3 handler — account-id suffix + publicWebsite bucket policy

Handler #8 in Phase 2.

Two upgrades over the Phase 0 baseline:

  1. **Account-id suffix.** S3 bucket names are globally unique
     across all AWS accounts. The handler awaits ctx.ensure_account_id()
     and appends `-{accountId}` to the translator's resource name
     before any SDK call. `ice-myapp-bucket` becomes
     `ice-myapp-bucket-111122223333`, eliminating the global-collision
     class. The provider_id ARN carries the post-suffix name so
     update + delete round-trip cleanly (bucket_name_from_arn parses
     it back out).

  2. **publicWebsite policy.** When the extractor sets `public_access`
     + `website_hosting` (today only Compute.StaticSite triggers this
     via the publicWebsiteSource role from the shared classifier
     table), the handler runs a 4-step create:
       CreateBucket
         -> PutPublicAccessBlock (loosen account-default block)
         -> PutBucketPolicy      (attach the public-read policy)
         -> PutBucketWebsite     (set index/404 pages)
     Plain Storage.Bucket skips all three follow-up commands.

Tests:
  - Existing test harness extended with a makeStsModule mock + a
    FAKE_ACCOUNT_ID constant; the makeFullRegistry now installs STS
    alongside the SDK clients.
  - All existing S3 ARN assertions updated to expect the suffixed form.
  - 3 new tests: account-id suffix lock-in, public-website 4-step
    sequence with policy + website config, plain-bucket negative path.

64 -> 67 AWS deployer tests passing.

* feat(deploy/aws): lambda handler — fail-fast role + code-source validation

Handler #9 in Phase 2.

Hardens the existing Lambda S3-ref handler with two pre-create
validations that turn cryptic AWS API errors into clear messages:

  1. **IAM role required.** The AWS SDK returns "Could not find
     resource ..." when CreateFunction is called with an empty Role
     ARN. The handler now refuses up front with:
     "Lambda function requires an IAM execution role ARN
     (properties.role). Wire one in or use the auto-role helper."

  2. **Code source required.** When neither s3_bucket + s3_key NOR a
     base64 zip_file is supplied, the handler refuses with:
     "Lambda function code source is missing. Provide
     properties.code.{s3Bucket,s3Key} or zip_file (auto-build from
     Source.Repository lands in a later commit)."

Both checks fire before any SDK call, so the failure surfaces in the
deployer's `error` field with full context instead of as an opaque
AWS error.

Tests updated so happy-path Lambda create tests now pass both role
and code source. 2 new tests pin the fail-fast paths.

* feat(deploy/aws): cloudwatch-logs handler + shared _result helpers

Handler #10 in Phase 2.

  providers/aws/handlers/cloudwatch-logs.ts (new):
    - aws.cloudwatch.logGroup handler — CreateLogGroup +
      PutRetentionPolicy (when retention_in_days set) on create.
      PutRetentionPolicy on update. DeleteLogGroup on delete.

  providers/aws/handlers/_result.ts (new):
    - ok / err / sdkMissing helpers shared across all AWS handlers.
      Stops the per-handler result/fail boilerplate copy-paste.

  providers/aws/sdk-loader.ts: load @aws-sdk/client-cloudwatch-logs
    under the 'cloudwatch-logs' client key.

  providers/aws/aws-deployer.ts: register cloudwatch_logs_handler
    in HANDLER_REGISTRY.

Tests: 3 new — create-with-retention, create-without-retention skips
PutRetentionPolicy, delete sequence. Test harness extended with
makeCloudWatchLogsModule + corresponding FakeImportRegistry entry.

* feat(deploy/aws): secrets-manager handler + shared test harness

Handler #11 in Phase 2.

  providers/aws/handlers/secrets-manager.ts (new):
    - aws.secretsmanager.secret handler. Mirrors the GCP Secret
      Manager contract: the schema-declared deploy-expansion pass
      emits one Secret per binding row; this handler just creates /
      updates / deletes ONE. Values are NOT written (operators
      populate via AWS console/CLI — same security tradeoff as GCP).
    - delete uses ForceDeleteWithoutRecovery=true (skips the 30-day
      recovery window — appropriate when ICE removes the binding).

  providers/aws/sdk-loader.ts: load @aws-sdk/client-secrets-manager
    under the 'secrets-manager' client key.

  providers/aws/aws-deployer.ts: register secrets_manager_handler.

  providers/__tests__/_aws-test-harness.ts (new):
    Extracts the Function-constructor stub + generic SDK-mock factory
    out of the original aws-deployer.test.ts so per-handler test
    files stay small. Strips the trailing 'Command' from command
    class names when building the __cmd label so assertions read the
    operation name (`CreateSecret`, not `CreateSecretCommand`).

  providers/__tests__/aws-secrets-manager.test.ts (new): 4 focused
    tests — create returns the SDK ARN, update + delete sequences,
    SDK-not-installed path.

* feat(deploy/aws): sqs handler — CreateQueue/SetQueueAttributes/DeleteQueue, FIFO .fifo suffix

Handler #12 in Phase 2. Standard + FIFO queues. FIFO queues get the
.fifo suffix appended to the name automatically (AWS enforces). 3
focused tests.

* feat(deploy/aws): sns handler — CreateTopic/SetTopicAttributes/DeleteTopic, FIFO .fifo suffix

* feat(deploy/aws): dynamodb handler — CreateTable + key schema + PITR

* feat(deploy/aws): elasticache handler — single-node + replication-group paths

* feat(deploy/aws): rds handler — no-default-password gate + provisioning poll

Handler #16 in Phase 2. CreateDBInstance + 20-min status-poll loop
that respects ctx.abort_signal and reports progress via on_step.
Refuses to create when master_user_password is empty (parity with
the extractor's no-default-password invariant).

* feat(deploy/aws): docdb handler — cluster + per-instance creation

* feat(deploy/aws): cognito handler — user pool with password policy + MFA

* feat(deploy/aws): cloudfront handler — us-east-1 ACM cert + minimal distribution

Handler #19. CloudFront requires ACM certs in us-east-1 regardless
of deploy region; the handler spins up a one-shot ACM client pinned
to us-east-1 for RequestCertificate, then attaches the ARN to the
distribution's ViewerCertificate. Falls back to CloudFrontDefaultCertificate
when ACM SDK is absent.

* feat(deploy/aws): elbv2 handler — LB + skeleton target group

* feat(deploy/aws): api-gateway handler — REST API + default-stage deployment

* feat(deploy/aws): events-rule handler (CronJob) — PutRule + PutTargets

* feat(deploy/aws): ecs handler — auto-cluster + task role + service create

Handler #23. Compute.Container 'just works' on AWS — the handler
idempotently bootstraps ecsTaskExecutionRole + ice-default-cluster
before RegisterTaskDefinition + CreateService. Mirrors the GCP
Cloud Run UX (no cluster to think about).

* feat(deploy/aws): opensearch handler — CreateDomain with cluster/EBS/encryption config

* feat(deploy/aws): bedrock handler — on-demand no-op + provisioned-throughput create

* feat(deploy/aws): sagemaker handler — EndpointConfig + Endpoint, requires model_name

* feat(deploy/aws): redshift handler — CreateCluster + no-default-password gate

* feat(deploy/aws): lambda auto-build from Source.Repository

Phase 3 (commit #28). When a Compute.ServerlessFunction block has a
connected Source.Repository AND no explicit S3 ref, the handler
auto-builds the zip and uploads it before CreateFunction:

  1. git clone --depth 1 --branch <branch> <repo>
  2. npm install --omit=dev (skipped if no package.json)
  3. zip -qr function.zip .
  4. PutObject to ice-bootstrap-{accountId}-{region}/lambda/{name}/{ts}.zip
     (HeadBucket -> CreateBucket if absent)
  5. Stamp s3_bucket + s3_key onto properties and continue.

Local-only — assumes git/npm/zip on the deploy host. AWS CodeBuild
integration deferred to a future commit. Existing manual S3-ref + zip
paths are unchanged; the auto-build branch only fires when
`properties.repository` is set AND no explicit code source exists.

* test(deploy/aws): unskip AWS Type Map block + end-to-end coverage

Phase 4 commit #29. With every aws.* resource type registered in
PROPERTY_EXTRACTORS (commits #2#6), the AWS Type Map test block can
finally turn on. Expanded the iceType matrix from 5 to 19 entries
covering every AWS-mapped block.

New end-to-end test wires Compute.StaticSite + Security.Secret (with
two bindings — exercising the schema-declared deploy-expansion pass)
+ Database.PostgreSQL into a single translator call, asserts the
resulting graph has 4 deployables resolving to s3.bucket /
secretsmanager.secret×2 / rds.dbInstance. The Azure block remains
skipped (deferred to a future Azure handler buildout).

* docs(deploy/aws): provider notes — quirks, assumptions, deferred work

Phase 4 commit #30 — final commit of the AWS buildout.

providers/aws/README.md documents the AWS-specific decisions the 30
commits in this series bake in:

  - architecture (mirrors gcp/, schema-driven HANDLER_REGISTRY)
  - S3 account-id suffix
  - CloudFront us-east-1 cert
  - ECS auto-cluster + task role
  - RDS / DocDB / Redshift no-default-password invariant
  - RDS provisioning poll
  - Lambda auto-build flow (git + npm + zip + bootstrap S3)
  - Bedrock on-demand no-op
  - Secrets Manager values-never-written contract
  - SQS / SNS .fifo suffix
  - SDK packages as optional peer deps
  - test harness layout
  - deferred work (VPC blocks, CodeBuild, drift detection, LocalStack)

Read this before changing any AWS handler.

* feat(aws): selectively enable safe categories via feature flags

Flip PROVIDER_FLAGS.aws.enabled to true with a hand-picked category
map (Storage, Messaging, Cache, Monitoring, Security, Source, Config).
Compute / Frontend / Scheduler / Network / Database / AI / Analytics
stay gated until their concrete unblockers land — ECS VPC blocks,
CloudFront cert-validation flow, update-paths, etc.

README.md gets a Rollout state table documenting why each gated
category is held back and what unblocks it.

Integrity test in packages/constants asserts the per-category map
stays exhaustive, so future CategoryId additions force a deliberate
on/off decision here.

* fix(palette): enable provider dropdown items when any block is available

Replace the project-provider lock on the palette provider dropdown
with an availability check: a provider option is selectable iff at
least one concept has it in providers and its category is enabled
for that provider.

Before: in a GCP project, AWS was greyed in the palette dropdown
even after AWS feature-flag enabled — so users couldn't browse the
AWS catalog from a GCP project.

After: AWS opens as long as it has any available block under the
current PROVIDER_FLAGS — drag-into-project compatibility remains
enforced at the canvas-drop layer.

availableProviderIds is derived in resource-palette.tsx from the
unfiltered component list using isCategoryEnabledForProvider — same
schema-driven gate the component filter already uses.

* docs(architecture): explain how canvas edges become cloud infra

New page docs/architecture/connections-to-cloud.md walks the five-layer
pipeline (connection-rules -> propagation -> type-maps -> extractors ->
handlers) and grounds it with two worked GCP examples:

- Storage.Bucket -> Compute.BackendAPI: env-var injection + IAM binding,
  no edge resource in GCP.
- Compute.CronJob -> Compute.BackendAPI: Cloud Scheduler HTTP target +
  run.invoker IAM binding.

Links the new page from architecture/README.md and the existing
core-engine.md "Computing flows" section so readers landing on either
find their way to the deep dive.

* docs(architecture): explain how canvas edges become cloud infra
julia-kafarska added a commit that referenced this pull request May 25, 2026
* fix(canvas): correct snap target + drag-snap stickiness for Custom Domain rows

Two bugs in connection drag:
1. dragCompatibility positions used getPortAnchorPoint (schema's
   side-distribution math) instead of getSocketCanvasPosition, so the
   snap target Y on Custom Domain row ports drifted progressively
   from the visible dot (~50px on row 0, ~0px on the last row).
2. snap was computed from currentPoint, which itself is set to the
   snapped port's position. Distance-to-self stayed at 0 so the snap
   locked onto the first port that ever won. Added cursorPoint to
   DrawingConnectionState and compute snap from it; currentPoint
   remains the visible (snapped or cursor) wire endpoint.

* refactor(secret-store): schema-driven 1→N deploy expansion

Properties + deploy model rebuilt around the canonical schema:

- Properties: name → "Store name"; secrets list → new `secret_bindings`
  field with two inputs per row (env-var key ← upstream ref); auto_rotate
  dropped (was inert + wrong-scoped). Bindings explained: the block does
  NOT hold secret values — values live in the cloud secret manager.
- Schema: add optional `iceType` and `deployExpansion` to
  HighLevelResource. Secret Store declares
  `deployExpansion: { partitionBy: 'bindings', nameFrom: { field: 'ref',
  fallback: 'key' }, labelFrom: 'key', tagPerEntry: ... }`.
- Lookup: getHighLevelResourceByIceType helper with a cached index.
- Generic pass: new deploy/passes/deploy-expansion.ts emits one cloud
  resource per partition entry, dedup'd within/across blocks, forwarding
  provider-shaped properties verbatim. Knows nothing about secrets.
- Translator: the previous `if (iceType === 'Security.Secret')` branch
  is replaced with `if (schemaResource?.deployExpansion)` — cardinal
  rule, no iceType hardcoded in cross-cutting code.

Adding AWS Secrets Manager or Azure Key Vault requires only an extractor
+ handler for the provider's resource type. Adding a new expanding block
requires only a `deployExpansion` declaration on its schema entry.

* refactor(canvas-renderer): drop hardcoded iceType branches via SPECIAL_NODE_RENDERERS table

Audit item #11 — cardinal rule violation. The dispatcher had three
hardcoded `if (iceType === 'X')` branches for Custom Domain, Reroute,
and Private Network, each wiring a bespoke component with its own
prop set + innerKey formula.

Consolidated into a single declarative `SPECIAL_NODE_RENDERERS` table
keyed by iceType, with factory entries that own their own component
AND innerKey formula. The dispatcher iterates this table generically
— no iceType-specific code paths remain. Adding a new bespoke renderer
extends the table; the dispatcher stays unchanged.

Dispatch order preserved by construction: the special table is
consulted BEFORE the container check so PrivateNetwork (a container
we render with a custom header) and Reroute (which the classifier
calls a container despite being a pass-through dot) hit their
bespoke factories first.

Tests: locked-in entries list + per-entry contract (element + innerKey)
+ innerKey-changes-on-relevant-data assertions for Custom Domain
(routes count) and PrivateNetwork (ingress mode).

* refactor(canvas-path): drop hardcoded iceType in socket-position via BESPOKE_SOCKET_POSITIONS table

Audit item #12 — cardinal rule violation. `getSocketCanvasPosition`
had an `if (iceType === 'Network.CustomDomain' && socketId.startsWith
('domain-out-'))` branch that resolved the bespoke row-Y for per-route
ports.

Consolidated into a `BESPOKE_SOCKET_POSITIONS` table keyed by iceType,
with resolver entries returning `Point | null` (null = fall through to
the standard layout). The dispatcher iterates this table generically
— no iceType branches in the resolver function. New bespoke layouts
register here; dispatch stays unchanged.

Tests: locked-in entries list, per-resolver contract (returns null on
miss), and dispatcher behaviour (bespoke hit, fall-through, dangling
socket id).

* refactor(properties): schema-drive tab visibility + deployment-target skip

Audit item #13 — cardinal rule violation. The tab builder and the
deployment-target card both branched on hardcoded iceType strings:

  - build-visible-tabs.ts:41-46 — config tab visible for 4 iceTypes
  - build-visible-tabs.ts:53    — domain tab visible for 2 iceTypes
  - build-visible-tabs.ts:56    — source tab visible for Source.Repository
  - node-properties-section.tsx:166 — deployment target hidden for 2 iceTypes

Consolidated into a single declarative table
`BLOCK_PROPERTY_PANEL_CONFIGS` keyed by iceType, with per-block
`forceTabs` and `skipDeploymentTarget` flags. Both the builder and the
panel iterate this table generically — no iceType branches remain.
Adding a bespoke panel experience adds an entry; both call sites pick
it up.

Per-tab SECTION rendering (CustomDomainPanel, EnvVarsEditor, etc.)
inside the panel body is still iceType-conditioned — covered by audit
item #14 in the next commit, which extends this same config table.

* refactor(properties): schema-drive per-tab section dispatch via SECTION_COMPONENTS

Audit item #14 — cardinal rule violation. The panel body had six
hardcoded `iceType === 'X'` branches choosing which bespoke section
component to render inside which tab:

  - domain tab: PublicEndpointDomainSection / CustomDomainPanel
  - config tab: EnvVarsEditor / CustomDomainPanel / PrivateNetworkPanel
                / MonitoringLogSection
  - source tab: SourceRepositorySection
  - config tab fallback: SourceRepositorySection when no source tab

Extended BLOCK_PROPERTY_PANEL_CONFIGS with a `sections: Record<TabId,
SectionId[]>` field. Each iceType declares which sections render under
which tabs; the new SECTION_COMPONENTS factory map renders them.
`renderSectionsForTab(iceType, tab, ctx)` is the generic dispatcher
the JSX calls — no iceType branches remain.

Dropped the dead `visibleTabs.length <= 1 && iceType === 'Source.
Repository'` config-tab fallback: with the source tab now always forced
for that block via forceTabs, the fallback could never fire.

Tests: per-iceType set + section-id-tab validity check.

* refactor(canvas-sizing): schema-drive bespoke node sizing via BESPOKE_NODE_SIZING table

Audit item #16 — cardinal rule violation. computeNodeSizes had three
hardcoded iceType checks driving the width/height/fold dispatch:

  - isCustomDomain = iceType === 'Network.CustomDomain'
  - isPrivateNetwork = isPrivateNetworkIce(iceType)
  - isCronJob = iceType === 'Compute.CronJob'

Plus 4 nested ternaries threading those flags through width, height,
expandedHeight, and visualHeight.

Consolidated into BESPOKE_NODE_SIZING — a Record<iceType,
BespokeSizingEntry> where each entry owns its width function, height
function, and an `alwaysExpanded` flag that opts the block out of
folding (so dynamic content like Custom Domain route slots can't
collapse to a pill).

The dispatcher does a single table lookup, falls through to the
compact-node helpers when no bespoke entry exists, and respects
`alwaysExpanded` uniformly. No iceType branches remain.

Tests: locked-in entries list + alwaysExpanded invariant.

* refactor(deploy/edge-classifier): schema-drive isolation + standalone classification

Audit items #5 + #6 — cardinal rule violations. Three hardcoded
iceType checks in cross-cutting classifier code:

  - edge-classifier.ts:65  — `parent.data?.iceType === 'Network.PrivateNetwork'`
                             in the ancestor walk
  - edge-classifier.ts:90  — guard: `iceType !== 'Network.CustomDomain'`
  - edge-classifier.ts:93  — `parent.iceType !== 'Network.PrivateNetwork'`
                             in the standalone-mode check

Introduced `BLOCK_DEPLOY_CLASSIFIERS` — a per-iceType flag table with
two flags:

  - `isolatesNetworkContext`: this iceType is a network-isolation
    container (services nested inside should be internal-only)
  - `metadataOnlyWhenStandalone`: this iceType has two deploy modes
    based on parent context (metadata-only standalone vs. deployable
    when nested in an isolation container)

Renamed predicates to match the generic shape:
  - `hasPrivateNetworkAncestor` → `hasNetworkIsolatingAncestor`
  - `isCustomDomainStandalone`  → `isStandaloneMetadataOnly`

Old names kept as `@deprecated` aliases so external callers and tests
don't break. Card-translator call sites switched to the new names.
Adding a new isolation container or a new standalone/nested block
adds a table entry; classifier code stays unchanged.

* refactor(deploy/passes): schema-drive public-ingress detection + domain propagation

Audit items #7 + #8 — cardinal rule violations in two passes:

  - pass-1-5-endpoint-wiring.ts:107-114 — inline `isEndpointIceType`
    branched on Network.PublicEndpoint AND Network.CustomDomain (with
    nested-in-PrivateNetwork check) to identify ingress endpoints
  - pass-1-45-domain-propagation.ts:55-58 — hardcoded srcIce / dstIce
    === 'Network.CustomDomain' to route domain propagation

Extended BLOCK_DEPLOY_CLASSIFIERS with two new flags:

  - `publicIngressMode`: 'always' (PublicEndpoint) or
    'when-nested-in-isolated-network' (CustomDomain — only counts as
    ingress when nested inside an isolatesNetworkContext container)
  - `isDomainPropagator`: true for CustomDomain — generic name so any
    future domain-source block can flow through the same pass

Added generic `isPublicIngressNode(node, allNodes)` predicate to
edge-classifier that reads the flag table. Refactored pass-1-5 to
call it; pass-1-45 reads `isDomainPropagator` directly. No iceType
strings remain in either pass.

Tests: 3 new flag assertions + 5 new isPublicIngressNode behaviour
cases (always-mode, standalone CD, nested-in-isolated-network, nested-
in-non-isolation, plain compute).

* refactor(deploy/security-rules): schema-drive iceType classifiers via SECURITY_ROLES table

Audit item #15 — cardinal rule violation. The pre-deploy security
scanner had 9+ tiny iceType-comparing classifier functions
(`isDatabase`, `isStorage`, `isGateway`, `isService`, `isAuth`,
`isSecret`, `isMonitoring`, `isVpc`, `isSubnet`, `isPrivateNetwork`,
`isVpcLike`), each inlining its own `iceType === 'X'` check.

Consolidated into a schema-shaped role table:

  - `SECURITY_ROLES_BY_ICE_TYPE`: per-iceType role list
  - `SECURITY_ROLES_BY_PREFIX`: category-prefix inheritance
    (Database./Compute./Monitoring.* automatically pick up their role
    without per-iceType table edits)
  - `hasSecurityRole(iceType, role)`: the single lookup function the
    classifier readers call

Classifier functions remain as thin one-line role readers; rule
evaluation code is unchanged. New blocks that need a security role
add an entry to the table.

Split the previous `isVpcLike` into two distinct roles:
`isolatesNestedChildren` (VPC + Subnet + PrivateNetwork — used by the
ancestor check) and `topLevelNetworkBoundary` (VPC + PrivateNetwork
only — used by Rule 6, where a Subnet at the canvas root doesn't
isolate anything). The original code conflated these into a single
overloaded helper.

* refactor(classifiers): unify connection-rules + propagation-rules iceType classifiers via shared @ice/constants table

Audit items #9 + #10 — cardinal rule violations across two packages.
Both `@ice/types/connection-rules/predicates.ts` and
`@ice/core/compute/propagation-rules.ts` had ~15 identical-ish
classifier functions (isBackend, isFrontend, isDatabase, isCache,
isStorage, isQueue, isSecrets, isCustomDomain, …), each duplicating
the same regex + prefix + exact-match bodies. The propagation-rules
copy carried a comment apologising for the duplication ("Minimal
copies of the classifiers from @ice/types/connection-rules. Kept
local to avoid cross-package moduleResolution conflicts.").

Introduced `@ice/constants/block-classifiers.ts` as the single source
of truth — a three-tier role table:

  - `BLOCK_ROLES_BY_ICE_TYPE`: exact iceType → roles
  - `BLOCK_ROLES_BY_PREFIX`:   category prefix → role (Compute.*,
                               Database.*, Storage.*, Messaging.*,
                               Monitoring.*, Log.*)
  - `BLOCK_ROLES_BY_REGEX`:    legacy provider-specific iceTypes
                               (PostgreSQL/Redis/Bucket/Worker/…)
                               authored under varied namespaces

`hasBlockRole(t, role)` queries all three tiers. Both packages import
it — `predicates.ts` and `propagation-rules.ts` predicate bodies are
now one-line lookups, no iceType strings remain in classifier code.

Adding a new role/iceType binding edits ONE table; both connection-
rules and propagation-rules pick it up automatically.

Tests: full equivalence preserved (4664 tests pass) + new
block-classifiers test suite covering exact / prefix / regex /
composite / negative cases + table integrity.

* refactor(deploy): dedup SERVICE_BACKEND_ICE_TYPES via shared serviceBackend role

Audit items #17 + #18 — duplicated static set in two cross-cutting
locations:

  - edge-classifier.ts:34-40 — exported SERVICE_BACKEND_ICE_TYPES_FOR_INGRESS
  - pass-1-5-endpoint-wiring.ts:188-194 — local SERVICE_BACKEND_ICE_TYPES

Both held the same 5 iceTypes (Compute.Container/BackendAPI/SSRSite/
Worker/ServerlessFunction). Two copies, two sources of drift.

Added a new `serviceBackend` role to the shared classifier table in
@ice/constants. The 5 iceTypes register the role via
BLOCK_ROLES_BY_ICE_TYPE — Compute.StaticSite is intentionally
excluded (compiles to backendBucket via Firebase Hosting, not a NEG).

- edge-classifier exports SERVICE_BACKEND_ICE_TYPES_FOR_INGRESS as a
  thin materialisation derived from the role table (kept for callers
  that want `.has(t)` membership).
- pass-1-5-endpoint-wiring.ts now uses `hasBlockRole(t, 'serviceBackend')`
  directly; its inline duplicate set is gone.

New iceTypes register the role in ONE table; both consumers pick it up.

* refactor(translator): drop hardcoded iceType + provider branches via type-map + override table

Audit items #1 + #2 — two critical violations in the otherwise-
provider-agnostic translator:

  - card-translator.ts:227 — `ice_type === 'Network.CustomDomain'
    ? 'gcp.compute.globalForwardingRule' : type_map[ice_type]`
    (iceType AND GCP-specific in one cross-cutting line)
  - card-translator.ts:313-322 — `if (gcp_type === 'gcp.run.service')
    ... else if (gcp_type === 'aws.ecs.service') ... else if
    (gcp_type === 'azure.containerapp.containerApp') ...` cascade
    applying provider-specific internal-mode mutations inline

#1: Added Network.CustomDomain to each provider's type-map (mirrors
PublicEndpoint — the nested CD acts as the network's gateway and
compiles to the same ingress chain on every provider; standalone CDs
are filtered earlier by isStandaloneMetadataOnly). Translator now does
a plain `type_map[ice_type]` lookup — no iceType branches.

#2: Introduced `internal-ingress-overrides.ts` — a per-provider
mutator table keyed by resolved resource type:

  - gcp.run.service                      → ingress=internal-and-cloud-load-balancing, allow_unauthenticated=false
  - aws.ecs.service                      → assign_public_ip=false, internal=true
  - azure.containerapp.containerApp      → ingress_external=false

Translator calls `applyInternalIngressOverride(resource_type, props)`
generically. The `SERVICE_BACKEND_ICE_TYPES_FOR_INGRESS.has(t)` set
membership is replaced with `hasBlockRole(t, 'serviceBackend')` per
the earlier dedup commit. No provider strings or iceType strings
appear in the translator's body.

New providers add an override entry; the translator stays unchanged.

Tests: existing type-map entry counts bumped by 1 + new CustomDomain
mapping assertions per provider + new internal-ingress-overrides
suite (table contents + per-provider behaviour + no-op fallthrough).

* refactor(deploy): drop hardcoded Compute.StaticSite in storage extractor + endpoint pass

Audit items #3 + #4 — last two cardinal-rule violations:

  - pass-1-5-endpoint-wiring.ts:204 — `if (be.targetIceType === 'Compute.StaticSite')`
    skipped LB wiring for static-site backends (GCP-Firebase-Hosting-specific behaviour)
  - extractors/network.ts:25 — `iceType === 'Compute.StaticSite'` flipped a Storage.Bucket
    extractor's `public_access` + `website_hosting` when the bucket backs a static site

Two schema-shaped declarations replace them, each scoped to its
natural layer:

  #3 → `self-serving-resources.ts`: a new `SELF_SERVING_PUBLIC_RESOURCES`
       set keyed by RESOLVED PROVIDER RESOURCE TYPE (gcp.firebase.hosting
       today; future: aws.amplify.app, azure.staticwebapps.staticSite).
       The endpoint-wiring pass reads `targetGraphNode.type` and calls
       `isSelfServingPublicResource(...)` — no iceType strings.

  #4 → New `publicWebsiteSource` role on the shared
       `BLOCK_ROLES_BY_ICE_TYPE` table. Compute.StaticSite registers
       this role; the storage extractor reads it via
       `hasBlockRole(iceType, 'publicWebsiteSource')`. Adding a future
       static-site-style block (Compute.JamstackSite, …) adds one
       table entry — extractor stays unchanged.

Tests:
  - existing pass-1-5 fixtures updated to mark static-site graph nodes
    with `type: 'gcp.firebase.hosting'` (the resolved type the new
    check reads); behavioural assertions unchanged
  - new self-serving-resources test suite covering registered +
    negative cases

With this commit, every audit item in the schema-driven-refactor
punch list is shipped.

* refactor(deploy/aws): modularise AWSDeployer to mirror gcp/ shape

Phase 0 of the AWS deploy buildout. The previous 496-LOC monolithic
aws-deployer.ts is replaced with the same dispatch shape the GCP
deployer uses:

  providers/aws/
    aws-deployer.ts   — thin dispatcher + HANDLER_REGISTRY map
    types.ts          — AWSHandlerContext + AWSResourceHandler
    sdk-loader.ts     — load_aws_sdk + initialize_aws_clients + destroy
    index.ts          — barrel
    handlers/
      ec2.ts          — migrated from aws-deployer.ts (no behaviour change)
      s3.ts           — migrated (account-id suffix arrives in commit #8)
      lambda.ts       — migrated (S3-ref only; auto-build in commit #28)

The old `providers/aws-deployer.ts` becomes a re-export shim so the
existing import paths in `providers/index.ts` and the test suite keep
resolving without edits.

Cardinal-rule schema-driven: HANDLER_REGISTRY is the single declarative
fact for "which handler runs for which resource type". The dispatcher
iterates it generically; no `if (type === 'aws.X')` branches. Adding
the next ~17 AWS services is now register-an-entry + drop-a-file.

Behaviour preserved verbatim — all 64 existing AWS deployer tests pass
unchanged (one minor wording fix in the dispatcher to match the
original "Unsupported resource type for creation/update/deletion"
phrasing the test suite pins).

* feat(deploy/aws): extractors for compute (ecs.service, lambda.function, events.rule)

Phase 1 commit 1/5 — first AWS extractor module.

  extractors/aws/compute.ts (new):
    - extract_ecs_service_properties   ← Compute.Container, Compute.BackendAPI,
                                          Compute.SSRSite, Compute.Worker
    - extract_lambda_function_properties ← Compute.ServerlessFunction
    - extract_events_rule_properties    ← Compute.CronJob

  extractors/dispatch.ts:
    Register the 3 extractors under their resolved aws.* resource types.
    Adds the first AWS section to PROPERTY_EXTRACTORS.

Provider parity notes (extractor only — handlers come later):
  - ECS multi-port uses the shared parse_exposed_ports() so the canvas
    contract matches what Cloud Run sees today.
  - Lambda accepts the nested code.{s3Bucket,s3Key} shape AND falls
    through to the flat s3_bucket / s3_key fields for back-compat with
    the existing Lambda test harness. Auto-build from Source.Repository
    lands in commit #28.
  - EventBridge cron is the 6-field format (not unix 5-field). The
    named "daily"/"hourly"/"weekly"/"monthly" presets that GCP Cloud
    Scheduler accepts are normalised to the AWS expression so cards
    stay provider-portable.

Tests: 24 new assertions covering defaults, passthrough, exposed_ports
parsing, code-source shape variants, cron preset normalisation. The
dispatch table test gets a new shape — counts GCP entries (27)
separately from AWS (>=3, will grow with commits #3#6), accepts
aws.* keys in the {provider}.{service}.{kind} regex.

* feat(deploy/aws): extractors for database (rds, dynamodb, elasticache, docdb)

Phase 1 commit 2/5.

  extractors/aws/database.ts (new):
    - extract_rds_db_instance_properties      ← Database.PostgreSQL, MySQL
    - extract_dynamodb_table_properties       ← Database.DynamoDB
    - extract_elasticache_cluster_properties  ← Database.Redis
    - extract_docdb_cluster_properties        ← Database.MongoDB

  extractors/dispatch.ts: register all 4 under aws.* resource types.

Provider parity:
  - RDS engine + version inferred from iceType + runtime string, same
    rule the GCP Cloud SQL extractor uses → cards stay portable.
  - master_user_password defaults to '' so the handler fails loudly
    rather than provisioning RDS / DocDB with no real credential.
  - ElastiCache exposes ELASTICACHE_REDIS_SIZE_MAP for the canvas
    M-series enum (M1 → cache.t3.micro, M5 → cache.m5.xlarge ×2 for HA
    parity with GCP STANDARD_HA).
  - DynamoDB defaults to PAY_PER_REQUEST (AWS-recommended for new
    workloads); PROVISIONED branch emits RCU/WCU only when set.

15 new test assertions across the four resources covering engine
detection, version extraction, size-enum translation, billing-mode
branching, and password defaults.

* feat(deploy/aws): extractors for network (s3, apigateway, cloudfront, elbv2)

Phase 1 commit 3/5.

  extractors/aws/network.ts (new):
    - extract_s3_bucket_properties             ← Storage.Bucket / Storage.ObjectStorage / Compute.StaticSite
    - extract_api_gateway_rest_api_properties  ← Network.Gateway
    - extract_cloudfront_distribution_properties ← Network.PublicEndpoint / Network.CustomDomain
    - extract_elbv2_load_balancer_properties   ← Network.LoadBalancer

  extractors/dispatch.ts: register all 4 under aws.* resource types.

Cross-provider parity:
  - S3 reads the publicWebsiteSource role from the shared block-
    classifier table; same flip-policy the GCP cloud_storage extractor
    uses for Compute.StaticSite. Plain Storage.Bucket stays private.
  - CloudFront defaults to HTTPS + auto-cert + PriceClass_100 (most-
    common cost-aware preset). Cert provisioning in us-east-1 is the
    handler's job in commit #19.
  - ELBv2 defaults to internet-facing ALB on HTTPS:443; flips to
    `internal` scheme when `internal: true` (parity with the
    INTERNAL_INGRESS_OVERRIDES table semantics).

11 new tests + dispatch table assertion updated (the previous
"aws.s3.bucket is intentionally absent" assertion is replaced with
a generic "aws.unknown.thing" check now that S3 has landed).

* feat(deploy/aws): extractors for ancillary (sqs, sns, cognito, secrets, cw-logs)

Phase 1 commit 4/5.

  extractors/aws/ancillary.ts (new):
    - extract_sqs_queue_properties                ← Messaging.Queue
    - extract_sns_topic_properties                ← Messaging.Topic / CloudPubSub
    - extract_cognito_user_pool_properties        ← Security.Identity
    - extract_secrets_manager_secret_properties   ← Security.Secret
    - extract_cloudwatch_log_group_properties     ← Monitoring.Log

  extractors/dispatch.ts: register all 5 under aws.* resource types.

Notable:
  - SQS content_based_deduplication is only emitted on FIFO queues
    (AWS rejects the field on standard SQS).
  - Secrets Manager extractor forwards data.secrets as `bindings` —
    same shape the schema-declared deploy-expansion pass already uses
    for GCP Secret Manager. Adding AWS doesn't require translator
    changes; the same expansion branch fires.
  - Cognito reads both signInProviders (canvas camelCase) and
    sign_in_providers (snake) so projects authored with either work.

14 new test assertions.

* feat(deploy/aws): extractors for AI/analytics (opensearch, bedrock, sagemaker, redshift)

Phase 1 commit 5/5 — last extractor module. Every aws.* resource type
in AWS_TYPE_MAP now has a registered property extractor.

  extractors/aws/ai.ts (new):
    - extract_opensearch_domain_properties    ← AI.VectorDB
    - extract_bedrock_endpoint_properties     ← AI.LLMGateway
    - extract_sagemaker_endpoint_properties   ← AI.ModelServing
    - extract_redshift_cluster_properties     ← Analytics.DataWarehouse

  extractors/dispatch.ts: register all 4 under aws.* resource types.

Notable defaults:
  - OpenSearch starts cost-conscious: single t3.small.search node with
    encryption-at-rest + node-to-node encryption on. Production users
    flip dedicated_master_enabled + bump instance_count ≥ 3.
  - Bedrock defaults to on-demand Claude 3 Haiku (zero provisioned
    model units → handler emits no resource). Provisioned throughput
    fires only when model_units > 0.
  - SageMaker defaults to a real-time ml.t2.medium endpoint.
  - Redshift defaults to a single-node dc2.large with the no-default-
    password invariant the RDS + DocDB extractors share.

12 new test assertions. With this commit Phase 1 is complete:
PROPERTY_EXTRACTORS table now has 27 GCP + 20 AWS entries.

* feat(deploy/aws): shared infra — STS account-id resolver + IAM ensure-role helper

Phase 2 commit 1 — the shared helpers later handlers depend on.

  providers/aws/account.ts (new):
    - create_account_id_resolver(region): memoised STS GetCallerIdentity
      caller. First call hits STS, subsequent calls return cached value.
      Concurrent first-calls coalesce into one STS request. Throws a
      clear "install @aws-sdk/client-sts" message when SDK is absent.

  providers/aws/iam-roles.ts (new):
    - ensureManagedRole(region, roleName, trustPolicyJson, managedPolicyArn):
      idempotent GetRole → CreateRole-on-NoSuchEntity → AttachRolePolicy
      pattern. Returns the role ARN. Tolerates already-attached
      policies (AlreadyExists swallowed; any other error fatal).
    - ensureEcsTaskExecutionRole(region): convenience wrapper for the
      standard Fargate execution role (consumed by the ECS handler in
      commit #23).

  providers/aws/types.ts:
    AWSHandlerContext gains `ensure_account_id: AccountIdResolver` —
    handlers `await ctx.ensure_account_id()` to get the cached id.

  providers/aws/aws-deployer.ts:
    initialize() wires the resolver into the context. A pre-init stub
    throws "called before initialize()" if a handler tries to use it
    out of band.

  providers/aws/index.ts: re-export the new helpers.

Tests: 9 new — memoisation, concurrent-call coalescing, missing-SDK
error path, missing-Account-field error path, ensureManagedRole
happy-path + create-on-miss + IAM-SDK-missing path.

* feat(deploy/aws): s3 handler — account-id suffix + publicWebsite bucket policy

Handler #8 in Phase 2.

Two upgrades over the Phase 0 baseline:

  1. **Account-id suffix.** S3 bucket names are globally unique
     across all AWS accounts. The handler awaits ctx.ensure_account_id()
     and appends `-{accountId}` to the translator's resource name
     before any SDK call. `ice-myapp-bucket` becomes
     `ice-myapp-bucket-111122223333`, eliminating the global-collision
     class. The provider_id ARN carries the post-suffix name so
     update + delete round-trip cleanly (bucket_name_from_arn parses
     it back out).

  2. **publicWebsite policy.** When the extractor sets `public_access`
     + `website_hosting` (today only Compute.StaticSite triggers this
     via the publicWebsiteSource role from the shared classifier
     table), the handler runs a 4-step create:
       CreateBucket
         → PutPublicAccessBlock (loosen account-default block)
         → PutBucketPolicy      (attach the public-read policy)
         → PutBucketWebsite     (set index/404 pages)
     Plain Storage.Bucket skips all three follow-up commands.

Tests:
  - Existing test harness extended with a makeStsModule mock + a
    FAKE_ACCOUNT_ID constant; the makeFullRegistry now installs STS
    alongside the SDK clients.
  - All existing S3 ARN assertions updated to expect the suffixed form.
  - 3 new tests: account-id suffix lock-in, public-website 4-step
    sequence with policy + website config, plain-bucket negative path.

64 → 67 AWS deployer tests passing.

* feat(deploy/aws): lambda handler — fail-fast role + code-source validation

Handler #9 in Phase 2.

Hardens the existing Lambda S3-ref handler with two pre-create
validations that turn cryptic AWS API errors into clear messages:

  1. **IAM role required.** The AWS SDK returns "Could not find
     resource ..." when CreateFunction is called with an empty Role
     ARN. The handler now refuses up front with:
     "Lambda function requires an IAM execution role ARN
     (properties.role). Wire one in or use the auto-role helper."

  2. **Code source required.** When neither s3_bucket + s3_key NOR a
     base64 zip_file is supplied, the handler refuses with:
     "Lambda function code source is missing. Provide
     properties.code.{s3Bucket,s3Key} or zip_file (auto-build from
     Source.Repository lands in a later commit)."

Both checks fire before any SDK call, so the failure surfaces in the
deployer's `error` field with full context instead of as an opaque
AWS error.

Tests updated so happy-path Lambda create tests now pass both role
and code source. 2 new tests pin the fail-fast paths.

* feat(deploy/aws): cloudwatch-logs handler + shared _result helpers

Handler #10 in Phase 2.

  providers/aws/handlers/cloudwatch-logs.ts (new):
    - aws.cloudwatch.logGroup handler — CreateLogGroup +
      PutRetentionPolicy (when retention_in_days set) on create.
      PutRetentionPolicy on update. DeleteLogGroup on delete.

  providers/aws/handlers/_result.ts (new):
    - ok / err / sdkMissing helpers shared across all AWS handlers.
      Stops the per-handler result/fail boilerplate copy-paste.

  providers/aws/sdk-loader.ts: load @aws-sdk/client-cloudwatch-logs
    under the 'cloudwatch-logs' client key.

  providers/aws/aws-deployer.ts: register cloudwatch_logs_handler
    in HANDLER_REGISTRY.

Tests: 3 new — create-with-retention, create-without-retention skips
PutRetentionPolicy, delete sequence. Test harness extended with
makeCloudWatchLogsModule + corresponding FakeImportRegistry entry.

* feat(deploy/aws): secrets-manager handler + shared test harness

Handler #11 in Phase 2.

  providers/aws/handlers/secrets-manager.ts (new):
    - aws.secretsmanager.secret handler. Mirrors the GCP Secret
      Manager contract: the schema-declared deploy-expansion pass
      emits one Secret per binding row; this handler just creates /
      updates / deletes ONE. Values are NOT written (operators
      populate via AWS console/CLI — same security tradeoff as GCP).
    - delete uses ForceDeleteWithoutRecovery=true (skips the 30-day
      recovery window — appropriate when ICE removes the binding).

  providers/aws/sdk-loader.ts: load @aws-sdk/client-secrets-manager
    under the 'secrets-manager' client key.

  providers/aws/aws-deployer.ts: register secrets_manager_handler.

  providers/__tests__/_aws-test-harness.ts (new):
    Extracts the Function-constructor stub + generic SDK-mock factory
    out of the original aws-deployer.test.ts so per-handler test
    files stay small. Strips the trailing 'Command' from command
    class names when building the __cmd label so assertions read the
    operation name (`CreateSecret`, not `CreateSecretCommand`).

  providers/__tests__/aws-secrets-manager.test.ts (new): 4 focused
    tests — create returns the SDK ARN, update + delete sequences,
    SDK-not-installed path.

* feat(deploy/aws): sqs handler — CreateQueue/SetQueueAttributes/DeleteQueue, FIFO .fifo suffix

Handler #12 in Phase 2. Standard + FIFO queues. FIFO queues get the
.fifo suffix appended to the name automatically (AWS enforces). 3
focused tests.

* feat(deploy/aws): sns handler — CreateTopic/SetTopicAttributes/DeleteTopic, FIFO .fifo suffix

* feat(deploy/aws): dynamodb handler — CreateTable + key schema + PITR

* feat(deploy/aws): elasticache handler — single-node + replication-group paths

* feat(deploy/aws): rds handler — no-default-password gate + provisioning poll

Handler #16 in Phase 2. CreateDBInstance + 20-min status-poll loop
that respects ctx.abort_signal and reports progress via on_step.
Refuses to create when master_user_password is empty (parity with
the extractor's no-default-password invariant).

* feat(deploy/aws): docdb handler — cluster + per-instance creation

* feat(deploy/aws): cognito handler — user pool with password policy + MFA

* feat(deploy/aws): cloudfront handler — us-east-1 ACM cert + minimal distribution

Handler #19. CloudFront requires ACM certs in us-east-1 regardless
of deploy region; the handler spins up a one-shot ACM client pinned
to us-east-1 for RequestCertificate, then attaches the ARN to the
distribution's ViewerCertificate. Falls back to CloudFrontDefaultCertificate
when ACM SDK is absent.

* feat(deploy/aws): elbv2 handler — LB + skeleton target group

* feat(deploy/aws): api-gateway handler — REST API + default-stage deployment

* feat(deploy/aws): events-rule handler (CronJob) — PutRule + PutTargets

* feat(deploy/aws): ecs handler — auto-cluster + task role + service create

Handler #23. Compute.Container 'just works' on AWS — the handler
idempotently bootstraps ecsTaskExecutionRole + ice-default-cluster
before RegisterTaskDefinition + CreateService. Mirrors the GCP
Cloud Run UX (no cluster to think about).

* feat(deploy/aws): opensearch handler — CreateDomain with cluster/EBS/encryption config

* feat(deploy/aws): bedrock handler — on-demand no-op + provisioned-throughput create

* feat(deploy/aws): sagemaker handler — EndpointConfig + Endpoint, requires model_name

* feat(deploy/aws): redshift handler — CreateCluster + no-default-password gate

* feat(deploy/aws): lambda auto-build from Source.Repository

Phase 3 (commit #28). When a Compute.ServerlessFunction block has a
connected Source.Repository AND no explicit S3 ref, the handler
auto-builds the zip and uploads it before CreateFunction:

  1. git clone --depth 1 --branch <branch> <repo>
  2. npm install --omit=dev (skipped if no package.json)
  3. zip -qr function.zip .
  4. PutObject to ice-bootstrap-{accountId}-{region}/lambda/{name}/{ts}.zip
     (HeadBucket → CreateBucket if absent)
  5. Stamp s3_bucket + s3_key onto properties and continue.

Local-only — assumes git/npm/zip on the deploy host. AWS CodeBuild
integration deferred to a future commit. Existing manual S3-ref + zip
paths are unchanged; the auto-build branch only fires when
`properties.repository` is set AND no explicit code source exists.

* test(deploy/aws): unskip AWS Type Map block + end-to-end coverage

Phase 4 commit #29. With every aws.* resource type registered in
PROPERTY_EXTRACTORS (commits #2#6), the AWS Type Map test block can
finally turn on. Expanded the iceType matrix from 5 to 19 entries
covering every AWS-mapped block.

New end-to-end test wires Compute.StaticSite + Security.Secret (with
two bindings — exercising the schema-declared deploy-expansion pass)
+ Database.PostgreSQL into a single translator call, asserts the
resulting graph has 4 deployables resolving to s3.bucket /
secretsmanager.secret×2 / rds.dbInstance. The Azure block remains
skipped (deferred to a future Azure handler buildout).

* docs(deploy/aws): provider notes — quirks, assumptions, deferred work

Phase 4 commit #30 — final commit of the AWS buildout.

providers/aws/README.md documents the AWS-specific decisions the 30
commits in this series bake in:

  - architecture (mirrors gcp/, schema-driven HANDLER_REGISTRY)
  - S3 account-id suffix
  - CloudFront us-east-1 cert
  - ECS auto-cluster + task role
  - RDS / DocDB / Redshift no-default-password invariant
  - RDS provisioning poll
  - Lambda auto-build flow (git + npm + zip + bootstrap S3)
  - Bedrock on-demand no-op
  - Secrets Manager values-never-written contract
  - SQS / SNS .fifo suffix
  - SDK packages as optional peer deps
  - test harness layout
  - deferred work (VPC blocks, CodeBuild, drift detection, LocalStack)

Read this before changing any AWS handler.

* feat(aws): selectively enable safe categories via feature flags

Flip PROVIDER_FLAGS.aws.enabled to true with a hand-picked category
map (Storage, Messaging, Cache, Monitoring, Security, Source, Config).
Compute / Frontend / Scheduler / Network / Database / AI / Analytics
stay gated until their concrete unblockers land — ECS VPC blocks,
CloudFront cert-validation flow, update-paths, etc.

README.md gets a Rollout state table documenting why each gated
category is held back and what unblocks it.

Integrity test in packages/constants asserts the per-category map
stays exhaustive, so future CategoryId additions force a deliberate
on/off decision here.

* fix(palette): enable provider dropdown items when any block is available

Replace the project-provider lock on the palette provider dropdown
with an availability check: a provider option is selectable iff at
least one concept has it in providers and its category is enabled
for that provider.

Before: in a GCP project, AWS was greyed in the palette dropdown
even after AWS feature-flag enabled — so users couldn't browse the
AWS catalog from a GCP project.

After: AWS opens as long as it has any available block under the
current PROVIDER_FLAGS — drag-into-project compatibility remains
enforced at the canvas-drop layer.

availableProviderIds is derived in resource-palette.tsx from the
unfiltered component list using isCategoryEnabledForProvider — same
schema-driven gate the component filter already uses.

* docs(architecture): explain how canvas edges become cloud infra

New page docs/architecture/connections-to-cloud.md walks the five-layer
pipeline (connection-rules → propagation → type-maps → extractors →
handlers) and grounds it with two worked GCP examples:

- Storage.Bucket → Compute.BackendAPI: env-var injection + IAM binding,
  no edge resource in GCP.
- Compute.CronJob → Compute.BackendAPI: Cloud Scheduler HTTP target +
  run.invoker IAM binding.

Links the new page from architecture/README.md and the existing
core-engine.md "Computing flows" section so readers landing on either
find their way to the deep dive.

* fix(typecheck): unblock blocks + templates + core deploy-expansion

- deploy-expansion: use get_node_by_name for name-based dedup; has_node
  expects a branded NodeId, not a plain string.
- requirements.test: vitest needs `beforeEach` in the named imports
  (was previously globalised but tsc no longer sees it).
- validate.test: annotate `.map((c) => ...)` callbacks; vitest's bare
  `ReturnType<typeof vi.spyOn>` no longer carries the called-fn signature
  so the implicit-any error fires.

* fix(typecheck): unblock packages/ui + packages/web

Sweep the workspace's pre-existing typecheck errors so the whole repo
compiles cleanly under tsc --noEmit:

- packages/constants: re-export IntegrationStatus from the barrel so
  packages/ui/src/store/slices/integrations-slice.ts resolves.
- vitest type tightening: `vi.fn(() => ...)` infers Parameters as [],
  breaking `.mock.calls[i][j]` indexing. Widen to (..._args: unknown[])
  on the mock decls that get indexed (deploy-panel, requirements-
  section, deploy-diagnosis, inline-table-view, axios-instance,
  use-cost-calculation, use-computing-flows, template-picker,
  invite-accept).
- stopPropagation event mocks: vitest no longer accepts
  `{ stopPropagation: vi.fn() } as React.MouseEvent` without an unknown
  intermediate. Sweep through compact-node / custom-domain / group-node
  / log-node / palette / inline-table tests.
- KNOWN_MOCKS includes: cast through unknown[] in canvas-context-menu
  and inline-table-view test helpers.
- block-summary-card: import CanvasNode from ../../../svg-canvas (the
  path resolved one level too shallow).
- blueprints test: import BlockBlueprint from ../types not ../../types.
- group-node + region-label test fixtures: type 'container' (the
  CanvasNode union doesn't include 'group' or 'region' yet).
- reroute-node: switch sockets prop from SocketDef[] to PortDef[] with
  role: 'any' to match TypedSockets' contract.
- use-wizard-state: add missing 'name' field on EnvironmentPreset
  fixture.
- store/index.test: cast resolveFirst to its original closure type so
  optional-call type-narrows correctly.
- use-mouse-handlers test: drop minZoom/maxZoom (not in
  UseMouseHandlersDeps).
- deploy-diagnosis: widen the diagnosis state shape; replace
  setImmediate with setTimeout(resolve, 0) — Node's setImmediate isn't
  surfaced in the test types.

* docs(architecture): explain how canvas edges become cloud infra

* docs(readme): link AWS rollout state + connections-to-cloud page

- root README: sharpen "AWS — in progress" to mention the handler /
  extractor count and link the staged-rollout table in the AWS README.
- docs/README: add connections-to-cloud to the architecture mermaid
  and to the contributors table.

* docs(aws): reflect handler buildout + staged rollout

- provider-status: AWS matrix entry now lists the 17 handlers + 20
  extractors and the per-category feature-flag state instead of the
  stale "EC2 / S3 / Lambda only" copy.
- deploying-to-aws: drop the dead reference to "provider-status.md - to
  be added" (the doc exists now), fix the broken handlers-source link
  (handlers live at packages/core/src/deploy/providers/aws/, not
  packages/providers/aws/), fix the broken architecture.md anchor.
- Add a quirks section pointing operators at the AWS README for S3
  account-id suffix, CloudFront us-east-1 cert pin, ECS auto-cluster,
  RDS password gate + provisioning poll, Lambda auto-build, FIFO
  suffix. Update the "known gaps" list to match the deferred items in
  the AWS README (VPC blocks, CodeBuild, update paths, LocalStack).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant