Skip to content

Commit 2c7186f

Browse files
hyperpolymathclaude
andcommitted
feat(ffi): scaffold and compile remaining 12 cartridges — FFI wiring complete
All 111 non-JS-only cartridges now have a compiled .so and canonical ffi block in cartridge.json. This commit adds the final 12 that were missing build.zig: Group A (had stub Zig source, replaced with clean ADR-0006 impl): academic-workflow-mcp, bofig-mcp, ephapax-mcp, fireflag-mcp, hesiod-mcp, sanctify-mcp Group B (had only README.adoc in ffi/, written from scratch): claude-agents-power-mcp, coderag-mcp, local-memory-mcp, notifyhub-mcp, opendatamcp, origenemcp Each cartridge receives: - *_ffi.zig: 5-symbol ADR-0006 ABI with stub tool dispatch table - build.zig: canonical dynamic-library target (x86_64-linux-gnu) - cartridge_shim.zig: copy of shared null-guard/tool-compare helper - cartridge.json ffi block: so_path=ffi/zig-out/lib/lib<module>.so, abi_version=ADR-0006, 5-symbol list All 12 build clean (zig build ReleaseFast), all 12 pass boj-invoke probe. 165 Elixir tests remain green (0 failures). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c6f53ad commit 2c7186f

49 files changed

Lines changed: 3193 additions & 796 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.machine_readable/6a2/STATE.a2ml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ milestones = [
4141

4242
[quality]
4343
cartridges-total = 112 # all 112 have cartridge.json + mod.js as of 2026-04-25
44-
cartridges-with-zig-ffi = 1 # boj-health only — the Zig .so reference implementation
45-
cartridges-with-js-mod = 111 # mod.js dispatch via Deno worker pool
44+
cartridges-with-zig-ffi = 111 # all cartridges with ffi key + compiled .so (model-router-mcp intentionally excluded)
45+
cartridges-with-js-mod = 111 # mod.js dispatch via Deno worker pool (model-router-mcp is canonical JS-only)
4646
cartridges-with-both = 0 # no cartridge uses both paths
4747
exunit-tests = 165 # 10 properties + 155 regular tests across 9 files; CRG C threshold met 2026-04-25
4848
zig-ffi-chain-verified = true # boj-invoke → dlopen → libboj_health.so end-to-end tested 2026-04-25
@@ -72,6 +72,7 @@ test-coverage = "CLOSED 2026-04-25 — 165 ExUnit tests; CRG C met"
7272

7373
[session-history]
7474
entries = [
75+
{ date = "2026-04-25", description = "FFI wiring complete — all 111 cartridges now have compiled .so + ffi key in cartridge.json. Scaffolded 12 remaining cartridges (6 with existing stub Zig source replaced with clean ADR-0006 impl: academic-workflow-mcp, bofig-mcp, ephapax-mcp, fireflag-mcp, hesiod-mcp, sanctify-mcp; 6 written from scratch: claude-agents-power-mcp, coderag-mcp, local-memory-mcp, notifyhub-mcp, opendatamcp, origenemcp). Each: *_ffi.zig (5-symbol ADR-0006 ABI + tool stubs) + build.zig + cartridge_shim.zig + canonical ffi block in cartridge.json with so_path=ffi/zig-out/lib/lib<module>.so. All 12 build clean, all 12 pass boj-invoke probe. 165 Elixir tests still 0 failures." },
7576
{ date = "2026-04-25", description = "CRG D→C test expansion: (1) Added invoker_test.exs (15 tests — cli_path resolution, exit-code classification via fake_boj_invoke fixture, successful probe/name/version/invoke, extra_env). (2) Added js_worker_pool_test.exs (9 tests — pool_size, consistent-hash routing, fallback, E2E). (3) Added catalog_properties_test.exs (10 StreamData properties — round-trips, structural invariants, CredentialDecryptor properties). (4) Added contract_test.exs (15 contract tests — Catalog↔Router, NodeKey↔CredentialDecryptor, Router↔Invoker dispatch). (5) Added aspect_test.exs (15 aspect tests — no-crash, content-type, security, idempotency). (6) Added benchmarks/boj_bench.exs (10 Benchee scenarios). (7) Extended catalog_test.exs + router_test.exs + node_key_test.exs + credential_decryptor_test.exs + js_invoker_test.exs with 80+ additional tests. (8) Fixed bofig-mcp, hesiod-mcp, academic-workflow-mcp cartridge.json (missing spdx/domain/tier/auth/so_path fields). Total: 165 tests (10 properties + 155 regular), 0 failures. Grade D→C achieved." },
7677
{ date = "2026-04-25", description = "v1.1.0 + v1.0.1 doc sweep: (1) ExUnit test suite 50 tests across 5 modules. (2) StreamData property test dep added. (3) BojRest.JsWorkerPool + JsWorker (persistent Deno pool) implemented; Router now dispatches via pool. (4) PolicyLoader.load_from_boj_catalog/1 in http-capability-gateway — 12 tests. (5) compose.dev.yaml updated: capability-gateway sidecar (port 7800 → boj-rest:7700, BOJ_CARTRIDGES_ROOT catalog mode). (6) gateway-policy.yaml retired (static policy superseded by catalog mode). (7) 4 cartridges fixed (academic-workflow, ephapax, fireflag, sanctify — missing domain/tier/auth/ffi fields). (8) Full doc sweep: TOPOLOGY.md, ROADMAP.adoc, EXPLAINME.adoc, QUICKSTART-USER.adoc, llm-warmup-dev.md — all V-lang refs removed, counts 97/99/106 → 112. (9) TEST-NEEDS.md rewritten as honest CRG D gap analysis." },
7778
{ date = "2026-04-25", description = "Earlier session: (1) Fixed Zig ElfDynLib bug — boj-invoke now targets x86_64-linux-gnu, forcing DlDynLib/real dlopen. (2) boj-health Zig FFI cartridge fully implemented and tested. (3) Elixir REST server wired: Containerfile rewritten (3-stage Zig+Elixir build), compose.dev.yaml created. (4) All 112 cartridges now have mod.js. (5) model-router-mcp gets cartridge.json. (6) JS dispatch path implemented: BojRest.JsInvoker + priv/js_runner.js + Router dispatch branch. (7) cartridge_so_path/1 fixed to read ffi.so_path from manifest. (8) Gateway policy + catalog-integration design doc added. (9) ROADMAP v1.1.0 milestone added." },

cartridges/academic-workflow-mcp/cartridge.json

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
"copyright": "Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>",
55
"name": "academic-workflow-mcp",
66
"version": "1.0.0",
7-
"description": "Academic workflow Zotero integration, citations, paper review",
7+
"description": "Academic workflow \u2014 Zotero integration, citations, paper review",
88
"domain": "Research",
99
"tier": "Ayo",
10-
"auth": { "method": "none" },
10+
"auth": {
11+
"method": "none"
12+
},
1113
"author": "Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>",
1214
"license": "PMPL-1.0-or-later",
1315
"tools": [
@@ -47,15 +49,24 @@
4749
"loopback_proof": "IsLoopback 5174"
4850
},
4951
"ffi": {
50-
"language": "zig",
51-
"entry": "ffi/academic_ffi.zig",
52-
"so_path": "libacademic_workflow_mcp.so"
52+
"so_path": "ffi/zig-out/lib/libacademic_workflow_mcp.so",
53+
"abi_version": "ADR-0006",
54+
"symbols": [
55+
"boj_cartridge_init",
56+
"boj_cartridge_deinit",
57+
"boj_cartridge_name",
58+
"boj_cartridge_version",
59+
"boj_cartridge_invoke"
60+
]
5361
},
5462
"adapter": {
5563
"language": "typescript",
5664
"runtime": "deno",
5765
"entry": "adapter/mod.ts",
58-
"permissions": ["net", "env"]
66+
"permissions": [
67+
"net",
68+
"env"
69+
]
5970
},
6071
"loopback": {
6172
"host": "127.0.0.1",
Lines changed: 49 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,70 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
2-
// Academic Workflow Cartridge FFI — Zotero & citation integration
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// academic-workflow-mcp FFI — ADR-0006 five-symbol cartridge ABI implementation.
35

46
const std = @import("std");
5-
const mem = std.mem;
7+
const shim = @import("cartridge_shim.zig");
68

7-
// Citation format constants
8-
pub const CITATION_BIBTEX = 0;
9-
pub const CITATION_CSL = 1;
10-
pub const CITATION_RIS = 2;
11-
pub const CITATION_ENDNOTE = 3;
9+
const CARTRIDGE_NAME_PTR: [*:0]const u8 = "academic-workflow-mcp";
10+
const CARTRIDGE_VERSION_PTR: [*:0]const u8 = "0.1.0";
1211

13-
// Result codes
14-
pub const RESULT_SUCCESS = 0;
15-
pub const RESULT_ZOTERO_ERROR = 1;
16-
pub const RESULT_INVALID_FORMAT = 2;
17-
pub const RESULT_NOT_FOUND = 3;
18-
19-
// Paper metadata struct
20-
pub const PaperMetadata = extern struct {
21-
title: [512]u8,
22-
authors: [1024]u8, // comma-separated
23-
doi: [256]u8,
24-
year: u32,
25-
abstract: [2048]u8,
26-
};
27-
28-
// Citation export function
29-
pub export fn academic_generate_citation(
30-
title: [*c]const u8,
31-
authors: [*c]const u8,
32-
doi: [*c]const u8,
33-
year: u32,
34-
format: u32,
35-
out_citation: [*c]u8,
36-
out_len: usize,
37-
) callconv(.C) i32 {
38-
if (title == null or out_citation == null) {
39-
return RESULT_INVALID_FORMAT;
40-
}
41-
42-
// Format based on citation style
43-
const citation = switch (format) {
44-
CITATION_BIBTEX => formatBibTeX(title, authors, doi, year),
45-
CITATION_CSL => formatCSL(title, authors, doi, year),
46-
CITATION_RIS => formatRIS(title, authors, doi, year),
47-
CITATION_ENDNOTE => formatEndNote(title, authors, doi, year),
48-
else => return RESULT_INVALID_FORMAT,
49-
};
50-
51-
if (citation.len > 0) {
52-
const copy_len = @min(citation.len, out_len -% 1);
53-
@memcpy(out_citation[0..copy_len], citation[0..copy_len]);
54-
out_citation[copy_len] = 0;
55-
return RESULT_SUCCESS;
56-
}
57-
return RESULT_INVALID_FORMAT;
12+
export fn boj_cartridge_init() callconv(.c) c_int {
13+
return 0;
5814
}
5915

60-
// Zotero collection search
61-
pub export fn academic_search_zotero(
62-
query: [*c]const u8,
63-
out_results: [*c]u32,
64-
out_count: [*c]usize,
65-
) callconv(.C) i32 {
66-
if (query == null) {
67-
return RESULT_ZOTERO_ERROR;
68-
}
69-
if (out_count) |count| {
70-
count.* = 0;
71-
}
72-
return RESULT_SUCCESS;
73-
}
16+
export fn boj_cartridge_deinit() callconv(.c) void {}
7417

75-
// Extract BibTeX keys from text
76-
pub export fn academic_extract_bibkeys(
77-
text: [*c]const u8,
78-
out_keys: [*c][256]u8,
79-
out_count: [*c]usize,
80-
) callconv(.C) i32 {
81-
if (text == null) {
82-
return RESULT_SUCCESS;
83-
}
84-
// Look for patterns like @cite{...} or \cite{...}
85-
if (out_count) |count| {
86-
count.* = 0; // Placeholder
87-
}
88-
return RESULT_SUCCESS;
18+
export fn boj_cartridge_name() callconv(.c) [*:0]const u8 {
19+
return CARTRIDGE_NAME_PTR;
8920
}
9021

91-
// Export collection as BibTeX
92-
pub export fn academic_export_collection(
93-
collection_id: [*c]const u8,
94-
out_bibtex: [*c]u8,
95-
out_len: usize,
96-
) callconv(.C) i32 {
97-
if (collection_id == null or out_bibtex == null) {
98-
return RESULT_NOT_FOUND;
99-
}
100-
// Would call Zotero API to export collection
101-
return RESULT_SUCCESS;
22+
export fn boj_cartridge_version() callconv(.c) [*:0]const u8 {
23+
return CARTRIDGE_VERSION_PTR;
10224
}
10325

104-
// Add review note to paper
105-
pub export fn academic_add_review_note(
106-
paper_id: [*c]const u8,
107-
page: u32,
108-
note_text: [*c]const u8,
109-
category: [*c]const u8,
110-
) callconv(.C) i32 {
111-
if (paper_id == null or note_text == null) {
112-
return RESULT_INVALID_FORMAT;
113-
}
114-
// Store review annotation
115-
return RESULT_SUCCESS;
26+
export fn boj_cartridge_invoke(
27+
tool_name: [*c]const u8,
28+
json_args: [*c]const u8,
29+
out_buf: [*c]u8,
30+
in_out_len: [*c]usize,
31+
) callconv(.c) i32 {
32+
_ = json_args;
33+
if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS;
34+
const body: []const u8 = if (shim.toolIs(tool_name, "search_zotero"))
35+
"{\"result\":{}}"
36+
else if (shim.toolIs(tool_name, "get_paper_metadata"))
37+
"{\"result\":{}}"
38+
else if (shim.toolIs(tool_name, "generate_citation"))
39+
"{\"result\":{}}"
40+
else if (shim.toolIs(tool_name, "extract_bibkeys"))
41+
"{\"result\":{}}"
42+
else if (shim.toolIs(tool_name, "export_collection"))
43+
"{\"result\":{}}"
44+
else if (shim.toolIs(tool_name, "add_review_note"))
45+
"{\"result\":{}}"
46+
else
47+
return shim.RC_UNKNOWN_TOOL;
48+
49+
return shim.writeResult(out_buf, in_out_len, body);
11650
}
11751

118-
// Helper: Format citation as BibTeX
119-
fn formatBibTeX(
120-
title: [*c]const u8,
121-
authors: [*c]const u8,
122-
doi: [*c]const u8,
123-
year: u32,
124-
) []const u8 {
125-
_ = authors;
126-
_ = doi;
127-
_ = year;
128-
// Placeholder — would build proper BibTeX entry
129-
return "BibTeX citation";
52+
test "boj_cartridge_name returns academic-workflow-mcp" {
53+
try std.testing.expectEqualStrings("academic-workflow-mcp", std.mem.span(boj_cartridge_name()));
13054
}
13155

132-
// Helper: Format citation as CSL-JSON
133-
fn formatCSL(
134-
title: [*c]const u8,
135-
authors: [*c]const u8,
136-
doi: [*c]const u8,
137-
year: u32,
138-
) []const u8 {
139-
_ = title;
140-
_ = authors;
141-
_ = doi;
142-
_ = year;
143-
return "CSL citation";
56+
test "boj_cartridge_init returns 0" {
57+
try std.testing.expectEqual(@as(c_int, 0), boj_cartridge_init());
14458
}
14559

146-
// Helper: Format citation as RIS
147-
fn formatRIS(
148-
title: [*c]const u8,
149-
authors: [*c]const u8,
150-
doi: [*c]const u8,
151-
year: u32,
152-
) []const u8 {
153-
_ = title;
154-
_ = authors;
155-
_ = doi;
156-
_ = year;
157-
return "RIS citation";
60+
test "invoke unknown tool returns RC_UNKNOWN_TOOL" {
61+
var buf: [256]u8 = undefined;
62+
var len: usize = buf.len;
63+
try std.testing.expectEqual(@as(i32, shim.RC_UNKNOWN_TOOL), boj_cartridge_invoke("unknown_xyz", "{}", &buf, &len));
15864
}
15965

160-
// Helper: Format citation as EndNote
161-
fn formatEndNote(
162-
title: [*c]const u8,
163-
authors: [*c]const u8,
164-
doi: [*c]const u8,
165-
year: u32,
166-
) []const u8 {
167-
_ = title;
168-
_ = authors;
169-
_ = doi;
170-
_ = year;
171-
return "EndNote citation";
66+
test "invoke search_zotero returns 0" {
67+
var buf: [256]u8 = undefined;
68+
var len: usize = buf.len;
69+
try std.testing.expectEqual(@as(i32, 0), boj_cartridge_invoke("search_zotero", "{}", &buf, &len));
17270
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
const std = @import("std");
5+
6+
pub fn build(b: *std.Build) void {
7+
const target = b.standardTargetOptions(.{});
8+
const optimize = b.standardOptimizeOption(.{});
9+
10+
const ffi_mod = b.addModule("academic_workflow_mcp", .{
11+
.root_source_file = b.path("academic_ffi.zig"),
12+
.target = target,
13+
.optimize = optimize,
14+
});
15+
16+
const lib = b.addLibrary(.{
17+
.name = "academic_workflow_mcp",
18+
.root_module = ffi_mod,
19+
.linkage = .dynamic,
20+
});
21+
lib.linkLibC();
22+
b.installArtifact(lib);
23+
24+
const tests = b.addTest(.{ .root_module = ffi_mod });
25+
tests.linkLibC();
26+
const run_tests = b.addRunArtifact(tests);
27+
const test_step = b.step("test", "Run FFI tests");
28+
test_step.dependOn(&run_tests.step);
29+
}

0 commit comments

Comments
 (0)