@kudzujs/core 0.8.29 → 0.8.31

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.29` 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.31` 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
 
@@ -28,6 +28,7 @@ Syntax compatibility does not mean reproducing React wholesale. Kudzu accepts th
28
28
  - Interactive routes ship only the capabilities they use.
29
29
  - Prefer build-time execution whenever all inputs are available during the build.
30
30
  - State setters update logical state immediately and batch direct DOM writes at synchronous-turn boundaries.
31
+ - Async handler state writes, queued commits, and captured ref resolution end when its mounted DOM owner is released; application promises and ordinary reads may finish without reviving that ownership.
31
32
  - Preserve familiar function components, props, children, JSX, hooks, conditions, collections, and event handlers where Kudzu can compile them safely.
32
33
  - Prefer compiler specialization over asking migrated applications to replace ordinary declarative React UI with imperative DOM code.
33
34
  - Unknown browser-only data receives a static shell plus only the ESM needed to obtain and patch it.
@@ -282,6 +283,19 @@ This queue orders the next investigations by general migration value. Start only
282
283
  - Same-named callback parameters, locals, imports, browser globals, state values, setters, reducers, observer handles, and animation-frame handles no longer alias through identifier spelling alone.
283
284
  - Imported, specialized, and compiler-synthesized trees retain the existing conservative fallback; no public API, source syntax, ModuleIR contract, browser runtime, or capability bytes were added.
284
285
 
286
+ ### Completed In 0.8.30
287
+
288
+ - Reachable ordinary source validates relative runtime imports and re-exports before compilation and reports unresolved edges at the original importer file, line, column, and specifier.
289
+ - Ordinary dynamic `import()` fails during graph discovery rather than surviving into generated `.kudzu` modules; relative, package, template, and computed forms share one source-located boundary.
290
+ - Ordinary and Worker graph ownership remain separate, while type-only and unreachable source stay excluded. Export-name validation, ProjectSession, runtime behavior, and public APIs remain unchanged.
291
+
292
+ ### Completed In 0.8.31
293
+
294
+ - Pending async native handlers lose state-write, queued-commit, and captured-ref authority when enhanced navigation, keyed removal, conditional removal, or document disposal releases their mounted DOM owner.
295
+ - Direct and captured setters cannot recreate released state, queued commits clear without touching replacement DOM, and captured object refs resolve to `null` after release.
296
+ - Chrome coverage removes and recreates the same route and keyed row before old work resolves, proving replacement ownership stays fresh without cancelling application promises or adding a scheduler.
297
+ - The focused 5,000-event Chrome dispatch median remains 6.4 ms; the native fixture retains its emitted paths with a measured 209 B raw / 94 B aggregate gzip correctness cost.
298
+
285
299
  ## Cross-Cutting Performance Gates
286
300
 
287
301
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,6 +1,41 @@
1
1
  # Performance Records
2
2
 
3
- Reproducibility classes: `npm run benchmark` and `npm run benchmark:keyed` 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.
3
+ Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, and `npm run benchmark:native` 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
+
5
+ ## 0.8.31 Async Native Handler Ownership
6
+
7
+ Measured UTC 2026-08-10 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, npm 11.18.0, and Chrome 151.0.7922.76. The baseline was clean tag `v0.8.30` at `9bb5ce8`; baseline and candidate used the same installed dependencies.
8
+
9
+ The implementation and maintained-check patch over `v0.8.30` had SHA-256 `c67956b67b558052885dd48a9dbe35f238b8ce042876537952355aa05b8cdc10`, produced by:
10
+
11
+ ```bash
12
+ git diff --binary v0.8.30 -- framework/native-runtime.js framework/serialization.js test/fixtures/navigation/src/Shell.tsx test/fixtures/navigation/src/pages/product.tsx test/fixtures/navigation/public/browser-test.js test/framework.test.mjs test/native-performance.mjs package.json | shasum -a 256
13
+ ```
14
+
15
+ The maintained browser runner built the same `test/fixtures/native-bubbling` source with each framework root. After one warm-up, 21 alternating headless Chrome processes dispatched 5,000 synchronous clicks through the real DOM listener, handler-module lookup, generated handler, state context, and queued-flush scheduling path. Both medians were 6.4 ms; the 6.1-6.8 ms baseline and 6.2-6.7 ms candidate ranges overlap, so no dispatch regression is established.
16
+
17
+ ```bash
18
+ git worktree add --detach /tmp/kudzu-v0.8.30 v0.8.30
19
+ ln -s "$PWD/node_modules" /tmp/kudzu-v0.8.30/node_modules
20
+ BASELINE_ROOT=/tmp/kudzu-v0.8.30 RUNS=21 ITERATIONS=5000 \
21
+ CHROME_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
22
+ npm run benchmark:native
23
+ ```
24
+
25
+ ```text
26
+ 0.8.30: [6.6,6.3,6.4,6.3,6.3,6.8,6.3,6.6,6.3,6.4,6.5,6.4,6.5,6.5,6.4,6.1,6.3,6.2,6.5,6.4,6.4]
27
+ 0.8.31: [6.4,6.7,6.2,6.4,6.6,6.6,6.5,6.4,6.5,6.4,6.4,6.4,6.6,6.5,6.4,6.3,6.4,6.4,6.6,6.2,6.4]
28
+ ```
29
+
30
+ The maintained `native-bubbling` fixture retained the same eight JavaScript paths. The browser runner hashes every file and permits only the ownership-bearing native and capture-deserialization runtimes to change:
31
+
32
+ | Artifact | 0.8.30 raw / gzip | 0.8.31 raw / gzip | Delta raw / gzip |
33
+ |---|---:|---:|---:|
34
+ | `assets/kudzu-native.js` | 1,528 B / 842 B | 1,715 B / 925 B | +187 B / +83 B |
35
+ | `assets/kudzu-serialization.js` | 675 B / 381 B | 697 B / 392 B | +22 B / +11 B |
36
+ | Complete native fixture JavaScript | 13,629 B / 6,177 B | 13,838 B / 6,271 B | +209 B / +94 B |
37
+
38
+ The timing is a focused synchronous event-dispatch measurement, not a general interaction or asynchronous task benchmark. The runtime does not cancel application promises or arbitrary browser API work; it invalidates Kudzu state writes, queued commits, and captured ref resolution after the listener's DOM ownership is released.
4
39
 
5
40
  ## 0.8.26 Goal B Benchmark Hardening
6
41
 
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.29 - Symbol-aware descriptor discovery.** Native handlers, effects, reactive bindings, keyed evaluators, optimized commands, and effect-resource validation now use source-local lexical identity when the binding index owns the complete AST, while synthesized trees retain the established fail-safe path. Read the [release notes](./RELEASES.md#0829---symbol-aware-descriptor-discovery), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.29), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.31 - Async native-handler ownership.** Pending async handlers lose state-write, queued-commit, and captured-ref authority when enhanced navigation or keyed/conditional removal releases their DOM owner. The focused synchronous native event-dispatch benchmark shows no material regression. Read the [release notes](./RELEASES.md#0831---async-native-handler-ownership), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.31), 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,66 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.31 - Async native-handler ownership
4
+
5
+ Kudzu 0.8.31 completes P0.4 by tying every mounted native-handler context to its DOM registration lifetime. Async work may finish after route, keyed, or conditional removal, but it cannot write through ownership that no longer exists.
6
+
7
+ ### Changed in 0.8.31
8
+
9
+ - Native registrations expose one active lifetime to direct setters, captured setters, queued commits, and captured object refs.
10
+ - `unmountNative()` invalidates the registration before removing its listener. Enhanced navigation, keyed removal, and conditional removal use the shared unmount hook; non-persisted `pagehide` directly releases document registrations.
11
+ - Late state writes become no-ops instead of recreating released state IDs; already queued commits clear without touching replacement DOM.
12
+ - Captured object refs resolve to `null` after release, so a late handler cannot find a replacement node that reused the same compiler-owned ref ID.
13
+ - Application promises are not cancelled, and no scheduler, task registry, component runtime, or public API is added.
14
+
15
+ ### Performance
16
+
17
+ - Twenty-one alternating headless Chrome processes measured 5,000 synchronous native event dispatches at a 6.4 ms median for both `0.8.30` and `0.8.31`; the recorded ranges overlap.
18
+ - The maintained `native-bubbling` fixture keeps the same eight JavaScript paths. `kudzu-native.js` and `kudzu-serialization.js` add 209 B raw / 94 B aggregate gzip; the runner rejects any other artifact change.
19
+ - Environment, raw arrays, artifact bytes, methodology, and limitations are recorded in `PERFORMANCE.md`.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm test`, `npm run test:package`, and all 189 tests pass.
24
+ - Chrome starts async route and keyed-row handlers, releases and recreates their owners, and proves late state writes and refs cannot mutate replacement ownership. A persistent layout handler also proves non-persisted document disposal invalidates pending work.
25
+ - Focused context checks cover queued direct commits and captured setters. The browser suite retains normal synchronous native event semantics; unaffected static routes remain zero JavaScript.
26
+ - P0.5 atomic and collision-safe output is next.
27
+
28
+ ### Upgrade
29
+
30
+ ```bash
31
+ npm install @kudzujs/core@^0.8.31
32
+ ```
33
+
34
+ ## 0.8.30 - Graph failure diagnostics
35
+
36
+ Kudzu 0.8.30 completes P0.3 by making ordinary source-graph failures stop at the original importer before compilation, generated module loading, or `.kudzu` paths can obscure the source error.
37
+
38
+ ### Changed in 0.8.30
39
+
40
+ - Reachability validates every relative runtime import and re-export in an ordinary reachable module instead of silently dropping unresolved edges.
41
+ - Missing bound imports, side-effect imports, reachable helper imports, named/default forwarding, and `export *` report the importer file, line, column, and written specifier.
42
+ - Every ordinary dynamic `import()` form is rejected during graph discovery, including relative, package, template-literal, and computed specifiers.
43
+ - Ordinary and Worker traversal retain separate ownership, so Worker-only modules continue through Worker-specific graph diagnostics and cleanup rules.
44
+ - Type-only edges and unreachable migration source remain excluded and cannot block a build.
45
+
46
+ ### Compiler Boundary
47
+
48
+ - `ordinaryRuntimeDependencies()` is the single pre-codegen validator for ordinary runtime graph edges; downstream import rewriting remains a fail-closed assertion.
49
+ - This release validates whether a relative runtime edge resolves to exactly one TypeScript file. Whether an existing target exports a requested name remains deferred to the planned module-symbol graph.
50
+ - P0.3 adds no accepted source syntax, ProjectSession, export cache, public API, browser runtime, or generated capability bytes. P0.4 async native-handler invalidation is next.
51
+
52
+ ### Validation
53
+
54
+ - `npm run check`, `npm test`, `npm run test:package`, and all 189 tests pass.
55
+ - Focused in-memory and build fixtures cover missing page imports, reachable helper imports, re-exports, relative/package/computed dynamic imports, type-only exclusion, unreachable exclusion, and absence of generated `.kudzu` paths.
56
+ - Existing Worker graph diagnostics, React/Router/Zustand migrations, keyed ownership, effects, navigation, and static-route zero-JavaScript behavior remain covered.
57
+
58
+ ### Upgrade
59
+
60
+ ```bash
61
+ npm install @kudzujs/core@^0.8.30
62
+ ```
63
+
3
64
  ## 0.8.29 - Symbol-aware descriptor discovery
4
65
 
5
66
  Kudzu 0.8.29 completes P0.2 by moving native handler, effect, binding, and list descriptor decisions from identifier spelling onto the source-local binding index introduced in 0.8.28.
@@ -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.29` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). It 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.31` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.4 native-handler ownership is complete; P0.5 atomic and collision-safe output is next. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
15
15
 
16
16
  ## Required Invariants
17
17
 
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the current `0.8.29` 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.31` 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
 
@@ -8,7 +8,7 @@ This maps the current `0.8.29` architecture built on the completed `0.8.23` Goal
8
8
  |---|---|---|
9
9
  | CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
10
10
  | Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteIR rendering, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
11
- | Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, and excludes unreachable migration source. |
11
+ | Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `ordinaryRuntimeDependencies()`, `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, excludes unreachable migration source, and fails unresolved ordinary edges or dynamic imports at the importer source location before code generation. |
12
12
  | Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every structural change. Imported source uses the same pipeline. |
13
13
  | Focused normalization passes | [`framework/compiler/`](../../framework/compiler/) | React, Router, browser signals, animation-frame refs, custom-hook timers, Zustand, and render control each validate and lower a narrow source shape. |
14
14
  | Shared AST/scope helpers | [`framework/compiler/ast-helpers.mjs`](../../framework/compiler/ast-helpers.mjs) | Binding, scope, reference, effect-return, and source-location analysis. |
@@ -29,7 +29,7 @@ This maps the current `0.8.29` architecture built on the completed `0.8.23` Goal
29
29
  | Effect entry generation | [`framework/compiler/effect-codegen.mjs`](../../framework/compiler/effect-codegen.mjs) | Generates ordinary, dependency, owned, and navigable effect entries from rendered descriptors. |
30
30
  | Runtime generation | [`framework/compiler/runtime-codegen.mjs`](../../framework/compiler/runtime-codegen.mjs), [`framework/compiler/list-runtime-codegen.mjs`](../../framework/compiler/list-runtime-codegen.mjs), [`framework/compiler/param-codegen.mjs`](../../framework/compiler/param-codegen.mjs) | Consumes versioned contracts, specializes authored capability sources with fail-closed anchors, and returns source/define results without filesystem ownership. |
31
31
  | Artifact emission | `framework/build.mjs` | Selects required files from CapabilityIR and writes or bundles generated sources with esbuild. Byte-identical native, parameter, and effect route entries reuse one exact-source transform result within the current build only. |
32
- | Browser capabilities | [`framework/*.js`](../../framework/) | Small optional modules for commands, bindings, lists, effects, native handlers, serialization, parameters, and navigation; no component runtime. |
32
+ | Browser capabilities | [`framework/*.js`](../../framework/) | Small optional modules for commands, bindings, lists, effects, native handlers, serialization, parameters, and navigation; native contexts invalidate writes and refs at DOM ownership release, with no component runtime. |
33
33
  | Opt-in navigation | [`framework/navigation-runtime.js`](../../framework/navigation-runtime.js) plus `framework/build.mjs` navigation configuration/emission | Fetches and validates complete same-origin documents, replaces only the marked route range, manages route/layout disposal, history, focus, finite prefetch retention, and native fallback. |
34
34
  | Development serving | [`framework/dev-server.mjs`](../../framework/dev-server.mjs) and [`framework/dev-state.js`](../../framework/dev-state.js) | Rebuild/watch/SSE and response-only short-lived state restoration; never changes production `dist/`. |
35
35
 
@@ -67,7 +67,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
67
67
  - `build()` still owns explicit artifact selection and filesystem writes after generator results are produced.
68
68
  - Runtime generators intentionally specialize readable authored sources through exact anchors; every required anchor fails closed, but a future generator format may remove this transitional dependency.
69
69
  - Source reachability and source compilation share one module because both consume the same normalization and import graph contracts.
70
- - Native handler, effect, state/setter, list, Router, React, and Zustand discovery still contains name-based analysis. The binding index is intentionally adopted one consumer family at a time; P0.2 moves the next descriptor consumers without changing cross-module semantics.
70
+ - Imported, specialized, and compiler-synthesized trees still use conservative name/scope fallback where the source-local binding index does not own the complete AST; cross-module semantics remain deferred to stable ModuleSymbol and SiteId work.
71
71
 
72
72
  These are future simplification opportunities, not incomplete Goal A contracts. Goal A changed no source support, browser output semantics, or browser architecture.
73
73
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Active execution plan after `0.8.29`. This document turns the current compiler audit into an ordered implementation program. It does not mark any 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
+ Active execution plan after `0.8.31`. This document turns the current compiler audit into an ordered implementation program. It does not mark any 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
7
  [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection. This plan is authoritative for the order and completion evidence of compiler generalization, large-application foundations, compatibility boundaries, AI tooling, and scale validation. If implementation evidence changes a boundary, update this document before broadening a patch.
8
8
 
@@ -43,7 +43,7 @@ The current limiting architecture is observable in these files:
43
43
  | ModuleIR | `compiler/ir/module-ir.mjs` | Numeric slots, names, export strings, and formatted owner strings coexist; several sections are descriptive rather than authoritative inputs |
44
44
  | RouteIR/CapabilityIR | `route-capability-planner.mjs` | Validation is shallow and capability selection is site-wide across interactive routes |
45
45
  | Build/codegen | `framework/build.mjs`, runtime generators | Full destructive builds, serialized-string handler reachability, source-text runtime surgery, no route chunk/CSS closure report |
46
- | Browser lifetime | `native-runtime.js`, `effect-runtime.js`, `navigation-runtime.js` | Effects invalidate stale work, but pending native handlers can write after route unmount |
46
+ | Browser lifetime | `native-runtime.js`, `effect-runtime.js`, `navigation-runtime.js` | Effects and native handlers invalidate stale writes at their existing effect or DOM ownership boundary |
47
47
  | Compatibility | React/Router/Zustand passes plus core branches | Package-specific knowledge is not contained behind one adapter boundary |
48
48
  | AI interface | CLI and string diagnostics | No stable diagnostic codes, semantic index, explain, fix, or migration analysis output |
49
49
 
@@ -131,7 +131,9 @@ This is an incremental evolution of the current repository:
131
131
 
132
132
  - [x] P0.1 Source-local binding index is complete in `0.8.28`. Reactive binding capture/import discovery and lowering use the index only when the complete expression is indexed; synthesized keyed expressions retain the existing fail-safe path. Focused scope tests, a 1,000-reference guard, browser integration, all 185 tests, and packed-package smoke pass.
133
133
  - [x] P0.2 Symbol-aware descriptor discovery is complete in `0.8.29`. Native handler, effect, remaining binding, list evaluator, optimized-command, and effect-resource discovery/lowering use the source-local binding index when it owns the complete AST; synthesized trees retain the existing fail-safe path. Focused lexical-shadow, JSON-safe IR, resource-ownership checks, all 187 tests, and packed-package smoke pass.
134
- - [ ] P0.3 Graph diagnostics is next. It must fail unresolved relative imports/re-exports and ordinary dynamic imports at the importer source location without broadening into ProjectSession or cross-module symbol work.
134
+ - [x] P0.3 Graph diagnostics is complete in `0.8.30`. Reachable ordinary modules validate relative runtime imports/re-exports and reject every dynamic `import()` at the importer source location before compilation or generated module loading. Ordinary and Worker traversal retain separate ownership, type-only and unreachable edges remain excluded, focused page/helper/re-export/dynamic fixtures pass with all 189 tests and packed-package smoke, and no export-symbol graph, ProjectSession, runtime, or public API is added.
135
+ - [x] P0.4 Async native handler ownership is complete in `0.8.31`. Mounted native registrations invalidate direct/captured setters, queued commits, and captured refs before listener removal. Chrome route, keyed-row, document-disposal, and synchronous event-dispatch checks, all 189 tests, and packed-package smoke pass without cancelling application promises or adding a scheduler.
136
+ - [ ] P0.5 Atomic and collision-safe output is next. Failed builds must preserve prior output and public content must not overwrite generated artifacts.
135
137
 
136
138
  ### P0: Semantic Correctness And Compiler Foundation
137
139
 
@@ -271,6 +273,8 @@ Every result carries a stable source-local binding slot, debug name, declaration
271
273
 
272
274
  **Done condition:** a handler resolving after enhanced navigation or row removal cannot recreate or mutate released state, refs, or DOM.
273
275
 
276
+ **Completed in `0.8.31`:** the existing native registration lifetime now guards state mutation, queued commits, and captured ref lookup. Route and keyed-row browser checks recreate the same ownership IDs before old work resolves, and document disposal invalidates a pending layout handler. Replacement state and DOM remain fresh. Both 5,000-event Chrome dispatch medians are 6.4 ms, and the 209 B raw / 94 B aggregate gzip runtime cost is recorded in `PERFORMANCE.md`.
277
+
274
278
  ### PR 5: Atomic And Collision-Safe Output
275
279
 
276
280
  **Objective:** make production builds safe before scaling the build graph.
@@ -453,6 +457,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
453
457
 
454
458
  ## Immediate Decision
455
459
 
456
- The first implementation PR is **PR 1: Source-Local Binding Index**. It is compile-time only, adds no browser bytes or public API, fixes an existing correctness class, and creates the stable semantic foundation required by component graphs, state operations, cross-module analysis, compatibility adapters, structured diagnostics, and AI explanation tooling.
457
-
458
- The next PR is **PR 3: Graph Failure Diagnostics**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
460
+ PR 1 through PR 4 are complete. The next PR is **PR 5: Atomic And Collision-Safe Output**. 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.29` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.31` 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
 
@@ -20,6 +20,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
20
20
  | `0.8.27` | Characterize the first Goal C browser-resource boundary and publish the ordered large-application and AI-native compiler execution plan. | The E2B-derived fixture fails with a source-located diagnostic, no resource runtime or API is added, and the next PR dependencies and completion evidence are explicit. |
21
21
  | `0.8.28` | Add the source-local binding index and adopt it for reactive binding capture/import discovery and lowering. | Lexical shadowing is identity-based for complete indexed bindings, synthesized keyed expressions retain their fallback, the 1,000-reference guard passes, and no browser bytes or public API are added. |
22
22
  | `0.8.29` | Move native handler, effect, remaining binding, list evaluator, optimized-command, and effect-resource discovery/lowering onto source-local lexical identity. | Shadowed imports/globals/state/resources remain distinct, HandlerIR/BindingIR round trips pass, synthesized trees retain the fallback, and no runtime or public API is added. |
23
+ | `0.8.30` | Validate ordinary runtime graph edges before code generation and reject dynamic imports at the importer source. | Page/helper/re-export/dynamic fixtures report original file/range/specifier without `.kudzu` paths; Worker, type-only, unreachable, output, and runtime behavior remain unchanged. |
24
+ | `0.8.31` | Invalidate pending native-handler contexts when their route, keyed, conditional, or document DOM owner is released. | Late setters, queued commits, and captured refs cannot mutate replacement ownership; synchronous dispatch remains within the performance gate and runtime bytes are recorded. |
23
25
 
24
26
  ## Sequence Rules
25
27
 
@@ -63,7 +63,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
63
63
  - `list-runtime.js`: optional keyed list validation, external item-expression evaluation, item-local conditional ranges, dynamic styles, item handler/effect scopes, moves, and cleanup.
64
64
  - `serialization.js`: capture deserialization shared by binding and native handlers.
65
65
  - `effect-runtime.js`: optional state and capture context for route-specific mount-effect entries.
66
- - `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers.
66
+ - `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers; DOM unmount invalidates late state writes, queued commits, and captured refs.
67
67
  - `navigation-runtime.js`: optional emitted-route complete-document prefetch, runtime segment matching, validation, finite memory caching, route-range replacement, history, focus, and native fallback, specialized once per configured shared-layout group.
68
68
  - `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
69
69
  - `*.d.ts`: public TypeScript and JSX declarations.
@@ -90,4 +90,4 @@ A direct setter may cross one same-file or relative-imported component boundary
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
 
93
- Cross-framework performance tables are historical snapshots from excluded workspaces and are not current rankings. `npm run benchmark` tracks the Worker build/graph fixture; `RUNS=21 npm run benchmark:keyed` tracks large keyed restoration with fresh Chrome profiles. Current methodology, raw arrays, external-fixture limits, and artifact deltas live in `PERFORMANCE.md` and the web docs.
93
+ Cross-framework performance tables are historical snapshots from excluded workspaces and are not current rankings. `npm run benchmark` tracks the Worker build/graph fixture; `RUNS=21 npm run benchmark:keyed` tracks large keyed restoration; `BASELINE_ROOT=... npm run benchmark:native` tracks native dispatch and exact artifact changes. Current methodology, raw arrays, external-fixture limits, and artifact deltas live in `PERFORMANCE.md` and the web docs.
@@ -18,7 +18,7 @@ import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
18
18
  import { createReactMigrationPass, reactMemoExpression } from "./react-migration-pass.mjs"
19
19
  import { normalizeRenderControlFlow } from "./render-control-pass.mjs"
20
20
  import { createRouterPass } from "./router-pass.mjs"
21
- import { parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
21
+ import { ordinaryRuntimeDependencies, parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
22
22
  import { createWorkerCompiler } from "./worker-compiler.mjs"
23
23
  import { createZustandPass } from "./zustand-pass.mjs"
24
24
 
@@ -92,20 +92,27 @@ function emittedPackageReference(source, file, packages) {
92
92
 
93
93
  export function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
94
94
  const reachable = new Set()
95
- const queue = [...entries]
95
+ const ordinary = new Set()
96
+ const workers = new Set()
97
+ const queue = entries.map(file => ({ file, owner: "ordinary" }))
96
98
  while (queue.length) {
97
- const file = queue.pop()
98
- if (reachable.has(file)) continue
99
+ const { file, owner } = queue.pop()
100
+ const visited = owner === "ordinary" ? ordinary : workers
101
+ if (visited.has(file)) continue
102
+ visited.add(file)
99
103
  reachable.add(file)
100
104
  const sourceFile = parseSourceFile(file, sourceIndex.get(file))
105
+ if (owner === "ordinary") for (const target of ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport)) queue.push({ file: target, owner: target.endsWith(".worker.ts") ? "worker" : owner })
101
106
  const visit = node => {
102
- const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
103
- if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
104
- try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
107
+ if (owner === "worker") {
108
+ const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
109
+ if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
110
+ try { queue.push({ file: resolveSourceImport(file, specifier.text, sourceFiles), owner }) } catch {}
111
+ }
105
112
  }
106
113
  const worker = workerCompiler.candidate(node, sourceFile)
107
114
  if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
108
- try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
115
+ try { queue.push({ file: resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles), owner: "worker" }) } catch {}
109
116
  }
110
117
  ts.forEachChild(node, visit)
111
118
  }
@@ -2667,15 +2674,6 @@ function hasFrameworkImport(sourceFile, name) {
2667
2674
 
2668
2675
  function packageImportBindings(sourceFile) {
2669
2676
  const bindings = new Map()
2670
- const rejectDynamic = node => {
2671
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
2672
- const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null
2673
- if (specifier === null) throw sourceNodeError(node, sourceFile, "Dynamic import specifiers are not supported")
2674
- if (!specifier.startsWith(".")) throw sourceNodeError(node, sourceFile, `Dynamic package import ${JSON.stringify(specifier)} is not supported`)
2675
- }
2676
- ts.forEachChild(node, rejectDynamic)
2677
- }
2678
- rejectDynamic(sourceFile)
2679
2677
  for (const node of sourceFile.statements) {
2680
2678
  if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue
2681
2679
  const target = node.moduleSpecifier.text
@@ -1,5 +1,6 @@
1
1
  import { dirname, extname, join, relative, resolve } from "node:path"
2
2
  import ts from "typescript"
3
+ import { sourceNodeError } from "./ast-helpers.mjs"
3
4
 
4
5
  const root = process.cwd()
5
6
 
@@ -24,6 +25,32 @@ export function runtimeModuleReference(node) {
24
25
  return clause.namedBindings?.elements.some(entry => !entry.isTypeOnly) ?? false
25
26
  }
26
27
 
28
+ export function ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport) {
29
+ const dependencies = []
30
+ const rejectDynamicImports = node => {
31
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
32
+ const argument = node.arguments[0]
33
+ const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(argument) ? JSON.stringify(argument.text) : argument?.getText(sourceFile) ?? "<missing>"
34
+ throw sourceNodeError(node, sourceFile, `Dynamic import ${specifier} is not supported in ordinary source modules`)
35
+ }
36
+ ts.forEachChild(node, rejectDynamicImports)
37
+ }
38
+ rejectDynamicImports(sourceFile)
39
+ for (const node of sourceFile.statements) {
40
+ if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !runtimeModuleReference(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier)) continue
41
+ const specifier = node.moduleSpecifier
42
+ if (!specifier.text.startsWith(".") || isStaticImport(specifier.text)) continue
43
+ try {
44
+ dependencies.push(resolveSourceImport(file, specifier.text, sourceFiles))
45
+ } catch (error) {
46
+ const detail = error.message.slice(error.message.indexOf("Relative import"))
47
+ const edge = ts.isExportDeclaration(node) ? "re-export" : "import"
48
+ throw sourceNodeError(specifier, sourceFile, detail.replace("Relative import", `Relative runtime ${edge}`))
49
+ }
50
+ }
51
+ return dependencies
52
+ }
53
+
27
54
  export function parseSourceFile(file, source) {
28
55
  return ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
29
56
  }
@@ -8,18 +8,20 @@ export function registerNativeModules(entries) {
8
8
  for (const [url, module] of entries) modules.set(url, module)
9
9
  }
10
10
 
11
- export function createNativeContext(state, stateIds, commit, serializedScope = {}) {
11
+ export function createNativeContext(state, stateIds, commit, serializedScope = {}, active = () => true) {
12
12
  const changed = new Set()
13
13
  let scheduled = false
14
14
 
15
15
  const flush = () => {
16
16
  scheduled = false
17
+ if (!active()) return changed.clear()
17
18
  const ids = [...changed]
18
19
  changed.clear()
19
20
  for (const id of ids) commit(id, state.get(id))
20
21
  }
21
22
 
22
23
  const setId = globalThis.__KUDZU_CAPTURE_SETTER__ ? (id, value) => {
24
+ if (!active()) return
23
25
  const current = state.get(id)
24
26
  state.set(id, typeof value === "function" ? value(current) : value)
25
27
  changed.add(id)
@@ -29,7 +31,7 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
29
31
  }
30
32
  } : undefined
31
33
 
32
- const scope = Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined)]))
34
+ const scope = Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined, active)]))
33
35
 
34
36
  return {
35
37
  get(name) {
@@ -43,6 +45,7 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
43
45
  setId(stateIds[name], value)
44
46
  return
45
47
  }
48
+ if (!active()) return
46
49
  const id = stateIds[name]
47
50
  const current = state.get(id)
48
51
  state.set(id, typeof value === "function" ? value(current) : value)
@@ -61,6 +64,9 @@ if (typeof document !== "undefined") {
61
64
  registerMountHook(mount)
62
65
  registerUnmountHook(unmountNative)
63
66
  mount(document)
67
+ addEventListener("pagehide", event => {
68
+ if (!event.persisted) unmountNative(document)
69
+ })
64
70
  }
65
71
 
66
72
  function mountNative(root, eventNames, modules) {
@@ -71,14 +77,16 @@ function mountNative(root, eventNames, modules) {
71
77
  let encoded = node.dataset[`kNative${capitalize(eventName)}`]
72
78
  let native = JSON.parse(encoded)
73
79
  let handler
74
- let context = createNativeContext(browserState, native.states, commitDom, native.scope)
80
+ const registration = { active: true }
81
+ const active = () => registration.active
82
+ let context = createNativeContext(browserState, native.states, commitDom, native.scope, active)
75
83
  const listener = event => {
76
84
  try {
77
85
  const current = node.dataset[`kNative${capitalize(eventName)}`]
78
86
  if (current !== encoded) {
79
87
  native = JSON.parse(current)
80
88
  encoded = current
81
- context = createNativeContext(browserState, native.states, commitDom, native.scope)
89
+ context = createNativeContext(browserState, native.states, commitDom, native.scope, active)
82
90
  handler = undefined
83
91
  }
84
92
  handler ??= modules.get(native.module)[native.handler]
@@ -88,8 +96,9 @@ function mountNative(root, eventNames, modules) {
88
96
  console.error(error)
89
97
  }
90
98
  }
99
+ registration.listener = listener
91
100
  node.addEventListener(eventName, listener)
92
- listeners.set(eventName, listener)
101
+ listeners.set(eventName, registration)
93
102
  registrations.set(node, listeners)
94
103
  }
95
104
  }
@@ -97,7 +106,10 @@ function mountNative(root, eventNames, modules) {
97
106
 
98
107
  function unmountNative(root) {
99
108
  for (const node of matching(root, "*")) {
100
- for (const [eventName, listener] of registrations.get(node) ?? []) node.removeEventListener(eventName, listener)
109
+ for (const [eventName, registration] of registrations.get(node) ?? []) {
110
+ registration.active = false
111
+ node.removeEventListener(eventName, registration.listener)
112
+ }
101
113
  registrations.delete(node)
102
114
  }
103
115
  }
@@ -1,8 +1,8 @@
1
- export function deserialize(value, getState, setState) {
1
+ export function deserialize(value, getState, setState, active) {
2
2
  if (!value || typeof value !== "object") return value
3
3
  if (value.type === "undefined") return undefined
4
4
  if (value.type === "number") return value.value === "NaN" ? NaN : value.value === "Infinity" ? Infinity : value.value === "-Infinity" ? -Infinity : -0
5
- if (value.type === "ref") return { get current() { return typeof document === "undefined" ? null : document.querySelector(`[data-k-ref="${value.id}"]`) } }
5
+ if (value.type === "ref") return { get current() { return typeof document === "undefined" || active?.() === false ? null : document.querySelector(`[data-k-ref="${value.id}"]`) } }
6
6
  if (globalThis.__KUDZU_CAPTURE_STATE__ && value.type === "state") return getState?.(value.id)
7
7
  if (globalThis.__KUDZU_CAPTURE_SETTER__ && value.type === "setter") return next => {
8
8
  if (!setState) throw new Error("Captured state setter is not available in this context")
@@ -10,20 +10,20 @@ export function deserialize(value, getState, setState) {
10
10
  }
11
11
  if (value.type === "array") {
12
12
  const array = []
13
- for (const [index, entry] of value.value.entries()) defineCapture(array, String(index), entry, getState, setState)
13
+ for (const [index, entry] of value.value.entries()) defineCapture(array, String(index), entry, getState, setState, active)
14
14
  return array
15
15
  }
16
16
  if (value.type === "object") {
17
17
  const object = value.nullPrototype ? Object.create(null) : {}
18
- for (const [key, entry] of value.value) defineCapture(object, key, entry, getState, setState)
18
+ for (const [key, entry] of value.value) defineCapture(object, key, entry, getState, setState, active)
19
19
  return object
20
20
  }
21
21
  return value
22
22
  }
23
23
 
24
- function defineCapture(target, key, entry, getState, setState) {
24
+ function defineCapture(target, key, entry, getState, setState, active) {
25
25
  const descriptor = globalThis.__KUDZU_CAPTURE_STATE__ && entry?.type === "state" && getState
26
26
  ? { get: () => getState(entry.id) }
27
- : { value: deserialize(entry, getState, setState), writable: true }
27
+ : { value: deserialize(entry, getState, setState, active), writable: true }
28
28
  Object.defineProperty(target, key, { ...descriptor, enumerable: true, configurable: true })
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.29",
3
+ "version": "0.8.31",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,6 +58,7 @@
58
58
  "benchmark": "node test/performance.mjs",
59
59
  "benchmark:keyed": "node test/keyed-performance.mjs",
60
60
  "benchmark:commerce": "node test/commerce-build-performance.mjs",
61
+ "benchmark:native": "node test/native-performance.mjs",
61
62
  "prepublishOnly": "npm run check && npm test",
62
63
  "deploy": "wrangler deploy",
63
64
  "preview": "wrangler dev"