From 86b4c221e5730b3eaeeb63d421c363bf06b992d9 Mon Sep 17 00:00:00 2001
From: tlkahn <joshgy@gmail.com>
Date: Sun, 5 Jul 2026 23:14:06 +0700
Subject: [PATCH 1/4] feat: project scaffold with module graph and CLI stub
 (#2)

- build.zig.zon: package manifest (Zig >= 0.16.0)
- build.zig: 7-module graph with compile-enforced dependency direction
  core (leaf), mlxbridge (leaf), chat->core, model->core+mlxbridge,
  engine->core+mlxbridge+model (NOT chat), registry->core,
  http->core+chat+model+engine+registry
- src/cli/main.zig: stub subcommand dispatch (serve/run/pull/list)
- Per-module test stubs under zig build test
- Empty subdirs with .gitkeep for future expansion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 build.zig                      | 130 +++++++++++++++++++++++++++++++++
 build.zig.zon                  |  11 +++
 src/chat/root.zig              |   5 ++
 src/cli/cmd/.gitkeep           |   0
 src/cli/main.zig               |  59 +++++++++++++++
 src/core/root.zig              |   1 +
 src/engine/constrain/.gitkeep  |   0
 src/engine/root.zig            |   7 ++
 src/engine/specdecode/.gitkeep |   0
 src/http/handlers/.gitkeep     |   0
 src/http/root.zig              |   9 +++
 src/mlx/root.zig               |   1 +
 src/model/root.zig             |   6 ++
 src/registry/root.zig          |   5 ++
 tests/fixtures/.gitkeep        |   0
 15 files changed, 234 insertions(+)
 create mode 100644 build.zig
 create mode 100644 build.zig.zon
 create mode 100644 src/chat/root.zig
 create mode 100644 src/cli/cmd/.gitkeep
 create mode 100644 src/cli/main.zig
 create mode 100644 src/core/root.zig
 create mode 100644 src/engine/constrain/.gitkeep
 create mode 100644 src/engine/root.zig
 create mode 100644 src/engine/specdecode/.gitkeep
 create mode 100644 src/http/handlers/.gitkeep
 create mode 100644 src/http/root.zig
 create mode 100644 src/mlx/root.zig
 create mode 100644 src/model/root.zig
 create mode 100644 src/registry/root.zig
 create mode 100644 tests/fixtures/.gitkeep

diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..c770f35
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,130 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+    const target = b.standardTargetOptions(.{});
+    const optimize = b.standardOptimizeOption(.{});
+
+    // -- Module graph (compile-enforced) --
+    // Zig's module import table makes undeclared @import a compile error,
+    // so the dependency direction declared here is the only one that compiles.
+
+    // Leaf modules (no dependencies)
+    const core_mod = b.createModule(.{
+        .root_source_file = b.path("src/core/root.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    const mlxbridge_mod = b.createModule(.{
+        .root_source_file = b.path("src/mlx/root.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    // chat -> core
+    const chat_mod = b.createModule(.{
+        .root_source_file = b.path("src/chat/root.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "core", .module = core_mod },
+        },
+    });
+
+    // model -> core, mlxbridge
+    const model_mod = b.createModule(.{
+        .root_source_file = b.path("src/model/root.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "core", .module = core_mod },
+            .{ .name = "mlxbridge", .module = mlxbridge_mod },
+        },
+    });
+
+    // engine -> core, mlxbridge, model (NOT chat)
+    const engine_mod = b.createModule(.{
+        .root_source_file = b.path("src/engine/root.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "core", .module = core_mod },
+            .{ .name = "mlxbridge", .module = mlxbridge_mod },
+            .{ .name = "model", .module = model_mod },
+        },
+    });
+
+    // registry -> core (no mlx)
+    const registry_mod = b.createModule(.{
+        .root_source_file = b.path("src/registry/root.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "core", .module = core_mod },
+        },
+    });
+
+    // http -> core, chat, model, engine, registry
+    const http_mod = b.createModule(.{
+        .root_source_file = b.path("src/http/root.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "core", .module = core_mod },
+            .{ .name = "chat", .module = chat_mod },
+            .{ .name = "model", .module = model_mod },
+            .{ .name = "engine", .module = engine_mod },
+            .{ .name = "registry", .module = registry_mod },
+        },
+    });
+
+    // -- Executable --
+    const exe_mod = b.createModule(.{
+        .root_source_file = b.path("src/cli/main.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "core", .module = core_mod },
+            .{ .name = "mlxbridge", .module = mlxbridge_mod },
+            .{ .name = "chat", .module = chat_mod },
+            .{ .name = "model", .module = model_mod },
+            .{ .name = "engine", .module = engine_mod },
+            .{ .name = "registry", .module = registry_mod },
+            .{ .name = "http", .module = http_mod },
+        },
+    });
+
+    const exe = b.addExecutable(.{
+        .name = "mlxd",
+        .root_module = exe_mod,
+    });
+    b.installArtifact(exe);
+
+    // -- Run step --
+    const run_cmd = b.addRunArtifact(exe);
+    run_cmd.step.dependOn(b.getInstallStep());
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+    const run_step = b.step("run", "Run mlxd");
+    run_step.dependOn(&run_cmd.step);
+
+    // -- Test step (per-module) --
+    const test_step = b.step("test", "Run all module tests");
+
+    const test_modules = [_]*std.Build.Module{
+        core_mod,
+        mlxbridge_mod,
+        chat_mod,
+        model_mod,
+        engine_mod,
+        registry_mod,
+        http_mod,
+    };
+
+    for (test_modules) |mod| {
+        const t = b.addTest(.{ .root_module = mod });
+        const run_t = b.addRunArtifact(t);
+        test_step.dependOn(&run_t.step);
+    }
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..16d89db
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,11 @@
+.{
+    .name = .mlxd,
+    .version = "0.1.0",
+    .minimum_zig_version = "0.16.0",
+    .fingerprint = 0x4fc94390cee4169,
+    .paths = .{
+        "build.zig",
+        "build.zig.zon",
+        "src",
+    },
+}
diff --git a/src/chat/root.zig b/src/chat/root.zig
new file mode 100644
index 0000000..7084984
--- /dev/null
+++ b/src/chat/root.zig
@@ -0,0 +1,5 @@
+comptime {
+    _ = @import("core");
+}
+
+test "chat compiles" {}
diff --git a/src/cli/cmd/.gitkeep b/src/cli/cmd/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/cli/main.zig b/src/cli/main.zig
new file mode 100644
index 0000000..850f028
--- /dev/null
+++ b/src/cli/main.zig
@@ -0,0 +1,59 @@
+const std = @import("std");
+
+comptime {
+    _ = @import("core");
+    _ = @import("mlxbridge");
+    _ = @import("chat");
+    _ = @import("model");
+    _ = @import("engine");
+    _ = @import("registry");
+    _ = @import("http");
+}
+
+const usage =
+    \\Usage: mlxd <command>
+    \\
+    \\Commands:
+    \\  serve   Start OpenAI-compatible HTTP server
+    \\  run     One-shot or interactive text generation
+    \\  pull    Download model from HuggingFace
+    \\  list    List locally available models
+    \\
+;
+
+fn writeAll(fd: std.posix.fd_t, bytes: []const u8) void {
+    var written: usize = 0;
+    while (written < bytes.len) {
+        const rc = std.posix.system.write(fd, bytes[written..].ptr, bytes[written..].len);
+        const signed: isize = @bitCast(rc);
+        if (signed < 0) return;
+        written += @intCast(signed);
+    }
+}
+
+pub fn main(init: std.process.Init.Minimal) u8 {
+    var args = std.process.Args.Iterator.init(init.args);
+    _ = args.next();
+
+    const cmd = args.next() orelse {
+        writeAll(std.posix.STDERR_FILENO, usage);
+        return 1;
+    };
+
+    const commands = .{
+        .{ "serve", "mlxd serve: not yet implemented\n" },
+        .{ "run", "mlxd run: not yet implemented\n" },
+        .{ "pull", "mlxd pull: not yet implemented\n" },
+        .{ "list", "mlxd list: not yet implemented\n" },
+    };
+
+    inline for (commands) |entry| {
+        if (std.mem.eql(u8, cmd, entry[0])) {
+            writeAll(std.posix.STDOUT_FILENO, entry[1]);
+            return 0;
+        }
+    }
+
+    writeAll(std.posix.STDERR_FILENO, usage);
+    return 1;
+}
diff --git a/src/core/root.zig b/src/core/root.zig
new file mode 100644
index 0000000..685a648
--- /dev/null
+++ b/src/core/root.zig
@@ -0,0 +1 @@
+test "core compiles" {}
diff --git a/src/engine/constrain/.gitkeep b/src/engine/constrain/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/engine/root.zig b/src/engine/root.zig
new file mode 100644
index 0000000..d71b7db
--- /dev/null
+++ b/src/engine/root.zig
@@ -0,0 +1,7 @@
+comptime {
+    _ = @import("core");
+    _ = @import("mlxbridge");
+    _ = @import("model");
+}
+
+test "engine compiles" {}
diff --git a/src/engine/specdecode/.gitkeep b/src/engine/specdecode/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/http/handlers/.gitkeep b/src/http/handlers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/http/root.zig b/src/http/root.zig
new file mode 100644
index 0000000..a8988c9
--- /dev/null
+++ b/src/http/root.zig
@@ -0,0 +1,9 @@
+comptime {
+    _ = @import("core");
+    _ = @import("chat");
+    _ = @import("model");
+    _ = @import("engine");
+    _ = @import("registry");
+}
+
+test "http compiles" {}
diff --git a/src/mlx/root.zig b/src/mlx/root.zig
new file mode 100644
index 0000000..8f11058
--- /dev/null
+++ b/src/mlx/root.zig
@@ -0,0 +1 @@
+test "mlxbridge compiles" {}
diff --git a/src/model/root.zig b/src/model/root.zig
new file mode 100644
index 0000000..dc5b615
--- /dev/null
+++ b/src/model/root.zig
@@ -0,0 +1,6 @@
+comptime {
+    _ = @import("core");
+    _ = @import("mlxbridge");
+}
+
+test "model compiles" {}
diff --git a/src/registry/root.zig b/src/registry/root.zig
new file mode 100644
index 0000000..4dbf2a9
--- /dev/null
+++ b/src/registry/root.zig
@@ -0,0 +1,5 @@
+comptime {
+    _ = @import("core");
+}
+
+test "registry compiles" {}
diff --git a/tests/fixtures/.gitkeep b/tests/fixtures/.gitkeep
new file mode 100644
index 0000000..e69de29

From 3d353fe40d47863ebca2edf367efbc25be762f6f Mon Sep 17 00:00:00 2001
From: tlkahn <joshgy@gmail.com>
Date: Sun, 5 Jul 2026 23:41:32 +0700
Subject: [PATCH 2/4] fix(cli): honest exit codes and write-error handling in
 main.zig

Unimplemented subcommands now write their stub message to stderr and
exit 1 instead of claiming success. writeAll surfaces write failures
via error.WriteFailed with errno-based handling (EINTR retried), and
every caller already returns 1, so a failed write can never mask a
non-zero exit. The duplicated command-name/message pairs collapse into
a single name list with a runtime loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qUaL3Er38EYQCRoExKwzA
---
 src/cli/main.zig | 31 +++++++++++++++----------------
 1 file changed, 15 insertions(+), 16 deletions(-)

diff --git a/src/cli/main.zig b/src/cli/main.zig
index 850f028..ea40eb2 100644
--- a/src/cli/main.zig
+++ b/src/cli/main.zig
@@ -21,13 +21,15 @@ const usage =
     \\
 ;
 
-fn writeAll(fd: std.posix.fd_t, bytes: []const u8) void {
+fn writeAll(fd: std.posix.fd_t, bytes: []const u8) error{WriteFailed}!void {
     var written: usize = 0;
     while (written < bytes.len) {
         const rc = std.posix.system.write(fd, bytes[written..].ptr, bytes[written..].len);
-        const signed: isize = @bitCast(rc);
-        if (signed < 0) return;
-        written += @intCast(signed);
+        switch (std.posix.errno(rc)) {
+            .SUCCESS => written += @intCast(@as(isize, @bitCast(rc))),
+            .INTR => continue,
+            else => return error.WriteFailed,
+        }
     }
 }
 
@@ -36,24 +38,21 @@ pub fn main(init: std.process.Init.Minimal) u8 {
     _ = args.next();
 
     const cmd = args.next() orelse {
-        writeAll(std.posix.STDERR_FILENO, usage);
+        writeAll(std.posix.STDERR_FILENO, usage) catch {};
         return 1;
     };
 
-    const commands = .{
-        .{ "serve", "mlxd serve: not yet implemented\n" },
-        .{ "run", "mlxd run: not yet implemented\n" },
-        .{ "pull", "mlxd pull: not yet implemented\n" },
-        .{ "list", "mlxd list: not yet implemented\n" },
-    };
+    const commands = [_][]const u8{ "serve", "run", "pull", "list" };
 
-    inline for (commands) |entry| {
-        if (std.mem.eql(u8, cmd, entry[0])) {
-            writeAll(std.posix.STDOUT_FILENO, entry[1]);
-            return 0;
+    for (commands) |name| {
+        if (std.mem.eql(u8, cmd, name)) {
+            writeAll(std.posix.STDERR_FILENO, "mlxd ") catch {};
+            writeAll(std.posix.STDERR_FILENO, name) catch {};
+            writeAll(std.posix.STDERR_FILENO, ": not yet implemented\n") catch {};
+            return 1;
         }
     }
 
-    writeAll(std.posix.STDERR_FILENO, usage);
+    writeAll(std.posix.STDERR_FILENO, usage) catch {};
     return 1;
 }

From 1f207eced00014a5ed550736e77c7c19bfad39a0 Mon Sep 17 00:00:00 2001
From: tlkahn <joshgy@gmail.com>
Date: Sun, 5 Jul 2026 23:42:09 +0700
Subject: [PATCH 3/4] build: run cli tests, add -Dgpu-tests option and itest
 step

The cli root module joins the test_modules loop so its inline tests
run under 'zig build test'. A shared build_options module carries the
new -Dgpu-tests flag so Metal-dependent tests can gate themselves via
'if (!build_options.gpu_tests) return error.SkipZigTest'. The 'itest'
step documented in CLAUDE.md now exists, backed by a stub
tests/itest.zig that skips without -Dmodel and fails honestly with
IntegrationTestsNotYetImplemented when one is given.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qUaL3Er38EYQCRoExKwzA
---
 build.zig       | 27 +++++++++++++++++++++++++++
 tests/itest.zig |  8 ++++++++
 2 files changed, 35 insertions(+)
 create mode 100644 tests/itest.zig

diff --git a/build.zig b/build.zig
index c770f35..2cf884f 100644
--- a/build.zig
+++ b/build.zig
@@ -4,6 +4,9 @@ pub fn build(b: *std.Build) void {
     const target = b.standardTargetOptions(.{});
     const optimize = b.standardOptimizeOption(.{});
 
+    const gpu_tests = b.option(bool, "gpu-tests", "Include Metal-dependent tests") orelse false;
+    const model_path = b.option([]const u8, "model", "Model path for integration tests");
+
     // -- Module graph (compile-enforced) --
     // Zig's module import table makes undeclared @import a compile error,
     // so the dependency direction declared here is the only one that compiles.
@@ -112,6 +115,10 @@ pub fn build(b: *std.Build) void {
     // -- Test step (per-module) --
     const test_step = b.step("test", "Run all module tests");
 
+    const build_opts = b.addOptions();
+    build_opts.addOption(bool, "gpu_tests", gpu_tests);
+    const build_opts_mod = build_opts.createModule();
+
     const test_modules = [_]*std.Build.Module{
         core_mod,
         mlxbridge_mod,
@@ -120,11 +127,31 @@ pub fn build(b: *std.Build) void {
         engine_mod,
         registry_mod,
         http_mod,
+        exe_mod,
     };
 
     for (test_modules) |mod| {
+        mod.addImport("build_options", build_opts_mod);
         const t = b.addTest(.{ .root_module = mod });
         const run_t = b.addRunArtifact(t);
         test_step.dependOn(&run_t.step);
     }
+
+    // -- Integration test step --
+    const itest_opts = b.addOptions();
+    itest_opts.addOption(?[]const u8, "model_path", model_path);
+
+    const itest_mod = b.createModule(.{
+        .root_source_file = b.path("tests/itest.zig"),
+        .target = target,
+        .optimize = optimize,
+        .imports = &.{
+            .{ .name = "build_options", .module = itest_opts.createModule() },
+        },
+    });
+
+    const itest = b.addTest(.{ .root_module = itest_mod });
+    const run_itest = b.addRunArtifact(itest);
+    const itest_step = b.step("itest", "Run integration tests against a real model");
+    itest_step.dependOn(&run_itest.step);
 }
diff --git a/tests/itest.zig b/tests/itest.zig
new file mode 100644
index 0000000..63e1d22
--- /dev/null
+++ b/tests/itest.zig
@@ -0,0 +1,8 @@
+const std = @import("std");
+const build_options = @import("build_options");
+
+test "integration: serve smoke test" {
+    if (build_options.model_path == null) return error.SkipZigTest;
+    // Real itest (spawn `mlxd serve`, drive via std.http.Client) not yet implemented.
+    return error.IntegrationTestsNotYetImplemented;
+}

From 26251cb399961cb0f9b3dad7a7bbae13c5135f58 Mon Sep 17 00:00:00 2001
From: tlkahn <joshgy@gmail.com>
Date: Sun, 5 Jul 2026 23:42:27 +0700
Subject: [PATCH 4/4] refactor: rename src/mlx to src/mlxbridge to match module
 name

All seven modules now map their @import name to src/<name>/root.zig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qUaL3Er38EYQCRoExKwzA
---
 build.zig                       | 2 +-
 src/{mlx => mlxbridge}/root.zig | 0
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename src/{mlx => mlxbridge}/root.zig (100%)

diff --git a/build.zig b/build.zig
index 2cf884f..33f8383 100644
--- a/build.zig
+++ b/build.zig
@@ -19,7 +19,7 @@ pub fn build(b: *std.Build) void {
     });
 
     const mlxbridge_mod = b.createModule(.{
-        .root_source_file = b.path("src/mlx/root.zig"),
+        .root_source_file = b.path("src/mlxbridge/root.zig"),
         .target = target,
         .optimize = optimize,
     });
diff --git a/src/mlx/root.zig b/src/mlxbridge/root.zig
similarity index 100%
rename from src/mlx/root.zig
rename to src/mlxbridge/root.zig
