@kudzujs/core 0.8.39 → 0.8.41

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.39` 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.41` 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,96 @@
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
+ ## Current 0.8.41 Release Snapshot
6
+
7
+ Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host, Node 24.14.0, and Chrome 142.0.7444.175. The tracked runtime matrix used one warm-up, seven interleaved clean builds, and seven rotating fresh Chrome profiles. Every correctness, accessibility, identity, effect-cleanup, and browser-error gate passed. Kudzu emits complete initial HTML while React, Vue, and Svelte start from client-rendered shells, so initial delivery and artifact totals are not architecture-equivalent.
8
+
9
+ | Runtime matrix target | Build median | HTML raw / gzip B | JS raw / gzip B | Total raw / gzip B | Complete initial DOM |
10
+ |---|---:|---:|---:|---:|---:|
11
+ | Kudzu `0.8.41` release tree | 1,475.480 ms | 179,388 / 37,008 | 33,575 / 12,928 | 212,963 / 49,936 | 532.9 ms |
12
+ | React 19.2.8 + Vite 8.1.5 | 856.231 ms | 282 / 219 | 193,685 / 60,043 | 193,967 / 60,262 | 510.6 ms |
13
+ | Vue 3.5.40 + Vite 8.1.5 | 1,053.062 ms | 281 / 221 | 64,023 / 24,772 | 64,304 / 24,993 | 357.6 ms |
14
+ | Svelte 5.56.7 + Vite 8.1.5 | 1,733.637 ms | 281 / 219 | 40,726 / 15,659 | 41,007 / 15,878 | 401.1 ms |
15
+
16
+ Raw runtime arrays, quartiles, checkout metadata, source hash, and validation results are checked in at `benchmarks/runtime-matrix/results/raw.json`. The maintained 2,000-row keyed run used one warm-up, seven clean builds, and seven fresh Chrome profiles: build 901.4 ms, append 14.7 ms, filter 25.4 ms, restore 126.2 ms, reverse 25.1 ms, and JavaScript 28,450 B raw / 11,036 B gzip. A requested 21-profile keyed run exceeded the 600-second limit and produced no result.
17
+
18
+ The maintained Worker benchmark recorded a 1,823.2 ms build median, 907 B raw / 477 B gzip Worker graph, and 12,148 B raw / 5,411 B gzip aggregate window graph. The tracked six-route commerce sources used one warm-up and seven rotating clean builds:
19
+
20
+ | Commerce target | Build median | Files | HTML raw / gzip B | JS raw / gzip B | Total raw / gzip B |
21
+ |---|---:|---:|---:|---:|---:|
22
+ | Kudzu | 867.188 ms | 17 | 17,123 / 5,376 | 18,428 / 8,261 | 37,434 / 14,689 |
23
+ | React 19.2.8 SSR + Vite hydration | 859.125 ms | 10 | 9,304 / 4,265 | 198,261 / 61,464 | 209,270 / 66,741 |
24
+ | Next.js 16.2.11 static export | 7,290.533 ms | 74 | 72,890 / 19,857 | 643,484 / 191,844 | 814,186 / 247,596 |
25
+ | Nuxt 4.5.0 generation | 7,334.287 ms | 26 | 17,557 / 7,850 | 191,758 / 70,925 | 215,851 / 82,067 |
26
+ | SvelteKit 2.70.1 static export | 4,725.279 ms | 19 | 15,832 / 6,511 | 85,095 / 33,477 | 102,659 / 41,047 |
27
+
28
+ The commerce targets share initial content and behavior contracts but use materially different architectures. The browser suite timed out in Kudzu's existing in-flight rejection-navigation wait before cross-target sampling, so no commerce browser timing is claimed.
29
+
30
+ ## P1 Direct Two-Boundary Callback And Ref Dataflow
31
+
32
+ Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.40` at `c88b94f91b40d76fad58a208f3261de399c6d2b4`. The compiler and migration checks had SHA-256 `f80ec9c532376a90805c6d99b8cbfa162578592c71c5e62f1cb92e16a64a5ba3`.
33
+
34
+ One warm-up and seven alternating clean fresh-process builds produced byte-identical 176-file deploy graphs, 3,850,245 raw bytes, 1,984,991 aggregate gzip bytes, deploy SHA-256 `d24f9d4608b9ae096fa5e334cf8c6556e51d8588bff02d15e3eacf0c6711db81`, and `kudzu-plan.json` SHA-256 `302d75dac6f58306c139b398a436480cf60cdd5743e6100ab1c728bd255e16be`.
35
+
36
+ | Target | Clean build median | Range | Peak RSS median |
37
+ |---|---:|---:|---:|
38
+ | `v0.8.40` | 3,565.236 ms | 2,960.844-3,995.094 ms | 364.6 MiB |
39
+ | Two-boundary candidate | 3,470.277 ms | 3,099.259-3,842.591 ms | 362.0 MiB |
40
+
41
+ The candidate unpaired median is 2.66% lower; round-paired candidate-minus-baseline differences have a +59.834 ms median. Peak RSS is 0.74% lower. Ranges overlap and no material performance change is claimed.
42
+
43
+ ```text
44
+ v0.8.40: [3671.298,3995.094,3776.594,3295.688,2960.844,3565.236,3563.737]
45
+ candidate: [3099.259,3674.641,3842.591,3400.780,3418.781,3470.277,3623.571]
46
+ paired candidate-baseline: [-572.039,-320.453,65.997,105.092,457.937,-94.959,59.834]
47
+ ```
48
+
49
+ The FIRE-derived callback/ref fixture now forwards both a direct setter adapter and a simple state callback through one imported presentation component. Browser checks preserve parent state updates, child-local state/effects/IDs, parent ref resolution, conditional cleanup, fresh remount, and a zero-JavaScript static sibling. ComponentAnalysis retains the same parent SignalIR on each nested specialization. A third callback-carrying boundary remains rejected. No browser runtime, callback registry, component function, route artifact, or deploy byte was added.
50
+
51
+ ## P1 Property-Level Object-State Effect Dependencies
52
+
53
+ Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, npm 11.9.0, and Chrome 142.0.7444.175. The baseline was clean tag `v0.8.39` at `090b42124d596260bdd6a0814c014e5b906dc0eb`. The focused implementation and migration checks had SHA-256 `c097c10e786ff5d09537b52384c1bf3d32cb46998ef62125c23dda3355b62277`, produced by:
54
+
55
+ ```bash
56
+ sha256sum framework/compiler/effect-analysis.mjs framework/core.mjs test/compiler-passes.test.mjs test/fixtures/zustand-migration/src/Shell.tsx test/framework.test.mjs | sha256sum
57
+ ```
58
+
59
+ The complete 154-page repository build used one warm-up and 21 alternating clean fresh-process samples. Both targets emit 175 files and byte-identical `kudzu-plan.json` (`be842101f08aab2d8b3af5daa3ed0539a6a08ea8c3cba15cd25f3a36d53ef61f`). The only deploy difference is the updated public explanation in `docs/index.html`: candidate output adds 98 raw bytes and 31 aggregate gzip bytes. Compiler, effect runtime, route JavaScript, and all other artifacts are byte-identical.
60
+
61
+ | Target | Clean build median | Range | Peak RSS median | Deploy raw / gzip bytes |
62
+ |---|---:|---:|---:|---:|
63
+ | `v0.8.39` | 3,753.276 ms | 3,097.211-5,294.316 ms | 356.9 MiB | 3,845,393 / 1,983,262 B |
64
+ | Property dependency candidate | 3,883.841 ms | 3,533.936-4,919.378 ms | 363.7 MiB | 3,845,491 / 1,983,293 B |
65
+
66
+ The candidate unpaired median is 3.48% higher and the round-paired candidate-minus-baseline median is +103.031 ms. Peak RSS is 1.92% higher. Timing ranges overlap substantially and both changes remain below the 5% material-regression threshold, so no material build or memory regression is established.
67
+
68
+ ```text
69
+ v0.8.39: [5294.316,4885.317,4462.086,3696.012,3105.264,3491.623,3852.100,3414.218,3753.276,3854.507,4311.174,3833.095,3097.211,3669.963,3567.084,3399.614,3626.351,3687.408,3883.556,4104.665,3880.847]
70
+ candidate: [4919.378,4548.065,4236.325,3752.842,3788.156,3738.609,3883.841,4110.706,4055.150,4116.317,3710.973,4016.578,3533.936,3625.603,3670.115,3840.795,4045.689,3988.434,3619.226,3944.068,3760.976]
71
+ paired candidate-baseline: [-374.938,-337.252,-225.761,56.830,682.892,246.986,31.741,696.488,301.874,261.810,-600.201,183.483,436.725,-44.360,103.031,441.181,419.338,301.026,-264.330,-160.597,-119.871]
72
+ ```
73
+
74
+ The tracked runtime matrix then ran one warm-up, seven interleaved clean builds, and seven rotating fresh Chrome profiles for matched Kudzu, React/Vite, Vue/Vite, and Svelte/Vite workloads. Every correctness, accessibility, identity, effect-cleanup, and browser-error gate passed. This matrix uses a primitive effect dependency, so it is broad regression evidence rather than a property-dependency-specific framework claim.
75
+
76
+ | Runtime matrix target | Build median | JS raw / gzip B | Total raw / gzip B | Initial DOM | Effect update + cleanup |
77
+ |---|---:|---:|---:|---:|---:|
78
+ | Kudzu `0.8.39` candidate | 862.432 ms | 33,575 / 12,928 | 212,963 / 49,936 | 363.4 ms | 1.9 ms |
79
+ | React 19.2.8 + Vite 8.1.5 | 498.572 ms | 193,685 / 60,043 | 193,967 / 60,262 | 355.7 ms | 4.4 ms |
80
+ | Vue 3.5.40 + Vite 8.1.5 | 655.787 ms | 64,023 / 24,772 | 64,304 / 24,993 | 283.0 ms | 1.9 ms |
81
+ | Svelte 5.56.7 + Vite 8.1.5 | 907.584 ms | 40,726 / 15,659 | 41,007 / 15,878 | 307.9 ms | 2.5 ms |
82
+
83
+ The tracked six-route commerce sources also completed seven rotating clean builds for Kudzu, React SSR + Vite hydration, Next.js static export, Nuxt generation, and SvelteKit adapter-static. These targets have matched initial content and behavior contracts but materially different architectures.
84
+
85
+ | Commerce target | Build median | Files | HTML raw / gzip B | JS raw / gzip B | Total raw / gzip B |
86
+ |---|---:|---:|---:|---:|---:|
87
+ | Kudzu | 1,062.059 ms | 17 | 17,123 / 5,376 | 18,428 / 8,261 | 37,434 / 14,689 |
88
+ | React 19.2.8 SSR + Vite hydration | 1,020.288 ms | 10 | 9,304 / 4,265 | 198,261 / 61,464 | 209,270 / 66,741 |
89
+ | Next.js 16.2.11 static export | 7,939.352 ms | 74 | 72,890 / 19,860 | 643,484 / 191,844 | 814,186 / 247,612 |
90
+ | Nuxt 4.5.0 generation | 8,566.437 ms | 26 | 17,557 / 7,857 | 191,758 / 70,925 | 215,851 / 82,075 |
91
+ | SvelteKit 2.70.1 static export | 5,778.467 ms | 19 | 15,832 / 6,520 | 85,095 / 33,482 | 102,659 / 41,061 |
92
+
93
+ Kudzu commerce output is 4.54x-16.86x smaller in aggregate gzip than the compared hydrated/client-navigation outputs. React's clean build median is 3.9% lower than Kudzu's on this small fixture; Kudzu builds 5.4x-8.1x faster than the three full static-export frameworks. The full commerce browser command was attempted twice, but both runs timed out in Kudzu's existing in-flight rejection-navigation wait before cross-target browser sampling; no commerce browser timing result is claimed. The complete runtime matrix browser run above passed. Property-path effects reuse existing DerivedIR, source subscriptions, expression evaluation, and `Object.is`; no runtime module or field-signal mechanism was added.
94
+
5
95
  ## P0.12 Deep RouteIR And CapabilityIR Validation
6
96
 
7
97
  Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.38` at `918f721d369fed486c5ff04a7423f79563e1c42e`. The focused implementation files had SHA-256 `f8f58432f264d5df12cadfba07986712cecf148be300b6071c03adaf42fd6594`, 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.39 - Fail-closed route contracts.** RouteIR, RouteBuildRecord, and CapabilityIR now reject invalid state, command, effect, binding, condition, list, ownership, and capability references before browser code generation. Read the [release notes](./RELEASES.md#0839---fail-closed-route-contracts), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.39), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.41 - Two-boundary callback ownership.** Direct setters, simple state callbacks, and object refs can now follow one additional presentation-component boundary while Kudzu preserves parent signals and child ownership without a browser component tree. Read the [release notes](./RELEASES.md#0841---two-boundary-callback-ownership), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.41), 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)
package/RELEASES.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.41 - Two-boundary callback ownership
4
+
5
+ Kudzu 0.8.41 extends ordinary React-shaped component migration through one additional proven callback/ref forwarding boundary without retaining component functions or adding browser runtime machinery.
6
+
7
+ ### Changed in 0.8.41
8
+
9
+ - A direct setter or inline/simple `const` state callback may cross a child component and one additional direct forwarding component before one intrinsic handler invokes it.
10
+ - The forwarding component must destructure the callback and pass it directly once as an `on*` JSX prop; aliases, spreads, intermediate adapters, repeated uses, and a third callback boundary remain fail-closed.
11
+ - Parent SignalIR remains stable through nested specialization, including direct setter input adapters and simple callbacks that capture parent state.
12
+ - Specialized children retain independent local state, effects, deterministic IDs, and `null`-initialized object refs through conditional cleanup and fresh remount.
13
+ - A parent-owned object ref may follow the same proven component tree to one direct intrinsic root.
14
+ - Static siblings still emit complete HTML with zero JavaScript; no callback registry, retained component tree, or browser runtime module was added.
15
+
16
+ ### Performance
17
+
18
+ - Seven alternating clean builds measured `v0.8.40` and candidate medians of 3,565.236 ms and 3,470.277 ms. The +59.834 ms paired candidate-minus-baseline median and overlapping ranges establish no material build change.
19
+ - Peak RSS medians were 364.6 MiB and 362.0 MiB.
20
+ - Both targets emit byte-identical 176-file deploy graphs: 3,850,245 raw bytes, 1,984,991 aggregate gzip bytes, and identical route plans and deploy digests.
21
+ - Current maintained Worker, keyed-list, runtime-matrix, and commerce build measurements are recorded in `PERFORMANCE.md`; architecture and timeout limitations are explicit.
22
+
23
+ ### Validation
24
+
25
+ - `npm run check`, `npm test`, and `npm run test:package` pass with all 206 tests and 156 generated pages.
26
+ - Browser coverage proves parent updates, child-local state/effects/IDs, parent ref resolution, conditional cleanup, and fresh remount across two boundaries.
27
+ - Compiler-result checks prove nested specializations retain the original parent signal, while a dedicated fixture rejects a third callback boundary at the authored source.
28
+ - Broader prop, callback, ref, and Context graphs remain migration-led work.
29
+
30
+ ### Upgrade
31
+
32
+ ```bash
33
+ npm install @kudzujs/core@^0.8.41
34
+ ```
35
+
36
+ ## 0.8.40 - Property-level effect dependencies
37
+
38
+ Kudzu 0.8.40 starts the large-application foundation sequence by allowing ordinary object state to expose property-level effect dependencies without artificial primitive-state decomposition.
39
+
40
+ ### Changed in 0.8.40
41
+
42
+ - Direct property paths over ordinary object state compile to existing tagged DerivedIR.
43
+ - Top-level immutable primitive locals derived from object properties use the same source subscription and expression evaluator.
44
+ - Source object commits schedule dependency comparison, while `Object.is` prevents cleanup and rerun when the selected property value is unchanged.
45
+ - Whole-object, dynamic-property, object-valued, and mixed whole-object/property dependencies remain fail-closed.
46
+ - The Zustand migration fixture proves property updates, equality across enhanced navigation, persistent layout ownership, and removal.
47
+ - No field signal, proxy, component rerender, RouteIR field, or browser runtime module was added.
48
+
49
+ ### Performance
50
+
51
+ - Twenty-one alternating clean builds measured `v0.8.39` and candidate medians of 3,753.276 ms and 3,883.841 ms, with a +103.031 ms paired median.
52
+ - Peak RSS medians were 356.9 MiB and 363.7 MiB. Build and memory deltas remain below the 5% material-regression threshold.
53
+ - Both targets emit 175 files and byte-identical `kudzu-plan.json`; only updated docs add 98 raw and 31 aggregate gzip bytes. Runtime and route JavaScript are unchanged.
54
+ - Seven-build/fresh-profile React, Vue, and Svelte runtime comparisons pass. The matched commerce build matrix records Kudzu output at 14,689 aggregate gzip bytes versus 66,741 for React hydration, 247,612 for Next, 82,075 for Nuxt, and 41,061 for SvelteKit.
55
+
56
+ ### Validation
57
+
58
+ - `npm run check`, `npm test`, and `npm run test:package` pass with all 205 tests and 155 generated pages.
59
+ - Focused checks cover direct object-property classification, canonical DerivedIR, whole-object/property ambiguity, RouteIR projection, browser equality behavior, and layout lifetime.
60
+ - The next migration-backed investigation is multi-boundary component/prop/callback/ref/context dataflow.
61
+
62
+ ### Upgrade
63
+
64
+ ```bash
65
+ npm install @kudzujs/core@^0.8.40
66
+ ```
67
+
3
68
  ## 0.8.39 - Fail-closed route contracts
4
69
 
5
70
  Kudzu 0.8.39 completes P0.12 by deeply validating the concrete RouteIR, RouteBuildRecord, and CapabilityIR contracts before artifact selection and browser code generation.
@@ -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.39` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 deep RouteIR and CapabilityIR validation is complete; the next migration-backed investigation is property-level derived dependencies over ordinary object state. 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.41` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 validation, property-level object-state dependencies, and the first direct two-boundary callback/ref slice are complete; broader multi-boundary component/prop/callback/ref/context dataflow remains the next migration-backed investigation. 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.39` 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.41` 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,7 +17,7 @@ This maps the current `0.8.39` 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) | 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. |
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. One additional proven callback/ref forwarding component specializes into the same parent signal and intrinsic ownership; AST identity remains private to its source-local session. |
21
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
22
  | Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler modules, Workers, package-client modules, and bundle entry/chunk closure derive from structural edges without serialized output searches or formatted composite keys. |
23
23
  | Route contract validation | [`framework/compiler/route-ir.mjs`](../../framework/compiler/route-ir.mjs), `assertRouteIR()` | Fails before artifact selection for invalid state/parameter identity, commands, native/effect captures and dependencies, reactive descriptors, conditions, keyed-list identity/ownership, marker fields, or JSON safety. Immutable in-memory contracts validate once by identity. |
@@ -25,7 +25,7 @@ This maps the current `0.8.39` architecture, built on the completed `0.8.23` Goa
25
25
  | 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. |
26
26
  | 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. |
27
27
  | 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. |
28
- | Effect analysis | [`framework/compiler/effect-analysis.mjs`](../../framework/compiler/effect-analysis.mjs) | Classifies ordered signal, derived, and keyed-item dependencies and validates cleanup-owned browser resources before EffectIR registration. |
28
+ | Effect analysis | [`framework/compiler/effect-analysis.mjs`](../../framework/compiler/effect-analysis.mjs) | Classifies ordered signal, ordinary object-property DerivedIR, and keyed-item dependencies and validates cleanup-owned browser resources before EffectIR registration. Object sources keep one signal subscription while the existing tagged evaluator and `Object.is` compare the selected primitive value. |
29
29
  | Worker graph | [`framework/compiler/worker-compiler.mjs`](../../framework/compiler/worker-compiler.mjs) | Returns functional Worker rewrite results and JSON-safe EffectIR edges, validates relative graphs, emits content-hashed ESM, and resolves placeholders only for rendered effects. |
30
30
  | Shared path conversion | [`framework/compiler/path-helpers.mjs`](../../framework/compiler/path-helpers.mjs) | Converts project-relative module, browser, asset, and base paths for build and development serving. |
31
31
  | Build-time JSX execution | [`framework/core.mjs`](../../framework/core.mjs), `renderPage()` | Executes compiled pages/layouts, allocates deterministic route/layout ownership IDs, emits complete HTML, and returns RouteIR v1 plus capability facts and exact retained handler references. |
@@ -166,7 +166,7 @@ P0 creates the semantic base required by every later large-application capabilit
166
166
  | Objective | Required result |
167
167
  |---|---|
168
168
  | Cross-module component and prop dataflow | Deep composition, callbacks, setters, refs, context, and children retain semantic links without repeated AST inlining |
169
- | Property-level derived dependencies | Object state can expose path-level dependencies without splitting application state into artificial primitives |
169
+ | Property-level derived dependencies | Object state exposes direct property-path effect dependencies through existing DerivedIR without splitting application state into artificial primitives; multi-boundary propagation remains ordered next |
170
170
  | Package-neutral shared state and actions | Existing Zustand support lowers through generic signal/action records; Redux-shaped research has a reusable target |
171
171
  | Resource ownership | WebSocket, SSE, subscriptions, timers, observers, Workers, and imperative SDKs can share acquire/owner/dependency/cleanup semantics |
172
172
  | Browser-only package modules | Native JS packages may be bundled only into effect/handler/resource chunks without build-time execution |
@@ -377,8 +377,8 @@ function increment(value) { setCount(value + 1) }; increment(count)
377
377
 
378
378
  After the relevant P0 foundations, investigate capabilities in this order:
379
379
 
380
- 1. Property-level derived dependencies over ordinary object state.
381
- 2. Multi-boundary component/prop/callback/ref/context dataflow.
380
+ 1. Property-level derived dependencies over ordinary object state. **Completed in `0.8.40`:** direct property paths and top-level immutable primitive locals over object state reuse tagged DerivedIR, subscribe to the source signal, and compare selected values with `Object.is`; whole-object and dynamic dependencies remain rejected.
381
+ 2. Multi-boundary component/prop/callback/ref/context dataflow. **In progress after `0.8.41`:** one additional direct setter-callback/ref forwarding component preserves the parent SignalIR and final intrinsic ownership; broader prop, callback, ref, and Context graphs remain migration-led work.
382
382
  3. Package-neutral shared state/actions and migration of current Zustand internals.
383
383
  4. Browser-only package imports in owned effect/resource modules.
384
384
  5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics.
@@ -480,4 +480,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
480
480
 
481
481
  ## Immediate Decision
482
482
 
483
- PR 1 through PR 12 are complete. The next migration-backed investigation is **property-level derived dependencies over ordinary object state**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
483
+ PR 1 through PR 12, the `0.8.40` property-dependency slice, and the `0.8.41` direct multi-boundary callback/ref slice are complete. Continue **multi-boundary component/prop/callback/ref/context dataflow** from the next real migration blocker. 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.39` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.41` 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
 
@@ -30,6 +30,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
30
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. |
31
31
  | `0.8.38` | Replace serialized route artifact discovery with validated RouteBuildRecord edges. | Handler, effect, Worker, CSS, package-client, and chunk retention is structural while deploy files, bytes, plans, and browser behavior remain unchanged. |
32
32
  | `0.8.39` | Deeply validate concrete RouteIR, RouteBuildRecord, and CapabilityIR contracts. | Invalid state, command, effect, binding, condition, list, ownership, capability, and JSON references fail before codegen without changing deploy output. |
33
+ | `0.8.40` | Add property-level derived effect dependencies over ordinary object state. | Direct property paths and immutable primitive locals reuse existing DerivedIR and `Object.is`; whole-object/dynamic dependencies fail, runtime JavaScript is unchanged, and measured build/RSS changes remain below 5%. |
34
+ | `0.8.41` | Preserve callback, setter, and ref ownership through one additional direct presentation-component boundary. | Parent signals, child hooks, IDs, effects, refs, conditional cleanup, static zero-JavaScript output, and byte-identical deploy graphs pass; a third callback boundary fails closed. |
33
35
 
34
36
  ## Sequence Rules
35
37
 
@@ -86,7 +86,7 @@ Rendered collection selectors compile immutable local aliases and inline `(item)
86
86
 
87
87
  The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. An optional inline, same-file, or relative-imported synchronous one-parameter initializer may derive a directly serializable literal only from its directly serializable initial argument; the compiler substitutes that argument and lowers the call to the ordinary two-argument ownership path. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing directly serializable literal defaults and direct intrinsic rest props in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
88
88
 
89
- A direct setter may cross one same-file or relative-imported component boundary when the child invokes it exactly once inside an intrinsic event handler. Inline and simple `const` setter callbacks may use that adapter shape or direct event forwarding. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. The child may also own directly serializable `useState()` values or initialize string state with one direct primitive state prop's zero-argument `.toString()`, plus `useId()` values, supported effects, and object refs initialized with `null`. Same-file and relative-imported nested components recursively specialize away; nested hooks join the same generated owner only on unconditional or statically truthy paths. Dynamic nested hook paths, forwarding the setter through a second boundary, and other dynamic state initializers remain unsupported. A parent-owned `null`-initialized object ref may cross the same boundary to the direct intrinsic root. These remain compiler-owned descriptors: conditional removal drops the handler with the element, deletes child state, resolves refs to `null`, and runs effect cleanup, while remount creates fresh ownership and dependency effects synchronize prop-derived state without retaining a component instance.
89
+ A direct setter or inline/simple `const` setter callback may cross one same-file or relative-imported component boundary and one additional direct forwarding component before an intrinsic handler invokes it exactly once. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. The intermediate component must destructure and directly forward the callback once as an `on*` JSX prop; third-boundary forwarding, aliases, spreads, intermediate adapters, and repeated uses remain unsupported. Specialized children may own directly serializable `useState()` values or initialize string state with one direct primitive state prop's zero-argument `.toString()`, plus `useId()` values, supported effects, and object refs initialized with `null`. Same-file and relative-imported presentation components recursively specialize away; nested hooks join the same generated owner only on unconditional or statically truthy paths. A parent-owned `null`-initialized object ref may follow the proven tree to the direct intrinsic root. These remain compiler-owned descriptors: conditional removal drops the handler with the element, deletes child state, resolves refs to `null`, and runs effect cleanup, while remount creates fresh ownership and dependency effects synchronize prop-derived state without retaining a component instance.
90
90
 
91
91
  `kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
92
92
 
@@ -20,9 +20,6 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
20
20
  ordinaryDependencies.push(dependency)
21
21
  }
22
22
  }
23
- const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
24
- if (invalidDependency) fail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
25
-
26
23
  const entries = []
27
24
  const dependencyStates = new Map()
28
25
  const substitutions = new Map()
@@ -30,28 +27,36 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
30
27
  let hasDerived = false
31
28
  const stateNames = new Set(setters.values())
32
29
  for (const dependency of ordinaryDependencies) {
33
- const declarations = localDeclarations?.get(dependency.text)
30
+ const direct = ts.isIdentifier(dependency)
31
+ if (!direct && !statePropertyDependency(dependency, stateNames)) fail(dependency, "useEffect() dependencies must be direct state or runtime parameter identifiers or property reads")
32
+ const declarations = direct ? localDeclarations?.get(dependency.text) : undefined
34
33
  const initializer = declarations?.length === 1 ? declarations[0].initializer : undefined
35
34
  const indexedInitializer = initializer && bindingIndex?.hasNode(initializer) ? bindingIndex : undefined
36
35
  const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text) && (!indexedInitializer || ["capture", "unresolved"].includes(indexedInitializer.resolveReference(unwrapExpression(initializer), initializer)?.kind))
37
- const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters, initializer, bindingIndex) : new Set()
36
+ const expressionSource = initializer && !directAlias ? initializer : direct ? undefined : dependency
37
+ let derivedStates = expressionSource ? referencedStateNames(expressionSource, setters, expressionSource, bindingIndex) : new Set()
38
+ if (!derivedStates.size && expressionSource) derivedStates = referencedStateNames(expressionSource, setters, expressionSource)
38
39
  if (derivedStates.size) {
39
40
  const usedStates = new Set()
40
- const expression = collectionExpression(initializer, { fail, stateNames, selectorStates: usedStates })
41
- if (!usedStates.size) fail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
42
- entries.push({ kind: "derived", name: dependency.text, expression, states: usedStates, source: initializer })
41
+ const expression = collectionExpression(expressionSource, { fail, stateNames, selectorStates: usedStates })
42
+ if (!usedStates.size) fail(dependency, `useEffect() derived dependency must read direct state`)
43
+ entries.push({ kind: "derived", name: direct ? dependency.text : dependency.getText(), expression, states: usedStates, source: expressionSource })
43
44
  for (const name of usedStates) {
44
45
  subscriptions.push(factory.createIdentifier(name))
45
46
  dependencyStates.set(name, factory.createIdentifier(name))
46
47
  }
47
- substitutions.set(dependency.text, initializer)
48
+ if (initializer) substitutions.set(dependency.text, initializer)
48
49
  hasDerived = true
49
50
  } else {
51
+ if (!direct) fail(dependency, "useEffect() dependencies must be direct state or runtime parameter identifiers or property reads")
50
52
  subscriptions.push(dependency)
51
53
  entries.push({ kind: "signal", name: dependency.text })
52
54
  dependencyStates.set(dependency.text, dependency)
53
55
  }
54
56
  }
57
+ const derivedSourceNames = new Set(entries.filter(entry => entry.kind === "derived").flatMap(entry => [...entry.states]))
58
+ const ambiguous = entries.find(entry => entry.kind === "signal" && derivedSourceNames.has(entry.name))
59
+ if (ambiguous) fail(ordinaryDependencies[entries.indexOf(ambiguous)], `useEffect() cannot mix whole-object and property dependencies for state ${JSON.stringify(ambiguous.name)}`)
55
60
  if (!hasDerived) dependencyStates.clear()
56
61
  return { dependencyItem, itemDependencies, ordinaryDependencies, entries, dependencyStates, substitutions, subscriptions, hasDerived }
57
62
  }
@@ -91,3 +96,14 @@ function directProperty(expression, objectName) {
91
96
  const value = unwrapExpression(expression)
92
97
  return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && value.expression.text === objectName ? value.name.text : undefined
93
98
  }
99
+
100
+ function statePropertyDependency(expression, stateNames) {
101
+ let value = unwrapExpression(expression)
102
+ let property = false
103
+ while (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) {
104
+ if (ts.isElementAccessExpression(value) && !ts.isStringLiteral(value.argumentExpression) && !ts.isNumericLiteral(value.argumentExpression)) return false
105
+ property = true
106
+ value = unwrapExpression(value.expression)
107
+ }
108
+ return property && ts.isIdentifier(value) && stateNames.has(value.text)
109
+ }
@@ -1013,7 +1013,21 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1013
1013
  folded.parent = root.parent
1014
1014
  return folded
1015
1015
  }
1016
- const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
1016
+ const validateSetterCallbackProps = (call, component, callbackProps) => {
1017
+ if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
1018
+ for (const prop of callbackProps) {
1019
+ const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
1020
+ if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
1021
+ const references = []
1022
+ const collectReferences = node => {
1023
+ if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
1024
+ ts.forEachChild(node, collectReferences)
1025
+ }
1026
+ collectReferences(component.body)
1027
+ if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1028
+ }
1029
+ }
1030
+ const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners, callbackDepth = 1) => {
1017
1031
  root = foldSetterStaticConditions(root)
1018
1032
  const replacements = new WeakMap()
1019
1033
  let count = 0
@@ -1049,10 +1063,27 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1049
1063
  for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
1050
1064
  const stateOwners = new Map(parentStateOwners)
1051
1065
  for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
1052
- if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
1066
+ const callbackProps = jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction))
1067
+ const callbackSubstitutions = new Map()
1068
+ if (callbackProps.length) {
1069
+ if (!callbackDepth) fail(node, "Setter callbacks cannot cross more than two component boundaries")
1070
+ const attributes = ts.isJsxElement(node) ? node.openingElement.attributes : node.attributes
1071
+ for (const prop of callbackProps) {
1072
+ const attribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === prop)
1073
+ const value = attribute?.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
1074
+ if (!value || !ts.isIdentifier(value)) fail(attribute ?? node, "A second-boundary setter callback must be forwarded directly as one JSX event prop")
1075
+ const callback = functionsForNode(value).get(value.text)
1076
+ if (callback) callbackSubstitutions.set(value.text, callback)
1077
+ }
1078
+ validateSetterCallbackProps(node, component, callbackProps)
1079
+ }
1053
1080
  const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
1081
+ if (callbackSubstitutions.size) {
1082
+ nested.root = substituteClone(nested.root, callbackSubstitutions, factory, context)
1083
+ for (const effect of nested.effects) effect.call = substituteClone(effect.call, callbackSubstitutions, factory, context)
1084
+ }
1054
1085
  if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
1055
- nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
1086
+ nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners, callbackDepth - Boolean(callbackProps.length))
1056
1087
  if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
1057
1088
  aggregate.calculations.push(...nested.calculations)
1058
1089
  aggregate.effects.push(...nested.effects)
@@ -1111,19 +1142,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1111
1142
  }
1112
1143
  const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
1113
1144
  if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
1114
- if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
1115
- for (const prop of callbackProps) {
1116
- const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
1117
- if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
1118
- const references = []
1119
- const collectReferences = node => {
1120
- if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
1121
- ts.forEachChild(node, collectReferences)
1122
- }
1123
- collectReferences(component.body)
1124
- if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1125
- }
1126
- const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1145
+ validateSetterCallbackProps(call, component, callbackProps)
1146
+ const specialization = specialize(call, component, "Setter-callback", true, true, new Set(settersForNode(call, settersByFunction).values()))
1127
1147
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1128
1148
  const substitutions = new Map()
1129
1149
  const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
@@ -174,12 +174,13 @@ export function useEffect(callback, dependencies, module, handler, states, scope
174
174
  if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
175
175
  if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
176
176
  if (itemDependencies.length && !renderContext.listDepth) throw new Error(`${source} useEffect() item-property dependencies are only supported in direct keyed row components`)
177
+ const derivedSources = new Set(dependencyStates.map(([, dependency]) => dependency))
177
178
  const dependencyIds = [...new Set(dependencies.map(dependency => {
178
- if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
179
+ if (!dependency?.[signalMarker] || !derivedSources.has(dependency) && !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
179
180
  return dependency.id
180
181
  }))]
181
182
  const dependencyStateIds = Object.fromEntries(dependencyStates.map(([name, dependency]) => {
182
- if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() derived dependency state ${JSON.stringify(name)} must be primitive Kudzu state`)
183
+ if (!dependency?.[signalMarker]) throw new Error(`${source} useEffect() derived dependency state ${JSON.stringify(name)} must be Kudzu state`)
183
184
  return [name, dependency.id]
184
185
  }))
185
186
  let owner
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.39",
3
+ "version": "0.8.41",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",