Skip to content

Commit c2d0664

Browse files
revert(scaffold): remove wrong-place adapter emission (standards#89 sub-issue 1) (#23)
The unified transaction-gated adapter belongs to the boj-server cartridge (boj-server/cartridges/<name>-mcp/adapter/), not to the new -iser repo. The pilot k9iser confirms this: k9iser has no adapter; its adapter lives at boj-server/cartridges/k9iser-mcp/adapter/k9iser_adapter.zig. iseriser PR#12 emitted adapter/<name>_adapter.zig into the new -iser repo where it had no FFI module to import (`@import("<name>_ffi")` against nothing), no build.zig, and no cartridge.json. The emitted dispatch was a stub that discarded tool+body, only two of four advertised protocols were routed, and a buffer-aliasing bug between dispatchRest and the HTTP response writer would have corrupted output if any FFI were attached. Surgical revert: - Drop generate_unified_adapter (213 lines). - Drop the call site and its doc-comment marker. - Update test threshold 22 → 21 (base + regen workflow only). - Drop the adapter-existence assertion in test_files_written_to_disk. - Update the CI/CD generator header to document why the adapter is not emitted here and where it belongs (cartridge-skeleton codegen is tracked under standards#89 Phase 2b). Keeps generate_regen_workflow unchanged — .github/workflows/<name>-regen.yml correctly belongs in the -iser repo (it triggers the cartridge in boj-server). Verification: cargo build clean; cargo test 49 unit + 9 integration green. Refs hyperpolymath/standards#90 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a644b03 commit c2d0664

1 file changed

Lines changed: 12 additions & 222 deletions

File tree

src/codegen/scaffold.rs

Lines changed: 12 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ pub fn scaffold_repo(manifest: &Manifest, output_dir: &Path) -> ScaffoldResult {
5858
files.push(generate_ci_workflow(&model, &iser_name));
5959
// Regeneration trigger (boj-server cartridge pattern, standards#89 sub-issue 1)
6060
files.push(generate_regen_workflow(&iser_name));
61-
// Unified transaction-gated adapter (single loopback port, SSE, standards#89 sub-issue 1)
62-
files.push(generate_unified_adapter(&model, &iser_name));
6361

6462
// Documentation and governance
6563
files.push(generate_readme(manifest, &model, &iser_name));
@@ -792,8 +790,13 @@ fn test_codegen_output() {{
792790
// ---------------------------------------------------------------------------
793791

794792
// ---------------------------------------------------------------------------
795-
// File generators — CI/CD (regeneration trigger + unified adapter)
793+
// File generators — CI/CD (regeneration trigger)
796794
// ---------------------------------------------------------------------------
795+
//
796+
// The unified transaction-gated adapter belongs to the boj-server cartridge
797+
// for this -iser (e.g. boj-server/cartridges/<name>-mcp/adapter/), NOT to the
798+
// -iser repo itself. Scaffolding the cartridge skeleton is tracked
799+
// separately under standards#89 Phase 2b — see iseriser ROADMAP.
797800

798801
/// Generate `.github/workflows/<iser_name>-regen.yml` — the central-trigger
799802
/// workflow that fires the boj-server cartridge to regenerate `generated/*`
@@ -852,220 +855,6 @@ jobs:
852855
}
853856
}
854857

855-
/// Generate `adapter/<iser_name>_adapter.zig` — the unified transaction-gated
856-
/// BoJ adapter (single loopback listener, SSE + REST + GraphQL + gRPC-compat,
857-
/// one gated dispatch → one Zig ABI).
858-
///
859-
/// Replaces the ssg-era 3-parallel-port adapter estate-wide as of standards#89
860-
/// sub-issue 1 (pilot: k9iser-mcp, boj-server#73). Internal-only per ADR-0004:
861-
/// the public surface is the http-capability-gateway (tier-2).
862-
fn generate_unified_adapter(model: &LanguageModel, iser_name: &str) -> GeneratedFile {
863-
let content = format!(
864-
r#"// SPDX-License-Identifier: PMPL-1.0-or-later
865-
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
866-
//
867-
// {iser_name}/adapter/{iser_name}_adapter.zig
868-
//
869-
// INTERNAL-ONLY unified transaction-gated adapter. Per ADR-0004 the only
870-
// governed public surface is the http-capability-gateway (tier-2) in front;
871-
// cartridge adapters bind loopback and sit behind it.
872-
//
873-
// One listener, one port, protocol-routed:
874-
// POST /invoke → REST (JSON in/out)
875-
// POST /sse → SSE (text/event-stream)
876-
// POST /graphql → GraphQL (op parsed from body)
877-
// POST /grpc/<Svc>/<Mthd> → gRPC-compat (tool = method)
878-
//
879-
// Every request passes the transaction gate (exposureGate, mirroring the
880-
// Idris2 `exposureSatisfied` contract) BEFORE dispatch. No request reaches
881-
// the ABI ungated — estate interface-safety policy.
882-
//
883-
// Generated by iseriser for {lang_name} (standards#89 sub-issue 1).
884-
// Pattern source: k9iser-mcp, boj-server#73.
885-
886-
const std = @import("std");
887-
const ffi = @import("{iser_name}_ffi");
888-
889-
// Loopback-only by construction: fronted by http-capability-gateway (ADR-0004).
890-
const BIND_IP = [4]u8{{ 127, 0, 0, 1 }};
891-
const PORT: u16 = 9390; // Override in cartridge manifest if port conflicts.
892-
893-
// ── Transaction gate (mirrors Idris2 {module_name}.exposureSatisfied) ──────
894-
//
895-
// Encoding: 0=Public 1=Authenticated 2=Internal.
896-
const Exposure = enum(u8) {{ public = 0, authenticated = 1, internal = 2 }};
897-
898-
// Default: require Public (for unauthenticated cartridges).
899-
// Set to .authenticated for credential-bearing cartridges.
900-
const REQUIRED_EXPOSURE: Exposure = .public;
901-
902-
/// Zig mirror of Idris2 `exposureSatisfied`. Cross-checked by the truth-table
903-
/// test below; the Idris2 module is the source-of-truth contract.
904-
fn exposureSatisfied(required: Exposure, presented: Exposure, is_local: bool) bool {{
905-
if (is_local) return true;
906-
return switch (required) {{
907-
.public => true,
908-
.authenticated => presented == .authenticated or presented == .internal,
909-
.internal => presented == .internal,
910-
}};
911-
}}
912-
913-
/// Parse the `X-Trust-Level` header the gateway/sidecar injects.
914-
fn presentedExposure(req: []const u8) Exposure {{
915-
const val = headerValue(req, "x-trust-level") orelse return .public;
916-
if (eqIgnoreCase(val, "internal")) return .internal;
917-
if (eqIgnoreCase(val, "authenticated")) return .authenticated;
918-
return .public;
919-
}}
920-
921-
// ── Request routing ──────────────────────────────────────────────────────────
922-
923-
fn dispatchTool(tool: []const u8, body: []const u8, resp: []u8) struct {{ status: u16, body: []u8 }} {{
924-
_ = body;
925-
// Route to the {iser_name}_ffi ABI symbol. Extend as tools are added.
926-
_ = tool;
927-
const msg = std.fmt.bufPrint(resp, "{{{{\"tool\":\"{iser_name}\",\"status\":\"dispatched\"}}}}", .{{}}) catch resp[0..0];
928-
return .{{ .status = 200, .body = msg }};
929-
}}
930-
931-
fn dispatchRest(path: []const u8, body: []const u8, is_local: bool, req: []const u8, resp: []u8) struct {{ status: u16, body: []u8 }} {{
932-
const presented = presentedExposure(req);
933-
if (!exposureSatisfied(REQUIRED_EXPOSURE, presented, is_local)) {{
934-
const msg = std.fmt.bufPrint(resp, "{{{{\"error\":\"forbidden\"}}}}", .{{}}) catch resp[0..0];
935-
return .{{ .status = 403, .body = msg }};
936-
}}
937-
const prefix = "/invoke";
938-
if (std.mem.startsWith(u8, path, prefix)) {{
939-
// Extract tool name from JSON body field "tool".
940-
const tool = jsonField(body, "tool") orelse "unknown";
941-
return dispatchTool(tool, body, resp);
942-
}}
943-
const msg = std.fmt.bufPrint(resp, "{{{{\"error\":\"not found\"}}}}", .{{}}) catch resp[0..0];
944-
return .{{ .status = 404, .body = msg }};
945-
}}
946-
947-
fn dispatchSse(body: []const u8, is_local: bool, req: []const u8, stream: std.net.Stream) void {{
948-
const presented = presentedExposure(req);
949-
if (!exposureSatisfied(REQUIRED_EXPOSURE, presented, is_local)) {{
950-
_ = stream.write("HTTP/1.1 403 Forbidden\r\n\r\n") catch {{}};
951-
return;
952-
}}
953-
const tool = jsonField(body, "tool") orelse "unknown";
954-
_ = stream.write("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\n\r\n") catch {{}};
955-
var buf: [256]u8 = undefined;
956-
const event = std.fmt.bufPrint(&buf, "data: {{{{\"tool\":\"{iser_name}\",\"dispatched\":\"{{}}\"}}}}\n\n", .{{tool}}) catch &buf;
957-
_ = stream.write(event) catch {{}};
958-
}}
959-
960-
// ── TCP listener ─────────────────────────────────────────────────────────────
961-
962-
fn handleConnection(stream: std.net.Stream) void {{
963-
defer stream.close();
964-
var buf: [4096]u8 = undefined;
965-
const n = stream.read(&buf) catch return;
966-
const req = buf[0..n];
967-
968-
const is_local = true; // All connections via loopback are locally trusted.
969-
970-
const path_start = std.mem.indexOf(u8, req, " ") orelse return;
971-
const path_end = std.mem.indexOf(u8, req[path_start + 1 ..], " ") orelse return;
972-
const path = req[path_start + 1 .. path_start + 1 + path_end];
973-
974-
var resp_buf: [2048]u8 = undefined;
975-
976-
if (std.mem.startsWith(u8, path, "/sse")) {{
977-
const body = extractBody(req);
978-
dispatchSse(body, is_local, req, stream);
979-
}} else {{
980-
const body = extractBody(req);
981-
const result = dispatchRest(path, body, is_local, req, &resp_buf);
982-
const http = std.fmt.bufPrint(&resp_buf, "HTTP/1.1 {{}} OK\r\nContent-Type: application/json\r\n\r\n{{s}}", .{{ result.status, result.body }}) catch &resp_buf;
983-
_ = stream.write(http) catch {{}};
984-
}}
985-
}}
986-
987-
pub fn main() !void {{
988-
const addr = std.net.Address.initIp4(BIND_IP, PORT);
989-
var server = try addr.listen(.{{ .reuse_address = true }});
990-
defer server.deinit();
991-
std.log.info("{iser_name} unified adapter listening on 127.0.0.1:{{}} (loopback-only, ADR-0004)", .{{PORT}});
992-
while (true) {{
993-
const conn = server.accept() catch continue;
994-
const t = std.Thread.spawn(.{{}}, handleConnection, .{{conn.stream}}) catch continue;
995-
t.detach();
996-
}}
997-
}}
998-
999-
// ── Helpers ──────────────────────────────────────────────────────────────────
1000-
1001-
fn headerValue(req: []const u8, name: []const u8) ?[]const u8 {{
1002-
var lines = std.mem.splitScalar(u8, req, '\n');
1003-
while (lines.next()) |line| {{
1004-
if (eqIgnoreCase(line[0..@min(name.len, line.len)], name)) {{
1005-
const col = std.mem.indexOf(u8, line, ":") orelse continue;
1006-
return std.mem.trim(u8, line[col + 1 ..], &std.ascii.whitespace);
1007-
}}
1008-
}}
1009-
return null;
1010-
}}
1011-
1012-
fn eqIgnoreCase(a: []const u8, b: []const u8) bool {{
1013-
if (a.len != b.len) return false;
1014-
for (a, b) |ca, cb| {{
1015-
if (std.ascii.toLower(ca) != std.ascii.toLower(cb)) return false;
1016-
}}
1017-
return true;
1018-
}}
1019-
1020-
fn jsonField(body: []const u8, key: []const u8) ?[]const u8 {{
1021-
var search_buf: [64]u8 = undefined;
1022-
const needle = std.fmt.bufPrint(&search_buf, "\"{{}}\": \"", .{{key}}) catch return null;
1023-
const start = std.mem.indexOf(u8, body, needle) orelse return null;
1024-
const val_start = start + needle.len;
1025-
const val_end = std.mem.indexOf(u8, body[val_start..], "\"") orelse return null;
1026-
return body[val_start .. val_start + val_end];
1027-
}}
1028-
1029-
fn extractBody(req: []const u8) []const u8 {{
1030-
const sep = "\r\n\r\n";
1031-
const pos = std.mem.indexOf(u8, req, sep) orelse return req;
1032-
return req[pos + sep.len ..];
1033-
}}
1034-
1035-
// ── Tests ────────────────────────────────────────────────────────────────────
1036-
1037-
test "exposureSatisfied: loopback always passes" {{
1038-
try std.testing.expect(exposureSatisfied(.internal, .public, true));
1039-
try std.testing.expect(exposureSatisfied(.authenticated, .public, true));
1040-
}}
1041-
1042-
test "exposureSatisfied: public gate passes all" {{
1043-
try std.testing.expect(exposureSatisfied(.public, .public, false));
1044-
try std.testing.expect(exposureSatisfied(.public, .authenticated, false));
1045-
try std.testing.expect(exposureSatisfied(.public, .internal, false));
1046-
}}
1047-
1048-
test "exposureSatisfied: authenticated gate rejects public" {{
1049-
try std.testing.expect(!exposureSatisfied(.authenticated, .public, false));
1050-
try std.testing.expect(exposureSatisfied(.authenticated, .authenticated, false));
1051-
try std.testing.expect(exposureSatisfied(.authenticated, .internal, false));
1052-
}}
1053-
1054-
test "exposureSatisfied: internal gate rejects public and authenticated" {{
1055-
try std.testing.expect(!exposureSatisfied(.internal, .public, false));
1056-
try std.testing.expect(!exposureSatisfied(.internal, .authenticated, false));
1057-
try std.testing.expect(exposureSatisfied(.internal, .internal, false));
1058-
}}
1059-
"#,
1060-
iser_name = iser_name,
1061-
lang_name = model.name,
1062-
module_name = idris2_module_name(model),
1063-
);
1064-
GeneratedFile {
1065-
path: PathBuf::from(format!("adapter/{iser_name}_adapter.zig")),
1066-
content,
1067-
}
1068-
}
1069858

1070859
/// Generate `.github/workflows/ci.yml` for the new -iser.
1071860
fn generate_ci_workflow(_model: &LanguageModel, iser_name: &str) -> GeneratedFile {
@@ -1380,10 +1169,11 @@ description = "Chapel distributed computing -iser"
13801169
);
13811170
let repo = result.repo().unwrap();
13821171
assert_eq!(repo.name, "chapeliser");
1383-
// 22+ files: 20 base + regen workflow + unified adapter (standards#89 sub-issue 1)
1172+
// 21+ files: 20 base + regen workflow (standards#89 sub-issue 1).
1173+
// The unified adapter belongs to the boj-server cartridge, not this repo.
13841174
assert!(
1385-
repo.file_count() >= 22,
1386-
"expected 22+ files, got {}",
1175+
repo.file_count() >= 21,
1176+
"expected 21+ files, got {}",
13871177
repo.file_count()
13881178
);
13891179
}
@@ -1429,9 +1219,9 @@ description = "Chapel distributed computing -iser"
14291219
assert!(repo_root.join("src/interface/abi/Types.idr").exists());
14301220
assert!(repo_root.join("ffi/zig/src/main.zig").exists());
14311221
assert!(repo_root.join(".github/workflows/ci.yml").exists());
1432-
// standards#89 sub-issue 1: regen trigger + unified adapter
1222+
// standards#89 sub-issue 1: regen trigger only.
1223+
// The unified adapter belongs to the boj-server cartridge, not this repo.
14331224
assert!(repo_root.join(".github/workflows/chapeliser-regen.yml").exists());
1434-
assert!(repo_root.join("adapter/chapeliser_adapter.zig").exists());
14351225
assert!(repo_root.join("README.adoc").exists());
14361226
assert!(repo_root.join("LICENSE").exists());
14371227
}

0 commit comments

Comments
 (0)