Commit Graph

49 Commits

Author SHA1 Message Date
27c30d7e50 Move allauth + auth UI to legacy/
allauth/ (44 files) is a django-allauth React UI — a separate concern
from the Mizan protocol. Moved to legacy/ pending extraction into a
standalone mizan-django-allauth package.

Also moved to legacy/:
- client/AuthContext.tsx — generic auth state from /me endpoint
- client/RouterContext.tsx — framework-agnostic router adapter
- client/routing.tsx — UserRoute/StaffRoute/AnonymousRoute guards
- client/nextjs.tsx — Next.js router adapter for auth

These are auth UI infrastructure, not Mizan protocol. The Mizan core
only needs JWT for auth header selection (jwt/ stays — MizanProvider
depends on useJWT() to decide between Bearer and session auth).

Cleaned up re-exports in client/react.ts and vitest aliases.

33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:41:22 -04:00
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
c237a6379b Add CLAUDE.md — exhaustive technical reference for the codebase
Documents the three protocols (RPC, Invalidation-on-Mutation,
Frontend-Agnostic Rendering), the full @client decorator API surface
with all parameters and _meta structure, the HMAC cache key derivation
scheme, Redis/Memory backends, the MWT/JWT token systems with secret
separation, the SSR template backend + Bun worker bridge, the Edge
manifest format, and the current codegen gap.

Written from reading every source file, not from memory or prior docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 02:46:13 -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
01d33173a4 Add ReactContext class for type-safe context and affects declarations
ReactContext('user') creates a reusable context marker that provides
proper linting, find-references, and autocomplete:

  UserContext = ReactContext('user')

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

  @client(affects=UserContext)
  def edit_profile(request, name: str) -> dict: ...

  @client(affects=[UserContext, OrderContext])
  def change_plan(request) -> dict: ...

- ReactContext class with name validation
- GlobalContext built-in instance for context='global'
- affects= accepts ReactContext, lists, strings, or function refs
- Backwards compat: raw strings still work for context= and affects=
- Exported from mizan and mizan.client

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
af7e22ffc1 Rewrite codegen for named contexts, mutation hooks, and Mizan naming
Generator (react/src/generator/lib/mizan.mjs):
- Rename djarea.mjs → mizan.mjs
- MizanContext replaces DjangoContext (legacy alias kept)
- Global contexts fetched via bundled GET /ctx/global/ through
  inner GlobalContextLoader component
- Named context providers generated per context group with param
  elevation (required/optional props from x-mizan-contexts)
- Mutation hooks auto-invalidate affected contexts on success
- SSR hydration uses single GET /ctx/global/ instead of N POSTs
- Output files: generated.provider.tsx, generated.server.ts

Runtime (react/src/context.tsx):
- Add setContextData() for bundle splitting without refetch
- Add request() for auth-transparent HTTP from generated code

Index generator updated for new export names and named contexts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
3523f2e3fe Add named contexts, bundled fetch endpoint, and affects invalidation
Phase 1 (Named Contexts):
- @client(context=) accepts any string, not just 'global'/'local'
- context='local' emits deprecation warning
- Registry groups functions by context name (get_context_groups)
- GET /api/mizan/ctx/<name>/ bundles all context functions in one response
- Schema export includes x-mizan-contexts with param elevation metadata

Phase 2 (Affects):
- @client(affects=) declares mutation invalidation targets
- Accepts context name strings, function refs, or lists
- Mutually exclusive with context=
- Exported in x-mizan-functions schema for codegen

React runtime:
- MizanContextValue gains invalidateContext, invalidateFunctions,
  registerContextProvider, and baseUrl
- Named context providers register for invalidation on mount

259 Django tests pass, 33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
f3c225ef49 Move Playwright, Docker, and package.json into examples/django-react-site
Root directory now contains only the two core packages (django/, react/),
examples/, and top-level docs. All e2e/integration test infrastructure
lives in examples/django-react-site/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
eee352d908 Move desktop and e2e into examples/ directory
- desktop/ → examples/django-react-desktop-app/
- e2e/ → examples/django-react-site/
- example/ → examples/django-react-site/backend/
- Update Dockerfile.test, Makefile, playwright config, and
  django.config.mjs path references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
c866142770 Rename djarea to mizan and fix React casing conventions
Rename the package from djarea to mizan across the entire codebase —
Python package, React library, generators, tests, and examples. Fix
JSX/hook casing (MizanProvider, useMizan, etc.) that broke when the
original PascalCase names were lowercased during the rename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:31 -04:00
bf837e598b Add Mizan architecture plan
Named contexts, param elevation, affects-based invalidation,
ReactContext classes for read/write coupling. This is the evolution
roadmap from Djarea to Mizan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:41:13 -04:00
70c817c2be Update README.md 2026-03-31 18:23:33 +00:00
5cd0223670 Update README.md 2026-03-31 17:53:06 +00:00
f0ab9fc575 Update README.md 2026-03-31 17:52:21 +00:00
d97cee12ee Update README.md 2026-03-31 17:51:43 +00:00
e8cf00fe0f Update README.md 2026-03-31 17:15:06 +00:00
51ed2b28c5 Fix self-referential shapes (CategoryShape)
Set field-only _spec before building the full spec with nested shapes.
Self-references resolve because cls._spec already exists when the
generator encounters shape._spec where shape is cls.

Also: pass cls into get_type_hints localns so forward ref strings
like list["CategoryShape"] resolve to the class being defined.

49 shapes tests, 0 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:04:15 -04:00
625d8cf9b9 Fix Optional[Shape] unwrapping and add comprehensive shapes stress tests
_extract_shape_class now handles `Shape | None` (Union types) by checking
isinstance(hint, types.UnionType) and iterating args for Shape subclasses.
This fixes nullable FK detection — any `editor: AuthorShape | None` field
is now correctly recognized as a nested shape.

48 stress tests covering:
- 5-level deep nesting (Publisher → Author → Book → Chapter → Section)
- Two FKs to same model (author + editor)
- Slug PK (Tag), UUID PK (Section)
- M2M relationships (Book.tags)
- Nullable FKs returning None
- Empty strings, zero integers, false booleans (truthiness traps)
- 100-record smoke test
- Query efficiency (assertNumQueries)
- All diff operations with deep nesting

Known gap documented: self-referential forward refs (CategoryShape)
crash get_type_hints() at __init_subclass__ time. Needs deferred resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:56:11 -04:00
5a56d7a4a5 Update shapes tests for pk abstraction, strict Diff, and diff_many
13 new tests covering three changes from claude.ai:
- pk abstraction: _pk_field resolved from model._meta, _get_pk helper
- Strict Diff.__getattr__: typos raise AttributeError with valid names,
  nested() method raises KeyError for explicit access
- diff_many: batched query (assertNumQueries(1)), mixed new/existing,
  empty list, all-new, nonexistent raises

38 shapes tests total, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:25:27 -04:00
9516c05f75 Add shapes tests and fix django-readers as core dependency
25 tests covering Shape, Diff, and NestedDiff:
- Shape metaclass: model resolution, field extraction, nested detection, spec/pair building
- Query: list, filter, nested relations, empty results, Pydantic serialization
- Diff (new): detects all fields as changed
- Diff (existing): no changes, single field, multiple fields, nonexistent ID
- Diff (nested): created, updated, deleted, combined, nonexistent relation

Fixes:
- django-readers moved from optional to core dependency
- Shape import lazy-loaded via __getattr__ (django_readers imports
  contenttypes which can't happen during apps.populate())
- Added Author, Book, Tag test models

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 02:16:36 -04:00
a726fd6863 Add shapes module: Pydantic API surface for Django models
Imported from separate development branch. Provides Shape, Diff, and
NestedDiff classes for defining typed Pydantic schemas backed by Django
model querysets via django-readers.

Optional dependency: install with djarea[shapes] to get django-readers.
Import is guarded so the rest of djarea works without it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 01:53:20 -04:00
558c7c6d6c Update documentation to reflect Djarea's RPC architecture
- Root README: full quick start, architecture diagram, feature table,
  code generation workflow, error handling, forms, channels, testing
- Django README: setup, auth variations, contexts, forms, channels
- React README: clarify that generated hooks are the API (not library
  primitives), DjangoContext is the only provider needed, sub-exports
  are internals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 01:22:32 -04:00
4451ec24a1 Full test infrastructure, code audit fixes, and real E2E integration tests
Test infrastructure:
- Django standalone test runner (pytest-django, test settings, EmailUser model)
- React unit tests via Vitest with jsdom, jest compat layer, path aliases
- Playwright E2E tests using generated hooks in a real Chromium browser
- Docker Compose test backend (Django + Redis) for integration testing
- Desktop integration test app (PyWebView + Django + uvicorn)
- Makefile with test/test-django/test-react/test-integration targets

Library bugs found and fixed:
- hasJWT truthiness: undefined !== null was true, skipping session init
- process.env crash: CSR client referenced process.env in non-Node browsers
- baseUrl not forwarded: DjareaProvider didn't pass baseUrl to CSR client
- Relative URL handling: new URL() failed with relative base paths
- call() race condition: HTTP requests fired before CSRF cookie was set
- Session init await: added sessionRef promise so call() waits for session
- path_prefix on schema export: both export commands failed with URL reverse
- NullBooleanField removed: referenced field doesn't exist in Django 5.0+
- lru_cache on JWT settings: get_settings() now cached as intended
- Channel message routing: broadcasts now include channel name and params
- httpFunctionCall: fixed URL and request body format

Generator fixes:
- Removed 1,100 lines of REST/OpenAPI client generation (not part of Djarea)
- Generator now works for djarea-only projects without django-ninja REST APIs
- Generated DjangoContext now includes ChannelProvider when channels exist
- Fixed env var passthrough for schema export commands
- Deduplicated fetch logic into single runDjangoCommand helper

Test quality:
- Fixed 33 tautological Django tests with real assertions
- Found hidden bug: benchmark functions were never registered
- Found hidden bug: unicode lookalike test used plain ASCII
- Deleted worthless React unit tests (duplicates, shape checks, Zod-tests-Zod)
- Replaced jsdom integration tests with Playwright browser tests

Example apps:
- example/: Integration test backend with 33 server functions, 5 forms,
  4 channels covering auth variations, contexts, class-based ServerFunction,
  error codes, DjareaFormMixin, formsets, and JWT
- desktop/: PyWebView desktop app with file system access, SQLite CRUD,
  system introspection, and 39 real HTTP integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 01:17:48 -04:00