Commit Graph

28 Commits

Author SHA1 Message Date
24ff0ae66d Cleanup: delete dead code, fix invalidateFunctions bug, deduplicate
Deleted:
- runtime/index.ts (146 lines) — never imported by anything
- httpFunctionCall + _csrClient cache — redundant third HTTP path
- 3 duplicate getCSRFToken() implementations → shared utils.ts

Fixed:
- invalidateFunctions() was ignoring function names and invalidating
  ALL mounted contexts. Now correctly passes names through.

33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:36:33 -04:00
1b5dca5ab3 SSR: file-path rendering, no component registry
The worker receives a file path in the JSON message, dynamically
imports it, renders it. No registerComponent API, no app entry file,
no export maps. Django's template backend resolves the template name
to an absolute path against DIRS, same as every other template engine.

  render(request, 'components/Hello.tsx', {'name': 'World'})

Verified working: curl http://localhost:8000/hello/ returns
  <div id="mizan-root"><div>Hello, World!</div></div>

Changes:
- worker.tsx: receives file path, dynamic import with cache
- bridge.py: sends file path instead of component name
- backend.py: resolves template name against DIRS to absolute path
- Fix bridge.py:147 bug (referenced deleted 'component' variable)
- Example app: Hello.tsx component, /hello/ view, template config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:33:01 -04:00
658cbebce1 Regenerate example code + fix broken paths from repo restructure
- Fix testapp/apps.py: import djarea_clients (file was never renamed)
- Fix fetch.mjs: command is export_djarea_schema not export_mizan_schema
- Fix harness package.json: dependency path to mizan-react after restructure
- Add package.json for generator (openapi-typescript dependency)
- Regenerate all example code with new protocol format:
  - generated.provider.tsx uses raw context responses + SSR hydration
  - generated.server.ts uses GET /ctx/global/ with response.ok check
  - generated.forms.ts, channels.ts, channels.hooks.tsx refreshed
- Remove stale generated.django.tsx and generated.django.server.ts
- Update imports: fixtures.tsx and main.tsx import from ./api (index)
- Use MizanContext instead of deprecated DjangoContext in examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:13:35 -04:00
711e92ac4d Fix protocol mismatch + add SSR hydration to codegen
Three bugs fixed:

1. MizanProvider.call() read data.data but server returns data.result.
   Now reads data.result and processes data.invalidate for server-driven
   invalidation (triggering refetch on mounted context providers).

2. GlobalContextLoader expected {error, data} wrapper but context GET
   returns raw bundled data. Fixed to iterate response directly.

3. Named context providers had same wrapper assumption. Fixed to
   setData(result) directly.

Two features added:

1. SSR hydration: GlobalContextLoader checks window.__MIZAN_SSR_DATA__
   on mount. If present, populates contexts from it and skips fetch.

2. SSR hydration: Named context providers check __MIZAN_SSR_DATA__ in
   useState initializer. If SSR data exists for their functions, they
   render immediately without fetching.

3. Server-driven invalidation in MizanProvider.call(): reads the
   invalidate array from mutation responses and triggers refetch on
   mounted providers. Generated mutation hooks' hardcoded invalidation
   is now redundant but idempotent — both paths coexist safely.

Also fixed FunctionSuccessResponse type to match new protocol:
  { result: T, invalidate?: [...] }

373 Django + 33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:08:32 -04:00
4147679e6b Add SSR bridge: Django template backend + Bun subprocess renderer
Mizan's SSR is a Django template backend. Configure in TEMPLATES:

    TEMPLATES = [{
        'BACKEND': 'mizan.ssr.MizanTemplates',
        'OPTIONS': {'worker_path': 'frontend/ssr-worker.tsx'},
    }]

Then render(request, 'ProfilePage', {'user_id': 5}) renders the React
component via a persistent Bun subprocess. The component name is the
template name. The context dict becomes props.

Architecture:
- Bun worker: stdin/stdout JSON-RPC, renderToString, component registry
- Django bridge: subprocess lifecycle, crash recovery, concurrent renders
- Template backend: implements Django's BaseEngine interface

This is the AFI's SSR boundary:
- Backend adapter implements mizan.ssr() (data gathering)
- Frontend adapter implements renderToHTML() (component rendering)
- Bun subprocess is the runtime hosting the frontend adapter

11 tests: ping, render, error handling, crash recovery, concurrent
renders (5 threads), template backend integration. All require Bun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 02:18:05 -04:00
e5f8fafc01 Remove CDN Cache-Control headers; fix cross-language sort bug
Mizan's protocol layers (origin Redis cache, Edge Worker) handle caching
autonomously. The origin emits Cache-Control: no-store on ALL responses —
browsers and non-Mizan intermediaries must not cache. The Edge Worker
controls CDN caching via cf object, independent of origin headers.

Also fixes:
- TS localeCompare → byte-order sort (localeCompare is locale-sensitive,
  would produce different HMAC keys for non-ASCII params vs Python)
- Python cache_purge: empty {} params no longer treated as falsy
  (was inconsistent with JS where {} is truthy)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:38:24 -04:00
7f5542e305 Simplify cache: remove reverse indexes, use direct key reconstruction
The reverse index approach (Redis sets tracking HMAC keys per context)
was over-engineered. Scoped purge doesn't need an index — recompute
the HMAC key from the invalidation params and DELETE directly. One
Redis command, no TOCTOU race, no atomicity concern, no stale members.

Broad purge uses key-prefix scan (keys are now "ctx:{context}:{hmac}").
This is rare (Tier 3 fallback) and acceptable as a SCAN operation.

Eliminated from both Python and TypeScript:
- All SET/SADD/SMEMBERS/SREM index operations
- CacheBackend.get_index, remove_from_index, delete_index, delete_indexes_by_prefix
- build_index_keys function
- Pipeline transaction complexity
- TOCTOU race condition (was critical, now impossible)

Backend interface is now 5 methods: get, set, delete, delete_by_prefix, clear.
Redis tests updated — prefix isolation test added, connection leak fixed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:21:24 -04:00
dbbb269696 Add Redis backend tests against real Redis instance
13 tests hitting Redis on localhost:6399 (docker run redis:alpine):
- get/put/delete, index tracking, remove_from_index, delete_by_prefix
- TTL verification on cache entries AND index sets
- Pipeline atomicity (value + indexes written together)
- Scoped purge (AND semantics) against real Redis
- Broad purge with sub-index cleanup
- Tests skip gracefully if Redis is not available

No mocks, no fakes. Real Redis or skip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:10:35 -04:00
4744ff052e Add TypeScript cache adapter with cross-language conformance tests
Port of Python's origin-side cache to TypeScript:
- cache/keys.ts: deriveCacheKey with stableStringify for JSON-canonical HMAC
- cache/backend.ts: MemoryCache (same API as Python)
- cache/index.ts: cacheGet, cachePut, cachePurge with AND semantics

Integrated into dispatch.ts:
- handleContextFetch: cache lookup before execution, store after
- handleMutationCall: purge on invalidation

Cross-language pin test proves Python and TypeScript produce identical
HMAC-SHA256 output for the same inputs:
  Public:      605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6
  User-scoped: 30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2

34 TypeScript tests (9 new), 165 Python tests (1 new pin test).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:03:24 -04:00
54581d184f Fix MWT security issues from expert review
Critical:
- Separate MIZAN_MWT_SECRET from MIZAN_CACHE_SECRET — compromising one
  no longer compromises the other (token forgery vs cache poisoning)
- Move kid from JWT payload to JOSE header per RFC 7515 — standard
  libraries use header kid for key selection before payload decode

High:
- Full SHA-256 pkey (64 chars) instead of truncated 16 — no reason to
  reduce collision resistance
- Add nbf (not-before) claim for clock skew protection
- Log warnings in _try_mwt_auth on missing secret and decode failures
  instead of silent swallow
- Rename _csrf_protect_unless_jwt to _csrf_protect_unless_token (accuracy)
- decode_mwt logs at DEBUG level on failures for observability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:52:30 -04:00
d7ec13c43c Add MWT (Mizan Web Token) — protocol-owned identity layer
MWT is a standard JWT with Mizan-specific claims on X-Mizan-Token header:
- sub: user_id for HMAC cache key derivation
- pkey: deterministic hash of user's permission state (staff + superuser + perms)
- kid: key ID for future secret rotation
- aud: audience binding for cross-tenant protection

Executor checks X-Mizan-Token first, falls back to Authorization: Bearer
for legacy JWT compat. Invalid tokens return 401 (no session fallback).

New: mizan/mwt.py (create_mwt, decode_mwt, MWTUser, compute_permission_key)
New: mwt_obtain server function for session-to-MWT issuance
New: MIZAN_MWT_TTL setting (default 300s = 5 min permission staleness window)
11 new tests covering creation, decode, pkey determinism, auth integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:41:18 -04:00
a2388b3ab2 Add rev and cache parameters to @client decorator
rev=N: bumped by developer when function logic changes. Becomes part of
the HMAC cache key — old cache entries are unreachable without purge.
Effective rev for a context is max(rev) across all functions in it.

cache=int|False|True: TTL escape hatch for unobservable mutations.
cache=60 emits s-maxage=60. cache=False emits no-store. Default (True)
emits s-maxage=31536000 (forever, purge on mutation).
Effective cache for a context is min(TTL) across functions, with False
taking precedence.

Both parameters flow through: decorator → meta → manifest → cache key
and Cache-Control headers. Implemented in both Python and TypeScript
with 13 Python tests and 4 TypeScript tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:20:32 -04:00
7daec1c2e2 Fix remaining cache issues: index TTL, sub-index cleanup, top-level imports
- RedisCache.put: add pipe.expire() on index sets matching entry TTL,
  prevents orphaned index entries when cache values expire
- Broad purge: delete_indexes_by_prefix() cleans per-param sub-indexes
  (mizan:idx:ctx:k=v) that previously leaked as dead sets
- Move cache imports to top of executor.py (were inline in view functions)
- Update KNOWN_ISSUES.md — all 16 issues now resolved or documented

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:09:22 -04:00
b06a65e133 Fix critical cache issues: user_id scoping, AND purge, error handling, TTL
Fixes from 8-expert review:
- Pass user_id from request.user.pk to cache key derivation (data leak fix)
- Scoped purge uses AND (intersection) not OR (union) semantics
- All cache ops in executor wrapped in try/except with logging fallthrough
- Thread-safe cache initialization with threading.Lock
- RedisCache: 24h safety-net TTL, connection timeouts, MULTI/EXEC pipeline
- RedisCache.clear() uses pipelined UNLINK instead of per-batch DELETE
- build_index_keys now stringifies values matching derive_cache_key
- get_cache() logs warnings for partial config and connection failures
- Wire-protocol internals removed from __all__

Remaining open: purge atomicity (Lua script), cross-language str() canon,
broad purge sub-index cleanup, thundering herd protection, RedisCache tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:50:05 -04:00
b2f990b4e5 Architecture rework: fix protocol bugs, add origin-side cache, document spec
8-expert review identified 3 bugs in shipped code (Vary header hallucination,
fn/function wire key mismatch, max-age=0 defeating PSR) — all fixed with
tests updated across Python and TypeScript.

Added: manifest version field, affects validation, wire format convention,
origin-side cache module (HMAC key derivation, MemoryCache + RedisCache
backends, reverse index for scoped invalidation, executor integration).

16 known issues documented in cache/KNOWN_ISSUES.md from expert review —
critical items (user_id not passed, purge race condition, no Redis error
handling) to be fixed in follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:40:55 -04:00
97237ed1a4 Add mizan-ts: TypeScript backend adapter proving AFI is language-agnostic
The TypeScript adapter produces the same manifest, the same
X-Mizan-Invalidate headers, the same JSON invalidation protocol,
and the same CDN-ready response headers as mizan-django.

One Edge Worker. Two backend languages. Same protocol.

Features:
- @client decorator (function wrapper + class method decorator)
- ReactContext class (same API as Django adapter)
- Registry with context groups and param tracking
- Context bundled GET: /api/mizan/ctx/<name>/
- Mutation POST: /api/mizan/call/ with server-driven invalidation
- Three-tier auto-scoping (argument name matching → broad fallback)
- Function-level affects targeting
- private=True (rejected from RPC, in manifest for Edge)
- X-Mizan-Invalidate header with URL-encoded params
- Edge manifest generation (identical format to Django's)
- render_strategy + user_scoped derivation

22 edge compatibility tests pass (Bun, 21ms):
- Deterministic JSON, sorted keys
- Cache-Control: public on GETs, no-store on mutations/errors
- Vary: Authorization, Cookie
- Header round-trip with special characters
- Auto-scoped invalidation matches body and header
- Function-level invalidation
- Private function rejection
- Manifest structure with PSR/dynamic_cached strategies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
d228c7ab1b Add private=True, route=, methods= to @client decorator
private=True: server-internal functions (webhooks, cron) that emit
invalidation but are not client-callable. Rejected from POST /call/
with 403. No codegen. Appears in manifest for Edge.

  @client(affects='subscription', private=True, route='/webhooks/stripe/', methods=['POST'])
  def stripe_webhook(request) -> HttpResponse: ...

route=: Mizan-owned URL pattern for view-path functions. Registered
during autodiscovery. Populates page_routes in the manifest for
Edge/PSR to resolve during invalidation.

methods=: HTTP methods for the route. Defaults to ['GET'] for context
functions, ['POST'] for mutations.

Extended Edge manifest with:
- mutations section: affects, auto_scoped_params, private, route
- render_strategy: "psr" (no user params) or "dynamic_cached" (user-scoped)
- user_scoped: derived from param names matching common identity params
- page_routes: from route= on view-path functions + external view_urls

323 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
28e517e6ee Edge manifest: static JSON for CDN cache invalidation routing
generate_edge_manifest() compiles the decorator registry into a
static JSON artifact that Edge reads at deploy time:

{
  "contexts": {
    "user": {
      "functions": [
        {"name": "user_profile", "path": "rpc"},
        {"name": "profile_page", "path": "view"}
      ],
      "endpoints": ["/api/mizan/ctx/user/"],
      "params": ["user_id"],
      "views": ["/profile/:user_id/"]
    }
  }
}

When Edge receives X-Mizan-Invalidate: user;user_id=5, it:
1. Looks up "user" in the manifest
2. Resolves URL patterns: /profile/:user_id/ → /profile/5/
3. Purges /profile/5/ and /api/mizan/ctx/user/?user_id=5

Features:
- Distinguishes view-path vs RPC-path functions
- Accepts optional view_urls mapping from developer
- Custom base URL support
- Deterministic JSON output (sorted keys)
- Management command: python manage.py export_edge_manifest

314 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
b4c7e783bd Return-type branching: one decorator, two paths
@client now handles both RPC and view functions based on return type:

  @client(affects=UserContext)
  def update_name(request, user_id: int, name: str) -> dict:
      ...  # RPC path: JSON response with invalidate key

  @client(affects=UserContext)
  def update_profile(request, user_id: int) -> HttpResponse:
      ...  # View path: HttpResponse with X-Mizan-Invalidate header

Detection: isinstance(result, HttpResponseBase) after execution.

RPC path (data return):
  - Serialized via Pydantic model_dump()
  - Wrapped in {"result": ..., "invalidate": [...]}
  - Invalidation in JSON body + X-Mizan-Invalidate header

View path (HttpResponse return):
  - Response passed through directly (redirect, HTML, etc.)
  - X-Mizan-Invalidate header added automatically
  - Cache-Control: no-store added
  - No codegen (view_path=True in _meta)
  - Registered in invalidation graph (for Edge manifest)

Auto-scoping works on both paths: if mutation args overlap
with context params, invalidation is scoped automatically.

308 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
89196a02c6 Edge compatibility tests + URL-encode header param values
19 tests that prove Edge caching is possible before Edge exists:

- Deterministic JSON (byte-identical responses for same input)
- Sorted JSON keys for consistent cache keys
- Cache-Control: public on context GETs, no-store on mutations/errors
- Vary: Authorization, Cookie differentiates by auth state
- Auth-dependent responses: same URL, different user → different body
- X-Mizan-Invalidate header round-trip: format → parse → verify
- Header matches JSON body invalidation targets
- Special characters in param values: semicolons, spaces, quotes
  are URL-encoded to prevent delimiter collisions
- Large invalidation sets (20 contexts) serialize and parse correctly
- Concurrent mutations produce independent, correct headers
- Empty invalidation: no affects → no header, no body key
- Param order irrelevant for response determinism

Design decision: param values in X-Mizan-Invalidate are URL-encoded
(percent-encoded). This prevents semicolon collision when values
contain the delimiter character.

301 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
1a4da68f8d Add HTTP integration tests through full Django stack
9 tests that use Django's test Client instead of RequestFactory.
These go through URL routing, middleware (sessions, CSRF, auth),
and real request parsing — proving the protocol works end-to-end:

- Mutation with auto-scoped invalidation (JSON body + header)
- Context fetch with bundled response + CDN headers
- String-to-int query param coercion
- Broad invalidation fallback (no matching args)
- Function-level affects targeting
- 404 for unknown functions and contexts
- Method enforcement (GET-only on /ctx/, POST-only on /call/)

282 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
a91ce78c3a Replace affects_params with three-tier auto-scoping
Remove affects_params lambda. Scoping is now automatic:

Tier 1 - Argument name matching:
  If the mutation's args overlap with the context's params by name,
  the invalidation is auto-scoped. No developer annotation needed.

  @client(context=UserContext)
  def user_profile(request, user_id: int) -> UserShape: ...

  @client(affects=UserContext)
  def update_profile(request, user_id: int, name: str) -> dict: ...
  # user_id matches → invalidate: [{context: "user", params: {user_id: 5}}]

Tier 2 - Auth inference (Edge-side, not implemented in framework)
Tier 3 - Broad fallback when no param names match

Also adds function-level affects targeting:
  @client(affects='user_profile')  # only user_profile, not user_orders
  def update_name(request, user_id: int, name: str) -> dict: ...

Function names resolve to their parent context for param lookup.
v1 runtime refetches the whole context regardless, but the protocol
carries the function-level signal for Edge and future optimization.

273 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
37f3f3d3eb Add affects_params for scoped invalidation
affects_params is a callable that extracts which specific params were
affected by a mutation. The server uses it to produce scoped
invalidation in both transports:

  @client(affects=UserContext, affects_params=lambda req: {'user_id': req.user.pk})
  def update_avatar(request, url: str) -> dict: ...

JSON body: {"result": ..., "invalidate": [{"context": "user", "params": {"user_id": 42}}]}
Header:    X-Mizan-Invalidate: user;user_id=42

Edge reads the scoped params to purge only /profile/42/ instead of
all user profiles. The runtime refetches only the UserContext mounted
with user_id=42, not all UserContext instances.

Requires affects= to be set. Falls back to broad invalidation if
the callable fails.

272 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
8aa20111b4 Add X-Mizan-Invalidate header (second invalidation transport)
Mutation responses now carry invalidation via two transports:

1. JSON body: {"result": ..., "invalidate": ["user"]}
2. HTTP header: X-Mizan-Invalidate: user, notifications

Both are set on every mutation response. The JSON body is consumed
by the client runtime (mizanCall). The header is consumed by Edge
for CDN cache purging and by XHR responses for htmx-style apps.

Header format: comma-separated contexts, semicolon-separated params.
  X-Mizan-Invalidate: user;user_id=5, notifications

Also: _resolve_invalidation and _format_invalidate_header extracted
as reusable helpers for when return-type branching adds HttpResponse
support (view-path mutations will only use the header transport).

Updated ROADMAP.md with full v1 plan including both transports,
return-type branching, affects_params, and Edge manifest.

270 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
f4d7c64e3c Add CDN-ready headers, ROADMAP, fold runtime into mizan-react
CDN headers on context GETs (Edge-ready):
- Cache-Control: public, max-age=0, stale-while-revalidate=300
- Vary: Authorization, Cookie
- Deterministic JSON (sorted keys) for consistent cache keys
- Error responses: Cache-Control: no-store
- Mutation POSTs: Cache-Control: no-store

ROADMAP.md documents v1 deliverables and Mizan Cloud (Edge, Render,
Deploy) as closed-source products built on the open-source protocol.

mizan-runtime folded into mizan-react/src/runtime/ — framework-agnostic
split deferred until a second frontend adapter exists.

268 Django + 33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
3f737132a2 Server-driven invalidation + raw context response format
Mutation responses now include invalidation directives from the server:

  POST /api/mizan/call/
  → {"result": {...}, "invalidate": ["user"]}

The client never hardcodes invalidation targets. The server resolves
affects= metadata and returns what to invalidate. mizan-runtime reads
the invalidate key and triggers refetches automatically.

Context fetch returns raw bundled data (not wrapped):

  GET /api/mizan/ctx/user/?user_id=5
  → {"user_profile": {...}, "user_orders": [...]}

Also fixed QueryDict handling (use .dict() not dict() to avoid
list-wrapped values).

267 Django tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
787f90fd12 Flatten to three packages + extract mizan-runtime
packages/
  mizan-runtime/   Framework-agnostic state engine (~150 lines)
                   Context registry, batched invalidation, fetch primitives
  mizan-django/    Django server adapter (was packages/mizan-rpc/adapters/django/)
                   Codegen moved to mizan-django/generate/
  mizan-react/     React adapter (was packages/mizan-csr/adapters/react/)

Removed premature abstractions: mizan-ast, mizan-schema, mizan-rpc,
mizan-csr, mizan-ssr stub packages. The actual architecture is three
concrete packages, not five abstract layers.

mizan-runtime implements the v1 spec: registerContext with params,
scoped invalidation via microtask batching, server-driven invalidation
from mutation responses, mizanFetch for context bundles, mizanCall for
mutations.

264 Django + 33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
b28ee72c67 Restructure repo into five-package AFI architecture
Mizan is an Application Framework Interface (AFI) with five
independent packages:

  packages/
    mizan-ast/       Language layer (source → KDL schema)
    mizan-schema/    IR layer (KDL schema definition)
    mizan-rpc/       Protocol layer (client gen + server adapters)
      adapters/django/   ← was django/
      generator/         ← was react/src/generator/
    mizan-csr/       State layer (client state engine)
      adapters/react/    ← was react/
    mizan-ssr/       Rendering layer (server-side rendering)

Each package is independent. The adapter directories contain the
framework-specific implementations. Stub packages (ast, schema, ssr)
establish the structure for future work.

264 Django tests + 33 React tests pass from new locations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00