@kudzujs/core 0.8.35 → 0.8.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
4
4
 
5
- The executable post-`0.8.35` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
5
+ The executable post-`0.8.37` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
6
6
 
7
7
  [`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
8
8
 
package/PERFORMANCE.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, and `npm run benchmark:module-cache` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
4
4
 
5
+ ## P0.10 Structural ModuleIR References
6
+
7
+ Measured UTC 2026-08-11 on an Apple M4 macOS arm64 host with 10 logical CPUs, 16 GiB RAM, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.36` at `268cd9023c9f47a912601f298963a9ffe9c00da2`. The compiler and focused-check patch had SHA-256 `f684c79027b290bc8c7d549667ef0d7f21a9d0057b5d6291fcce18b91947eff2`, produced by:
8
+
9
+ ```bash
10
+ git diff --binary v0.8.36 -- framework/compiler/analysis/binding-index.mjs framework/compiler/analysis/component-analysis.mjs framework/compiler/descriptor-session.mjs framework/compiler/ir/module-ir.mjs framework/compiler/source-compiler.mjs test/compiler-passes.test.mjs test/framework.test.mjs | shasum -a 256
11
+ ```
12
+
13
+ The maintained 100-importer fixture used one warm-up and seven alternating fresh-process samples. Both targets retain 103 parse misses, 103 export-summary misses, and 100 importer-local clones. ModuleIR and ComponentAnalysis v2 intentionally change the serialized compiler graph, so the previous equal-digest gate correctly rejects direct v1/v2 comparison: normalized result size changes from 382,603 B to 447,977 B and source-result size changes from 395,346 B to 460,720 B. The additional 65,374 B is deterministic structural slot, signal, symbol, and ownership metadata in compiler scratch; it is not deployed browser JavaScript.
14
+
15
+ | Target | Compiler median | Range | Peak RSS median | Source-result bytes |
16
+ |---|---:|---:|---:|---:|
17
+ | `v0.8.36` | 224.506 ms | 220.888-313.782 ms | 281.6 MiB | 395,346 B |
18
+ | `0.8.37` | 227.158 ms | 224.744-272.594 ms | 282.9 MiB | 460,720 B |
19
+
20
+ The candidate's unpaired median is 1.18% higher. Round-paired candidate-minus-baseline differences had a +6.270 ms median, with the candidate faster in two of seven pairs and the baseline faster in five. Timing ranges overlap, peak RSS differs by 1.3 MiB, and neither result crosses the 5% material-regression threshold.
21
+
22
+ ```text
23
+ v0.8.36: [220.888,222.461,313.782,224.506,249.086,222.746,242.721]
24
+ candidate: [227.158,224.744,225.098,236.027,225.158,239.672,272.594]
25
+ paired candidate-baseline: [6.270,2.283,-88.684,11.521,-23.928,16.926,29.873]
26
+ ```
27
+
28
+ ModuleIR and ComponentAnalysis are build-scratch contracts. Runtime-facing state names and export spellings remain only where generated module and browser ABIs require them. The standard suite verifies static zero-JavaScript output, capability exclusion, keyed identity, effect cleanup, Workers, navigation, and migration behavior; no browser runtime source was added for P0.10.
29
+
30
+ ## P0.9 Semantic State Operations
31
+
32
+ Measured UTC 2026-08-11 on the Intel Core i5-9500 Linux x64 host with Node 24.14.0. The baseline was clean tag `v0.8.35` at `f25700d9d2b247c01db19f0e8c95f16cb1fa81a5`. The compiler and focused-check patch had SHA-256 `4c3c8a3de18b1e792ea84cf7608a89971850195036a58f37dd76e359bfc8a58d`, produced by:
33
+
34
+ ```bash
35
+ git diff --binary v0.8.35 -- framework/compiler/optimize/command-specialization.mjs framework/compiler/descriptor-session.mjs test/compiler-passes.test.mjs test/fixtures/effect-isolation/src/pages/command.tsx test/framework.test.mjs | sha256sum
36
+ ```
37
+
38
+ The maintained 100-importer fixture used three warm-ups and 21 alternating fresh-process samples. P0.9 keeps the existing direct command fast path first and invokes whole-handler semantic analysis only after direct specialization fails. Baseline and candidate produced the same normalized 382,603-byte graph, SHA-256 `8c35b3f6d2c571306bd97c4d51d4af76ca244badd36c57363bf579ef961f41aa`, 395,346-byte source result, and 103 / 103 / 100 parse, summary, and clone counts.
39
+
40
+ | Target | Compiler median | Range | Peak RSS median | Source-result bytes |
41
+ |---|---:|---:|---:|---:|
42
+ | `v0.8.35` | 947.100 ms | 839.270-1,064.444 ms | 258.0 MiB | 395,346 B |
43
+ | P0.9 candidate | 942.596 ms | 849.656-1,215.997 ms | 258.1 MiB | 395,346 B |
44
+
45
+ The candidate's unpaired median is 0.48% lower. Round-paired candidate-minus-baseline differences had a +1.754 ms median with the candidate faster in 10/21 pairs and the baseline faster in 11/21. Timing and peak-RSS ranges overlap, so no material improvement or regression is claimed. An initial implementation that routed every direct handler through whole-handler analysis measured a +12.584 ms paired median and 10.6 MiB higher RSS median; restoring the direct fast path and narrowing the analyzer removed that regression before this final record.
46
+
47
+ ```text
48
+ v0.8.35: [955.382,927.389,935.312,852.622,839.270,918.424,943.013,1026.033,936.514,1019.725,921.591,1064.444,949.671,975.729,933.380,896.773,949.751,951.418,947.100,998.950,1041.266]
49
+ candidate: [942.596,1215.997,965.312,849.656,877.468,928.334,971.812,966.482,938.268,1016.268,932.327,939.217,928.284,1082.782,1086.319,976.534,881.665,915.266,962.642,905.596,1000.939]
50
+ paired candidate-baseline: [-12.786,288.608,30.000,-2.966,38.198,9.910,28.799,-59.551,1.754,-3.457,10.736,-125.227,-21.387,107.053,152.939,79.761,-68.086,-36.152,15.542,-93.354,-40.327]
51
+ ```
52
+
53
+ The four required source forms lower to the same existing command HandlerIR and command-only browser path. Alias/helper forms therefore avoid the native handler module and native runtime they previously required; no command ABI, state batching, ownership, runtime source, or unaffected route artifact changes.
54
+
5
55
  ## P0.8 Stable ModuleSymbol And SiteId
6
56
 
7
57
  Measured UTC 2026-08-11 on the same Intel Core i5-9500 Linux x64 host with Node 24.14.0. The baseline was clean tag `v0.8.34` at `007fcb6e23c7d5bc742fa37c28388d070da9f598`. The compiler and maintained-check patch had SHA-256 `10ed6beb448b9e86961afab0b798f010932b8a29f98475d41f7bd8cfad04a872`, produced by:
package/README.md CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.35 - Stable module identity.** Cross-module declarations now resolve through stable ModuleSymbol and source-local SiteId records across aliases, barrel chains, `export *`, and cloned transformer trees. Read the [release notes](./RELEASES.md#0835---stable-module-identity), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.35), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.37 - Structural ModuleIR references.** Compiler ownership and dependency edges now use validated source-local slots and stable symbols instead of formatted names. Read the [release notes](./RELEASES.md#0837---structural-moduleir-references), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.37), or follow the [architecture packet](./docs/next-architecture/README.md).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
@@ -32,6 +32,7 @@ Kudzu requires Node.js 22 or newer.
32
32
  ```bash
33
33
  npm create kudzu@latest my-app
34
34
  cd my-app
35
+ npm install
35
36
  npm run dev
36
37
  ```
37
38
 
package/RELEASES.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.37 - Structural ModuleIR references
4
+
5
+ Kudzu 0.8.37 completes P0.10 by replacing mixed state names, export strings, formatted owner keys, and partial slots with one validated structural reference graph.
6
+
7
+ ### Changed in 0.8.37
8
+
9
+ - ModuleIR v2 assigns deterministic slots to symbols, signals, handlers, bindings, derived values, effects, keyed blocks, and imports.
10
+ - ComponentAnalysis v2 assigns deterministic owner, specialization, state, ref, and ID slots.
11
+ - StateRef, OwnerRef, source-local SymbolRef, and stable ModuleSymbol records connect state, capture, import, effect, collection, specialization, prop-signal, and row ownership edges.
12
+ - Descriptor finalization resolves effect handlers and imports structurally instead of searching by export spelling.
13
+ - Readable state and export names remain only as codegen, runtime ABI, and diagnostic metadata.
14
+ - One fail-closed pre-codegen validator rejects unsupported versions, malformed slots, duplicate exports, invalid effect handlers, broken keyed parent-child reciprocity, ownership cycles, and dangling specialization/ref edges.
15
+
16
+ ### Performance
17
+
18
+ - The maintained 100-importer fixture retains 103 parse misses, 103 export-summary misses, and 100 importer-local clones.
19
+ - Seven alternating fresh-process samples measured `v0.8.36` and 0.8.37 compiler medians of 224.506 ms and 227.158 ms. The +6.270 ms paired median and overlapping timing/RSS ranges establish no material regression.
20
+ - Source-result size increases from 395,346 B to 460,720 B because ModuleIR and ComponentAnalysis now serialize structural symbol, signal, owner, and slot metadata. This is compiler scratch data, not deployed browser JavaScript.
21
+ - Existing static, command, binding, keyed, effect, Worker, navigation, and migration fixtures retain their browser behavior and zero-unused-runtime checks.
22
+
23
+ ### Validation
24
+
25
+ - `npm run check`, `npm test`, and `npm run test:package` pass with all 198 tests and 152 generated pages.
26
+ - Focused checks cover unsupported versions, malformed references, duplicate exports, parent-child reciprocity, cycles, effect-handler roles, specialization ownership, and JSON round trips.
27
+ - Package smoke installation passes from the packed tarball.
28
+ - P0.11 explicit route artifact graph is next.
29
+
30
+ ### Upgrade
31
+
32
+ ```bash
33
+ npm install @kudzujs/core@^0.8.37
34
+ ```
35
+
36
+ ## 0.8.36 - Semantic state operations
37
+
38
+ Kudzu 0.8.36 completes P0.9 by proving equivalent direct, aliased, and local-helper state updates and lowering them through the existing command HandlerIR path.
39
+
40
+ ### Changed in 0.8.36
41
+
42
+ - Direct `setCount(count + 1)`, an immutable result alias, a zero-argument arrow helper, and a one-parameter function helper emit the same structured `add` command.
43
+ - Binding identity proves state, setter, helper, and parameter ownership before specialization.
44
+ - Recursion, helper or alias escape, mutation, optional calls, and object-based dynamic dispatch fail at authored source locations.
45
+ - The existing direct command fast path remains first; whole-handler analysis runs only when direct specialization fails.
46
+ - Quick Start instructions now include an explicit `npm install`, and `create-kudzu@0.1.102` reports install-aware next steps and includes the install command in generated READMEs.
47
+
48
+ ### Performance
49
+
50
+ - The maintained 100-importer fixture preserves the same normalized graph, source-result bytes, digest, and 103 / 103 / 100 parse, summary, and clone counts.
51
+ - Twenty-one alternating `v0.8.35`/candidate samples measured compiler medians of 947.100 ms and 942.596 ms. The +1.754 ms paired candidate-minus-baseline median and overlapping RSS ranges establish no material improvement or regression.
52
+ - Alias and helper forms avoid native handler ESM and reuse the existing command runtime; direct command artifacts remain unchanged.
53
+
54
+ ### Validation
55
+
56
+ - `npm run check`, `npm test`, and `npm run test:package` pass with all 197 tests and 151 generated pages.
57
+ - Focused coverage proves identical JSON-safe command IR, signal ownership, build-module behavior calls, RouteIR commands, and zero native-handler artifacts for all four source forms.
58
+ - Existing command ABI, synchronous batching, ownership, browser runtime, and unrelated native handlers remain unchanged.
59
+ - P0.10 ModuleIR reference unification is next.
60
+
61
+ ### Upgrade
62
+
63
+ ```bash
64
+ npm install @kudzujs/core@^0.8.36
65
+ ```
66
+
3
67
  ## 0.8.35 - Stable module identity
4
68
 
5
69
  Kudzu 0.8.35 completes P0.8 by giving cross-module declarations and authored compiler sites stable identities that do not depend on transformed TypeScript AST objects or readable names.
@@ -11,7 +11,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
11
11
  | C: state/resource model | Research only | Reduced fixtures expose a limitation |
12
12
  | D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
13
13
 
14
- The active post-`0.8.35` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.8 stable ModuleSymbol and SiteId is complete; P0.9 semantic state operations is next. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
14
+ The active post-`0.8.37` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.10 ModuleIR reference unification is complete; P0.11 explicit route artifact graph is next. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
15
15
 
16
16
  ## Required Invariants
17
17
 
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the current `0.8.35` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
3
+ This maps the current `0.8.37` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -17,9 +17,9 @@ This maps the current `0.8.35` architecture, built on the completed `0.8.23` Goa
17
17
  | Source-local binding index | [`framework/compiler/analysis/binding-index.mjs`](../../framework/compiler/analysis/binding-index.mjs) | After normalization, assigns deterministic lexical slots and classifies local, parameter, import, capture, global, and unresolved references. Native handler, effect, binding, list evaluator, optimized-command, and effect-resource consumers use complete index-owned AST; synthesized expressions retain the existing fallback. |
18
18
  | Pure collection language | [`framework/compiler/collection-analysis.mjs`](../../framework/compiler/collection-analysis.mjs) | Analyzes collection roots/selectors and serializes the allowed pure expression language used by lists and derived dependencies. |
19
19
  | Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, keyed, and effect ownership results. |
20
- | Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Retains ordered JSON-safe owner and specialization records for state, setters, props, refs, IDs, direct signal links, source-local SiteIds, and source provenance; AST identity remains private to its source-local session. |
21
- | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, imports, and client roots into ModuleIR and validates every local slot reference. |
22
- | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Supported command handlers specialize to JSON-safe ModuleIR, then emit the existing `__kBehavior` AST without changing route plans. |
20
+ | Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, structural OwnerRefs, source-local SiteIds, and source provenance; AST identity remains private to its source-local session. |
21
+ | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
22
+ | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
23
23
  | Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
24
24
  | Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback/binding/list AST rewriting and diagnostics before the JSON-safe IR boundary. |
25
25
  | Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
@@ -68,7 +68,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
68
68
  ## Residual Coupling
69
69
 
70
70
  - `createKudzuTransformer()` remains one large source-local analysis unit combining validation, specialization, descriptor registration, and transformed-source emission.
71
- - Transient component rewrite indexes remain source-local AST indexes; handler, binding, derived, keyed, effect, and component ownership now have explicit JSON-safe source results.
71
+ - Transient component rewrite indexes remain source-local AST indexes; finalized handler, binding, derived, keyed, effect, signal, import, and component ownership edges use explicit JSON-safe slots or stable symbols.
72
72
  - `build()` still owns explicit artifact selection and filesystem writes after generator results are produced.
73
73
  - Runtime generators intentionally specialize readable authored sources through exact anchors; every required anchor fails closed, but a future generator format may remove this transitional dependency.
74
74
  - Source reachability and source compilation remain in one session-bound compiler factory because both consume the same normalization and import graph contracts. ModuleSymbol resolution is stable across canonical and cloned trees; unsupported export syntax remains intentionally narrow.
@@ -137,6 +137,8 @@ This is an incremental evolution of the current repository:
137
137
  - [x] P0.6 Explicit ProjectSession is complete in `0.8.33`. Each build owns an absolute root, project paths, source records, bound graph operations, and Worker compiler. Explicit-root build/dev entry points preserve omitted-root CLI CWD behavior. One imported build function compiles two same-shaped roots with isolated config, HTML, `.kudzu`, source results, and Worker bundles; all 191 tests and package checks pass without browser or source-syntax changes.
138
138
  - [x] P0.7 Parsed module and export summary caching is complete in `0.8.34`. Canonical read-only source trees and narrow export summaries are invalidated together by source text and remain ProjectSession-local; every normalization context receives a deep clone with independent parent links. A 100-importer fixture parses and summarizes 103 unique page/barrel/component/helper modules exactly once and creates 200 importer-local clones. The maintained paired benchmark preserves the complete source-result digest and establishes no material timing or peak-RSS conclusion; all 193 tests and package checks pass without broadening exports, source syntax, or browser output.
139
139
  - [x] P0.8 Stable ModuleSymbol and SiteId is complete in `0.8.35`. ProjectSession records source-local declaration, import, and re-export sites and resolves stable ModuleSymbol records through default/named exports, aliases, barrel chains, `export *`, ambiguity, and cycles. Cross-module compiler consumers locate resolved declarations by SiteId in their private normalized clones, while component calls, hooks, keyed blocks, effects, and ownership records expose deterministic source-local SiteIds. Repeated sessions and compilations preserve IDs, and symbol-only barrel traversal avoids cloning intermediate modules without changing browser artifacts or source syntax.
140
+ - [x] P0.9 Semantic State Operations is complete in `0.8.36`. Direct setters, one immutable state-value alias, one synchronous zero-argument arrow helper, and one synchronous one-parameter function helper lower to identical existing command HandlerIR. Binding identity proves state/setter/helper/parameter ownership; recursion, escape, mutation, and dynamic helper dispatch fail at authored source locations. Existing direct command specialization remains first, unrelated safe handlers retain native ESM, and no command ABI, runtime, JavaScript VM, or general expression IR is added.
141
+ - [x] P0.10 ModuleIR Reference Unification is complete in `0.8.37`. ModuleIR and ComponentAnalysis v2 assign deterministic slots to symbols, signals, handlers, bindings, derived values, effects, keyed blocks, imports, owners, specializations, states, refs, and IDs. State, capture, import, effect, collection, parent/child, specialization, and row edges use structural slots or ModuleSymbol records while readable names remain codegen/debug metadata. A fail-closed pre-codegen validator rejects malformed slots, unsupported versions, duplicate exports, broken reciprocity, and ownership cycles; focused JSON round-trip checks and all 198 tests pass without changing browser runtime behavior.
140
142
 
141
143
  ### P0: Semantic Correctness And Compiler Foundation
142
144
 
@@ -339,6 +341,8 @@ function increment(value) { setCount(value + 1) }; increment(count)
339
341
 
340
342
  **Done condition:** the equivalent forms produce the same StateWrite semantics, browser result, ownership references, and zero-unused-runtime behavior; recursion, escape, mutation, and dynamic dispatch fail with explicit diagnostics.
341
343
 
344
+ **Completed in `0.8.36`:** The command specializer now performs narrow whole-handler analysis only when the existing direct fast path fails. The four required forms emit the same `add` command, signal slot, owner, build-module behavior call, RouteIR command, and browser command runtime; alias/helper forms emit no native handler module. Focused compiler and command-only fixture coverage proves JSON-safe IR equivalence, source diagnostics, no native behavior artifacts, and unchanged batching/runtime ABI. The maintained 100-importer benchmark retains identical output and cache counts with no material timing or peak-RSS conclusion.
345
+
342
346
  ### PR 10: ModuleIR Reference Unification
343
347
 
344
348
  **Objective:** replace mixed state names, export strings, formatted owner strings, and slots with validated source-local slots and SymbolRef records.
@@ -347,6 +351,8 @@ function increment(value) { setCount(value + 1) }; increment(count)
347
351
 
348
352
  **Done condition:** every HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and component specialization edge validates before build-module generation.
349
353
 
354
+ **Completed in `0.8.37`:** ModuleIR and ComponentAnalysis v2 replace formatted owner keys and cross-record export/name lookups with source-local slots, StateRef/OwnerRef records, SymbolRef slots, and stable ModuleSymbol records. Descriptor finalization resolves imports, handlers, bindings, effects, derived dependencies, keyed collections, row ownership, and component prop signals before one fail-closed validation boundary. The validator checks slot/index integrity, state/setter ownership, captures/imports, duplicate exports, effect-handler roles, keyed parent/child reciprocity and cycles, specialization/ref edges, and JSON-safe round trips. Existing export names and state spellings remain only where emitted module/runtime ABIs require them.
355
+
350
356
  ### PR 11: RouteBuildRecord And Artifact Graph
351
357
 
352
358
  **Objective:** make route artifact selection structural.
@@ -468,4 +474,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
468
474
 
469
475
  ## Immediate Decision
470
476
 
471
- PR 1 through PR 8 are complete. The next PR is **PR 9: Semantic State Operations**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
477
+ PR 1 through PR 10 are complete. The next PR is **PR 11: RouteBuildRecord And Artifact Graph**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
@@ -1,6 +1,6 @@
1
1
  # Planned Version Sequence
2
2
 
3
- This is an execution sequence, not release history. `0.8.16` through `0.8.35` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.37` are completed scopes represented by package/release records.
4
4
 
5
5
  Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
6
6
 
@@ -26,6 +26,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
26
26
  | `0.8.33` | Move root, graph, source records, compiler paths, and Worker ownership into an explicit build-scoped ProjectSession. | Two independent roots compile through one imported build entry without config, source, `.kudzu`, Worker, or output leakage; CLI CWD behavior remains unchanged. |
27
27
  | `0.8.34` | Cache canonical parsed modules and narrow export summaries within one ProjectSession while cloning transformer input. | A 100-importer fixture parses and summarizes 103 unique modules once, creates independent normalization trees, preserves the complete source-result digest, and leaks no cache state across projects. |
28
28
  | `0.8.35` | Resolve cross-module declarations through stable ModuleSymbol and source-local SiteId records. | Default/named exports, aliases, barrel chains, `export *`, cycles, ambiguity, repeated compilation, source invalidation, private clones, and measured build behavior remain deterministic. |
29
+ | `0.8.36` | Lower equivalent direct, aliased, and local-helper state updates through existing command HandlerIR. | The four required forms emit identical commands and no native handler module; recursion, escape, mutation, and dynamic dispatch fail at authored source locations. |
30
+ | `0.8.37` | Unify ModuleIR and ComponentAnalysis references through deterministic source-local slots and stable symbols. | Every handler, binding, derived, effect, keyed, specialization, state, ref, capture, and import edge validates before build-module generation. |
29
31
 
30
32
  ## Sequence Rules
31
33
 
@@ -38,7 +40,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
38
40
 
39
41
  ## Generator Versions
40
42
 
41
- Keep `create-kudzu@0.1.101` while its template remains unchanged. Its `@kudzujs/core` range `^0.8.15` already accepts every planned Goal A patch. Publish `0.1.102` only when the template or minimum core range changes; a future `0.9.0` template must use `^0.9.0`.
43
+ `create-kudzu@0.1.102` adds an explicit install step to the generated README and clearer completion output. Its `@kudzujs/core` range `^0.8.15` accepts every current `0.8.x` patch; a future `0.9.0` template must use `^0.9.0`.
42
44
 
43
45
  ## Release Boundary
44
46
 
@@ -8,6 +8,7 @@ export function createBindingIndex(sourceFile) {
8
8
  const scopeByNode = new WeakMap()
9
9
  const referenceBindings = new WeakMap()
10
10
  const originalReferenceBindings = new WeakMap()
11
+ const declarationBindings = new WeakMap()
11
12
  const bindings = []
12
13
  const rootScope = createScope(undefined, sourceFile, "module")
13
14
  const pseudoBindings = new Map()
@@ -31,6 +32,8 @@ export function createBindingIndex(sourceFile) {
31
32
  bindings.push(binding)
32
33
  scope.bindings.set(identifier.text, binding)
33
34
  }
35
+ declarationBindings.set(identifier, binding)
36
+ declarationBindings.set(ts.getOriginalNode(identifier), binding)
34
37
  return binding
35
38
  }
36
39
 
@@ -218,6 +221,17 @@ export function createBindingIndex(sourceFile) {
218
221
  }
219
222
  }
220
223
 
224
+ function resolveBinding(identifier) {
225
+ const binding = declarationBindings.get(identifier) ?? declarationBindings.get(ts.getOriginalNode(identifier)) ?? lexicalBinding(scopeByNode.get(identifier) ?? rootScope, identifier.text)
226
+ if (!binding) return undefined
227
+ return {
228
+ slot: binding.slot,
229
+ debugName: binding.debugName,
230
+ declarationKind: binding.declarationKind,
231
+ ...(binding.declaration ? { declarationRange: range(binding.declaration) } : {})
232
+ }
233
+ }
234
+
221
235
  function references(root, boundary = root) {
222
236
  const found = []
223
237
  let complete = true
@@ -234,7 +248,7 @@ export function createBindingIndex(sourceFile) {
234
248
  return complete ? found : undefined
235
249
  }
236
250
 
237
- return { bindings: () => bindings.map(({ declaration, ...binding }) => ({ ...binding, ...(declaration ? { declarationRange: range(declaration) } : {}) })), hasNode: node => scopeByNode.has(node), references, resolveReference }
251
+ return { bindings: () => bindings.map(({ declaration, ...binding }) => ({ ...binding, ...(declaration ? { declarationRange: range(declaration) } : {}) })), hasNode: node => scopeByNode.has(node), references, resolveBinding, resolveReference }
238
252
  }
239
253
 
240
254
  function inside(node, boundary) {
@@ -1,5 +1,5 @@
1
1
  export function createComponentAnalysis(file) {
2
- return { version: 1, file, owners: [], specializations: [] }
2
+ return { version: 2, file, owners: [], specializations: [] }
3
3
  }
4
4
 
5
5
  export function createComponentAnalysisSession(analysis) {
@@ -40,7 +40,13 @@ export function createComponentAnalysisSession(analysis) {
40
40
  }
41
41
 
42
42
  function registerSpecialization(descriptor) {
43
- const specialization = { slot: analysis.specializations.length, ...descriptor }
43
+ const specialization = {
44
+ slot: analysis.specializations.length,
45
+ ...descriptor,
46
+ states: (descriptor.states ?? []).map((state, slot) => ({ slot, ...state })),
47
+ refs: (descriptor.refs ?? []).map((ref, slot) => ({ slot, ...ref })),
48
+ ids: (descriptor.ids ?? []).map((id, slot) => ({ slot, ...id }))
49
+ }
44
50
  analysis.specializations.push(specialization)
45
51
  return specialization
46
52
  }
@@ -3,23 +3,45 @@ import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
3
3
  import { knownGlobalNames } from "./analysis/binding-index.mjs"
4
4
  import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
5
5
  import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
6
- import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
6
+ import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler, registerSignal } from "./ir/module-ir.mjs"
7
7
 
8
8
  export function createSemanticArtifact(file) {
9
9
  return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
10
10
  }
11
11
 
12
- export function createDescriptorSession({ semantic, handlerUrl, factory, context, bindingIndex, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
12
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, bindingIndex, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, stateReferences = () => new Map(), symbolReference, sourceName = source => source.fileName }) {
13
13
  const { moduleIR } = semantic
14
+ moduleIR.symbols = bindingIndex.bindings()
14
15
  const nativeHandlers = []
15
16
  const effectHandlers = []
16
17
  const reactiveBindings = []
17
18
  const listExpressions = []
19
+ const pendingEffects = []
18
20
  const clientModules = new Set()
19
21
 
22
+ const signal = (name, node, references, aliases = []) => {
23
+ let reference = references?.get(name) ?? stateReferences(node).get(name)
24
+ if (!reference && bindingIndex) {
25
+ const original = ts.getOriginalNode(node)
26
+ const names = new Set([name, ...aliases])
27
+ let symbol = bindingIndex.references(original, original)?.find(entry => names.has(entry.debugName))?.slot
28
+ if (symbol === undefined) {
29
+ for (let current = node; current && symbol === undefined; current = current.parent) if (isFunctionLike(current)) {
30
+ const parameter = current.parameters.map(parameter => bindingIdentifier(parameter.name, names)).find(Boolean)
31
+ symbol = parameter && bindingIndex.resolveBinding(parameter)?.slot
32
+ break
33
+ }
34
+ }
35
+ if (symbol !== undefined) reference = { kind: "symbol", symbol }
36
+ }
37
+ reference ??= symbolReference?.(name, node, aliases)
38
+ if (!reference) throw new Error(`ModuleIR state ${JSON.stringify(name)} has no resolved StateRef`)
39
+ return registerSignal(moduleIR, reference, name).slot
40
+ }
41
+
20
42
  function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock, indexedBindingIndex) {
21
43
  const exportName = `listExpression${listExpressions.length}`
22
- listExpressions.push({ exportName, expression, item, index, states, role: "list-expression", keyedBlock, bindingIndex: indexedBindingIndex })
44
+ listExpressions.push({ exportName, expression, item, index, states, signalRefs: new Map([...states].map(name => [name, signal(name, expression)])), role: "list-expression", keyedBlock, bindingIndex: indexedBindingIndex })
23
45
  const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
24
46
  if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
25
47
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
@@ -28,7 +50,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
28
50
  function compileListConditional(entry) {
29
51
  const exportName = `listExpression${listExpressions.length}`
30
52
  const indexed = indexedReferences(bindingIndex, entry.condition, entry.condition)
31
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional", keyedBlock: entry.keyedBlock, bindingIndex: indexed ? bindingIndex : undefined })
53
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, states: new Set(), signalRefs: new Map(), role: "list-conditional", keyedBlock: entry.keyedBlock, bindingIndex: indexed ? bindingIndex : undefined })
32
54
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
33
55
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
34
56
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -57,9 +79,10 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
57
79
  const parts = conditionalParts(expression)
58
80
  const state = parts && directStateIdentifier(parts.condition, setters, bindingIndex)
59
81
  if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
60
- return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
82
+ return { node: factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy]) }
61
83
  }
62
- return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock))
84
+ const binding = reactiveBindings.length
85
+ return { node: factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock)), binding }
63
86
  }
64
87
 
65
88
  function compileConditional(kind, expression, truthy, falsy, setters) {
@@ -81,7 +104,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
81
104
  ? new Set(indexed.filter(reference => ["capture", "unresolved"].includes(reference.kind) && !setters.has(reference.debugName) && !allStateNames.has(reference.debugName) && !importedNames.has(reference.debugName)).map(reference => reference.debugName))
82
105
  : new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
83
106
  const exportName = `binding${reactiveBindings.length}`
84
- reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock, ...(indexed ? { bindingIndex } : {}) })
107
+ reactiveBindings.push({ slot: reactiveBindings.length, exportName, expression, captures, states: usedStates, signalRefs: new Map([...usedStates].map(name => [name, signal(name, expression)])), imports, role: "binding", keyedBlock, ...(indexed ? { bindingIndex } : {}) })
85
108
  const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
86
109
  const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
87
110
  const stateNames = new Set(usedStates)
@@ -107,7 +130,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
107
130
  const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock)
108
131
  if (optimized) return optimized
109
132
  rejectWorkerConstructions(expression)
110
- const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock })
133
+ const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock, stateOwners })
111
134
  return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
112
135
  factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
113
136
  ])
@@ -117,7 +140,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
117
140
  return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect", role: "effect" })
118
141
  }
119
142
 
120
- function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
143
+ function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, stateOwners, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
121
144
  const indexedBindingIndex = indexedReferences(bindingIndex, expression, expression) ? bindingIndex : undefined
122
145
  const allCaptures = nativeCaptureNames(expression, setters, indexedBindingIndex)
123
146
  const usedReducers = referencedReducerDispatches(expression.body, reducers, expression, indexedBindingIndex)
@@ -131,11 +154,13 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
131
154
  if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction, bindingIndex)) usedStates.add(state)
132
155
  }
133
156
  const exportName = `${prefix}${entries.length}`
134
- entries.push({ exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates, bindingIndex: indexedBindingIndex })
157
+ const entry = { exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates, signalRefs: new Map([...usedStates].map(name => [name, signal(name, expression, stateOwners, [...setters].filter(([, state]) => state === name).map(([setter]) => setter))])), bindingIndex: indexedBindingIndex }
158
+ entries.push(entry)
135
159
  const value = name => deferValues
136
160
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
137
161
  : factory.createIdentifier(name)
138
162
  return {
163
+ entry,
139
164
  exportName,
140
165
  states: factory.createArrayLiteralExpression([...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), value(name)]))),
141
166
  scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
@@ -149,11 +174,12 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
149
174
 
150
175
  function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
151
176
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
152
- const commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
153
- if (!commands.length || commands.some(command => !command)) return undefined
177
+ let commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
178
+ if ((!commands.length || commands.some(command => !command)) && compileEventCommand.handler) commands = compileEventCommand.handler(expression, setters, bindingIndex)
179
+ if (!commands?.length || commands.some(command => !command)) return undefined
154
180
  const original = ts.getOriginalNode(expression)
155
181
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
156
- const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
182
+ const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, reference: stateOwners.get(command.state) ?? stateReferences(expression).get(command.state) })), source)
157
183
  if (keyedBlock !== undefined) handler.keyedBlock = keyedBlock
158
184
  return generateCommandBehavior(moduleIR, handler, factory)
159
185
  }
@@ -175,7 +201,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
175
201
 
176
202
  function registerDerivedResult(kind, value, states = [], node) {
177
203
  const normalized = JSON.parse(JSON.stringify(value))
178
- return registerDerived(moduleIR, { kind, [kind]: normalized, states: [...states], ...(source(node) ? { source: source(node) } : {}) })
204
+ return registerDerived(moduleIR, { kind, [kind]: normalized, signals: [...states].map(name => signal(name, node)), ...(source(node) ? { source: source(node) } : {}) })
179
205
  }
180
206
 
181
207
  function registerKeyedBlockResult(descriptor) {
@@ -183,68 +209,79 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
183
209
  }
184
210
 
185
211
  function registerEffectResult(handler, descriptor) {
186
- return registerEffect(moduleIR, { ...descriptor, setup: { exportName: handler.exportName } })
212
+ const effect = { ...descriptor, setup: handler }
213
+ pendingEffects.push(effect)
214
+ return effect
187
215
  }
188
216
 
189
217
  function finalize() {
190
218
  const callbacks = [...nativeHandlers, ...effectHandlers]
219
+ const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
220
+ moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()].map((entry, slot) => ({ slot, ...entry }))
221
+ const importSlots = new Map(moduleIR.imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry.slot]))
222
+ const importSlot = entry => importSlots.get(`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`)
191
223
  for (const entry of callbacks) {
192
224
  const lowered = handlerLowering.lowerNativeHandler(entry)
193
225
  const setters = Map.groupBy(entry.setters, ([, state]) => state)
194
- registerModuleHandler(moduleIR, {
226
+ entry.handler = registerModuleHandler(moduleIR, {
195
227
  role: entry.role,
196
228
  ...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
197
229
  exportName: entry.exportName,
198
230
  async: Boolean(entry.expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)),
199
231
  generator: Boolean(entry.expression.asteriskToken),
200
232
  signals: [...entry.usedStates].map(name => ({
233
+ signal: entry.signalRefs.get(name),
201
234
  name,
202
235
  setters: (setters.get(name) ?? []).map(([setter]) => setter),
203
236
  value: entry.deferValues ? "deferred" : "direct",
204
237
  snapshot: lowered.stateSnapshots.includes(name)
205
238
  })),
206
239
  captures: [...entry.captures].map(name => ({
240
+ ...(symbolSlot(entry, name) !== undefined ? { symbol: symbolSlot(entry, name) } : {}),
207
241
  name,
208
242
  source: name === (typeof entry.listItem === "string" ? entry.listItem : entry.listItem?.item) ? "list-item" : name === entry.listItem?.index ? "list-index" : "scope",
209
243
  value: entry.deferValues ? "deferred" : "direct",
210
244
  snapshot: lowered.captureSnapshots.includes(name)
211
245
  })),
212
- imports: entry.imports.map(importRecord),
246
+ imports: entry.imports.map(entry => importSlot(importRecord(entry))),
213
247
  code: lowered.code,
214
248
  ...(source(entry.expression) ? { source: source(entry.expression) } : {})
215
249
  })
216
250
  }
217
251
  for (const entry of reactiveBindings) registerBinding(moduleIR, {
252
+ slot: entry.slot,
218
253
  role: entry.role,
219
254
  ...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
220
255
  exportName: entry.exportName,
221
256
  parameters: ["__k"],
222
- states: [...entry.states],
223
- captures: [...entry.captures].map(name => ({ name, source: "scope" })),
224
- imports: entry.imports.map(importRecord),
257
+ signals: [...entry.states].map(name => entry.signalRefs.get(name)),
258
+ captures: [...entry.captures].map(name => ({ ...(symbolSlot(entry, name) !== undefined ? { symbol: symbolSlot(entry, name) } : {}), name, source: "scope" })),
259
+ imports: entry.imports.map(entry => importSlot(importRecord(entry))),
225
260
  code: handlerLowering.lowerReactiveBinding(entry),
226
261
  ...(source(entry.expression) ? { source: source(entry.expression) } : {})
227
262
  })
228
- for (const entry of listExpressions) registerBinding(moduleIR, {
263
+ for (const [index, entry] of listExpressions.entries()) registerBinding(moduleIR, {
264
+ slot: reactiveBindings.length + index,
229
265
  role: entry.role,
230
266
  ...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
231
267
  exportName: entry.exportName,
232
268
  parameters: [entry.item, entry.index ?? "__kIndex", "__k"],
233
- states: [...(entry.states ?? [])],
269
+ signals: [...(entry.states ?? [])].map(name => entry.signalRefs.get(name)),
234
270
  captures: [],
235
271
  imports: [],
236
272
  code: handlerLowering.lowerListExpression(entry),
237
273
  ...(source(entry.expression) ? { source: source(entry.expression) } : {})
238
274
  })
239
- for (const effect of moduleIR.effects) {
240
- const handler = moduleIR.handlers.find(candidate => candidate.kind === "module-export" && candidate.role === "effect" && candidate.exportName === effect.setup.exportName)
241
- if (!handler) throw new Error(`Effect handler ${JSON.stringify(effect.setup.exportName)} was not finalized`)
242
- effect.setup = { handler: handler.slot }
275
+ for (const effect of pendingEffects) {
276
+ if (!effect.setup.entry.handler) throw new Error(`Effect handler ${JSON.stringify(effect.setup.exportName)} was not finalized`)
277
+ registerEffect(moduleIR, { ...effect, setup: { handler: effect.setup.entry.handler.slot } })
243
278
  }
244
- const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
245
- moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()]
246
279
  moduleIR.clientModules = [...clientModules]
247
- assertModuleIRReferences(moduleIR)
280
+ assertModuleIRReferences(moduleIR, semantic.componentAnalysis)
281
+ }
282
+
283
+ function symbolSlot(entry, name) {
284
+ return entry.bindingIndex?.references(entry.expression, entry.expression)?.find(reference => reference.debugName === name)?.slot
248
285
  }
249
286
 
250
287
  function source(node) {
@@ -255,7 +292,15 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
255
292
 
256
293
  const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
257
294
 
258
- return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult }
295
+ return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult, signal }
296
+ }
297
+
298
+ function bindingIdentifier(name, names) {
299
+ if (ts.isIdentifier(name)) return names.has(name.text) ? name : undefined
300
+ for (const element of name.elements) if (ts.isBindingElement(element)) {
301
+ const identifier = bindingIdentifier(element.name, names)
302
+ if (identifier) return identifier
303
+ }
259
304
  }
260
305
 
261
306
  function directStateIdentifier(expression, setters, bindingIndex) {