diff --git a/CLAUDE.md b/CLAUDE.md
index 228255c..4038130 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -110,13 +110,13 @@ Format: comma-separated contexts, semicolon-separated URL-encoded params per con
### 3. Frontend-Agnostic Rendering (SSR + PSR)
-**SSR** — Django template backend integration. `render(request, 'ProfilePage', props)` calls a persistent Bun subprocess that runs `renderToString`.
+**SSR** — Django template backend integration. `render(request, 'components/Hello.tsx', props)` — the template name is a `.tsx`/`.jsx` **file path** (resolved against `DIRS`), not a component name — calls a persistent Bun subprocess that runs `renderToString`.
**PSR** (Preemptive Static Rendering) — pages re-rendered on mutation, not on request. Edge caches the result. Controlled by the manifest's `render_strategy` field.
-**The Bun worker protocol** — JSON-RPC over stdin/stdout:
+**The Bun worker protocol** — JSON-RPC over stdin/stdout. The worker `import()`s the file and renders it (no component registry):
```
-→ {"id": 1, "method": "render", "params": {"component": "ProfilePage", "props": {"userId": 5}}}
+→ {"id": 1, "method": "render", "params": {"file": "/abs/path/Hello.tsx", "props": {"name": "World"}}}
← {"id": 1, "html": "
...
"}
```
@@ -356,8 +356,9 @@ Three independent secrets, each with its own blast radius:
TEMPLATES = [
{
'BACKEND': 'mizan.ssr.MizanTemplates',
+ 'DIRS': [BASE_DIR / 'frontend'],
'OPTIONS': {
- 'worker_path': 'frontend/ssr-worker.tsx',
+ 'worker': 'path/to/mizan-ssr/src/worker.tsx',
'timeout': 5,
},
},
@@ -371,14 +372,14 @@ from django.shortcuts import render
def profile_page(request, user_id):
profile = get_user_profile(user_id)
- return render(request, 'ProfilePage', {'profile': profile})
+ return render(request, 'components/Profile.tsx', {'profile': profile})
```
-`render()` calls `MizanTemplates.get_template('ProfilePage')` which returns a `MizanTemplate`. The template's `render(context)` sends JSON-RPC to the Bun worker.
+`render()` calls `MizanTemplates.get_template('components/Profile.tsx')` — the name is a file path resolved to an absolute path against `DIRS` — which returns a `MizanTemplate`. The template's `render(context)` sends JSON-RPC (`{file, props}`) to the Bun worker.
### SSR Bridge (bridge.py)
-- Spawns `bun run ` on first render
+- Spawns `bun run ` on first render
- Persistent subprocess — stays alive across requests
- JSON-RPC over stdin/stdout with message ID correlation
- Thread-safe: multiple Django workers can call `render()` concurrently
@@ -388,8 +389,8 @@ def profile_page(request, user_id):
### Bun Worker (worker.tsx)
- Reads newline-delimited JSON from stdin
-- Component registry: `registerComponent('ProfilePage', ProfilePage)`
-- Calls `renderToString(createElement(Component, props))`
+- Resolves the component by **file path** — `import(file)` (cached) — no registry
+- Calls `renderToString(createElement(Component, props))` on the imported default export
- Returns `{"id": N, "html": "..."}` or `{"id": N, "error": "..."}`
- Health check: `{"method": "ping"}` → `{"pong": true}`
diff --git a/ISSUES.md b/ISSUES.md
index da73931..60c76a5 100644
--- a/ISSUES.md
+++ b/ISSUES.md
@@ -11,9 +11,9 @@ no longer exist.
- [ ] **Vue / Svelte frontend packages are unimplemented stubs.** `frontends/mizan-vue` and `frontends/mizan-svelte` contain only a `package.json` — no `src/`. The Rust codegen emits Vue composables and Svelte stores (`src/emit/vue.rs`, `src/emit/svelte.rs`, byte-checked by `vue_svelte_parity.rs`), but there is no runtime kernel-adapter package for either and no example app exercises them against a live backend. React is the only frontend with full integration verification.
- [ ] **Svelte adapter emits Svelte 4 stores.** `src/emit/svelte.rs` generates `readable` stores from `svelte/store`. Svelte 5 `$state`/`$derived` runes are the current idiom.
- [ ] **Forms have no codegen target.** `mizan-react/src/forms.ts` (form core hooks) is hand-written and consumed via the pre-kernel `MizanProvider`; the e2e harness has its form fixtures removed. A form codegen target wired to `mizanCall` is owed.
-- [ ] **Upload dispatch not wired for Rust/Axum + Tauri.** The `Upload` type is first-class end to end — IR (`upload` KDL node), codegen (TS `File`), kernel (auto-multipart), and dispatch+constraint binding on Django and FastAPI. Rust/Axum and Tauri parse the IR node and emit the field, but their dispatch does not yet bind multipart file parts.
+- [ ] **Upload dispatch not wired for Rust/Axum + Tauri.** The `Upload` type is first-class end to end — IR (`upload` KDL node), codegen (TS `File`; the Rust target lowers it to `Vec`), kernel (auto-multipart), and dispatch+constraint binding on Django and FastAPI. The Rust/Axum and Tauri *adapters* have no upload concept at dispatch — they don't bind multipart file parts yet.
- [ ] **Pre-kernel MizanProvider still shipped.** `mizan-react/src/context.tsx` (~750 lines) is the pre-kernel provider, still imported by the desktop example. It coexists with the codegen-emitted `MizanContext` (which subscribes to `@mizan/base`). Migrating the desktop example onto the generated provider retires it.
-- [ ] **Cache module open issues.** See `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`: purge atomicity, cross-language stringification, per-param sub-index cleanup, thundering-herd protection, `cache_get`/`cache_put` argument inconsistency, RedisCache test coverage.
+- [ ] **Cache module open issues.** See `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`: cross-language stringification of un-normalized value types, and no thundering-herd / single-flight protection.
- [ ] **Packages missing a README.** `frontends/mizan-base` (the kernel everything imports), `protocol/mizan-codegen` (the codegen binary), `frontends/mizan-vue`, `frontends/mizan-svelte`, `frontends/mizan-rust`, `backends/mizan-ts`, `backends/mizan-rust-axum`, `cores/mizan-python`.
## Resolved this pass
diff --git a/README.md b/README.md
index 2a08f40..a1fa87d 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ The surface every Mizan adapter implements.
| Invalidation — JSON body | ✅ | ✅ | ✅ | ✅ | ✅ |
| Invalidation auto-scoping (three-tier) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Function discovery / registration | ✅ | ✅ | ✅ | ✅ | ✅ |
-| Codegen IR export (KDL) | ✅ | ✅ | ✅ ⁶ | ✅ ⁶ | — ⁸ |
+| Codegen IR export (KDL) | ✅ | ✅ | ✅ ⁶ | ✅ ⁶ | ❌ ⁸ |
| File uploads (multipart, `Upload` type) | ✅ | ✅ | ❌ ⁹ | ❌ ⁹ | — ¹⁰ |
### Edge, cache & enforcement
@@ -107,12 +107,14 @@ target stack calls for them.
rather than fetching over HTTP.
7. FastAPI and Rust/Axum expose `GET /session/` returning a null CSRF token for wire
parity; CSRF is Django-only.
-8. TypeScript is an edge/protocol-reference adapter (HMAC cache, manifest, PSR), not a
- codegen source — it demonstrates the cache + invalidation protocol is
- language-agnostic.
-9. The IR carries the `upload` type and the codegen emits the field across targets, but
- multipart dispatch binding is wired for Django and FastAPI only; Rust/Axum and Tauri
- parse the IR node but do not yet bind uploads.
+8. `mizan-ts` emits the Edge manifest (JSON) but has no KDL IR emitter, so it can't yet
+ feed the codegen — an unbuilt gap. A TypeScript backend still needs the generated
+ client (types + `callXxx`/`fetchXxx` + framework hooks); same language doesn't remove
+ the need for it.
+9. The `mizan-codegen` crate parses the `upload` KDL node and emits the field across
+ targets (the Rust target lowers it to `Vec`). Multipart dispatch binding is wired
+ for Django and FastAPI only; the Rust/Axum and Tauri *adapters* have no upload concept
+ at dispatch yet.
10. The TypeScript column is the `mizan-ts` backend adapter, which has no upload
dispatch. The matching client side lives in the kernel (`@mizan/base`): `mizanCall`
auto-switches to `multipart/form-data` when any argument is a `File`.
diff --git a/ROADMAP.md b/ROADMAP.md
index 8de5c2b..dd79e79 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -34,7 +34,7 @@
- [ ] **Svelte 5 runes** — the Svelte target emits Svelte 4 `readable` stores; migrate to `$state`/`$derived`.
- [ ] **Forms codegen target** — emit form clients wired to `mizanCall` from the kernel; retire the hand-written `mizan-react/src/forms.ts` and its dependence on the pre-kernel provider.
- [ ] **Desktop example onto the generated provider** — migrate `examples/django-react-desktop-app` off the pre-kernel `MizanProvider` (`mizan-react/src/context.tsx`) so it can be retired.
-- [ ] **Cache hardening** — purge atomicity, per-param sub-index cleanup, thundering-herd protection, RedisCache coverage (see `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`).
+- [ ] **Cache hardening** — thundering-herd / single-flight protection, and pinning cross-language stringification of un-normalized value types (see `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`).
- [ ] **Package READMEs** — `mizan-base`, `mizan-codegen`, and the other packages missing one (see `ISSUES.md`).
---
diff --git a/backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md b/backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md
index 0f0dcaa..ad0a94d 100644
--- a/backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md
+++ b/backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md
@@ -1,16 +1,12 @@
# Cache Module — Known Issues
-Open issues against the current cache implementation. Resolved items are
-removed once their fix lands.
+Open issues against the current cache implementation. The cache uses
+HMAC-derived keys with **no reverse indexes** (scoped purge recomputes the key;
+broad purge is a prefix SCAN+UNLINK), so there are no index/sub-index races to
+track. Resolved items are removed once their fix lands.
## Correctness
-### Purge race condition (non-atomic index operations)
-`cache_purge` reads the index and deletes as separate operations. A
-concurrent `cache_put` between the two steps can orphan entries. Mitigated
-by AND-intersection purge semantics, but full atomicity (Lua script or
-`WATCH`/`MULTI` on the Redis backend) is still owed.
-
### Cross-language stringification divergence
Python `str(True)` → `"True"` vs JS `String(true)` → `"true"`. `_normalize`
canonicalizes `True`/`False`/`None` today, but the rules for the remaining
@@ -19,22 +15,6 @@ TypeScript HMAC keys can still diverge on an un-normalized type.
## Performance / Operability
-### Broad purge leaves per-param sub-indexes
-A broad `cache_purge(context)` deletes the entries but not the per-param
-sub-indexes — a slow Redis memory leak.
-
### No thundering-herd protection
Concurrent cold misses on the same key all execute and write. No
single-flight / request-coalescing.
-
-## API shape
-
-### cache_get / cache_put argument inconsistency
-`cache_get`/`cache_put` take explicit args while the executor resolves some
-inputs from module globals — two access patterns for one concern.
-
-## Coverage
-
-### RedisCache lacks test coverage
-Only `MemoryCache` is exercised by the suite. `RedisCache` (connection
-pooling, TTL, SCAN/UNLINK batching, socket timeouts) is untested.