mizan-webview-transport + webview-channels: VSCode webview as Mizan frontend

Two new frontend packages let a VSCode webview consume a Mizan backend
through its postMessage channel — peer transports to `mizan-tauri-transport`
and the default `httpTransport()`.

- `@mizan/webview-transport` implements `MizanTransport` (call/fetch)
  over postMessage with correlation-id pairing. Drop-in for `configure({
  transport: webviewTransport() })`; codegen output and React adapter
  are unchanged.

- `@mizan/webview-channels` mirrors mizan-react's WebSocket-based
  ChannelConnection — RPC + subscribe over the same postMessage channel
  for long-running ops where short request/reply isn't enough.

Both expect an extension-host-side dispatcher that reads envelopes via
`webview.onDidReceiveMessage` and routes them through mizan-ts's
`handleMutationCall` / `handleContextFetch`. First consumer is the
holomorphic VSCode extension.

mizan-codegen: new `[source.script]` generic source. Spawns an arbitrary
command and reads stdout as KDL IR. Keeps mizan-codegen out of the
business of knowing every possible backend language while preserving
the "subprocess emits KDL" contract every other source already follows.
Holomorphic uses it to invoke `python -m holomorphic.emit_ir` against
the mizan_core registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 21:51:12 -04:00
parent 22dcf0e3c1
commit a5ef93b879
6 changed files with 473 additions and 2 deletions

View File

@@ -69,6 +69,16 @@ pub struct SourceConfig {
/// usage (no Pydantic) just omits the sub-block.
#[serde(default)]
pub rust: Option<RustSource>,
/// `[source.script]` — generic source. Spawn an arbitrary command and
/// read its stdout as KDL IR. Use when none of the language-specific
/// sources fit — e.g. a Python module that walks `mizan_core.registry`
/// for a non-Django/non-FastAPI consumer, or a custom IR emitter.
/// Keeps mizan-codegen out of the business of knowing every possible
/// backend language while preserving the "subprocess emits KDL"
/// contract every other source already follows.
#[serde(default)]
pub script: Option<ScriptSource>,
}
@@ -206,6 +216,35 @@ fn default_pydantic_derives() -> Vec<String> {
}
/// `[source.script]` — generic stdout-of-arbitrary-command source.
///
/// Spawns `command` with `args`, reads its stdout, and parses it as KDL
/// Mizan IR. The same contract every other source follows; this one just
/// doesn't bake in any language-specific assumptions.
///
/// Example:
///
/// ```toml
/// [source.script]
/// command = ["uv", "run", "python", "-m", "holomorphic.emit_ir"]
/// ```
#[derive(Debug, Deserialize)]
pub struct ScriptSource {
/// Full command vector. First entry is the program; rest are argv.
/// Must be non-empty.
pub command: Vec<String>,
/// Working directory for the subprocess, relative to the codegen
/// config directory. Defaults to the config directory itself.
#[serde(default)]
pub cwd: Option<PathBuf>,
/// Environment overrides.
#[serde(default)]
pub env: BTreeMap<String, String>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum RustKernelSpec {

View File

@@ -21,7 +21,7 @@ use std::process::{Command, Stdio};
use anyhow::{anyhow, Context, Result};
use serde_json::json;
use crate::config::{Config, DjangoSource, FastapiSource, PydanticPreStep, RustSource};
use crate::config::{Config, DjangoSource, FastapiSource, PydanticPreStep, RustSource, ScriptSource};
use crate::ir::{parse_ir, MizanIR};
@@ -43,9 +43,11 @@ pub fn fetch_schema(config: &Config, config_dir: &Path) -> Result<MizanIR> {
run_fastapi(fa, config_dir)?
} else if let Some(dj) = &config.source.django {
run_django(dj, config_dir)?
} else if let Some(sc) = &config.source.script {
run_script(sc, config_dir)?
} else {
return Err(anyhow!(
"config.source must declare one of [source.rust], [source.fastapi], or [source.django]"
"config.source must declare one of [source.rust], [source.fastapi], [source.django], or [source.script]"
));
};
@@ -70,6 +72,18 @@ fn run_fastapi(src: &FastapiSource, config_dir: &Path) -> Result<String> {
}
fn run_script(src: &ScriptSource, config_dir: &Path) -> Result<String> {
let cwd = match &src.cwd {
Some(rel) => config_dir.join(rel),
None => config_dir.to_path_buf(),
};
let (program, args) = src.command.split_first().ok_or_else(|| {
anyhow!("[source.script]: command must be non-empty")
})?;
run_subprocess(program, args, &cwd, &src.env, "script IR export")
}
fn run_django(src: &DjangoSource, config_dir: &Path) -> Result<String> {
let manage_path = config_dir.join(&src.manage_path);
let manage_dir = manage_path