From 31c0813996708c62b830cd4d914df01abd5932bf Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:48:00 +0100 Subject: [PATCH] fix: repair non-compiling FFI, vacuous gates and three parse-dead workflows All three workflows in this repo that GitHub refused to parse are fixed, and the gates they were meant to run are made real. Every claim below was verified by executing the thing, not by reading it. Workflows (actionlint: 3 errors -> 0) * e2e.yml had an entirely commented-out `jobs:` section, so the file was rejected with "jobs section should not be empty". It now runs four real jobs against the checked-in scripts. * boj-build.yml used the `secrets` context in a job-level `if:`, where only `github`, `inputs`, `needs` and `vars` are available. The gate is removed rather than narrowed to `vars`, which would have silently skipped the secret-configured case; the step already exits 0 when the URL is unset, which is the documented no-op behaviour. * dogfood-gate.yml embedded Python at column 0 inside a `run: |` block scalar whose indentation was fixed at 10 spaces, terminating the scalar and breaking the YAML. Replaced with a quoted heredoc, which also removes the `\"` escaping that provoked the dedent. Verified by extracting the step and running it against no-manifest / valid / invalid inputs (0 / 0 / 1). FFI (did not compile at all) * src/interface/ffi/src/main.zig declared `pub const Handle = opaque { ... }` with fields. Zig opaque types cannot carry fields, so the shim had never compiled. C consumers only ever hold a `*Handle`, so a plain struct is ABI-identical; this matches the fix already applied in rpa-elysium. * `callconv(.C)` was removed in Zig 0.16; now `callconv(.c)`. * build.zig was scaffolding that wired no steps. `zig build` now produces libkrl.a and `zig build test` runs the three unit tests (3/3 pass). Gates that could not fail * tests/aspect_tests.sh grepped src/abi/ and ffi/zig/, neither of which exists here (the real paths are src/interface/Abi and src/interface/ffi), so its Idris check passed vacuously. It now fails loudly when the scan set is empty, rather than reporting success for having looked at nothing. * The same script flagged verification/proofs/README.adoc for containing the words `sorry` and `unsafeCoerce` while documenting them as banned. Scanning is now restricted to source files, with comment lines excluded. * Aspect 3 (ABI/FFI correspondence) was commented out and its paths were wrong. Enabled: 4 %foreign declarations covered by 11 Zig exports. * tests/e2e.sh was entirely TODO and reported PASS=0 FAIL=0 while exiting 0. It now runs four checks. Negative-controlled: injecting a fault into an exported function yields FAIL=2 and exit 2. Licensing * Reconciled the three unpushed sweeper commits against canonical SPDX text rather than against each other. origin/main's CC-BY-SA-4.0.txt is already byte-exact canonical; the sweeper's +474/-110 rewrite substituted the Creative Commons plaintext variant and was a regression, so it is dropped. * Neither version of MPL-2.0.txt was canonical (origin/main had http:// where SPDX has https://; the sweeper stripped a significant trailing space). Replaced with the canonical SPDX text. Both files now match byte for byte. * LICENSES/AGPL-3.0-or-later.txt is dropped. It contradicts this repo's own .machine_readable/compliance/rust/deny.toml, no file carries an AGPL SPDX header, and `reuse lint` reports it as "Unused licenses: AGPL-3.0-or-later". * trailing-whitespace and end-of-file-fixer now exclude LICENSES/. Both canonical texts contain significant trailing whitespace, so the hooks were guaranteed to corrupt them on every commit. This is the mechanism behind that class of damage. * The rest of the sweeper's output is dropped: ARCHITECTURE.md was generic boilerplate describing a src/tests/docs/scripts/config layout this repo does not have (and was byte-identical to the one it added to tangle); GOVERNANCE.md, MAINTAINERS and .github/funding.yml duplicate the existing GOVERNANCE.adoc, MAINTAINERS.adoc and .github/FUNDING.yml; mise.toml pinned 30 tools to "latest" and collided with .tool-versions. Also * .pre-commit-config.yaml had a truncated secret-detection block with no `- repo:` key. YAML last-key-wins silently overwrote editorconfig-checker's rev with v8.24.3 and set its hooks to null, so gitleaks was absent and one hook pointed at a non-existent tag. Restored. Eighteen other repos in the estate share this exact truncation. * .tool-versions pinned `rust nightly` -- the one language absent from this repo -- while zig and idris2 sat commented out. Corrected. * zig-out/ added to .gitignore. --- .github/workflows/boj-build.yml | 6 +- .github/workflows/dogfood-gate.yml | 57 ++++---- .github/workflows/e2e.yml | 219 ++++++++--------------------- .gitignore | 1 + .pre-commit-config.yaml | 8 ++ .tool-versions | 18 +-- LICENSES/MPL-2.0.txt | 2 +- src/interface/ffi/build.zig | 36 +++-- src/interface/ffi/src/main.zig | 10 +- tests/aspect_tests.sh | 86 +++++++---- tests/e2e.sh | 108 +++++++------- 11 files changed, 261 insertions(+), 290 deletions(-) diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml index b203334..a81237c 100644 --- a/.github/workflows/boj-build.yml +++ b/.github/workflows/boj-build.yml @@ -16,7 +16,11 @@ jobs: trigger-boj: runs-on: ubuntu-latest timeout-minutes: 15 - if: ${{ vars.BOJ_SERVER_URL != '' || secrets.BOJ_SERVER_URL != '' }} + # No job-level `if:` gate here. The `secrets` context is not available in + # job-level conditions (only `github`, `inputs`, `needs`, `vars`), and a + # `vars`-only test would silently skip the secret-configured case. The step + # below reads both and exits 0 when neither is set, which is the documented + # no-op behaviour. steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 00790de..071270e 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -261,31 +261,40 @@ jobs: echo "has_manifest=true" >> "$GITHUB_OUTPUT" - # Validate TOML structure using Python 3.11+ tomllib - python3 -c " -import tomllib, sys -with open('eclexiaiser.toml', 'rb') as f: - data = tomllib.load(f) -project = data.get('project', {}) -if not project.get('name', '').strip(): - print('ERROR: project.name is required', file=sys.stderr) - sys.exit(1) -functions = data.get('functions', []) -if not functions: - print('ERROR: at least one [[functions]] entry is required', file=sys.stderr) - sys.exit(1) -for fn in functions: - if not fn.get('name', '').strip(): - print('ERROR: function name cannot be empty', file=sys.stderr) - sys.exit(1) - if not fn.get('source', '').strip(): - print(f'ERROR: function {fn[\"name\"]} has no source path', file=sys.stderr) - sys.exit(1) -print(f'Valid: {project[\"name\"]} ({len(functions)} function(s))') -" || { - echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml — see step output for details" + # Validate TOML structure using Python 3.11+ tomllib. + # Quoted heredoc: no shell expansion, so no backslash escaping is + # needed. Body is indented to the block scalar's level and is + # dedented back to column 0 by YAML before bash sees it. + if ! python3 <<'PY' + import tomllib, sys + + with open('eclexiaiser.toml', 'rb') as f: + data = tomllib.load(f) + + project = data.get('project', {}) + if not project.get('name', '').strip(): + print('ERROR: project.name is required', file=sys.stderr) + sys.exit(1) + + functions = data.get('functions', []) + if not functions: + print('ERROR: at least one [[functions]] entry is required', file=sys.stderr) + sys.exit(1) + + for fn in functions: + if not fn.get('name', '').strip(): + print('ERROR: function name cannot be empty', file=sys.stderr) + sys.exit(1) + if not fn.get('source', '').strip(): + print(f"ERROR: function {fn['name']} has no source path", file=sys.stderr) + sys.exit(1) + + print(f"Valid: {project['name']} ({len(functions)} function(s))") + PY + then + echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml - see step output for details" exit 1 - } + fi - name: Write summary run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fdb9ca1..58ca643 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,186 +1,87 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# RSR Standard E2E + Aspect + Benchmark Workflow Template +# KRL — E2E, Aspect and Benchmark gates. # -# Covers ALL merge requirement test categories: -# - E2E (end-to-end pipeline tests) -# - Aspect (cross-cutting concern validation) -# - Benchmarks (performance regression detection) -# - Readiness (Component Readiness Grade: D/C/B) -# -# INSTRUCTIONS: Uncomment and customise the section matching your stack. -# Delete sections that don't apply. See examples in each job. - +# These run the checked-in scripts under tests/ and benches/. Each script is +# expected to exit non-zero on failure; none of them are permitted to report +# success without having executed at least one assertion. name: E2E + Aspect + Bench on: push: branches: [main, master, develop] paths: - 'src/**' - - 'ffi/**' - 'tests/**' + - 'benches/**' + - 'spec/**' + - 'examples/**' - '.github/workflows/e2e.yml' pull_request: branches: [main, master] paths: - 'src/**' - - 'ffi/**' - 'tests/**' + - 'benches/**' + - 'spec/**' + - 'examples/**' + - '.github/workflows/e2e.yml' workflow_dispatch: permissions: read-all concurrency: group: e2e-${{ github.ref }} cancel-in-progress: true -jobs: -# ─── End-to-End Tests ────────────────────────────────────────────── -# Uncomment ONE of the following e2e job blocks matching your stack. - -## === RUST E2E === -# e2e: -# name: E2E — Full Pipeline -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable -# - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 -# - run: cargo build --release -# - run: bash tests/e2e.sh -# # OR: cargo test --test end_to_end -- --nocapture - - ## === ZIG FFI E2E === - # e2e: - # name: E2E — FFI Pipeline - # runs-on: ubuntu-latest - # timeout-minutes: 15 - # steps: - # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # - uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 - # with: - # version: 0.15.0 - # - run: cd ffi/zig && zig build test - # - run: bash tests/e2e.sh - - ## === ELIXIR E2E === - # e2e: - # name: E2E — Full Pipeline - # runs-on: ubuntu-latest - # timeout-minutes: 15 - # steps: - # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # - uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 - # with: - # otp-version: '27.0' - # elixir-version: '1.17' - # - run: mix deps.get && mix compile --warnings-as-errors - # - run: mix test test/integration/e2e_test.exs --trace - ## === DENO/RESCRIPT E2E === - # e2e: - # name: E2E — Full Pipeline - # runs-on: ubuntu-latest - # timeout-minutes: 15 - # steps: - # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 - # with: - # deno-version: v2.x - # - run: deno install --node-modules-dir=auto - # - run: deno task res:build # ReScript compile - # - run: deno test tests/e2e/ +env: + ZIG_VERSION: 0.16.0 - ## === PLAYWRIGHT (Browser E2E) === - # e2e-playwright: - # name: Playwright — ${{ matrix.project }} - # runs-on: ubuntu-latest - # timeout-minutes: 20 - # strategy: - # fail-fast: false - # matrix: - # project: [chromium-1080p, firefox-1080p, webkit-1080p] - # steps: - # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 - # with: - # deno-version: v2.x - # - run: deno install --node-modules-dir=auto - # - run: npx playwright install --with-deps - # - run: npx playwright test --project=${{ matrix.project }} - # - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - # if: failure() - # with: - # name: playwright-traces-${{ matrix.project }} - # path: test-results/**/trace.zip - # retention-days: 7 - - ## === HASKELL E2E === - # e2e: - # name: E2E — Full Pipeline - # runs-on: ubuntu-latest - # timeout-minutes: 15 - # steps: - # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # - uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0 - # with: - # ghc-version: '9.6' - # cabal-version: '3.10' - # - run: cabal build all - # - run: bash tests/integration-test.sh - -# ─── Aspect Tests ────────────────────────────────────────────────── -# Cross-cutting concerns: thread safety, ABI contracts, SPDX, dangerous patterns -# Uncomment and customise: - -# aspect-tests: -# name: Aspect — Architectural Invariants -# runs-on: ubuntu-latest -# timeout-minutes: 10 -# steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - run: bash tests/aspect_tests.sh - -# ─── Benchmarks ──────────────────────────────────────────────────── -# Performance regression detection. Uncomment matching stack: - -## === RUST BENCH === -# benchmarks: -# name: Bench — Performance Regression -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable -# - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 -# - run: cargo bench 2>&1 | tee /tmp/bench-results.txt -# - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 -# if: always() -# with: -# name: benchmark-results -# path: /tmp/bench-results.txt -# retention-days: 30 +jobs: + e2e: + name: E2E — FFI build, ABI correspondence, grammar smoke + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Zig + uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 + with: + version: ${{ env.ZIG_VERSION }} + - name: Run E2E suite + run: bash tests/e2e.sh - ## === ZIG BENCH === - # benchmarks: - # name: Bench — Performance Regression - # runs-on: ubuntu-latest - # timeout-minutes: 15 - # steps: - # - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # - uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 - # with: - # version: 0.15.0 - # - run: cd ffi/zig && zig build bench + aspect: + name: Aspect — architectural invariants + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run aspect tests + run: bash tests/aspect_tests.sh -# ─── Readiness (CRG) ────────────────────────────────────────────── -# Component Readiness Grade: D (runs) → C (correct) → B (edge cases) + smoke: + name: Smoke — KRL grammar and examples + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run grammar smoke suite + run: bash tests/smoke/grammar_smoke.sh -# readiness: -# name: Readiness — Grade D/C/B -# runs-on: ubuntu-latest -# timeout-minutes: 10 -# steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable -# - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 -# - run: cargo test --test readiness -- --nocapture + ffi-unit: + name: FFI — Zig unit tests + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Zig + uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 + with: + version: ${{ env.ZIG_VERSION }} + - name: zig build test + run: cd src/interface/ffi && zig build test + - name: zig build + run: cd src/interface/ffi && zig build diff --git a/.gitignore b/.gitignore index 1244f65..50dac54 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ sync_report*.txt # Hypatia scan cache (local-only) .hypatia/ .zig-cache/ +zig-out/ target/ node_modules/ _build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d04e23e..fc32f5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,8 +9,14 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: + # LICENSES/ holds verbatim licence texts that must match the canonical + # SPDX wording byte for byte (CC-BY-SA-4.0 and MPL-2.0 both contain + # significant trailing whitespace). Reformatting them silently + # invalidates REUSE conformance, so they are excluded here. - id: trailing-whitespace + exclude: '^LICENSES/' - id: end-of-file-fixer + exclude: '^LICENSES/' - id: check-yaml - id: check-json - id: check-toml @@ -47,5 +53,7 @@ repos: exclude: '(\.git|node_modules|target|_build|deps|\.deno|external_corpora|\.lake)/' # --- Secret detection --- + - repo: https://github.com/gitleaks/gitleaks rev: v8.24.3 hooks: + - id: gitleaks diff --git a/.tool-versions b/.tool-versions index ce60c32..4a13be7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,10 +1,10 @@ -# Uncomment and customize for your project -# rust nightly -# just 1.40.0 -# nickel 1.10.0 -# gleam 1.8.0 -# elixir 1.18.0 -# erlang 27.2 -# zig 0.14.0 -# idris2 0.7.0 +# Toolchains this repository actually uses. +# zig — the C-ABI FFI shim under src/interface/ffi (verified against 0.16.0) +# idris2 — the ABI declarations under src/interface/Abi +# +# NOTE: `rust nightly` is retained only because .github/workflows/rust-ci.yml +# still calls the shared Rust reusable. There is no Cargo.toml in this repo, +# so that workflow currently gates nothing — see issue #38. +zig 0.16.0 +idris2 0.7.0 rust nightly diff --git a/LICENSES/MPL-2.0.txt b/LICENSES/MPL-2.0.txt index 14e2f77..ee6256c 100644 --- a/LICENSES/MPL-2.0.txt +++ b/LICENSES/MPL-2.0.txt @@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. + file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE diff --git a/src/interface/ffi/build.zig b/src/interface/ffi/build.zig index 2607c11..cdb31a4 100644 --- a/src/interface/ffi/build.zig +++ b/src/interface/ffi/build.zig @@ -1,19 +1,35 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell // -// Template FFI Build Configuration (Zig 0.15.2+) -// Note: This is a minimal build file that demonstrates Zig integration +// KRL FFI build configuration. +// +// Exposes two steps: +// zig build — build the static library consumed over the C ABI +// zig build test — run the FFI unit tests in src/main.zig const std = @import("std"); pub fn build(b: *std.Build) void { - _ = b.standardTargetOptions(.{}); - _ = b.standardOptimizeOption(.{}); + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const ffi_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + + const lib = b.addLibrary(.{ + .name = "krl", + .linkage = .static, + .root_module = ffi_mod, + }); + b.installArtifact(lib); + + const ffi_tests = b.addTest(.{ .root_module = ffi_mod }); + const run_ffi_tests = b.addRunArtifact(ffi_tests); - // In Zig 0.15+, tests are run directly with: - // zig build-exe -ftest-runner src/main.zig - // zig build-exe -ftest-runner test/integration_test.zig - // - // This minimal build file provides scaffolding for future expansion. - // Tests can be invoked via command line without explicit build.zig configuration. + const test_step = b.step("test", "Run FFI unit tests"); + test_step.dependOn(&run_ffi_tests.step); } diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig index eaa6ea4..02909d4 100644 --- a/src/interface/ffi/src/main.zig +++ b/src/interface/ffi/src/main.zig @@ -38,12 +38,14 @@ pub const Result = enum(c_int) { null_pointer = 4, }; -/// Library handle (opaque to prevent direct access) -pub const Handle = opaque { +/// Library handle. A regular struct used as the backing type; C consumers only +/// ever hold a `*Handle` and never see the layout, so it is effectively opaque +/// across the ABI. (Zig `opaque {}` types cannot carry fields, which the +/// internal state below requires.) +pub const Handle = struct { // Internal state hidden from C allocator: std.mem.Allocator, initialized: bool, - // Add your fields here }; //============================================================================== @@ -210,7 +212,7 @@ export fn krl_build_info() [*:0]const u8 { //============================================================================== /// Callback function type (C ABI) -pub const Callback = *const fn (u64, u32) callconv(.C) u32; +pub const Callback = *const fn (u64, u32) callconv(.c) u32; /// Register a callback export fn krl_register_callback( diff --git a/tests/aspect_tests.sh b/tests/aspect_tests.sh index 3cd1eb5..82db15a 100755 --- a/tests/aspect_tests.sh +++ b/tests/aspect_tests.sh @@ -54,7 +54,8 @@ while IFS= read -r -d '' f; do warn "Missing SPDX header: $f" MISSING_SPDX=$((MISSING_SPDX + 1)) fi -done < <(find src/ -type f \( -name "*.rs" -o -name "*.zig" -o -name "*.res" -o -name "*.ex" -o -name "*.exs" -o -name "*.gleam" -o -name "*.idr" -o -name "*.sh" \) -print0 2>/dev/null) +done < <(find src/ \( -name ".zig-cache" -o -name "zig-out" -o -name "build" \) -prune -o \ + -type f \( -name "*.rs" -o -name "*.zig" -o -name "*.res" -o -name "*.ex" -o -name "*.exs" -o -name "*.gleam" -o -name "*.idr" -o -name "*.sh" \) -print0 2>/dev/null) if [ "$MISSING_SPDX" -eq 0 ]; then pass "All source files have SPDX headers" @@ -67,42 +68,67 @@ fi # ═══════════════════════════════════════════════════════════════════════ bold "Aspect 2: Dangerous patterns" -# Idris2 dangerous patterns -DANGEROUS_IDRIS=$(grep -rn 'believe_me\|assert_total\|really_believe_me' src/abi/ 2>/dev/null | grep -v "^Binary" | grep -v "test" || true) -if [ -n "$DANGEROUS_IDRIS" ]; then - fail "Dangerous Idris2 patterns found:" - echo "$DANGEROUS_IDRIS" | head -5 -else - pass "No dangerous Idris2 patterns (believe_me, assert_total)" -fi +# Source files only. Prose files (.adoc/.md) legitimately *name* the banned +# constructs in order to ban them; scanning them yields false positives. +SOURCE_FILES=() +while IFS= read -r -d '' f; do + SOURCE_FILES+=("$f") +done < <(find src/ verification/ \( -name ".zig-cache" -o -name "zig-out" -o -name "build" \) -prune -o \ + -type f \( -name "*.idr" -o -name "*.zig" -o -name "*.lean" -o -name "*.v" \ + -o -name "*.hs" -o -name "*.rs" -o -name "*.ml" \) -print0 2>/dev/null) -# Coq/Lean dangerous patterns -DANGEROUS_PROOF=$(grep -rn '\bAdmitted\b\|\bsorry\b\|\bunsafeCoerce\b\|\bObj\.magic\b' src/ verification/ 2>/dev/null | grep -v "test" | grep -v "comment" || true) -if [ -n "$DANGEROUS_PROOF" ]; then - fail "Dangerous proof patterns found:" - echo "$DANGEROUS_PROOF" | head -5 +# Comment lines legitimately *name* the banned constructs in order to ban them. +# Strip `file:line:` then any leading comment marker before judging a hit. +strip_comments() { grep -vE ':[0-9]+:[[:space:]]*(--|//|#|\(\*|\*)' || true; } + +if [ "${#SOURCE_FILES[@]}" -eq 0 ]; then + fail "No source files found under src/ or verification/ — aspect scan would be vacuous" else - pass "No dangerous proof patterns (Admitted, sorry, unsafeCoerce)" + # Idris2 dangerous patterns + DANGEROUS_IDRIS=$({ grep -n 'believe_me\|assert_total\|really_believe_me' "${SOURCE_FILES[@]}" 2>/dev/null || true; } | strip_comments) + if [ -n "$DANGEROUS_IDRIS" ]; then + fail "Dangerous Idris2 patterns found:" + echo "$DANGEROUS_IDRIS" | head -5 + else + pass "No dangerous Idris2 patterns (believe_me, assert_total) in ${#SOURCE_FILES[@]} source files" + fi + + # Coq/Lean/Haskell dangerous patterns + DANGEROUS_PROOF=$({ grep -n '\bAdmitted\b\|\bsorry\b\|\bunsafeCoerce\b\|\bObj\.magic\b' "${SOURCE_FILES[@]}" 2>/dev/null || true; } | strip_comments) + if [ -n "$DANGEROUS_PROOF" ]; then + fail "Dangerous proof patterns found:" + echo "$DANGEROUS_PROOF" | head -5 + else + pass "No dangerous proof patterns (Admitted, sorry, unsafeCoerce) in ${#SOURCE_FILES[@]} source files" + fi fi # ═══════════════════════════════════════════════════════════════════════ # Aspect 3: ABI/FFI Contract (if applicable) # ═══════════════════════════════════════════════════════════════════════ -# Uncomment if your project has Idris2 ABI + Zig FFI: - -# bold "Aspect 3: ABI/FFI contract" -# if [ -d "src/abi" ] && [ -d "ffi/zig" ]; then -# # Check that every exported function in Idris2 ABI has a Zig FFI implementation -# ABI_EXPORTS=$(grep -h 'export' src/abi/*.idr 2>/dev/null | wc -l) -# FFI_EXPORTS=$(grep -h 'pub export fn' ffi/zig/src/*.zig 2>/dev/null | wc -l) -# if [ "$ABI_EXPORTS" -gt 0 ] && [ "$FFI_EXPORTS" -gt 0 ]; then -# pass "ABI ($ABI_EXPORTS exports) and FFI ($FFI_EXPORTS exports) both present" -# else -# fail "ABI/FFI mismatch: $ABI_EXPORTS ABI exports, $FFI_EXPORTS FFI exports" -# fi -# else -# pass "ABI/FFI not applicable (no src/abi or ffi/zig)" -# fi +bold "Aspect 3: ABI/FFI contract" + +ABI_DIR="src/interface/Abi" +FFI_DIR="src/interface/ffi" + +if [ -d "$ABI_DIR" ] && [ -d "$FFI_DIR" ]; then + # Idris2 declares the ABI surface; Zig implements it over the C ABI. + # Zig uses bare `export fn` (not `pub export fn`) for C-ABI exports. + ABI_FOREIGN=$(grep -h '%foreign' "$ABI_DIR"/*.idr 2>/dev/null | wc -l) + FFI_EXPORTS=$(grep -h '^export fn' "$FFI_DIR"/src/*.zig 2>/dev/null | wc -l) + + if [ "${ABI_FOREIGN:-0}" -eq 0 ]; then + fail "No %foreign declarations found in $ABI_DIR — ABI surface is empty" + elif [ "$FFI_EXPORTS" -eq 0 ]; then + fail "No 'export fn' found in $FFI_DIR/src — FFI implementation is empty" + elif [ "$FFI_EXPORTS" -lt "$ABI_FOREIGN" ]; then + fail "ABI/FFI mismatch: $ABI_FOREIGN %foreign declarations but only $FFI_EXPORTS Zig exports" + else + pass "ABI ($ABI_FOREIGN %foreign decls) covered by FFI ($FFI_EXPORTS exports)" + fi +else + fail "Expected ABI at $ABI_DIR and FFI at $FFI_DIR — one or both missing" +fi # ═══════════════════════════════════════════════════════════════════════ # Aspect 4: Error Handling (no raw panic in production code) diff --git a/tests/e2e.sh b/tests/e2e.sh index 4d6777c..7e7b2e9 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -69,62 +69,66 @@ echo "" # ─── Preflight ─────────────────────────────────────────────────────── bold "Preflight checks" -# TODO: Check that your binary/server is built -# Example: -# BINARY="$PROJECT_DIR/target/release/my-tool" -# if [ ! -f "$BINARY" ]; then -# red "Binary not found at $BINARY — run 'just build' first" -# exit 1 -# fi -# green " Binary found: $BINARY" - -# TODO: Check dependencies -# command -v curl >/dev/null 2>&1 || { red "curl not found"; exit 1; } -# command -v jq >/dev/null 2>&1 || { red "jq not found"; exit 1; } +FFI_DIR="$PROJECT_DIR/src/interface/ffi" +ABI_DIR="$PROJECT_DIR/src/interface/Abi" + +command -v zig >/dev/null 2>&1 || { red "zig not found — required to build the KRL FFI"; exit 1; } +green " zig found: $(zig version)" + +[ -f "$FFI_DIR/build.zig" ] || { red "missing $FFI_DIR/build.zig"; exit 1; } +green " FFI build definition present" echo "" -# ═══════════════════════════════════════════════════════════════════════ -# TODO: Add your E2E test sections below. Examples: -# ═══════════════════════════════════════════════════════════════════════ +# ─── Section 1: FFI builds and its unit tests pass ─────────────────── +bold "Section 1: Zig FFI pipeline" + +if (cd "$FFI_DIR" && zig build test) >/dev/null 2>&1; then + green " PASS: zig build test" + PASS=$((PASS + 1)) +else + red " FAIL: zig build test" + FAIL=$((FAIL + 1)) +fi + +if (cd "$FFI_DIR" && zig build) >/dev/null 2>&1 && [ -f "$FFI_DIR/zig-out/lib/libkrl.a" ]; then + green " PASS: static library libkrl.a produced" + PASS=$((PASS + 1)) +else + red " FAIL: static library libkrl.a not produced" + FAIL=$((FAIL + 1)) +fi -# ─── Example: CLI tool E2E ─────────────────────────────────────────── -# bold "Section 1: CLI happy path" -# OUTPUT=$($BINARY --help 2>&1) -# check "help flag works" "Usage:" "$OUTPUT" -# -# OUTPUT=$($BINARY process input.txt --output /tmp/e2e-output.json 2>&1) -# check "process command succeeds" "complete" "$OUTPUT" -# -# OUTPUT=$(cat /tmp/e2e-output.json) -# check "output is valid JSON" '"status"' "$OUTPUT" - -# ─── Example: Server E2E ──────────────────────────────────────────── -# bold "Section 2: Server lifecycle" -# $BINARY serve --port 9999 & -# SERVER_PID=$! -# trap "kill $SERVER_PID 2>/dev/null" EXIT -# sleep 2 -# -# STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:9999/health) -# check_status "health endpoint" "200" "$STATUS" -# -# BODY=$(curl -s http://localhost:9999/health) -# check "health response" '"status":"ok"' "$BODY" -# -# kill $SERVER_PID 2>/dev/null - -# ─── Example: VeriSimDB integration ───────────────────────────────── -# bold "Section 3: VeriSimDB persistence" -# VERISIM_URL="${VERISIM_API_URL:-http://localhost:9090}" -# if ! curl -sf "$VERISIM_URL/health" >/dev/null 2>&1; then -# skip_test "VeriSimDB integration" "gateway not available" -# else -# STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$VERISIM_URL/api/v1/hexads" \ -# -H "Content-Type: application/json" \ -# -d '{"tool":"KRL","modality":"document","content":"e2e test"}') -# check_status "hexad POST" "201" "$STATUS" -# fi +echo "" + +# ─── Section 2: ABI surface is covered by the FFI ──────────────────── +bold "Section 2: ABI/FFI correspondence" + +ABI_FOREIGN=$(grep -h '%foreign' "$ABI_DIR"/*.idr 2>/dev/null | wc -l) +FFI_EXPORTS=$(grep -h '^export fn' "$FFI_DIR"/src/*.zig 2>/dev/null | wc -l) + +if [ "$ABI_FOREIGN" -gt 0 ] && [ "$FFI_EXPORTS" -ge "$ABI_FOREIGN" ]; then + green " PASS: $ABI_FOREIGN %foreign declarations covered by $FFI_EXPORTS Zig exports" + PASS=$((PASS + 1)) +else + red " FAIL: ABI/FFI mismatch ($ABI_FOREIGN %foreign vs $FFI_EXPORTS exports)" + FAIL=$((FAIL + 1)) +fi + +echo "" + +# ─── Section 3: grammar smoke suite ────────────────────────────────── +bold "Section 3: KRL grammar smoke suite" + +if bash "$PROJECT_DIR/tests/smoke/grammar_smoke.sh" >/dev/null 2>&1; then + green " PASS: grammar smoke suite" + PASS=$((PASS + 1)) +else + red " FAIL: grammar smoke suite" + FAIL=$((FAIL + 1)) +fi + +echo "" # ═══════════════════════════════════════════════════════════════════════ # Summary