Move codegen out of mizan-django: protocol/mizan-generate/

The codegen consumes a schema from any backend and emits typed client
code for any frontend — it doesn't belong inside a backend adapter.
That placement was historical sediment from when there was only a
Django backend; it predates the AFI generalization.

New top-level slot: `protocol/` for protocol-level tooling. Tree is
now:

  backends/    server protocol adapters
  frontends/   client kernel + per-framework adapters
  cores/       shared language-level primitives
  protocol/    protocol-level tooling
  workers/     runtime workers / bridges

Codegen moves to `protocol/mizan-generate/`. Same file layout under
`generator/` (cli.mjs, lib/), preserved via git mv.

Package metadata cleaned up:
- name: "generate" (placeholder) → "mizan-generate"
- description filled in
- type: module (cli.mjs is .mjs ESM, was previously declared "commonjs")
- bin entry added so `npx mizan-generate --config <config.mjs>` works
  once the package is published, instead of `node path/to/cli.mjs`.

Path-reference fixups:
- backends/mizan-django/README.md: `node path/to/...` → `npx mizan-generate`
- backends/mizan-fastapi/README.md: same
- ISSUES.md: file paths in three issue entries
- CLAUDE.md: codegen description + Package Layout section refreshed
  (added protocol/, mizan-fastapi entry, mizan-python entry)
- docs/AFI_ARCHITECTURE.md: Package Layout refreshed identically

Verified codegen runs from new location: regenerated the FastAPI
example harness's api/ output, identical to pre-move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 00:16:11 -04:00
parent f0f7a93ed2
commit cc887fb1f6
16 changed files with 34 additions and 29 deletions

View File

@@ -0,0 +1,117 @@
/**
* Schema Fetching — dispatches on the backend type configured in
* `source.django` or `source.fastapi`.
*
* Both flavors spawn a Python subprocess that prints schema JSON to stdout:
* Django: `python manage.py export_mizan_schema --indent 0`
* FastAPI: `python -m mizan_fastapi.cli <module>`
*/
import { spawn } from 'child_process'
import path from 'path'
function runSubprocess(cmd, args, opts) {
const { cwd, env, label } = opts
return new Promise((resolve, reject) => {
const proc = spawn(cmd, args, {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
shell: process.platform === 'win32',
env,
})
let stdout = ''
let stderr = ''
proc.stdout.on('data', (data) => { stdout += data.toString() })
proc.stderr.on('data', (data) => { stderr += data.toString() })
proc.on('close', (code) => {
if (code !== 0) {
reject(new Error(`${label} command failed (exit ${code}):\n${stderr}`))
return
}
const jsonStart = stdout.indexOf('{')
if (jsonStart === -1) {
reject(new Error(`No JSON found in ${label} output:\n${stdout}\n${stderr}`))
return
}
try {
resolve(JSON.parse(stdout.slice(jsonStart)))
} catch (err) {
reject(new Error(`Failed to parse JSON from ${label}:\n${err.message}\n${stdout}`))
}
})
proc.on('error', (err) => {
reject(new Error(`Failed to spawn ${label} command: ${err.message}`))
})
})
}
function runDjangoCommand(source, cwd, command) {
const managePath = path.resolve(cwd, source.django.managePath)
const manageDir = path.dirname(managePath)
let cmd, args
if (source.django.command) {
cmd = source.django.command[0]
args = [...source.django.command.slice(1), 'manage.py', command, '--indent', '0']
} else {
const python = source.django.python || 'python'
cmd = python
args = [managePath, command, '--indent', '0']
}
const env = source.django.env ? { ...process.env, ...source.django.env } : undefined
return runSubprocess(cmd, args, { cwd: manageDir, env, label: 'Django' })
}
function runFastapiSchemaCommand(source, cwd) {
const fastapiCwd = source.fastapi.cwd
? path.resolve(cwd, source.fastapi.cwd)
: cwd
let cmd, args
if (source.fastapi.command) {
cmd = source.fastapi.command[0]
args = [...source.fastapi.command.slice(1), '-m', 'mizan_fastapi.cli', source.fastapi.module]
} else {
cmd = source.fastapi.python || 'python'
args = ['-m', 'mizan_fastapi.cli', source.fastapi.module]
}
const env = source.fastapi.env ? { ...process.env, ...source.fastapi.env } : undefined
return runSubprocess(cmd, args, { cwd: fastapiCwd, env, label: 'FastAPI' })
}
/**
* Fetch channels schema. Channels are a Django-only feature; FastAPI
* projects use native WebSockets and don't go through this path.
*/
export async function fetchChannelsSchema(source, cwd) {
if (!source.django) {
throw new Error('Channels schema export requires django source configuration')
}
return runDjangoCommand(source, cwd, 'export_channels_schema')
}
/**
* Fetch mizan schema. Dispatches on whichever backend source is configured.
*/
export async function fetchMizanSchema(source, cwd) {
if (source.fastapi) {
return runFastapiSchemaCommand(source, cwd)
}
if (source.django) {
return runDjangoCommand(source, cwd, 'export_mizan_schema')
}
throw new Error('mizan schema export requires source.django or source.fastapi')
}