AFI parity: close all 35 gaps — every adapter wires every AFI-common capability

The conformance board (tests/afi/test_capability_parity.py) is now fully green:
90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The
gaps the prose table used to launder as "Django-only" / "out of scope" are
wired, against the pinned-spec model (single-authored spec, byte-identical
conformance across languages) — never per-language reimplementation.

FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest),
WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic
SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes
(SQLAlchemy projection, same declaration surface as django-readers), Forms
(Pydantic schema/validate/submit).

Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth=
enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC
subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT
mint+verify and cache-key derivation byte-pinned to the Python reference
(cache_keys_pin, token_pin, invalidate_header_pin).

TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS
backend can feed the codegen — the largest gap), multipart upload, session-init,
WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms.

Verified in the merged tree: core 25, fastapi 74, django 353/21-skip,
mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 13:44:35 -04:00
parent 58d2cb2848
commit 6c5f6f1fba
81 changed files with 9893 additions and 463 deletions

View File

@@ -0,0 +1,409 @@
/**
* KDL emitter — byte-equivalent to `cores/mizan-python/src/mizan_core/ir.py`.
*
* The Python emitter is the spec; this is a second implementation under the
* same contract. `buildIr()` walks the registry, resolves the canonical named
* types each function references (`_collect_named_types`), and emits KDL the
* Rust codegen consumes. Any divergence is a bug here, not a contract change —
* `tests/ir.test.ts` pins byte-equality against the live Python `build_ir()`.
*/
import { getAllFunctions, getContextGroups, getFunction } from '../registry'
import type { RegistryEntry } from '../types'
import type { DefaultValue, NamedType, Primitive, StructField, TypeShape } from './types'
const INDENT = ' '
// ─── KDL value formatting (mirrors ir.py `_kdl_*`) ────────────────────────────
function kdlString(s: string): string {
const escaped = s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
return `"${escaped}"`
}
function kdlBool(b: boolean): string {
return b ? '#true' : '#false'
}
function kdlDefault(v: DefaultValue): string {
switch (v.kind) {
case 'null':
return '#null'
case 'boolean':
return kdlBool(v.value)
case 'integer':
return String(v.value)
case 'number':
// Match Python's repr(float): whole-number floats render as "1.0".
return Number.isInteger(v.value) ? `${v.value}.0` : String(v.value)
case 'string':
return kdlString(v.value)
}
}
/** snake_case → camelCase. Matches ir.py `_snake_to_camel`. */
export function snakeToCamel(name: string): string {
const parts = name.replace(/\./g, '_').replace(/-/g, '_').split('_')
return parts[0] + parts.slice(1).filter(Boolean).map(p => p[0].toUpperCase() + p.slice(1)).join('')
}
function primitiveName(p: Primitive): string {
return p
}
// ─── Emitter ──────────────────────────────────────────────────────────────
class Emitter {
lines: string[] = []
private prefix(indent: number): string {
return INDENT.repeat(indent)
}
leaf(indent: number, ...parts: string[]): void {
this.lines.push(this.prefix(indent) + parts.join(' '))
}
open(indent: number, ...parts: string[]): void {
this.lines.push(this.prefix(indent) + parts.join(' ') + ' {')
}
close(indent: number): void {
this.lines.push(this.prefix(indent) + '}')
}
blank(): void {
this.lines.push('')
}
emitTypeChild(indent: number, shape: TypeShape): void {
switch (shape.kind) {
case 'primitive':
this.leaf(indent, 'primitive', kdlString(primitiveName(shape.primitive)))
return
case 'ref':
this.leaf(indent, 'ref', kdlString(shape.name))
return
case 'list':
this.open(indent, 'list')
this.emitTypeChild(indent + 1, shape.inner)
this.close(indent)
return
case 'optional':
this.open(indent, 'optional')
this.emitTypeChild(indent + 1, shape.inner)
this.close(indent)
return
case 'enum':
this.leaf(indent, 'enum', ...shape.variants.map(kdlString))
return
case 'union':
this.open(indent, 'union')
for (const b of shape.branches) this.emitTypeChild(indent + 1, b)
this.close(indent)
return
case 'upload':
this.emitUpload(indent, shape)
return
}
}
private emitUpload(indent: number, shape: Extract<TypeShape, { kind: 'upload' }>): void {
const props: string[] = []
if (shape.maxSize !== undefined) props.push(`max-size=${shape.maxSize}`)
if (shape.contentTypes && shape.contentTypes.length > 0) {
this.open(indent, 'upload', ...props)
for (const ct of shape.contentTypes) this.leaf(indent + 1, 'content-type', kdlString(ct))
this.close(indent)
} else {
this.leaf(indent, 'upload', ...props)
}
}
emitNamedType(indent: number, name: string, body: NamedType): void {
this.open(indent, 'type', kdlString(name))
if (body.kind === 'struct') {
this.open(indent + 1, 'struct')
for (const field of body.fields) this.emitStructField(indent + 2, field)
this.close(indent + 1)
} else if (body.kind === 'alias') {
this.open(indent + 1, 'alias')
this.emitTypeChild(indent + 2, body.inner)
this.close(indent + 1)
} else {
this.leaf(indent + 1, 'enum', ...body.variants.map(kdlString))
}
this.close(indent)
}
emitStructField(indent: number, field: StructField): void {
const header: string[] = ['field', kdlString(field.name)]
if (!field.required) {
header.push(`required=${kdlBool(false)}`)
if (field.default !== undefined) header.push(`default=${kdlDefault(field.default)}`)
}
this.open(indent, ...header)
this.emitTypeChild(indent + 1, field.shape)
this.close(indent)
}
intoString(): string {
const lines = [...this.lines]
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
return lines.join('\n') + '\n'
}
}
// ─── Named-type collection (mirrors ir.py `_collect_named_types`) ─────────────
/** Strip Optional[T] → [inner, isOptional]. */
function stripOptional(shape: TypeShape): [TypeShape, boolean] {
if (shape.kind === 'optional') return [shape.inner, true]
return [shape, false]
}
/** list element type, or null. */
function listElement(shape: TypeShape): TypeShape | null {
if (shape.kind === 'list') return shape.inner
return null
}
/** All ref names reachable inside a shape. */
function refsIn(shape: TypeShape): string[] {
switch (shape.kind) {
case 'ref':
return [shape.name]
case 'list':
case 'optional':
return refsIn(shape.inner)
case 'union':
return shape.branches.flatMap(refsIn)
default:
return []
}
}
/** All ref names a NamedType body references. */
function refsInBody(body: NamedType): string[] {
if (body.kind === 'struct') return body.fields.flatMap(f => refsIn(f.shape))
if (body.kind === 'alias') return refsIn(body.inner)
return []
}
interface FnTypeInfo {
schema: import('./types').IrSchema
camel: string
}
/**
* First pass: collect every named type the IR's `function` section references,
* keyed by emitted name. Two kinds, exactly as `_collect_named_types`:
* - structs visited anywhere in input/output traversal (under their ref name,
* and under the canonical `<camel>Input` / `<camel>Output` rename)
* - output wrapper aliases (`<camel>Output = list[T]` / primitive / renamed
* model) so the consumer has one named type to reference.
*/
function collectNamedTypes(fns: Map<string, FnTypeInfo>): Record<string, NamedType> {
const seen: Record<string, NamedType> = {}
function visitModel(name: string, types: Record<string, NamedType>): void {
if (name in seen) return
const body = types[name]
if (body === undefined) {
throw new Error(
`IR schema references type "${name}" but no definition was provided in the function's \`types\`.`,
)
}
seen[name] = body
for (const ref of refsInBody(body)) visitModel(ref, types)
}
function visitShape(shape: TypeShape, types: Record<string, NamedType>): void {
for (const ref of refsIn(shape)) visitModel(ref, types)
}
for (const { schema, camel } of fns.values()) {
const types = schema.types ?? {}
// Input — named `<camel>Input`, emitted as a struct.
if (schema.input && schema.input.length > 0) {
const inputName = `${camel}Input`
if (!(inputName in seen)) seen[inputName] = { kind: 'struct', fields: schema.input }
// Visit nested refs in the input fields.
for (const field of schema.input) visitShape(field.shape, types)
}
// Output.
if (schema.output === undefined) continue
const outputName = `${camel}Output`
const [inner] = stripOptional(schema.output)
const elem = listElement(inner)
if (elem !== null) {
// list[T] (possibly Optional) — list alias. Visit element type.
visitShape(schema.output, types)
if (!(outputName in seen)) seen[outputName] = { kind: 'alias', inner: schema.output }
} else if (inner.kind === 'ref') {
// <Model> or Optional[<Model>] — emit the model under the canonical
// output name (rename). Python renames the Pydantic model to
// `<camel>Output`; we emit the referenced struct under that name.
const refName = inner.name
const body = types[refName]
if (body === undefined) {
throw new Error(
`IR schema output references type "${refName}" but no definition was provided in the function's \`types\`.`,
)
}
if (body.kind === 'struct') {
// Emit the struct under the canonical output name (the rename),
// and visit its nested refs.
if (!(outputName in seen)) {
seen[outputName] = body
for (const ref of refsInBody(body)) visitModel(ref, types)
}
} else {
// Non-struct named type referenced as output — emit under its
// own name plus a canonical alias.
visitModel(refName, types)
if (!(outputName in seen)) seen[outputName] = { kind: 'alias', inner: schema.output }
}
} else {
// Primitive-wrapped output (`result: int`) — alias.
if (!(outputName in seen)) seen[outputName] = { kind: 'alias', inner: schema.output }
}
}
return seen
}
// ─── Function / context emission ──────────────────────────────────────────
function resolveOutput(entry: RegistryEntry): { name: string; nullable: boolean } {
const camel = snakeToCamel(entry.name)
const canonical = `${camel}Output`
const schema = entry.ir
if (!schema || schema.output === undefined) return { name: canonical, nullable: false }
const [, nullable] = stripOptional(schema.output)
return { name: canonical, nullable }
}
function emitFunction(em: Emitter, entry: RegistryEntry): void {
const camel = snakeToCamel(entry.name)
const schema = entry.ir ?? {}
const hasInput = !!(schema.input && schema.input.length > 0)
const { name: outputName, nullable } = resolveOutput(entry)
em.open(0, 'function', kdlString(entry.name))
em.leaf(1, 'camel', kdlString(camel))
em.leaf(1, 'has-input', kdlBool(hasInput))
if (hasInput) em.leaf(1, 'input', kdlString(`${camel}Input`))
em.leaf(1, 'output', kdlString(outputName))
if (nullable) em.leaf(1, 'output-nullable', kdlBool(true))
em.leaf(1, 'transport', kdlString(entry.websocket ? 'websocket' : 'http'))
if (entry.context) em.leaf(1, 'context', kdlString(entry.context))
// Only context-typed affects make it into the KDL (matches ir.py).
for (const a of entry.affects ?? []) {
if (a.type === 'context') em.leaf(1, 'affects', kdlString(a.name))
}
for (const m of entry.merge ?? []) em.leaf(1, 'merge', kdlString(m))
if (entry.form) {
em.leaf(1, 'is-form', kdlBool(true))
if (entry.formName) em.leaf(1, 'form-name', kdlString(entry.formName))
if (entry.formRole) em.leaf(1, 'form-role', kdlString(entry.formRole))
}
em.close(0)
}
function annotationToPrimitive(shape: TypeShape | undefined): Primitive {
if (shape === undefined) return 'string'
const [inner] = stripOptional(shape)
if (inner.kind === 'primitive') return inner.primitive
return 'string'
}
function emitContext(em: Emitter, ctxName: string, fnNames: string[]): void {
// Collect param info across every function in the context.
interface Slot {
type: Primitive
sharedBy: string[]
}
const paramInfo = new Map<string, Slot>()
for (const fnName of fnNames) {
const entry = getFunction(fnName)
if (!entry) continue
const input = entry.ir?.input
if (!input || input.length === 0) continue
for (const field of input) {
let slot = paramInfo.get(field.name)
if (!slot) {
slot = { type: 'string', sharedBy: [] }
paramInfo.set(field.name, slot)
}
slot.type = annotationToPrimitive(field.shape)
slot.sharedBy.push(fnName)
}
}
em.open(0, 'context', kdlString(ctxName))
// Members alphabetical — canonical order.
for (const fnName of [...fnNames].sort()) em.leaf(1, 'function', kdlString(fnName))
for (const paramName of [...paramInfo.keys()].sort()) {
const slot = paramInfo.get(paramName)!
const required = slot.sharedBy.length === fnNames.length
em.open(1, 'param', kdlString(paramName))
em.leaf(2, 'type', kdlString(slot.type))
em.leaf(2, 'required', kdlBool(required))
for (const sharer of [...slot.sharedBy].sort()) em.leaf(2, 'shared-by', kdlString(sharer))
em.close(1)
}
em.close(0)
}
// ─── Top-level builder ──────────────────────────────────────────────────────
/**
* Build the Mizan IR (KDL) for every registered function. Byte-equivalent to
* the Python `build_ir()` against the same registry.
*
* `private` and view-path functions are excluded from the function section,
* matching ir.py.
*/
export function buildIr(): string {
const functions = getAllFunctions()
const contextGroups = getContextGroups()
// Functions contributing to the type/function sections (skip private + view).
const typeFns = new Map<string, FnTypeInfo>()
const emitFns: RegistryEntry[] = []
for (const [name, entry] of functions) {
if (entry.private || entry.viewPath) continue
typeFns.set(name, { schema: entry.ir ?? {}, camel: snakeToCamel(name) })
emitFns.push(entry)
}
const namedTypes = collectNamedTypes(typeFns)
const em = new Emitter()
// Types — alphabetical by name (canonical IR ordering).
const typeNames = Object.keys(namedTypes).sort()
for (const typeName of typeNames) em.emitNamedType(0, typeName, namedTypes[typeName])
if (typeNames.length > 0) em.blank()
// Functions — alphabetical by wire name.
emitFns.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
for (const entry of emitFns) emitFunction(em, entry)
if (emitFns.length > 0) em.blank()
// Contexts — alphabetical by name.
const ctxNames = Object.keys(contextGroups).sort()
for (const ctxName of ctxNames) emitContext(em, ctxName, contextGroups[ctxName])
if (ctxNames.length > 0) em.blank()
return em.intoString()
}

View File

@@ -0,0 +1,17 @@
/**
* Mizan IR (KDL) — the codegen contract.
*
* `buildIr()` emits KDL byte-identical to the Python `build_ir()` against the
* same registry. This is what lets a TypeScript backend feed
* `protocol/mizan-codegen`.
*/
export { buildIr, snakeToCamel } from './build'
export type {
Primitive,
TypeShape,
DefaultValue,
StructField,
NamedType,
IrSchema,
} from './types'

View File

@@ -0,0 +1,70 @@
/**
* IR data model — mirrors `cores/mizan-python/src/mizan_core/ir.py` and
* `cores/mizan-rust/src/ir.rs` 1:1.
*
* The IR is the contract. Backends emit it; the codegen consumes it. The
* TypeScript side produces byte-equivalent KDL to the Python emitter against
* the same function registry.
*
* TypeScript has no Pydantic to introspect, so the `@client` decorator carries
* an explicit IR type schema (input fields + output shape). That schema is the
* binding: a TS backend declares its IR types, and `buildIr()` emits the KDL
* the codegen reads — exactly as the Rust adapter declares typed `StructField`
* / `TypeShape` registrations.
*/
export type Primitive = 'integer' | 'number' | 'boolean' | 'string'
/**
* An in-place type shape — referenced from struct fields, function
* inputs/outputs, and alias bodies.
*/
export type TypeShape =
| { kind: 'primitive'; primitive: Primitive }
| { kind: 'ref'; name: string }
| { kind: 'list'; inner: TypeShape }
| { kind: 'optional'; inner: TypeShape }
| { kind: 'enum'; variants: string[] }
| { kind: 'union'; branches: TypeShape[] }
| { kind: 'upload'; maxSize?: number; contentTypes?: string[] }
export type DefaultValue =
| { kind: 'integer'; value: number }
| { kind: 'number'; value: number }
| { kind: 'boolean'; value: boolean }
| { kind: 'string'; value: string }
| { kind: 'null' }
export interface StructField {
name: string
required: boolean
default?: DefaultValue
shape: TypeShape
}
/** A named type that appears in the IR's `type "<Name>" { ... }` section. */
export type NamedType =
| { kind: 'struct'; fields: StructField[] }
| { kind: 'alias'; inner: TypeShape }
| { kind: 'enum'; variants: string[] }
/**
* The IR type schema a `@client` function carries.
*
* `input` is the ordered list of input fields (already excluding the implicit
* request/identity arg). When absent or empty, the function `has-input #false`.
*
* `output` is the function's return shape: a `ref` to a named struct, a `list`,
* an `optional`, or a `primitive`. The emitter derives the canonical
* `<camel>Input` / `<camel>Output` names and the struct-vs-alias split exactly
* as `_collect_named_types` does.
*
* `types` resolves every `ref` used in `input`/`output` (and transitively) to
* its `NamedType` definition — Python gets this from Pydantic model
* introspection; TS declares it explicitly.
*/
export interface IrSchema {
input?: StructField[]
output?: TypeShape
types?: Record<string, NamedType>
}