Skip to content

Commit d961a1a

Browse files
ci(abi-drift): wire emit→verify gate per cartridge (standards#92 Phase 2) (#98)
## Summary **Phase 2** of standards#89 sub-issue 3 / standards#92. Adds `.github/workflows/abi-drift.yml` — a per-cartridge CI gate that runs `iseriser abi-emit-manifest` → `iseriser abi-verify` on every PR or push touching a cartridge's `abi/` or `ffi/` tree. ## Allowlist (starts narrow) Enforced for: **ssg-mcp, k9iser-mcp**. 80 cartridges in the tree have a paired `Safe*.idr` + `*_ffi.zig`. Spot-check of 5: | Cartridge | Result | |---|---| | ssg-mcp | ✓ clean | | k9iser-mcp | ✓ clean | | 007-mcp | ✗ **REAL DRIFT** — `ToolRisk` enum declared in Idris2 but absent from Zig | | airtable-mcp | ✗ false-positive — `Error → err` (Zig reserved-word workaround; converter doesn't know about `error`) | | postgresql-mcp | ✗ same false-positive | So: 1 genuine drift, 2 reserved-word false-positives, ~77 unsurveyed. The allowlist expands as cartridges are audited individually and the reserved-word handling is added to `iseriser`. ## Dependency chain (block-on) | | State | Provides | |---|---|---| | iseriser#13 | ✅ MERGED earlier today | `abi-verify` subcommand | | **iseriser#14** | 🟡 DRAFT (opened earlier this session) | `abi-emit-manifest` subcommand | | **this PR (boj-server)** | 🟡 DRAFT | the CI gate that uses both | This PR's CI will fail with `unknown subcommand 'abi-emit-manifest'` until iseriser#14 merges. **That is the intended block, not a CI defect.** Mark this ready for review only after iseriser#14 is in. ## Test plan - [x] workflow YAML syntax valid (single new file, 123 LOC) - [x] cartridge discovery shell tested locally against the real tree (returns the 80 cartridges; allowlist correctly narrows to 2) - [x] live emit→verify on the 2 allowlisted cartridges confirmed clean (run via release-mode iseriser, exit 0) - [ ] blocked: cannot test in CI itself until iseriser#14 lands (the workflow installs from iseriser main branch) - [ ] reviewer: confirm the install-from-git approach (vs vendored or precompiled) is acceptable for the initial Phase 2 shape - [ ] reviewer: confirm the allowlist approach (vs opt-in marker file in each cartridge) is acceptable ## Why `Refs` not `Closes` standards#92 is `major,requirements-target` — joint-close on owner agreement. Phase 2 is one step of a 4-phase plan; not closing here. Refs hyperpolymath/standards#92 Refs hyperpolymath/standards#89 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f52e9e4 commit d961a1a

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

.github/workflows/abi-drift.yml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# ABI Drift Gate (standards#92 Phase 2)
5+
#
6+
# For every cartridge that has a paired `abi/.../Safe*.idr` (Idris2 ABI
7+
# source of truth) and `ffi/*_ffi.zig` (Zig FFI mirror), this workflow:
8+
# 1. emits the ABI manifest from the Idris2 source (`iseriser abi-emit-manifest`)
9+
# 2. verifies the Zig FFI against the emitted manifest (`iseriser abi-verify`)
10+
# Fails the PR check on any structural drift in: enum encoding,
11+
# transition table, or accept-by-omission. See iseriser/README and
12+
# `iseriser/examples/abi-manifests/README.adoc` for the drift taxonomy.
13+
#
14+
# Triggers only when an ABI- or FFI-bearing file under cartridges/
15+
# changes, so unrelated PRs do not pay for it.
16+
17+
name: ABI Drift Gate
18+
19+
on:
20+
push:
21+
branches: [main]
22+
paths:
23+
- 'cartridges/**/abi/**'
24+
- 'cartridges/**/ffi/**'
25+
- '.github/workflows/abi-drift.yml'
26+
pull_request:
27+
branches: [main]
28+
paths:
29+
- 'cartridges/**/abi/**'
30+
- 'cartridges/**/ffi/**'
31+
- '.github/workflows/abi-drift.yml'
32+
33+
concurrency:
34+
group: abi-drift-${{ github.workflow }}-${{ github.ref }}
35+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
36+
37+
permissions:
38+
contents: read
39+
40+
jobs:
41+
verify:
42+
name: Emit manifest + verify FFI
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
46+
47+
- name: Install Rust toolchain (stable)
48+
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
49+
with:
50+
toolchain: stable
51+
52+
- name: Cache cargo bin (iseriser install)
53+
uses: actions/cache@v4
54+
with:
55+
path: |
56+
~/.cargo/bin/iseriser
57+
~/.cargo/registry
58+
~/.cargo/git
59+
key: iseriser-${{ runner.os }}-${{ github.sha }}
60+
restore-keys: |
61+
iseriser-${{ runner.os }}-
62+
63+
- name: Install iseriser
64+
run: |
65+
set -euo pipefail
66+
if ! command -v iseriser >/dev/null 2>&1; then
67+
cargo install --git https://github.com/hyperpolymath/iseriser --branch main --locked
68+
fi
69+
iseriser --version || true
70+
71+
- name: Resolve cartridge allowlist
72+
id: discover
73+
# 80 cartridges in the tree carry a paired Safe*.idr + *_ffi.zig.
74+
# The gate starts narrow: only cartridges audited and known clean
75+
# against the Phase 1b emitter + Phase 1 verifier are enforced.
76+
# Expand this list as cartridges are individually verified — the
77+
# broader spot-check (PR body) found real drift (e.g. 007-mcp's
78+
# `ToolRisk` enum) plus Zig-reserved-word false positives (e.g.
79+
# `Error → err` in airtable-mcp / postgresql-mcp) that need the
80+
# converter taught about Zig reserved words first.
81+
run: |
82+
set -euo pipefail
83+
{
84+
echo 'carts<<EOF'
85+
echo 'ssg-mcp'
86+
echo 'k9iser-mcp'
87+
echo 'EOF'
88+
} >> "$GITHUB_OUTPUT"
89+
90+
- name: Emit + verify each cartridge
91+
env:
92+
CARTS: ${{ steps.discover.outputs.carts }}
93+
run: |
94+
set -euo pipefail
95+
failed=""
96+
while IFS= read -r cart; do
97+
[ -z "$cart" ] && continue
98+
echo "::group::abi-drift: $cart"
99+
idris_src="$(ls cartridges/${cart}/abi/*/Safe*.idr | head -n1)"
100+
zig_ffi="$(ls cartridges/${cart}/ffi/*_ffi.zig | head -n1)"
101+
manifest="$(mktemp --suffix=.json)"
102+
if iseriser abi-emit-manifest \
103+
--idris "$idris_src" \
104+
--cartridge "$cart" \
105+
--source-path "$idris_src" \
106+
--out "$manifest"; then
107+
if iseriser abi-verify --manifest "$manifest" --zig-ffi "$zig_ffi"; then
108+
echo " ✓ $cart"
109+
else
110+
echo " ✗ $cart DRIFT"
111+
failed="$failed $cart"
112+
fi
113+
else
114+
echo " ✗ $cart EMIT FAILED"
115+
failed="$failed $cart"
116+
fi
117+
rm -f "$manifest"
118+
echo "::endgroup::"
119+
done <<< "$CARTS"
120+
if [ -n "$failed" ]; then
121+
echo "::error::ABI drift detected in:$failed"
122+
exit 1
123+
fi

0 commit comments

Comments
 (0)