Skip to content

Deployment tests#18

Merged
julia-kafarska merged 681 commits into
mainfrom
deployment-tests
May 20, 2026
Merged

Deployment tests#18
julia-kafarska merged 681 commits into
mainfrom
deployment-tests

Conversation

@julia-kafarska
Copy link
Copy Markdown
Member

No description provided.

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.
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.
…lidator

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.
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.
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.
…oader

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.
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.
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.
…oad)

- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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
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.
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).
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.
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.
julia-kafarska and others added 27 commits May 8, 2026 18:51
Leaves-first foundation for the in-house tour engine. Adds the public
type surface (Tour, TourStep, Placement, TourLifecycleCtx, AutoStartCtx),
a module-scoped registry with dev-strict / prod-warn duplicate handling,
and the feature barrel. 18 tests cover the validation policy from
blueprint §2.4.
Pure presentational spotlight + click-shield component for the tour
engine. Uses the box-shadow technique for the dim region (zero extra
DOM nodes per dim region, GPU-animated) and four shield strips around
the rect so clicks within the spotlight pass through to the page while
clicks outside trigger onSkip. Portal'd to document.body via
createPortal. Honors useReducedMotion (no transition).

First jsdom test under the new test-env decision (state/decisions.md
2026-05-08): jsdom installed at the workspace root; tour-3..6 stay on
node fake-DOM, tour-8/10/12/13 use jsdom.

18 tests covering: null rect, rect+pad math, default pad/radius, click
on each shield → onSkip, click on inner rect → no skip, reduced-motion
on/off, pointer-events, four-strip math, negative-pad clamp, re-render
on rect change, portal placement, z-index, fresh closure.
Feature-local Redux slice at packages/ui/src/features/tour/store/
with the persistence thunk and the public useTour() hook. Wired the
reducer into the root store and added 'tour/' to the action-logger
prefix list.

Slice (tour-slice.ts, ~256 LOC): startTour validates against the
registry and seeds per-tour telemetry, advance/skip/stop fan out
through the hook, hydrateFromUser unions localStorage + server with
server-wins-on-conflict semantics, persistCompletedTour is optimistic
(failures warn but don't roll back).

Hook (use-tour.ts, ~171 LOC): selectors + dispatchers per blueprint
§2.2; advance() dispatches recordAdvance + setStep + setPhase, or on
the terminal step markCompleted + persistCompletedTour. start()
unregistered → no-op + dev warn.

Tests: 44 slice tests + 17 useTour tests, node env per the test-
ceiling decision (2026-05-08). Two learnings appended:
tour-6-localstorage-fastpath-needs-vi-resetmodules-to-re-evaluate-
initialstate (vi.resetModules + dynamic re-import for top-level IO)
and tour-6-react-usecallback-stub-keeps-hook-driveable-without-
renderer (vi.mock react useCallback identity stub for hook unit
tests outside a fiber tree).
useTourKeyboard: capture-phase keydown listener wiring Esc (skip),
ArrowRight/Enter (advance), ArrowLeft (previous). Suppresses
advance/previous when an INPUT/TEXTAREA/SELECT is focused (Enter) or
when an editable element including [contenteditable] holds focus
(arrows). preventDefault on consumed keys.

useTourRoute: router-aware navigation gate. Phase derives from
useLocation().pathname.startsWith(targetRoute); imperative navigateTo
no-ops when already arrived or targetRoute is undefined. Caller
sequences onEnter side-effects around the navigation.

22 keyboard tests + 12 route tests, all node-env per the test-env
ceiling (decision 2026-05-08).

Refs: state/blueprints/tour.md §3.4 §3.6 §6/tour-11
Anchored Radix Popover wrapping the tour-2 primitive plus tour-5 focus
trap. Composes title / body / counter / Back / Skip / Next / Close X
with role=dialog, aria-modal=false, aria-labelledby, aria-describedby.

- Auto-placement helper picks the side with the most viewport space
  (tie-break top -> bottom -> right -> left); inlined per blueprint
  guidance, exported for direct testing.
- Honors useReducedMotion via data-reduced-motion attribute and
  motion-reduce:animate-none / data-[state=open|closed]:animate-none.
- Hands focus management to installFocusTrap on mount; Radix's
  PopoverContent onOpenAutoFocus / onCloseAutoFocus are e.preventDefault
  so our trap owns initial and return focus.
- Skip is hidden on the last step (avoids skip-vs-finish UX confusion)
  and when actions.hideSkip is set; Back is hidden on the first step.
- Close (X) is distinct from Skip: dispatches onClose so the runner
  can stopTour without marking completed.

27 tests (jsdom env per state/decisions.md 2026-05-08), all green.

Learning: tour-10-radix-popper-virtualref-must-be-identity-stable-or-ooms.
Critic-flagged: wizard step-4 Create button shared the wizard-btn-next
anchor. Different semantics (handleCreate vs handleAdvance, different
label, different tour copy "Click *Create* to finish") — distinct anchor
keeps tour configs precise. Added a focused step-4 assertion alongside.
Critic-flagged: TourRunner forwards liveRect from useElementPosition
but had no test asserting the overlay rect updates when the anchor
moves. Added: dispatch scroll → re-read getBoundingClientRect → assert
overlay's rect prop reflects the new position. 25 runner tests now.
- New `useTourAutostart` hook reads `?tour=<id>` from the URL, dispatches
  `start(id)` once when registered + not in completedTours, then strips
  the param via `navigate(..., { replace: true })`. Other query params
  and the hash are preserved. StrictMode-safe via a `(pathname, tour)`
  tuple ref.
- TourRunner now calls `useTourAutostart()` so the URL surface lights up
  on every project page.
- AppBar gains a Help menu (HelpCircle icon → DropdownMenu with
  "Show me around" submenu). Each registered tour gets a
  DropdownMenuItem; click → `useTour().start(tour.id)`.
- i18n: new `appBar.help.tooltip` + `appBar.help.showMeAround` keys
  (en/zh).
- 12 new hook tests (jsdom + MemoryRouter + Provider) covering the
  registered-tour, unknown-id, completed-id, no-param, hash/extra-param
  preservation, double-effect, multi-value, empty-value, and re-paste
  paths.
- 5 new AppBar tests (tree-walker pattern) covering Help button,
  submenu trigger, per-tour items, click→start dispatch, and empty
  registry.

Per blueprint §4.3 + §6/tour-13: v1 auto-fire is OFF by default —
predicate-based ("first project after wizard") is v2 territory. v1 only
ships URL `?tour=` + Help menu.
Authored alongside the 14-unit tour initiative (tour-1 .. tour-14).
Multiple feat(tour) commits reference §-numbers in this blueprint;
tracking it now keeps cross-references resolvable.
The OnboardingChecklist appears bottom-left on every route, including
the folder view at /. The canvas-tour highlights elements that only
exist on a project canvas (#ice-canvas-svg, palette, properties, AI
panel) — clicking "Show me how" from / would silently auto-skip every
step (each missing anchor → console.warn + advance) and "complete" the
tour without showing anything. Fix: gate startTour() on the canvas
anchor being in the DOM; otherwise surface "Open a project to start
the tour." inline.

No org/project URL plumbing — the click handler is a single DOM check.
Multi-agent workflow definitions (planner/implementer/critic/ux-tester
+ decomposer/util-broker/test-author) referenced by CLAUDE.md were
unintentionally swept into 647adf8 as deletions. Restoring from HEAD~1.
* labeling fix and GH actions

* GH actions

* GH actions

* linter errors

* unit tests fixes
* labeling fix and GH actions

* GH actions

* linter errors

* unit tests fix

* build fixes, clean up
…conflicts)

# Conflicts:
#	apps/desktop/schema.prisma
#	docs/architecture.md
#	docs/community-edition.md
#	docs/core-engine.md
#	docs/database.md
#	docs/desktop.md
#	docs/frontend.md
#	docs/services.md
#	packages/ui/src/features/onboarding/components/connect-cloud-step.tsx
#	packages/ui/src/features/onboarding/components/onboarding-checklist.tsx
#	packages/ui/src/features/onboarding/components/onboarding-page.tsx
#	packages/ui/src/store/slices/onboarding-slice.ts
@julia-kafarska julia-kafarska merged commit 340fd11 into main May 20, 2026
1 check failed
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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant