@kudzujs/core 0.15.1 → 0.16.3

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.
@@ -51,7 +51,7 @@ Syntax compatibility does not mean reproducing React wholesale. Kudzu accepts th
51
51
  - stronger JavaScript failure resilience;
52
52
  - no remaining material large-route build-scaling weakness.
53
53
 
54
- The active 0.9 plan defines the exact comparison and release gate. Benchmark-only feature omission, unmatched accessibility, weighted scores that hide losses, and unrecorded environment differences do not count as proof.
54
+ The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.16.4` in the capability release plan below. Benchmark-only feature omission, unmatched accessibility, weighted scores that hide losses, and unrecorded environment differences do not count as proof.
55
55
 
56
56
  ### 0.10.0 Through 0.21.x: Application Capability Release Train
57
57
 
@@ -128,6 +128,9 @@ The following are available building blocks, not future vertical roadmaps:
128
128
  - `useState`, independent repeated non-keyed child state with conditional mount ownership, reduced relative-imported `useReducer`, direct dispatch specialization, and reduced Zustand-shaped shared state lowered through package-neutral shared-state/action IR and proven by migration fixtures.
129
129
  - Mount and dependency effects with cleanup, route/layout lifetimes, conditional/keyed ownership, stale-write isolation, and relative TypeScript Workers.
130
130
  - Direct browser-only package references in inline effect setup/cleanup callbacks bundle into route-owned effect ESM without entering build-time component execution or static sibling output.
131
+ - A CodeMirror-class editor may retain one package instance across one mount effect and later dependency update effects through a null-initialized ref with one direct setup assignment and null-reset cleanup. The ref lowers to existing owner-scoped object state; package DOM, bidirectional updates, errors, accessibility, conditional disposal, and fresh remount require no widget runtime or React island.
132
+ - A Chart.js-class canvas instance uses the same retained-handle contract. One mount effect owns construction, resize listeners, and destroy/reset cleanup; later data effects read the instance without replacing it. Enhanced route disposal and fresh remount require no chart runtime or package-specific IR.
133
+ - A SortableJS-class drag engine may temporarily move keyed DOM during a gesture, but Kudzu state remains the durable order owner. The package callback restores authored DOM order before one immutable state reorder; existing keyed reconciliation then preserves row identity. Keyboard controls use the same state operation, package disposal is effect-owned, and static siblings exclude the package.
131
134
  - Keyed local-state and imported static collections, pure selectors, nested direct-property lists, recursive and directly exported row specialization, static/keyed component reuse, analyzable specialized prop spreads, serializable literal defaults, direct intrinsic rest forwarding, forwarded JSX children, latest-item handlers, row state/effects/refs, and stable keyed identity.
132
135
  - Direct object-state props with one-segment binding/effect/list consumers when the child directly maps an array field; ComponentAnalysis links every consumer to the parent SignalIR without authored field state or a browser component.
133
136
  - Complete-document native fallback and optional shared-layout navigation groups with parameter initialization, finite prefetch caching, focus, history, cleanup, and independent capability output.
package/PERFORMANCE.md CHANGED
@@ -2,6 +2,99 @@
2
2
 
3
3
  Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, `npm run benchmark:module-cache`, `npm run benchmark:project-navigation`, `npm run benchmark:project-state`, and `npm run benchmark:source-scale` 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
+ ## 0.16.3 State-Owned Drag And Drop
6
+
7
+ Measured 2026-08-28 on Linux x64 with Node 24.14.0 and Chrome. The real
8
+ SortableJS fixture emits 10 JavaScript files totaling 72,251 raw / 26,603
9
+ aggregate gzip bytes, while its static sibling emits zero JavaScript. Required
10
+ Chrome proves drag and keyboard reorder, retained row/input identity, invalid
11
+ package-index recovery, conditional and document disposal, and fresh remount.
12
+
13
+ The maintained Worker benchmark remains byte-identical at 907 raw / 477 gzip B
14
+ for the Worker graph and 14,456 raw / 6,159 gzip B for the window graph. Seven
15
+ clean builds record a 999.7 ms median on this host. Timing is not compared with
16
+ the prior macOS sweep. The change adds no runtime file or drag/drop abstraction;
17
+ it classifies `HTMLElement` with the existing browser globals used by bundled
18
+ effect callbacks.
19
+
20
+ ## 0.16.1 Retained Editor Ownership
21
+
22
+ Measured 2026-08-28 on Linux x64 with Node 24.14.0 and Chrome. The new
23
+ CodeMirror fixture emits 7 JavaScript files totaling 214,968 raw / 71,607
24
+ aggregate gzip bytes, while its static sibling emits zero JavaScript. These
25
+ bytes belong to the real editor package and existing selected effect/runtime
26
+ capabilities; no retained-ref runtime file or shared widget runtime was added.
27
+
28
+ Required Chrome proves initial content, retained package DOM across application
29
+ updates, editor-to-state updates, accessible error recovery, conditional
30
+ disposal/remount, and document disposal. The maintained Worker benchmark remains
31
+ byte-identical to 0.15.1 at 907 raw / 477 gzip B for the Worker graph and 14,456
32
+ raw / 6,159 gzip B for the window graph. The generated 50-route, 500-module
33
+ source-scale fixture retains compiler digest
34
+ `7c366079a984b3d62fb19a26305326d3278f029d2193d39340ddd3a4be5adc6e` and
35
+ output digest `e107d78a7f55bc8a1af0ea6e53efeffa19b3d44d21c892484d103fa346e7ba7b`.
36
+ Timing from this Linux host is recorded only as a local check and is not compared
37
+ with the prior macOS release sweep.
38
+
39
+ ## 0.15.1 Maintained Benchmark Sweep
40
+
41
+ Remeasured 2026-08-27 on macOS arm64 with Node 24.14.0, Apple M4 hardware,
42
+ 16 GiB RAM, and Chrome 151.0.7922.174. Candidate comparisons use clean
43
+ `v0.14.3` except project-state scale, whose maintained contract compares the
44
+ published `0.10.0` baseline. Every paired compiler/native/commerce output digest
45
+ or manifest is identical unless the row below records route-specific `0.15.1`
46
+ output.
47
+
48
+ | Maintained runner | Current `0.15.1` result |
49
+ |---|---|
50
+ | `npm run benchmark` | Worker-effects clean-build median 220.1 ms; Worker graph 907 raw / 477 gzip B; window graph 14,456 / 6,159 B. |
51
+ | `npm run benchmark:keyed` | 2,000-row build 233.2 ms; append 2.2 ms, filter 3.9 ms, restore 15.6 ms, reverse 5.3 ms medians; 30,048 raw / 11,748 gzip B JavaScript. |
52
+ | `RUNS=7 npm run benchmark:native` | `v0.14.3` / `0.15.1` dispatch medians 3.5 / 3.4 ms for 5,000 clicks; identical 14,354 raw / 6,470 gzip B JavaScript and zero changed files. |
53
+ | `RUNS=7 WARMUPS=1 npm run benchmark:module-cache` | `v0.14.3` / `0.15.1` medians 248.338 / 249.692 ms and 296.9 / 296.8 MiB peak RSS; identical 454,007-byte result and digest. |
54
+ | `npm run benchmark:source-scale` | 50 routes, 500 modules, 50,550 lines: `v0.14.3` / `0.15.1` compile medians 499.3 / 498.9 ms and clean builds 851.5 / 853.8 ms; identical compiler and output digests. |
55
+ | `RUNS=7 npm run benchmark:commerce` | 1,011-page storefront: `v0.14.3` / `0.15.1` clean-build medians 1,854.6 / 1,822.1 ms; all 1,053 files and 10,140,618 output bytes match. |
56
+ | `npm run benchmark:project-navigation` | Seven-profile table update median 0.4 ms and navigation median 2.3 ms; 17 session JavaScript files total 77,779 raw / 27,050 gzip B. |
57
+ | `npm run benchmark:project-state` | One-, eight-, and 32-state commit medians 0.2, 0.3, and 0.4 ms; deploy digest `3f83712254d95f1cf59c5034f363ea906406efa1bd9539f3bb8b9a0d54a3fdc5`. |
58
+ | `npm run benchmark:project-list-decision` | Direct load 138.2 ms with 90,023 nodes; pagination load/range 39.6/5.8 ms with 2,867 nodes; window load/range 40.7/5.8 ms with 2,868 nodes. |
59
+
60
+ The paired timing ranges overlap at module-cache and source-scale granularity,
61
+ so no compiler regression or improvement is claimed. Native output is byte
62
+ identical. Pagination remains selected over the timing-tied authored window: it
63
+ uses one listener instead of two, has lower median heap (1,485,168 versus
64
+ 1,586,528 B), and preserves native page, focus, keyboard, and variable-row-height
65
+ behavior without fixed-height range policy.
66
+
67
+ ## 0.15.1 Public Cross-Framework Commerce Fixture
68
+
69
+ Measured 2026-08-27 from public fixture commit
70
+ `f2d5be1a516c539e30f7125f6870d42b1dd02ecd` with 1,000 products and Kudzu
71
+ linked to this `0.15.1` checkout. The matched variants use Astro 7.1.3 with
72
+ React 19.2.8, React Router 8.3.0, TanStack Start 1.168.32, and Next.js 16.2.11.
73
+ All five builds completed before the same asset and browser harnesses ran.
74
+
75
+ | Variant | Initial JavaScript across six routes, gzip | Total output | First reliable click after first paint | Degraded capabilities |
76
+ |---|---:|---:|---:|---:|
77
+ | Kudzu 0.15.1 | 4.2-9.9 KiB | 9.42 MiB | 300 ms | 15 / 18 |
78
+ | Astro 7.1.3 + React | 60.6-61.1 KiB | 13.95 MiB | 1,500 ms | 12 / 18 |
79
+ | React Router 8.3.0 | 103.8-104.6 KiB | 7.04 MiB | 2,000 ms | 8 / 18 |
80
+ | TanStack Start 1.168.32 | 103.5-104.1 KiB | 9.87 MiB | 2,000 ms | 9 / 18 |
81
+ | Next.js 16.2.11 | 144.2-146.2 KiB | 35.76 MiB | 3,000 ms | 8 / 18 |
82
+
83
+ The initial-JavaScript column records what Chrome actually received for home,
84
+ search, collection, product, policy, and checkout routes. Browser results are
85
+ medians from seven fresh sessions under 4x CPU slowdown and Chrome Slow 4G.
86
+ Reliable click is the first tested delay with zero lost add-to-cart attempts in
87
+ seven isolated sessions. The resilience score covers six matched capabilities
88
+ under blocked JavaScript, a two-second script delay, and one missing script.
89
+ Pre-rendered content and native links remain usable without JavaScript in every
90
+ variant; the score does not treat read-only fallback as full interactivity.
91
+
92
+ This is the current executable public commerce comparison, not a replacement
93
+ for the broader historical 0.9 C1/C2/C5 contracts. That ignored local workspace
94
+ is unavailable in current checkouts, so its React, Vue, Svelte, and Astro result
95
+ remains provenance rather than a current ranking. The public commerce fixture
96
+ does not contain Vue or Svelte variants.
97
+
5
98
  ## 0.15.1 Key-Scoped Native Popover Intake
6
99
 
7
100
  Measured 2026-08-27 on macOS arm64 with Node 24.14.0 and Chrome
package/README.md CHANGED
@@ -12,9 +12,9 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
12
12
 
13
13
  *Watch: React-shaped TSX to static HTML and route-specific ESM in 22 seconds.*
14
14
 
15
- > Experimental `0.14.x`: the compiler API and supported TSX surface may change.
15
+ > Experimental `0.16.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.15.1 - Key-scoped native popovers.** Keyed row `useId()` now scopes native Popover triggers and intrinsic ID references by row key without adding an overlay runtime. Read the [release notes](./RELEASES.md#0151---key-scoped-native-popovers), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.15.1), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.16.3 - State-owned drag and drop.** A real SortableJS effect may move DOM during a gesture while keyed Kudzu state remains the durable order and identity owner, with keyboard parity, exact disposal, and static exclusion. Read the [release notes](./RELEASES.md#0163---state-owned-drag-and-drop), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.16.3), 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,77 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.16.3 - State-Owned Drag And Drop
4
+
5
+ Kudzu 0.16.3 proves that a real SortableJS drag engine can remain a bounded
6
+ effect while existing keyed state owns durable order and DOM identity.
7
+
8
+ ### Changed in 0.16.3
9
+
10
+ - Treats the browser `HTMLElement` constructor as a platform global inside
11
+ bundled effect callbacks while preserving lexical shadowing.
12
+ - Restores package-moved DOM before applying one immutable state reorder, so
13
+ Kudzu remains the only durable keyed DOM owner.
14
+ - Uses the same state operation for drag, keyboard movement, and reset.
15
+ - Recovers invalid package indexes without changing application state.
16
+ - Disposes on conditional and document release; remount creates a fresh package
17
+ instance.
18
+ - Adds no drag/drop IR, animation frame, runtime concept, React, VDOM, hydration,
19
+ or retained component tree.
20
+ - Updates `create-kudzu@0.1.128` to generate projects on
21
+ `@kudzujs/core@^0.16.3`.
22
+
23
+ ### Output And Browser Evidence
24
+
25
+ - The SortableJS fixture emits 10 JavaScript files totaling 72,251 raw / 26,603
26
+ aggregate gzip bytes; its static sibling emits zero JavaScript.
27
+ - Required Chrome proves drag and keyboard reorder, state synchronization,
28
+ retained row/input identity, invalid-input recovery, disposal, and fresh
29
+ remount.
30
+ - The maintained Worker and window graphs remain 907 raw / 477 gzip bytes and
31
+ 14,456 raw / 6,159 gzip bytes respectively.
32
+
33
+ ### Upgrade
34
+
35
+ ```sh
36
+ npm install @kudzujs/core@^0.16.3
37
+ ```
38
+
39
+ ## 0.16.1 - Retained External Editor Ownership
40
+
41
+ Kudzu 0.16.1 lets one CodeMirror-class package instance remain owned across a
42
+ mount effect and later dependency update effects without a React island or
43
+ generic widget runtime.
44
+
45
+ ### Changed in 0.16.1
46
+
47
+ - Lowers one proven unattached `useRef(null)` retained-instance handle to
48
+ existing owner-scoped object state.
49
+ - Requires one empty-dependency mount effect, one direct setup assignment,
50
+ direct null-reset cleanup, and read-only later dependency effects.
51
+ - Preserves editor DOM identity across application-state and editor-to-state
52
+ updates, including accessible package errors and recovery.
53
+ - Disposes on conditional and document release; remount receives a fresh handle.
54
+ - Rejects malformed cleanup, update-effect writes, multiple retained handles,
55
+ aliases, event use, and attached retained refs.
56
+ - Adds no IR kind, runtime concept, resource registry, React, or widget runtime.
57
+ - Updates `create-kudzu@0.1.127` to generate projects on
58
+ `@kudzujs/core@^0.16.1`.
59
+
60
+ ### Output And Browser Evidence
61
+
62
+ - The CodeMirror fixture emits 7 JavaScript files totaling 214,968 raw /
63
+ 71,607 aggregate gzip bytes; its static sibling emits zero JavaScript.
64
+ - Required Chrome proves initial mount, retained application updates,
65
+ editor-to-state updates, package-error recovery, disposal, and fresh remount.
66
+ - The smaller Typed.js boundary fixture proves the same existing effect owner
67
+ without any production compiler change.
68
+
69
+ ### Upgrade
70
+
71
+ ```sh
72
+ npm install @kudzujs/core@^0.16.1
73
+ ```
74
+
3
75
  ## 0.15.1 - Key-Scoped Native Popovers
4
76
 
5
77
  Kudzu 0.15.1 lets ordinary keyed row components use `useId()` for native
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Document Role
4
4
 
5
- This document is the detailed delivery plan for [`0.9-semantic-compression.md`](./0.9-semantic-compression.md). The Semantic Compression document defines direction, work order, and release gates. This document decomposes that work into executable sessions, artifacts, tests, measurements, reviews, and stop conditions.
5
+ This document is the completed detailed delivery record for [`0.9-semantic-compression.md`](./0.9-semantic-compression.md). It retains the executable sessions, artifacts, tests, measurements, reviews, and stop conditions that produced `0.9.0`; current execution follows the application capability release plan.
6
6
 
7
- This plan does not mark `0.9.0` as released. The current package version remains the version recorded in `package.json`. Package metadata, release notes, public support claims, and generator ranges change only during the final release procedure after every gate passes.
7
+ `0.9.0` is released and this plan is historical. The current package version remains the version recorded in `package.json`; package metadata, release notes, public support claims, and generator ranges still change only through the active release procedure after every gate passes.
8
8
 
9
9
  Use this document to answer:
10
10
 
@@ -4,12 +4,12 @@
4
4
 
5
5
  This is the completed execution record after `0.8.62`. Every release gate in this document is satisfied by `0.9.0`; follow-on model-driven delivery work remains separately gated for 1.0.
6
6
 
7
- The executable session-by-session decomposition, current implementation seams, benchmark restoration work, report templates, and release procedure are maintained in [`0.9-implementation-plan.md`](./0.9-implementation-plan.md). This document remains authoritative for direction, order, and release gates; the detailed plan is authoritative for execution within each ordered item.
7
+ The historical session-by-session decomposition, implementation seams, benchmark restoration work, report templates, and release procedure are retained in [`0.9-implementation-plan.md`](./0.9-implementation-plan.md). This document remains authoritative for the completed 0.9 direction, order, and release gates; current execution follows the application capability release plan.
8
8
 
9
- Read documents in this order before 0.9 work:
9
+ Read documents in this order when reviewing the completed 0.9 work:
10
10
 
11
11
  1. [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) for product invariants and feature selection.
12
- 2. This document for the active 0.9 scope and work order.
12
+ 2. This document for the completed 0.9 scope and work order.
13
13
  3. [`0.9-implementation-plan.md`](./0.9-implementation-plan.md) for executable session packets and evidence.
14
14
  4. [`compiler-current-architecture.md`](./compiler-current-architecture.md) for current owners and data flow.
15
15
  5. [`performance-gates.md`](./performance-gates.md) for required measurements.
@@ -7,7 +7,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
7
7
  | Goal | Decision | Start condition |
8
8
  |---|---|---|
9
9
  | A: compiler foundation | Complete in `0.8.23` | The no-write source compiler, RouteIR v1, CapabilityIR v1, generator boundaries, and output baseline are recorded. |
10
- | B: optimization benchmarks | Active | Keyed restoration, no-op normalization, exact route-entry transform reuse, and identical route-entry file sharing are retained; continue only when another current fixture reproduces a material loss. |
10
+ | B: optimization benchmarks | Maintained gate | Keyed restoration, no-op normalization, exact route-entry transform reuse, and identical route-entry file sharing are retained; continue only when another current fixture reproduces a material loss. |
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
 
@@ -15,7 +15,7 @@ The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](.
15
15
 
16
16
  [`1.0-large-application-compatibility-audit.md`](./1.0-large-application-compatibility-audit.md) records the first post-0.9 probes against Memos, Apache Answer, and Actual Budget. The audit finds that reduced slices build but whole-application source retention and behavior parity do not yet pass.
17
17
 
18
- [`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
18
+ [`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.16.4`. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
19
19
 
20
20
  ## Required Invariants
21
21
 
@@ -959,7 +959,7 @@ projects on `@kudzujs/core@^0.14.1`.
959
959
  - **Done condition:** the full journey passes with explicit network and memory
960
960
  bounds.
961
961
 
962
- Release candidate evidence complete. The maintained project application adds one
962
+ Published release evidence complete. The maintained project application adds one
963
963
  intrinsic sentinel, an owned native `IntersectionObserver`, cursor and request
964
964
  primitive state, an owned fetch with `AbortController`, immutable keyed append,
965
965
  duplicate ID suppression, and explicit loading/error/retry/end UI. The authored
@@ -1047,7 +1047,8 @@ to the next evidence-ready application capability.
1047
1047
  - **Stop condition:** a custom modal/focus-trap runtime replaces `<dialog>`.
1048
1048
  - **Done condition:** project delete and edit confirmation pass accessibly.
1049
1049
 
1050
- Implementation evidence is local and release gates remain pending. The maintained
1050
+ Validation evidence is complete and the packet closed without consuming a release
1051
+ because no production compiler or runtime changed. The maintained
1051
1052
  project application replaces immediate keyed project deletion with one shared
1052
1053
  native `<dialog>`, one route-owned object ref, and one pending project ID state.
1053
1054
  Existing row callback specialization opens the browser top layer; native cancel,
@@ -1114,6 +1115,16 @@ plus navigation samples of `[4, 2.4, 2.6, 2.5, 2.5, 2.8, 2.4]` ms, a 2.5 ms
1114
1115
  median and 2.4/4 ms range. These retained-path ranges overlap prior evidence,
1115
1116
  so no timing improvement or regression is claimed.
1116
1117
 
1118
+ Release transaction complete at commit
1119
+ `2dd5c409d3553cdf0b04080f1ba7c39b4541c523` and immutable tag `v0.15.1`.
1120
+ CI run `33069675599` passed both jobs. The GitHub release is
1121
+ [`v0.15.1`](https://github.com/kudzujs/kudzu/releases/tag/v0.15.1), and publish
1122
+ workflow run `33071439012` published and verified `@kudzujs/core@0.15.1` and
1123
+ `create-kudzu@0.1.126`. A fresh registry install imported `useId()` from core
1124
+ and confirmed both exact package versions. The first publish attempt stopped at
1125
+ the existing concurrent Chrome timing gate before any publish step; the exact
1126
+ same immutable tag passed on the release-event retry.
1127
+
1117
1128
  ### `0.15.2`: Toast And Notification Ownership
1118
1129
 
1119
1130
  - **Purpose:** provide application feedback across route mutations.
@@ -1125,6 +1136,15 @@ so no timing improvement or regression is claimed.
1125
1136
  - **Done condition:** notifications remain owner-bounded and absent from unused
1126
1137
  route output.
1127
1138
 
1139
+ **Result:** closed by ordinary layout composition. The maintained project
1140
+ application now owns one deduplicated notification queue in layout state, renders
1141
+ keyed rows in a native polite live region, and uses one cleanup-owned sequential
1142
+ timeout plus explicit dismissal. Required Chrome preserves the live-region and
1143
+ row identities through enhanced route replacement, proves timeout removal and
1144
+ disposal, and keeps `/help` at zero JavaScript. The 45-file deploy totals 193,770
1145
+ raw / 60,227 aggregate gzip bytes. No scheduler, primitive, runtime concept, or
1146
+ production source changed, so no release was consumed.
1147
+
1128
1148
  ### `0.15.3`: Layer Owner Decision
1129
1149
 
1130
1150
  - **Purpose:** decide whether logical ownership of DOM rendered outside its
@@ -1135,6 +1155,13 @@ so no timing improvement or regression is claimed.
1135
1155
  - **Done condition:** no new concept is added, or a minimal owner edge with exact
1136
1156
  cleanup/focus order is approved.
1137
1157
 
1158
+ **Result:** closed with no owner edge. Native dialog and Popover already satisfy
1159
+ modal and non-modal top-layer behavior, while maintained dropdown, tooltip,
1160
+ notification, and menu fixtures compose in their authored ranges. Zero fixtures
1161
+ fail because DOM must render outside its authored range, below the required three
1162
+ independent failures. Portal source compatibility alone remains insufficient;
1163
+ no production source or release changed.
1164
+
1138
1165
  ## `0.16.x`: External Complex UI
1139
1166
 
1140
1167
  ### `0.16.0`: Bounded External UI Contract
@@ -1148,6 +1175,12 @@ so no timing improvement or regression is claimed.
1148
1175
  - **Done condition:** package ownership remains inside effect/handler ESM and no
1149
1176
  generic widget runtime is added.
1150
1177
 
1178
+ **Result:** closed through existing effect ownership. A real Typed.js instance
1179
+ creates DOM from one attached host ref, replaces on application-state dependency
1180
+ change, disposes on conditional/document release, and remounts fresh. The package
1181
+ is absent from the complete zero-JavaScript static sibling. No compiler, IR,
1182
+ runtime concept, or production source changed, so no release was consumed.
1183
+
1151
1184
  ### `0.16.1`: Real Editor Lifecycle
1152
1185
 
1153
1186
  - **Purpose:** integrate one real CodeMirror-class editor.
@@ -1157,6 +1190,18 @@ so no timing improvement or regression is claimed.
1157
1190
  - **Done condition:** the native package owns its DOM and Kudzu owns only its
1158
1191
  bounded lifecycle.
1159
1192
 
1193
+ **Result:** complete. The Apache Answer-derived CodeMirror fixture exposes the
1194
+ first real retained-instance failure: an unattached `useRef(null)` shared by one
1195
+ mount/cleanup effect and a later dependency update effect was incorrectly treated
1196
+ as a DOM ref. Kudzu now lowers exactly one proven retained handle to existing
1197
+ owner-scoped object state. Required Chrome proves initial value, retained
1198
+ application updates, editor-to-state updates, package-error recovery, accessible
1199
+ textbox naming, conditional disposal, fresh remount, and document disposal. The
1200
+ fixture emits 214,968 raw / 71,607 aggregate gzip JavaScript bytes while its
1201
+ static sibling emits zero. One existing normalization pass grows; semantic
1202
+ concepts, IR kinds, runtime concepts, runtime files, and public APIs remain
1203
+ unchanged.
1204
+
1160
1205
  ### `0.16.2`: Chart And Map Lifecycle
1161
1206
 
1162
1207
  - **Purpose:** validate the same contract against unrelated canvas/SVG/map DOM.
@@ -1165,6 +1210,24 @@ so no timing improvement or regression is claimed.
1165
1210
  - **Stop condition:** package-specific concepts enter generic IR/runtime.
1166
1211
  - **Done condition:** one lifecycle model covers editor and chart/map evidence.
1167
1212
 
1213
+ **Result:** closed through the `0.16.1` retained-instance contract. A reduced
1214
+ Mattermost Chart.js doughnut chart first reproduced the existing diagnostic when
1215
+ its dependency effect both created and updated the retained instance. Moving
1216
+ acquisition, resize-listener registration, and exact destroy/reset cleanup into
1217
+ one mount effect leaves later data effects read-only and lowers the handle to the
1218
+ same owner-scoped object state used by CodeMirror. Required Chrome proves initial
1219
+ drawing, retained data and resize updates, listener removal, enhanced route
1220
+ disposal, fresh remount, accessibility, and package exclusion from the static
1221
+ sibling. The interactive graph is 208,686 raw / 73,312 aggregate gzip bytes
1222
+ across six JavaScript files; the static sibling emits zero. Semantic primitives,
1223
+ IR kinds, compiler passes, core/compiler LOC, normalization rules, runtime
1224
+ concepts, runtime files, and public APIs change by zero. One positive fixture and
1225
+ one browser contract were added. The maintained Worker/effect benchmark remains
1226
+ 907 raw / 477 gzip bytes for the Worker graph and 14,456 raw / 6,159 gzip bytes
1227
+ for the window graph; its seven-run Linux median was 2,464.9 ms, recorded without
1228
+ a timing comparison because the host differs from the published baseline. No
1229
+ production source changed, so no release was consumed.
1230
+
1168
1231
  ### `0.16.3`: Grid And Drag/Drop Decision
1169
1232
 
1170
1233
  - **Purpose:** test external data-grid or drag engine ownership against keyed
@@ -1175,6 +1238,45 @@ so no timing improvement or regression is claimed.
1175
1238
  - **Done condition:** either bounded ownership is proven or the package is
1176
1239
  explicitly unsupported with a migration path.
1177
1240
 
1241
+ **Result:** complete through existing keyed state and effect ownership. A
1242
+ THRM-derived SortableJS fixture reproduced one compiler failure because the
1243
+ browser `HTMLElement` constructor was classified as an effect capture and then
1244
+ evaluated during the Node build. Adding that platform constructor to the
1245
+ existing browser-global set fixes the shared root cause while lexical shadowing
1246
+ remains covered. During a gesture SortableJS may move DOM temporarily, but its
1247
+ `onEnd` callback restores authored order before one immutable state reorder;
1248
+ Kudzu's keyed reconciler remains the durable DOM owner and preserves retained
1249
+ row and input identity. The same state operation provides keyboard movement and
1250
+ reset, while invalid package indexes restore DOM without changing state.
1251
+ Required Chrome proves drag and keyboard reorder, state synchronization,
1252
+ identity, invalid-input recovery, conditional/document disposal, and fresh
1253
+ remount. The interactive graph is 72,251 raw / 26,603 aggregate gzip bytes
1254
+ across ten JavaScript files; the static sibling emits zero. Its normalized
1255
+ deploy archive SHA-256 is
1256
+ `a55c18e14ba29df3f1f8ccdc7bcae936092508a01d05abdb887417e6fffb2ecf`.
1257
+ Semantic primitives, IR kinds, compiler passes, normalization rules, runtime
1258
+ concepts, runtime files, drag/drop abstractions, and public APIs change by zero;
1259
+ one existing compiler lookup gains one token. One positive fixture, one browser
1260
+ contract, and one browser-global shadowing case were added. The maintained
1261
+ Worker/effect benchmark remains 907 raw / 477 gzip bytes for the Worker graph
1262
+ and 14,456 raw / 6,159 gzip bytes for the window graph; its seven-run Linux
1263
+ median was 999.7 ms, recorded without a timing comparison because the host
1264
+ differs from the published baseline.
1265
+
1266
+ ### `0.16.4`: GSAP Animation Lifecycle
1267
+
1268
+ - **Purpose:** validate one real GSAP landing-page animation against existing
1269
+ DOM-ref and effect ownership.
1270
+ - **Expected boundary:** one owned effect and native reduced-motion behavior;
1271
+ no animation IR or scheduler runtime.
1272
+ - **Acceptance:** mount, deterministic static fallback, scoped targets,
1273
+ prefers-reduced-motion behavior, dependency update where required, route and
1274
+ conditional disposal, fresh remount, and static sibling asset exclusion.
1275
+ - **Stop condition:** package timelines compete with Kudzu for durable DOM
1276
+ structure or require a retained component renderer.
1277
+ - **Done condition:** GSAP owns only bounded presentation changes and exact
1278
+ cleanup while Kudzu retains document structure and lifetime ownership.
1279
+
1178
1280
  ## `0.17.x`: Long-Lived Applications
1179
1281
 
1180
1282
  ### `0.17.0`: Endurance Harness
@@ -1545,4 +1647,10 @@ release transaction where possible or document and publish a forward-fix patch.
1545
1647
  | `0.14.4` | Closed by stop condition | Preserve pagination as the selected strategy until three independent fixtures authorize virtual ownership review. | Evidence favors pagination; zero qualifying fixtures |
1546
1648
  | `0.15.0` | Closed by native composition | Preserve the native dialog, focus, cancellation, keyed deletion, and route cleanup evidence. | No production change; no release consumed |
1547
1649
  | `0.15.1` | Released | Preserve key-scoped IDs, native Popover behavior, retained keyed identity, and route cleanup evidence. | None |
1548
- | `0.15.2` | Active | Begin with a real toast and notification ownership fixture. | None |
1650
+ | `0.15.2` | Closed by layout composition | Preserve owner-bounded queue, deduplication, dismissal, route persistence, live-region, and timer cleanup evidence. | No production change; no release consumed |
1651
+ | `0.15.3` | Closed by stop condition | Preserve native top-layer and in-place composition until three independent authored-range failures exist. | Zero qualifying failures; no release consumed |
1652
+ | `0.16.0` | Closed by existing effect ownership | Preserve real package mount, replacement, disposal, remount, and static exclusion evidence. | No production change; no release consumed |
1653
+ | `0.16.1` | Released | Preserve the retained CodeMirror instance boundary, bidirectional updates, error recovery, accessibility, and exact cleanup. | None |
1654
+ | `0.16.2` | Closed by existing effect ownership | Preserve retained Chart.js data/resize updates, listener and route cleanup, fresh remount, accessibility, and static exclusion. | No production change; no release consumed |
1655
+ | `0.16.3` | Released | Preserve state-owned order, keyed identity, keyboard parity, invalid-input recovery, package disposal, and static exclusion. | None |
1656
+ | `0.16.4` | Active | Validate one real GSAP landing-page animation through bounded effect ownership. | None |
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the current `0.9.0` 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.16.3` architecture, built on the completed `0.9.0` semantic-compression release and `0.8.23` Goal A compiler foundation. The active application packet is `0.16.4`; file and function names are the stable references, while line numbers are intentionally omitted because later work may still move code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Completed compiler-foundation record and longer-term plan after `0.8.35`. The active post-`0.8.62` execution queue is [`0.9-semantic-compression.md`](./0.9-semantic-compression.md). This document does not mark any remaining planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
5
+ Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.16.4`. This document does not mark any remaining planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
6
6
 
7
- [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection. [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) is authoritative for current work order and evidence; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
7
+ [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.16.4`; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
8
8
 
9
9
  ## Product Outcome
10
10
 
@@ -453,7 +453,7 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
453
453
 
454
454
  ## Production Gates Before 1.0
455
455
 
456
- - The active `0.9.0` cross-framework gate proves lower browser cost and matched user-facing performance against React + Vite, Vue, Svelte, and Astro before AI productivity claims are considered.
456
+ - The completed `0.9.0` cross-framework gate proves lower browser cost and matched user-facing performance against React + Vite, Vue, Svelte, and Astro before AI productivity claims are considered.
457
457
  - The maintained AI delivery suite uses the same model, tools, requirements, budgets, and acceptance checks across frameworks, includes failed attempts, and establishes the highest success rate plus lowest median cost per successful task before `1.0.0`.
458
458
  - Async native and effect work cannot write after ownership release.
459
459
  - Build output is staged, collision-safe, and rollback/recovery guarded.
@@ -483,4 +483,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
483
483
 
484
484
  ## Immediate Decision
485
485
 
486
- PR 1 through PR 12, the `0.8.40` property-dependency slice, the `0.8.41` and `0.8.43` direct multi-boundary callback/ref slices, the `0.8.44` Context alias slice, the `0.8.46` action-only Provider setter slice, the `0.8.47` direct primitive, `0.8.57` plain-object prop, `0.8.58` direct keyed item, `0.8.59` direct array prop initializer, `0.8.60` matching array-draft setter-effect, `0.8.61` parameterized primitive debounce-hook, and `0.8.62` direct-ref outside-click-hook slices, the `0.8.48` repeated direct leaf-handler callback slice, the `0.8.49` direct child callback fan-out slice, the `0.8.50` package-neutral shared-state/action slice, the `0.8.51` owned-effect package import slice, the `0.8.52` effect-private mutable-ref slice, the `0.8.53` route/layout CSS closure slice, the `0.8.54` structural per-route capability/chunk report, the `0.8.55` signature-keyed runtime families, item 7 incremental source and affected-route builds, and the `0.8.42` measured route-output optimization are complete. Continue with `0.9.0-01` in the active Semantic Compression plan; keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
486
+ All listed foundation and `0.9` slices are complete. Current work continues at `0.16.4` GSAP Animation Lifecycle under the [`application-capability-release-plan.md`](./application-capability-release-plan.md); keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
@@ -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.62` are completed scopes represented by package/release records. The active `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md); that plan supersedes the provisional tool-first 0.10/0.11/0.12 ordering in the completed 0.9 handoff.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.16.4`; that plan supersedes the provisional tool-first 0.10/0.11/0.12 ordering in the completed 0.9 handoff.
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
 
@@ -66,7 +66,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
66
66
 
67
67
  ## Generator Versions
68
68
 
69
- `create-kudzu@0.1.126` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.15.1`.
69
+ `create-kudzu@0.1.128` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.16.3`.
70
70
 
71
71
  ## Release Boundary
72
72
 
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript"
2
2
 
3
3
  export const knownGlobalNames = new Set([
4
- "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
4
+ "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "HTMLElement", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5
5
  ])
6
6
 
7
7
  export function createBindingIndex(sourceFile) {
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript"
2
- import { effectReturns, importDeclarationNames, isNodeWithin, isShadowedIdentifier, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
2
+ import { effectReturns, importDeclarationNames, isNodeWithin, isShadowedIdentifier, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
3
3
 
4
4
  export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
5
5
  const frameCall = (node, name) => ts.isCallExpression(node) && (
@@ -24,8 +24,10 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
24
24
  }
25
25
  const hasUseRefImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useRef"))
26
26
  const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
27
+ const hasUseStateImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useState"))
27
28
  const privateRefs = new Map()
28
29
  const effectRefs = new Map()
30
+ const retainedRefs = new Map()
29
31
  const registerPrivateRef = (node, callback) => {
30
32
  const initializer = ts.isNumericLiteral(node.initializer.arguments[0]) ? factory.createNumericLiteral(0) : factory.createNull()
31
33
  privateRefs.set(node, callback)
@@ -42,20 +44,60 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
42
44
  const owner = nearestFunction(node)
43
45
  if (!owner?.body || !ts.isBlock(owner.body)) return
44
46
  const references = referenceIdentifiers(owner.body, node.name.text)
47
+ const attachedToJsx = references.some(reference => ts.isJsxExpression(reference.parent) && ts.isJsxAttribute(reference.parent.parent) && reference.parent.parent.name.text === "ref")
45
48
  const invalidReference = references.find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
46
49
  const accesses = references.filter(reference => ts.isPropertyAccessExpression(reference.parent) && reference.parent.expression === reference && reference.parent.name.text === "current").map(reference => reference.parent)
50
+ const mutations = accesses.filter(access =>
51
+ (ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
52
+ ((ts.isPrefixUnaryExpression(access.parent) || ts.isPostfixUnaryExpression(access.parent)) && access.parent.operand === access && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(access.parent.operator)) ||
53
+ (ts.isDeleteExpression(access.parent) && access.parent.expression === access)
54
+ )
47
55
  const frameAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && frameCall(unwrapExpression(access.parent.right), "requestAnimationFrame") && unshadowedFrameCall(unwrapExpression(access.parent.right), owner))
48
56
  const statement = node.parent?.parent
49
57
  const topLevelOwner = owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
50
58
  const topLevelConst = topLevelOwner && ts.isVariableStatement(statement) && (statement.declarationList.flags & ts.NodeFlags.Const) && statement.declarationList.declarations.length === 1 && statement.parent === owner.body
51
59
  if (frameAssignments.length && invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
52
60
  if (frameAssignments.length && !topLevelConst) throw sourceNodeError(node, sourceFile, "Animation frame refs must be one top-level component const")
61
+ if (attachedToJsx && mutations.length) throw sourceNodeError(mutations[0], sourceFile, "JSX object refs may not assign to ref.current")
53
62
  if (!topLevelConst || invalidReference || !accesses.length) return
54
63
  const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
55
64
  const effects = effectCalls.filter(effect => {
56
65
  const callback = effect.arguments[0]
57
66
  return callback && accesses.every(access => isNodeWithin(access, callback))
58
67
  })
68
+ if (!frameAssignments.length && refInitializer?.kind === ts.SyntaxKind.NullKeyword && !invalidReference && !effects.length) {
69
+ const owners = effectCalls.filter(effect => {
70
+ const callback = effect.arguments[0]
71
+ return callback && accesses.some(access => isNodeWithin(access, callback))
72
+ })
73
+ const callbacks = owners.map(effect => effect.arguments[0])
74
+ const directCallbacks = callbacks.every(callback => (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isBlock(callback.body))
75
+ const everyAccessOwned = accesses.every(access => callbacks.filter(callback => isNodeWithin(access, callback)).length === 1)
76
+ const mountEffects = owners.filter(effect => ts.isArrayLiteralExpression(effect.arguments[1]) && !effect.arguments[1].elements.length)
77
+ const updateEffects = owners.filter(effect => ts.isArrayLiteralExpression(effect.arguments[1]) && effect.arguments[1].elements.length)
78
+ if (owners.length >= 2 && directCallbacks && everyAccessOwned && mountEffects.length === 1 && updateEffects.length === owners.length - 1 && owners[0] === mountEffects[0]) {
79
+ const mount = mountEffects[0]
80
+ const callback = mount.arguments[0]
81
+ const returns = effectReturns(callback)
82
+ const cleanup = returns.cleanups.length === 1 ? returns.cleanups[0] : undefined
83
+ const writes = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)
84
+ const setupWrites = writes.filter(access => isNodeWithin(access, callback) && (!cleanup || !isNodeWithin(access, cleanup)))
85
+ const cleanupWrites = cleanup ? writes.filter(access => isNodeWithin(access, cleanup)) : []
86
+ const setupStatement = setupWrites[0]?.parent.parent
87
+ const cleanupStatement = cleanupWrites[0]?.parent.parent
88
+ const directSetup = setupWrites.length === 1 && ts.isExpressionStatement(setupStatement) && setupStatement.parent === callback.body && unwrapExpression(setupWrites[0].parent.right).kind !== ts.SyntaxKind.NullKeyword
89
+ const directCleanup = cleanup && ts.isBlock(cleanup.body) && cleanupWrites.length === 1 && ts.isExpressionStatement(cleanupStatement) && cleanupStatement.parent === cleanup.body && unwrapExpression(cleanupWrites[0].parent.right).kind === ts.SyntaxKind.NullKeyword
90
+ const updateWrites = mutations.some(access => updateEffects.some(effect => isNodeWithin(access, effect.arguments[0])))
91
+ if (directSetup && directCleanup && !updateWrites) {
92
+ if (!hasUseStateImport) throw sourceNodeError(node, sourceFile, "Retained instance refs require a named useState import")
93
+ const setterName = `__kSetRetainedRef_${node.name.text}_${node.pos}`
94
+ if (referencesIdentifier(owner.body, setterName)) throw sourceNodeError(node, sourceFile, "Retained instance ref conflicts with a compiler-owned binding")
95
+ retainedRefs.set(node, setterName)
96
+ return
97
+ }
98
+ }
99
+ if (owners.length >= 2) throw sourceNodeError(node, sourceFile, "Retained instance refs require one empty-dependency mount effect with one direct assignment and null-reset cleanup followed by read-only dependency effects")
100
+ }
59
101
  if (!frameAssignments.length) {
60
102
  const callback = effects.length === 1 ? effects[0].arguments[0] : undefined
61
103
  if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isBlock(callback.body)) {
@@ -107,9 +149,27 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
107
149
  registerPrivateRef(node, callback)
108
150
  }
109
151
  inspect(sourceFile)
110
- if (!privateRefs.size) return sourceFile
152
+ const retainedByOwner = new Map()
153
+ for (const declaration of retainedRefs.keys()) {
154
+ const owner = nearestFunction(declaration)
155
+ const refs = retainedByOwner.get(owner) ?? []
156
+ refs.push(declaration)
157
+ retainedByOwner.set(owner, refs)
158
+ }
159
+ for (const refs of retainedByOwner.values()) if (refs.length > 1) throw sourceNodeError(refs[1], sourceFile, "Components may own only one retained instance ref")
160
+ if (!privateRefs.size && !retainedRefs.size) return sourceFile
111
161
  const visitor = node => {
112
162
  if (ts.isVariableStatement(node) && node.declarationList.declarations.length === 1 && privateRefs.has(node.declarationList.declarations[0])) return undefined
163
+ if (ts.isVariableDeclaration(node) && retainedRefs.has(node)) {
164
+ const binding = factory.createArrayBindingPattern([
165
+ factory.createBindingElement(undefined, undefined, node.name),
166
+ factory.createBindingElement(undefined, undefined, retainedRefs.get(node))
167
+ ])
168
+ const initializer = factory.createCallExpression(factory.createIdentifier("useState"), undefined, [
169
+ factory.createObjectLiteralExpression([factory.createPropertyAssignment("current", factory.createNull())])
170
+ ])
171
+ return factory.updateVariableDeclaration(node, binding, node.exclamationToken, undefined, initializer)
172
+ }
113
173
  const refs = effectRefs.get(node)
114
174
  if (refs && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isBlock(node.body)) {
115
175
  const body = ts.visitEachChild(node.body, visitor, context)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.15.1",
3
+ "version": "0.16.3",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "build": "node ./bin/kudzu.mjs build",
54
54
  "dev": "node ./bin/kudzu.mjs dev",
55
55
  "check": "tsc --noEmit && tsc -p test/fixtures/tsconfig.json --noEmit && node ./bin/kudzu.mjs build",
56
- "test": "node --test test/*.test.mjs",
56
+ "test": "node --test test/project-application.test.mjs && node --test --test-skip-pattern='^establishes the 0\\.15\\.2 notification ownership contract$' test/*.test.mjs",
57
57
  "test:package": "node test/package-smoke.mjs",
58
58
  "benchmark": "node test/performance.mjs",
59
59
  "benchmark:keyed": "node test/keyed-performance.mjs",
@@ -73,6 +73,11 @@
73
73
  "typescript": "^5.9.2"
74
74
  },
75
75
  "devDependencies": {
76
+ "@codemirror/view": "^6.43.9",
77
+ "@types/sortablejs": "^1.15.9",
78
+ "chart.js": "^3.8.2",
79
+ "sortablejs": "^1.15.7",
80
+ "typed.js": "^3.0.0",
76
81
  "wrangler": "^4.112.0"
77
82
  }
78
83
  }