From 5c25f990db637609490203c64965087ca22aae3b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:56:42 +0100 Subject: [PATCH 1/2] fix(ci): consolidate dogfood gate and repair K9 envelopes --- .github/workflows/dogfood-gate.yml | 453 ++++++------------ .../self-validating/examples/ci-config.k9.ncl | 2 +- .../examples/project-metadata.k9.ncl | 2 +- .../examples/setup-repo.k9.ncl | 2 +- .../self-validating/methodology-guard.k9.ncl | 12 + .../self-validating/template-hunt.k9.ncl | 2 +- .../self-validating/template-kennel.k9.ncl | 2 +- .../self-validating/template-yard.k9.ncl | 2 +- container/deploy.k9.ncl | 3 +- coordination.k9 | 13 + session/custom-checks.k9 | 13 + 11 files changed, 196 insertions(+), 310 deletions(-) diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 1071552..df042ee 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -1,9 +1,7 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# dogfood-gate.yml β€” Hyperpolymath Dogfooding Quality Gate -# Validates that the repo uses hyperpolymath's own formats and tools. -# Companion to static-analysis-gate.yml (security) β€” this is for format compliance. +# One bounded runner validates every dogfood format. Individual tools remain +# visible as steps without allocating six separate runners. name: "🟑 Check: RSR self-use (dogfooding)" on: @@ -15,122 +13,66 @@ on: permissions: contents: read +concurrency: + group: dogfood-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: - # --------------------------------------------------------------------------- - # Job 1: A2ML manifest validation - # --------------------------------------------------------------------------- - a2ml-validate: - name: Validate A2ML manifests + dogfood: + name: Dogfood gate (consolidated) runs-on: ubuntu-latest - timeout-minutes: 15 - + timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Check for A2ML files - id: detect + - name: Inventory dogfood formats + id: inventory + shell: bash run: | - COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ]; then - echo "::warning::No .a2ml manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml" + set -euo pipefail + a2ml_count=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l) + k9_count=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l) + config_count=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \ + -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \ + -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l) + { + echo "a2ml_count=$a2ml_count" + echo "k9_count=$k9_count" + echo "config_count=$config_count" + } >> "$GITHUB_OUTPUT" + if [ "$a2ml_count" -eq 0 ]; then + echo "::warning::No .a2ml manifests found; every RSR repository should have 0-AI-MANIFEST.a2ml" + fi + if [ "$k9_count" -eq 0 ] && [ "$config_count" -gt 0 ]; then + echo "::warning::Found $config_count config files but no K9 contracts" fi - name: Validate A2ML manifests - if: steps.detect.outputs.count > 0 + id: a2ml + if: steps.inventory.outputs.a2ml_count != '0' + continue-on-error: true uses: hyperpolymath/a2ml-validate-action@05bcb78917c09702e90ed18004298a6728753914 # main with: path: '.' strict: 'false' - - name: Write summary - run: | - A2ML_COUNT="${{ steps.detect.outputs.count }}" - if [ "$A2ML_COUNT" -eq 0 ]; then - cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - ## A2ML Validation - - :warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`. - - Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). - EOF - else - echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${A2ML_COUNT}** .a2ml file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" - fi - - # --------------------------------------------------------------------------- - # Job 2: K9 contract validation - # --------------------------------------------------------------------------- - k9-validate: - name: Validate K9 contracts - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Check for K9 files - id: detect - run: | - COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l) - CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \ - -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \ - -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l) - echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT" - echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT" - if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then - echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts." - fi - - name: Validate K9 contracts - if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-validate-action@bddcd9109ee96f9ea3fdb4bf51084fe9cd0909ce # main + id: k9 + if: steps.inventory.outputs.k9_count != '0' + continue-on-error: true + uses: hyperpolymath/k9-validate-action@d0d9970fb08705c5a99df88af7b334b6a2682966 # comment-safe Nickel envelope with: path: '.' strict: 'false' - - name: Write summary - run: | - K9_COUNT="${{ steps.detect.outputs.k9_count }}" - CFG_COUNT="${{ steps.detect.outputs.config_count }}" - if [ "$K9_COUNT" -eq 0 ]; then - cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - ## K9 Contract Validation - - :warning: **No K9 contract files found.** Repos with configuration files should have K9 contracts. - - Generate contracts with: `k9iser generate .` - EOF - else - echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY" - fi - - # --------------------------------------------------------------------------- - # Job 3: Empty-linter β€” invisible character detection - # --------------------------------------------------------------------------- - empty-lint: - name: Empty-linter (invisible characters) - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Scan for invisible characters - id: lint + id: empty_lint + shell: bash run: | - # Inline invisible character detection (from empty-linter's core patterns). - # Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens, - # non-breaking spaces, null bytes, and other invisible Unicode in source files. - set +e - PATTERNS='\xc2\xa0|\xe2\x80\x8b|\xe2\x80\x8c|\xe2\x80\x8d|\xef\xbb\xbf|\xc2\xad|\xe2\x80\x8e|\xe2\x80\x8f|\xe2\x80\xaa|\xe2\x80\xab|\xe2\x80\xac|\xe2\x80\xad|\xe2\x80\xae|\x00' + set -euo pipefail + patterns='\xc2\xa0|\xe2\x80\x8b|\xe2\x80\x8c|\xe2\x80\x8d|\xef\xbb\xbf|\xc2\xad|\xe2\x80\x8e|\xe2\x80\x8f|\xe2\x80\xaa|\xe2\x80\xab|\xe2\x80\xac|\xe2\x80\xad|\xe2\x80\xae|\x00' + results_file="${RUNNER_TEMP}/empty-lint-results.txt" find "$GITHUB_WORKSPACE" \ -not -path '*/.git/*' -not -path '*/node_modules/*' \ -not -path '*/.deno/*' -not -path '*/target/*' \ @@ -141,231 +83,136 @@ jobs: -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ - -exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null - EL_EXIT=$? - set -e - - FINDINGS=$(wc -l < /tmp/empty-lint-results.txt 2>/dev/null || echo 0) - echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT" - echo "exit_code=$EL_EXIT" >> "$GITHUB_OUTPUT" - echo "ready=true" >> "$GITHUB_OUTPUT" - - # Emit annotations for each file with invisible chars + -exec grep -Prl "$patterns" {} \; > "$results_file" 2>/dev/null || true + findings=$(wc -l < "$results_file") + echo "findings=$findings" >> "$GITHUB_OUTPUT" while IFS= read -r filepath; do [ -z "$filepath" ] && continue - REL_PATH="${filepath#$GITHUB_WORKSPACE/}" - echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)" - done < /tmp/empty-lint-results.txt - - - name: Write summary - run: | - if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then - FINDINGS="${{ steps.lint.outputs.findings }}" - if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then - echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY" - else - echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo ":white_check_mark: No invisible character issues found." >> "$GITHUB_STEP_SUMMARY" - fi - else - echo "## Empty-Linter" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: empty-linter not available." >> "$GITHUB_STEP_SUMMARY" - fi + relative_path="${filepath#"$GITHUB_WORKSPACE"/}" + echo "::warning file=${relative_path}::Invisible Unicode characters detected" + done < "$results_file" - # --------------------------------------------------------------------------- - # Job 4: Groove manifest check (for repos that should expose services) - # --------------------------------------------------------------------------- - groove-check: - name: Groove manifest check - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Check for Groove manifest + - name: Check Groove integration id: groove + shell: bash run: | - # Check for static or dynamic Groove endpoints - HAS_MANIFEST="false" - HAS_GROOVE_CODE="false" - - if [ -f ".well-known/groove/manifest.json" ]; then - HAS_MANIFEST="true" - # Validate the manifest JSON + set -euo pipefail + has_manifest=false + has_groove_code=false + has_server=false + manifest_valid=true + if [ -f '.well-known/groove/manifest.json' ]; then + has_manifest=true if ! jq empty .well-known/groove/manifest.json 2>/dev/null; then + manifest_valid=false echo "::error file=.well-known/groove/manifest.json::Invalid JSON in Groove manifest" - else - SVC_ID=$(jq -r '.service_id // "unknown"' .well-known/groove/manifest.json) - echo "service_id=$SVC_ID" >> "$GITHUB_OUTPUT" fi fi - - # Check for Groove endpoint code (Rust, Elixir, Zig, V) - if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then - HAS_GROOVE_CODE="true" + if grep -rql 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null; then + has_groove_code=true fi - - # Check if this repo likely serves HTTP (has server/listener code) - HAS_SERVER="false" - if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null | head -1 | grep -q .; then - HAS_SERVER="true" + if grep -rql 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null; then + has_server=true fi - - echo "has_manifest=$HAS_MANIFEST" >> "$GITHUB_OUTPUT" - echo "has_groove_code=$HAS_GROOVE_CODE" >> "$GITHUB_OUTPUT" - echo "has_server=$HAS_SERVER" >> "$GITHUB_OUTPUT" - - if [ "$HAS_SERVER" = "true" ] && [ "$HAS_MANIFEST" = "false" ] && [ "$HAS_GROOVE_CODE" = "false" ]; then - echo "::warning::This repo has server code but no Groove endpoint. Add .well-known/groove/manifest.json for service discovery." + if [ "$has_server" = true ] && [ "$has_manifest" = false ] && [ "$has_groove_code" = false ]; then + echo "::warning::Server code found without a Groove endpoint" fi + { + echo "has_manifest=$has_manifest" + echo "has_groove_code=$has_groove_code" + echo "has_server=$has_server" + echo "manifest_valid=$manifest_valid" + } >> "$GITHUB_OUTPUT" - - name: Write summary - run: | - echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Static manifest (.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY" - - # --------------------------------------------------------------------------- - # Job 5: eclexiaiser manifest validation - # --------------------------------------------------------------------------- - eclexiaiser-validate: - name: Validate eclexiaiser manifest - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Check and validate eclexiaiser manifest - id: eclex + - name: Validate eclexiaiser manifest + id: eclexiaiser + continue-on-error: true + shell: bash run: | - if [ ! -f "eclexiaiser.toml" ]; then - # Check if repo has a Containerfile β€” if so, recommend eclexiaiser - if [ -f "Containerfile" ]; then - echo "::warning::Containerfile present but no eclexiaiser.toml. Run \`eclexiaiser init\` to scaffold energy/carbon budgets." + set -euo pipefail + if [ ! -f 'eclexiaiser.toml' ]; then + echo 'has_manifest=false' >> "$GITHUB_OUTPUT" + if [ -f 'Containerfile' ]; then + echo "::warning::Containerfile present but no eclexiaiser.toml" fi - echo "has_manifest=false" >> "$GITHUB_OUTPUT" exit 0 fi - - echo "has_manifest=true" >> "$GITHUB_OUTPUT" - - # Validate eclexiaiser.toml structure (bash + grep; no Python β€” estate no-Python rule). + echo 'has_manifest=true' >> "$GITHUB_OUTPUT" ok=1 - grep -qE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || { echo "::error::project.name is required"; ok=0; } - nfun=$(grep -cE '^[[:space:]]*\[\[functions\]\]' eclexiaiser.toml || true) - [ "${nfun:-0}" -ge 1 ] || { echo "::error::at least one [[functions]] entry is required"; ok=0; } - nname=$(grep -cE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || true) - nsrc=$(grep -cE '^[[:space:]]*source[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || true) - [ "${nname:-0}" -ge "$(( ${nfun:-0} + 1 ))" ] || { echo "::error::a function has an empty or missing name"; ok=0; } - [ "${nsrc:-0}" -ge "${nfun:-0}" ] || { echo "::error::a function has an empty or missing source"; ok=0; } - if [ "$ok" = 1 ]; then echo "Valid: eclexiaiser.toml (${nfun:-0} function(s))"; else echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml"; exit 1; fi - - - name: Write summary - run: | - if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then - echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY" - else - echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo ":ballot_box_with_check: No eclexiaiser.toml. Add one with \`eclexiaiser init\` for energy/carbon tracking." >> "$GITHUB_STEP_SUMMARY" - fi - - # --------------------------------------------------------------------------- - # Job 6: Dogfooding summary - # --------------------------------------------------------------------------- - dogfood-summary: - name: Dogfooding compliance summary - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [a2ml-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate] - if: always() - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Generate dogfooding scorecard + grep -qE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || { echo '::error::project.name is required'; ok=0; } + functions=$(grep -cE '^[[:space:]]*\[\[functions\]\]' eclexiaiser.toml || true) + [ "${functions:-0}" -ge 1 ] || { echo '::error::at least one [[functions]] entry is required'; ok=0; } + names=$(grep -cE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || true) + sources=$(grep -cE '^[[:space:]]*source[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || true) + [ "${names:-0}" -ge "$(( ${functions:-0} + 1 ))" ] || { echo '::error::a function has an empty or missing name'; ok=0; } + [ "${sources:-0}" -ge "${functions:-0}" ] || { echo '::error::a function has an empty or missing source'; ok=0; } + [ "$ok" -eq 1 ] + + - name: Write consolidated scorecard + if: always() + shell: bash + env: + A2ML_COUNT: ${{ steps.inventory.outputs.a2ml_count }} + K9_COUNT: ${{ steps.inventory.outputs.k9_count }} + CONFIG_COUNT: ${{ steps.inventory.outputs.config_count }} + A2ML_OUTCOME: ${{ steps.a2ml.outcome }} + K9_OUTCOME: ${{ steps.k9.outcome }} + EMPTY_FINDINGS: ${{ steps.empty_lint.outputs.findings }} + GROOVE_MANIFEST: ${{ steps.groove.outputs.has_manifest }} + GROOVE_CODE: ${{ steps.groove.outputs.has_groove_code }} + GROOVE_SERVER: ${{ steps.groove.outputs.has_server }} + GROOVE_VALID: ${{ steps.groove.outputs.manifest_valid }} + ECLEX_MANIFEST: ${{ steps.eclexiaiser.outputs.has_manifest }} + ECLEX_OUTCOME: ${{ steps.eclexiaiser.outcome }} run: | - SCORE=0 - MAX=6 - - # A2ML manifest present? - if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - A2ML_STATUS=":white_check_mark:" - else - A2ML_STATUS=":x:" - fi - - # K9 contracts present? - if find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - K9_STATUS=":white_check_mark:" - else - K9_STATUS=":x:" - fi - - # .editorconfig present? - if [ -f ".editorconfig" ]; then - SCORE=$((SCORE + 1)) - EC_STATUS=":white_check_mark:" - else - EC_STATUS=":x:" - fi - - # Groove manifest or code? - if [ -f ".well-known/groove/manifest.json" ] || grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' . 2>/dev/null | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - GROOVE_STATUS=":white_check_mark:" - else - GROOVE_STATUS=":ballot_box_with_check:" - fi - - # VeriSimDB integration? - if grep -rl 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null | head -1 | grep -q .; then - SCORE=$((SCORE + 1)) - VSDB_STATUS=":white_check_mark:" - else - VSDB_STATUS=":ballot_box_with_check:" - fi - - # eclexiaiser energy tracking? - if [ -f "eclexiaiser.toml" ]; then - SCORE=$((SCORE + 1)) - ECLEX_STATUS=":white_check_mark:" - else - ECLEX_STATUS=":ballot_box_with_check:" - fi - + set -euo pipefail + score=0 + a2ml_status=':x:' + k9_status=':x:' + editor_status=':x:' + groove_status=':ballot_box_with_check:' + verisim_status=':ballot_box_with_check:' + eclex_status=':ballot_box_with_check:' + if [ "${A2ML_COUNT:-0}" -gt 0 ] && [ "$A2ML_OUTCOME" = success ]; then score=$((score + 1)); a2ml_status=':white_check_mark:'; fi + if [ "${K9_COUNT:-0}" -gt 0 ] && [ "$K9_OUTCOME" = success ]; then score=$((score + 1)); k9_status=':white_check_mark:'; fi + if [ -f '.editorconfig' ]; then score=$((score + 1)); editor_status=':white_check_mark:'; fi + if [ "$GROOVE_MANIFEST" = true ] || [ "$GROOVE_CODE" = true ]; then score=$((score + 1)); groove_status=':white_check_mark:'; fi + if grep -rql 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null; then score=$((score + 1)); verisim_status=':white_check_mark:'; fi + if [ "$ECLEX_MANIFEST" = true ] && [ "$ECLEX_OUTCOME" = success ]; then score=$((score + 1)); eclex_status=':white_check_mark:'; fi + empty_status=':warning:' + if [ "${EMPTY_FINDINGS:-0}" -eq 0 ]; then empty_status=':white_check_mark:'; fi cat <> "$GITHUB_STEP_SUMMARY" ## Dogfooding Scorecard - **Score: ${SCORE}/${MAX}** - - | Tool/Format | Status | Notes | - |-------------|--------|-------| - | A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos | - | K9 contracts | ${K9_STATUS} | Required for repos with config files | - | .editorconfig | ${EC_STATUS} | Required for all repos | - | Groove endpoint | ${GROOVE_STATUS} | Required for service repos | - | VeriSimDB integration | ${VSDB_STATUS} | Required for stateful repos | - | eclexiaiser | ${ECLEX_STATUS} | Energy/carbon budgets for container services | - - --- - *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.* - *Dogfooding is guinea pig fooding β€” we test our tools on ourselves.* + **Score: ${score}/6** Β· **one runner** Β· A2ML ${A2ML_COUNT:-0} Β· K9 ${K9_COUNT:-0} Β· configs ${CONFIG_COUNT:-0} + + | Tool/format | Result | Detail | + |-------------|--------|--------| + | A2ML | ${a2ml_status} | validator: ${A2ML_OUTCOME} | + | K9 | ${k9_status} | validator: ${K9_OUTCOME} | + | Empty-linter | ${empty_status} | ${EMPTY_FINDINGS:-0} finding(s) | + | .editorconfig | ${editor_status} | repository policy | + | Groove | ${groove_status} | manifest=${GROOVE_MANIFEST}, code=${GROOVE_CODE}, server=${GROOVE_SERVER}, valid=${GROOVE_VALID} | + | VeriSimDB | ${verisim_status} | required only for stateful repositories | + | eclexiaiser | ${eclex_status} | validator: ${ECLEX_OUTCOME} | EOF + + - name: Enforce validator results + if: always() + shell: bash + env: + A2ML_OUTCOME: ${{ steps.a2ml.outcome }} + K9_OUTCOME: ${{ steps.k9.outcome }} + ECLEX_OUTCOME: ${{ steps.eclexiaiser.outcome }} + GROOVE_VALID: ${{ steps.groove.outputs.manifest_valid }} + run: | + set -euo pipefail + failed=0 + for result in "$A2ML_OUTCOME" "$K9_OUTCOME" "$ECLEX_OUTCOME"; do + if [ "$result" = failure ]; then failed=1; fi + done + if [ "$GROOVE_VALID" = false ]; then failed=1; fi + if [ "$failed" -ne 0 ]; then + echo '::error::One or more dogfood validators failed; see the consolidated scorecard' + exit 1 + fi diff --git a/.machine_readable/self-validating/examples/ci-config.k9.ncl b/.machine_readable/self-validating/examples/ci-config.k9.ncl index 9fe314e..5f7d493 100644 --- a/.machine_readable/self-validating/examples/ci-config.k9.ncl +++ b/.machine_readable/self-validating/examples/ci-config.k9.ncl @@ -1,4 +1,4 @@ -K9! +# K9! # SPDX-License-Identifier: MPL-2.0 # Example Yard-level K9 component: CI/CD configuration with validation # Security Level: Yard (Nickel evaluation, contract validation) diff --git a/.machine_readable/self-validating/examples/project-metadata.k9.ncl b/.machine_readable/self-validating/examples/project-metadata.k9.ncl index 5de965d..b83b8c0 100644 --- a/.machine_readable/self-validating/examples/project-metadata.k9.ncl +++ b/.machine_readable/self-validating/examples/project-metadata.k9.ncl @@ -1,4 +1,4 @@ -K9! +# K9! # SPDX-License-Identifier: MPL-2.0 # Example Kennel-level K9 component: Project metadata # Security Level: Kennel (pure data, no execution) diff --git a/.machine_readable/self-validating/examples/setup-repo.k9.ncl b/.machine_readable/self-validating/examples/setup-repo.k9.ncl index d1fc8bb..b21c9b3 100644 --- a/.machine_readable/self-validating/examples/setup-repo.k9.ncl +++ b/.machine_readable/self-validating/examples/setup-repo.k9.ncl @@ -1,4 +1,4 @@ -K9! +# K9! # SPDX-License-Identifier: MPL-2.0 # Example Hunt-level K9 component: Repository setup automation # Security Level: Hunt (full execution with Just recipes) diff --git a/.machine_readable/self-validating/methodology-guard.k9.ncl b/.machine_readable/self-validating/methodology-guard.k9.ncl index 17f1409..0239bb9 100644 --- a/.machine_readable/self-validating/methodology-guard.k9.ncl +++ b/.machine_readable/self-validating/methodology-guard.k9.ncl @@ -1,3 +1,4 @@ +# K9! # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # @@ -12,6 +13,17 @@ let methodology_guard = { version = "1.0.0", description = "Validates that agent work respects declared methodology constraints", + pedigree = { + schema_version = "1.0.0", + metadata = { + name = "methodology-guard", + version = "1.0.0", + }, + security = { + leash = 'Yard, + }, + }, + checks = { divergent_invariant_language = { description = "No files in languages violating the divergent language invariant", diff --git a/.machine_readable/self-validating/template-hunt.k9.ncl b/.machine_readable/self-validating/template-hunt.k9.ncl index b3fcb47..7217988 100644 --- a/.machine_readable/self-validating/template-hunt.k9.ncl +++ b/.machine_readable/self-validating/template-hunt.k9.ncl @@ -1,4 +1,4 @@ -K9! +# K9! # SPDX-License-Identifier: MPL-2.0 # K9 Hunt-level template: Full execution with Just recipes # Security Level: Hunt (full system access) diff --git a/.machine_readable/self-validating/template-kennel.k9.ncl b/.machine_readable/self-validating/template-kennel.k9.ncl index 4228b26..03bbd0f 100644 --- a/.machine_readable/self-validating/template-kennel.k9.ncl +++ b/.machine_readable/self-validating/template-kennel.k9.ncl @@ -1,4 +1,4 @@ -K9! +# K9! # SPDX-License-Identifier: MPL-2.0 # K9 Kennel-level template: Pure data configuration # Security Level: Kennel (data-only, no execution) diff --git a/.machine_readable/self-validating/template-yard.k9.ncl b/.machine_readable/self-validating/template-yard.k9.ncl index a723f5a..328de27 100644 --- a/.machine_readable/self-validating/template-yard.k9.ncl +++ b/.machine_readable/self-validating/template-yard.k9.ncl @@ -1,4 +1,4 @@ -K9! +# K9! # SPDX-License-Identifier: MPL-2.0 # K9 Yard-level template: Configuration with validation # Security Level: Yard (Nickel evaluation with contracts) diff --git a/container/deploy.k9.ncl b/container/deploy.k9.ncl index d08054e..769737c 100644 --- a/container/deploy.k9.ncl +++ b/container/deploy.k9.ncl @@ -1,3 +1,4 @@ +# K9! # SPDX-License-Identifier: MPL-2.0 # deploy.k9.ncl β€” Haec deployment component (Hunt level) # @@ -143,7 +144,7 @@ echo "K9: Rollback complete." # Export the component { - pedigree = component_pedigree, + pedigree = component_pedigree & { name = "haec-deploy" }, deployment = deployment, scripts = scripts, diff --git a/coordination.k9 b/coordination.k9 index ba31125..d40f3eb 100644 --- a/coordination.k9 +++ b/coordination.k9 @@ -1,5 +1,18 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 # Thin coordination bindings for central session-management standards +pedigree = { + schema_version = "1.0.0", + metadata = { + name = "coordination", + version = "1.0.0", + }, + security = { + leash = 'Kennel, + }, +} + session_management: source_of_truth: "standards/session-management-standards" canonical_commands: diff --git a/session/custom-checks.k9 b/session/custom-checks.k9 index bd932fa..8ce6225 100644 --- a/session/custom-checks.k9 +++ b/session/custom-checks.k9 @@ -1,6 +1,19 @@ +K9! +# SPDX-License-Identifier: MPL-2.0 # Local repository session checks (thin policy layer) version: "0.1" +pedigree = { + schema_version = "1.0.0", + metadata = { + name = "session-custom-checks", + version = "0.1.0", + }, + security = { + leash = 'Kennel, + }, +} + checks: - id: "session-state-has-next-action" applies_to: ["close planned", "close urgent", "handover full", "handover split", "handover model", "handover human"] From e5c86e83a5383ecba580720203995e7d90a4a4c9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:34:25 +0100 Subject: [PATCH 2/2] ci: consolidate Haec gates and repair FFI validation --- .github/dependabot.yml | 50 +----- .github/workflows/boj-build.yml | 45 ----- .github/workflows/ci.yml | 65 +++++++ .github/workflows/codeql.yml | 3 + .github/workflows/dependabot-automerge.yml | 136 -------------- .github/workflows/e2e.yml | 186 -------------------- .github/workflows/estate-rules.yml | 9 +- .github/workflows/governance.yml | 6 +- .github/workflows/hypatia-scan.yml | 19 -- .github/workflows/instant-sync.yml | 7 +- .github/workflows/mirror.yml | 12 -- .github/workflows/npm-bun-blocker.yml | 30 ---- .github/workflows/openssf-compliance.yml | 3 + .github/workflows/pages.yml | 53 ------ .github/workflows/push-email-notify.yml | 33 ---- .github/workflows/quality.yml | 63 ------- .github/workflows/release.yml | 186 +++++++------------- .github/workflows/rhodibot.yml | 2 - .github/workflows/rsr-antipattern.yml | 12 -- .github/workflows/rust-ci.yml | 14 -- .github/workflows/scorecard.yml | 8 +- .github/workflows/security-policy.yml | 53 ------ .github/workflows/static-analysis-gate.yml | 72 ++++---- .github/workflows/trope-check.yml | 5 +- .github/workflows/wellknown-enforcement.yml | 10 +- .github/workflows/workflow-linter.yml | 4 + Justfile | 95 +++------- docs/status/TEST-NEEDS.adoc | 168 +++++++----------- docs/tech-debt-2026-05-26.adoc | 81 +++++++++ docs/tech-debt-2026-05-26.md | 70 -------- src/interface/Abi/Foreign.idr | 26 +-- src/interface/ffi/build.zig | 61 +++++-- src/interface/ffi/src/main.zig | 106 ++++++----- src/interface/ffi/test/integration_test.zig | 121 +++++++------ tests/aspect_tests.sh | 19 +- tests/workflows/validate_workflows_test.sh | 42 +++-- 36 files changed, 610 insertions(+), 1265 deletions(-) delete mode 100644 .github/workflows/boj-build.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/dependabot-automerge.yml delete mode 100644 .github/workflows/e2e.yml delete mode 100644 .github/workflows/hypatia-scan.yml delete mode 100644 .github/workflows/mirror.yml delete mode 100644 .github/workflows/npm-bun-blocker.yml delete mode 100755 .github/workflows/pages.yml delete mode 100644 .github/workflows/push-email-notify.yml delete mode 100644 .github/workflows/quality.yml delete mode 100644 .github/workflows/rsr-antipattern.yml delete mode 100644 .github/workflows/rust-ci.yml delete mode 100644 .github/workflows/security-policy.yml create mode 100644 docs/tech-debt-2026-05-26.adoc delete mode 100644 docs/tech-debt-2026-05-26.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1af529e..b8d2993 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,57 +1,17 @@ # SPDX-License-Identifier: MPL-2.0 -# Dependabot configuration for RSR-compliant repositories -# Covers common ecosystems - remove unused ones for your project +# Dependabot configuration for Haec's current dependency surface. version: 2 updates: - # GitHub Actions - always include - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + day: "wednesday" + time: "13:37" + timezone: "Europe/London" + open-pull-requests-limit: 1 groups: actions: patterns: - "*" - - # Rust/Cargo - # - # `open-pull-requests-limit: 0` suppresses routine version-update PRs - # (no weekly patch-bump noise) while leaving Dependabot SECURITY PRs - # flowing. Under GitHub's current Dependabot behaviour (2024+), using - # an `ignore:` rule with `version-update:semver-patch` would ALSO - # silence security PRs that happen to be patch-level β€” historically - # the cause of estate-wide vulns sitting un-PR'd for weeks. - # `open-pull-requests-limit: 0` is the GitHub-endorsed way to say - # "security only, not routine bumps". Pair with the - # dependabot-automerge.yml workflow for low-touch security - # maintenance. - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 - - # Elixir/Mix - - package-ecosystem: "mix" - directory: "/" - schedule: - interval: "weekly" - - # Node.js/npm - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - - # Python/pip - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - - # Nix flakes - - package-ecosystem: "nix" - directory: "/" - schedule: - interval: "weekly" diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml deleted file mode 100644 index ad42433..0000000 --- a/.github/workflows/boj-build.yml +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# -# OPTIONAL: BoJ Server Build Trigger -# This workflow notifies a BoJ Server instance when code is pushed. -# It is a no-op if BOJ_SERVER_URL is not set or the server is unreachable. -# To enable: set BOJ_SERVER_URL as a repository secret or variable. -# To disable: delete this file or leave BOJ_SERVER_URL unset. -name: "βš™ Auto: BoJ server build trigger" -on: - push: - branches: [main, master] - workflow_dispatch: -permissions: - contents: read -jobs: - trigger-boj: - runs-on: ubuntu-latest - timeout-minutes: 15 - if: ${{ vars.BOJ_SERVER_URL != '' || secrets.BOJ_SERVER_URL != '' }} - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Trigger BoJ Server (Casket/ssg-mcp) - env: - BOJ_URL: ${{ secrets.BOJ_SERVER_URL || vars.BOJ_SERVER_URL }} - REPO_NAME: ${{ github.repository }} - BRANCH_NAME: ${{ github.ref_name }} - run: | - set -euo pipefail - - if [ -z "$BOJ_URL" ]; then - echo "BOJ_SERVER_URL not configured - skipping" - exit 0 - fi - - payload="$(jq -cn \ - --arg repo "$REPO_NAME" \ - --arg branch "$BRANCH_NAME" \ - --arg engine "casket" \ - '{repo:$repo, branch:$branch, engine:$engine}')" - - curl -sf -X POST "${BOJ_URL}/cartridges/ssg-mcp/invoke" \ - -H "Content-Type: application/json" \ - --data "$payload" \ - || echo "BoJ server unreachable - skipping (non-fatal)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3f2868e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: MPL-2.0 +name: "CI / required" + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + required: + name: "CI / required" + runs-on: ubuntu-latest + timeout-minutes: 20 + container: + # Idris 2 0.7.0, pinned by immutable image digest. + image: ghcr.io/stefan-hoeck/idris2-pack@sha256:de9781906050dc44704ec6de0108c86f899ef17b2642932b79e00245d613b8ad + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Zig + uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 + with: + version: 0.15.2 + + - name: Typecheck the Idris2 ABI + shell: bash + run: | + set -euo pipefail + idris2 --version | grep -Fx 'Idris 2, version 0.7.0' + idris2 --typecheck abi.ipkg + + - name: Compile and test the Zig FFI + shell: bash + run: | + set -euo pipefail + test "$(zig version)" = '0.15.2' + zig fmt --check \ + src/interface/ffi/build.zig \ + src/interface/ffi/src/main.zig \ + src/interface/ffi/test/integration_test.zig + ( + cd src/interface/ffi + zig build --summary all + zig build test --summary all + ) + + - name: Validate repository contracts + shell: bash + run: | + set -euo pipefail + find tests scripts -type f -name '*.sh' -print0 \ + | xargs -0 -r bash -n + bash tests/check-examples.sh + bash tests/aspect_tests.sh + bash scripts/check-root-shape.sh . + bash tests/workflows/validate_workflows_test.sh diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9303bdc..6ac0936 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,6 +9,9 @@ on: - cron: '0 6 * * 1' permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: analyze: runs-on: ubuntu-latest diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml deleted file mode 100644 index c2ada47..0000000 --- a/.github/workflows/dependabot-automerge.yml +++ /dev/null @@ -1,136 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# -# dependabot-automerge.yml β€” enable GitHub's native auto-merge on -# Dependabot pull requests that match a declared severity / ecosystem -# policy. Pairs with `.github/dependabot.yml`'s -# `open-pull-requests-limit: 0` + security-only pattern (see the -# cargo block there). -# -# What this does: -# - Triggers on every Dependabot PR. -# - Reads the PR's update-type metadata via the dependabot/fetch-metadata -# action (no free-text parsing). -# - Requires CI to be green before merge (GitHub's auto-merge enforces -# required status checks). -# - Gates merge behind a severity+ecosystem policy table. Default is -# low+medium security updates only. -# -# Why auto-merge on GitHub (not via a bot like rhodibot) is the right -# layer: GitHub enforces branch protection + required checks natively, -# and the PR author is already `dependabot[bot]`. Rhodibot doesn't need -# to know anything about ecosystems β€” GitHub handles the merge mechanics -# once we approve. -# -# Threat model: -# - A compromised upstream package with a bogus security advisory -# could propose a malicious version bump. Mitigation: require at -# least one non-automated reviewer for HIGH+CRITICAL severity -# (done below β€” we explicitly refuse to auto-approve those). -# - A compromised Dependabot itself is an Akerlof claim-grounder -# problem. Not in scope here; track under -# `project_claim_grounders_dual_use_akerlof.md`. -# -# Dogfooding: this workflow template is itself subject to the same -# Dependabot config via the github-actions ecosystem block, so SHA -# bumps for dependabot/fetch-metadata flow through the same path. - -name: "βš™ Auto: Dependabot auto-merge" -on: - pull_request: - types: [opened, reopened, synchronize] -permissions: - contents: write # needed to enable auto-merge - pull-requests: write # needed to approve - # NB: keep narrow β€” do NOT add secrets: read or id-token: write here. -jobs: - automerge: - # Only run for PRs actually authored by Dependabot. - if: github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Fetch Dependabot metadata - id: meta - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - # --- Policy gate ------------------------------------------------------- - # Outputs from fetch-metadata we care about: - # update-type β†’ version-update:semver-{patch,minor,major} - # dependency-type β†’ direct:{development,production} | indirect - # alert-state β†’ AUTO_DISMISSED | DISMISSED | FIXED | OPEN - # ghsa-id β†’ GHSA-... if this is a security PR - # --- Policy ------------------------------------------------------------- - # AUTO-APPROVE + AUTO-MERGE when: - # 1. This is a SECURITY update (ghsa-id present), AND - # 2. Update is patch or minor, AND - # 3. Severity ≀ moderate (Dependabot doesn't expose severity - # directly in fetch-metadata; infer from the absence of - # HIGH/CRITICAL labels added by Dependabot). - # Otherwise: do nothing. Human reviews HIGH+CRITICAL security - # updates and all non-security bumps. - - name: Decide policy outcome - id: policy - env: - GHSA_ID: ${{ steps.meta.outputs.ghsa-id }} - UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} - PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} - run: | - set -euo pipefail - - is_security=false - is_patch_or_minor=false - is_high_or_critical=false - - [ -n "$GHSA_ID" ] && is_security=true - case "$UPDATE_TYPE" in - version-update:semver-patch|version-update:semver-minor) - is_patch_or_minor=true ;; - esac - - # Dependabot adds severity labels like "severity: high", - # "severity: critical". Look for those in the PR labels JSON. - if echo "$PR_LABELS" | grep -qiE '"(severity: (high|critical))"'; then - is_high_or_critical=true - fi - - if $is_security && $is_patch_or_minor && ! $is_high_or_critical; then - echo "action=automerge" >> "$GITHUB_OUTPUT" - else - echo "action=skip" >> "$GITHUB_OUTPUT" - fi - echo "security=$is_security" >> "$GITHUB_OUTPUT" - echo "update_type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT" - echo "ghsa=$GHSA_ID" >> "$GITHUB_OUTPUT" - - name: Approve PR (if policy allows) - if: steps.policy.outputs.action == 'automerge' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ github.event.pull_request.html_url }} - run: | - gh pr review --approve "$PR_URL" \ - --body "Auto-approving Dependabot security update (${{ steps.policy.outputs.ghsa }}, ${{ steps.policy.outputs.update_type }}). Policy: low/moderate security patches/minors only." - - name: Enable auto-merge (if policy allows) - if: steps.policy.outputs.action == 'automerge' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ github.event.pull_request.html_url }} - run: | - gh pr merge --auto --squash "$PR_URL" - - name: Write decision to step summary - env: - ACTION: ${{ steps.policy.outputs.action }} - IS_SECURITY: ${{ steps.policy.outputs.security }} - UPDATE_TYPE: ${{ steps.policy.outputs.update_type }} - GHSA: ${{ steps.policy.outputs.ghsa }} - run: | - { - echo "## Dependabot Auto-Merge Decision" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| Policy action | \`$ACTION\` |" - echo "| Security update | \`$IS_SECURITY\` |" - echo "| Update type | \`$UPDATE_TYPE\` |" - echo "| GHSA ID | \`${GHSA:-n/a}\` |" - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index 0b1cd64..0000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,186 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# RSR Standard E2E + Aspect + Benchmark Workflow Template -# -# 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. - -name: "🟑 Check: E2E + aspect + bench" -on: - push: - branches: [main, master, develop] - paths: - - 'src/**' - - 'ffi/**' - - 'tests/**' - - '.github/workflows/e2e.yml' - pull_request: - branches: [main, master] - paths: - - 'src/**' - - 'ffi/**' - - 'tests/**' - 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.1 -# - 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/ - -## === 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 - -## === 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.1 -# - run: cd ffi/zig && zig build bench - -# ─── Readiness (CRG) ────────────────────────────────────────────── -# Component Readiness Grade: D (runs) β†’ C (correct) β†’ B (edge cases) - -# 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 diff --git a/.github/workflows/estate-rules.yml b/.github/workflows/estate-rules.yml index 8bd5626..072cd4d 100644 --- a/.github/workflows/estate-rules.yml +++ b/.github/workflows/estate-rules.yml @@ -4,7 +4,9 @@ # Estate Rules β€” enforces hyperpolymath estate-wide conventions: # * root shape (allowlist of permitted root entries) # * AsciiDoc-by-default (no .md files under docs/) -# * zig is banned (no zig scaffolding or references) +# +# Haec intentionally implements its FFI in Zig, so the generic estate-wide +# "no Zig" migration rule is not applicable to this repository. # # Each rule is enforced by an executable check script under scripts/. Failures # are surfaced as workflow errors so the rule can't silently regress. @@ -16,6 +18,9 @@ on: pull_request: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: estate-rules: runs-on: ubuntu-latest @@ -27,5 +32,3 @@ jobs: run: bash scripts/check-root-shape.sh . - name: AsciiDoc by default (no .md under docs/) run: bash scripts/check-no-md-in-docs.sh . - - name: No zig references - run: bash scripts/check-no-vlang.sh . diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 19c0831..7b13dd2 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -11,6 +11,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@d7c22711e830e1f383846472f6e9b99debdb201e \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@d7c22711e830e1f383846472f6e9b99debdb201e diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml deleted file mode 100644 index 9e729eb..0000000 --- a/.github/workflows/hypatia-scan.yml +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: "🟑 Check: Hypatia baseline security scan" - -on: - push: - branches: [main, master, develop] - pull_request: - branches: [main, master] - schedule: - - cron: '0 0 * * 0' - workflow_dispatch: - -permissions: - contents: read - security-events: read - -jobs: - scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@d7c22711e830e1f383846472f6e9b99debdb201e \ No newline at end of file diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index 9dbd3f4..199b9c3 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -2,10 +2,9 @@ # Instant Forge Sync - Triggers propagation to all forges on push/release name: "βš™ Auto: Instant sync" on: - push: - branches: [main, master] - release: - types: [published] + # Automatic propagation stays off until FARM_DISPATCH_TOKEN and a canary + # dispatch have been verified. This prevents one push becoming fleet fan-out. + workflow_dispatch: permissions: contents: read jobs: diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml deleted file mode 100644 index cfbf907..0000000 --- a/.github/workflows/mirror.yml +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: "βš™ Auto: Mirror to forges" -on: - push: - branches: [main] - workflow_dispatch: -permissions: - contents: read -jobs: - mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236 - secrets: inherit diff --git a/.github/workflows/npm-bun-blocker.yml b/.github/workflows/npm-bun-blocker.yml deleted file mode 100644 index e1016ac..0000000 --- a/.github/workflows/npm-bun-blocker.yml +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: "πŸ”΄ Gate: No-npm/bun policy" -on: - push: - branches: [main, master] - pull_request: - -# Estate guardrail: scope push to default branches so a PR fires once (not -# push+PR), and cancel superseded runs. Safe β€” read-only PR-triggered check. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read -jobs: - check: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Block npm/bun - run: | - if [ -f "package-lock.json" ] || [ -f "bun.lockb" ] || [ -f ".npmrc" ]; then - echo "❌ npm/bun artifacts detected. Use Deno instead." - exit 1 - fi - echo "βœ… No npm/bun violations" diff --git a/.github/workflows/openssf-compliance.yml b/.github/workflows/openssf-compliance.yml index bd1671f..84a2ee1 100644 --- a/.github/workflows/openssf-compliance.yml +++ b/.github/workflows/openssf-compliance.yml @@ -10,6 +10,9 @@ on: workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: openssf-compliance: runs-on: ubuntu-latest diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100755 index 649dcb1..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: GitHub Pages (Ddraig SSG) -on: - push: - branches: [main, master] - workflow_dispatch: -permissions: - contents: read - pages: write - id-token: write -concurrency: - group: "pages" - cancel-in-progress: false -jobs: - build: - runs-on: ubuntu-latest - timeout-minutes: 15 - container: - image: ghcr.io/stefan-hoeck/idris2-pack@sha256:f0758996a931fb35d9ecb1de273c4d59dabe2a09b433afc7e357f65a08b7e1ff - steps: - - name: Checkout Site - uses: actions/checkout@v4 - - name: Checkout Ddraig SSG - uses: actions/checkout@v4 - with: - repository: hyperpolymath/ddraig-ssg - path: .ddraig-ssg - - name: Compile Ddraig - working-directory: .ddraig-ssg - run: idris2 Ddraig.idr -o ddraig - - name: Build site - run: | - mkdir -p src - if [ ! -f src/index.md ] && [ -f README.md ]; then - cp README.md src/index.md - elif [ ! -f src/index.md ]; then - echo "# ${GITHUB_REPOSITORY}" > src/index.md - fi - ./.ddraig-ssg/build/exec/ddraig build src _site https://hyperpolymath.github.io/${GITHUB_REPOSITORY#*/} - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: '_site' - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/push-email-notify.yml b/.github/workflows/push-email-notify.yml deleted file mode 100644 index 0816771..0000000 --- a/.github/workflows/push-email-notify.yml +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Dormant push-email notification. ARMED by setting the repo variable -# PUSH_EMAIL_ENABLED=true (the single on/off switch). Addresses are pre-filled; -# sending needs the org SMTP secrets (SMTP_HOST/PORT/USER/PASS). Inherited by -# new repos from the template; placed on existing repos by the farm sweep. -name: Push email notification -on: - push: {} -permissions: - contents: read -jobs: - notify: - name: Email on push - if: ${{ vars.PUSH_EMAIL_ENABLED == 'true' }} - runs-on: ubuntu-latest - steps: - - name: Send push notification email - uses: dawidd6/action-send-mail@c50dc4cc848ade21f848990889906d804fae78c5 # pinned - with: - server_address: ${{ secrets.SMTP_HOST }} - server_port: ${{ secrets.SMTP_PORT }} - secure: true - username: ${{ secrets.SMTP_USER }} - password: ${{ secrets.SMTP_PASS }} - from: "GitHub Push <${{ secrets.SMTP_USER }}>" - to: "jonathan.jewell@gmail.com j.d.a.jewell@open.ac.uk" - subject: "[${{ github.repository }}] push to ${{ github.ref_name }} by ${{ github.actor }}" - body: | - Repository: ${{ github.repository }} - Branch: ${{ github.ref_name }} - Pusher: ${{ github.actor }} - Compare: ${{ github.event.compare }} - Head msg: ${{ github.event.head_commit.message }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml deleted file mode 100644 index f62a379..0000000 --- a/.github/workflows/quality.yml +++ /dev/null @@ -1,63 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: "πŸ”΄ Gate: Code quality (fmt/lint/test)" -on: - push: - branches: [main, master] - pull_request: - -# Estate guardrail: scope push to default branches so a PR fires once (not -# push+PR), and cancel superseded runs. Safe β€” read-only PR-triggered check. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - -permissions: - contents: read -jobs: - lint: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Check file permissions - run: | - find . -type f -perm /111 -name "*.sh" | head -10 || true - - name: Check for secrets - uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.93.3 - with: - path: ./ - base: ${{ github.event.pull_request.base.sha || github.event.before }} - head: ${{ github.sha }} - continue-on-error: true - - name: Check TODO/FIXME - run: | - echo "=== TODOs ===" - grep -rn "TODO\|FIXME\|HACK\|XXX" --include="*.rs" --include="*.res" --include="*.py" --include="*.ex" . | head -20 || echo "None found" - - name: Check for large files - run: | - find . -type f -size +1M -not -path "./.git/*" | head -10 || echo "No large files" - - name: EditorConfig check - uses: editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c # v2.2.0 - continue-on-error: true - docs: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Check documentation - run: | - MISSING="" - [ ! -f "README.md" ] && [ ! -f "README.adoc" ] && MISSING="$MISSING README" - [ ! -f "LICENSE" ] && [ ! -f "LICENSE.txt" ] && [ ! -f "LICENSE.md" ] && MISSING="$MISSING LICENSE" - [ ! -f "CONTRIBUTING.md" ] && [ ! -f "CONTRIBUTING.adoc" ] && MISSING="$MISSING CONTRIBUTING" - - if [ -n "$MISSING" ]; then - echo "::warning::Missing docs:$MISSING" - else - echo "βœ… Core documentation present" - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c41cbc..bcf4e7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,153 +1,87 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# Release workflow β€” triggered by version tags (v*). -# Builds artifacts, generates changelog via git-cliff, creates a GitHub Release, -# and produces SLSA provenance attestations. name: "βš™ Auto: Release" + on: push: tags: - 'v*' + permissions: contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: build: - name: Build Artifacts + name: Build libhaec runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read + timeout-minutes: 20 + container: + image: ghcr.io/stefan-hoeck/idris2-pack@sha256:de9781906050dc44704ec6de0108c86f899ef17b2642932b79e00245d613b8ad steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Detect project type and build - id: build - run: | - # Auto-detect build system from project files. - # Order matters: more specific markers checked first. - if [ -f "mix.exs" ]; then - echo "::notice::Detected Elixir/Gleam project (mix.exs)" - echo "build_type=mix" >> "$GITHUB_OUTPUT" - mix local.hex --force --if-missing - mix local.rebar --force --if-missing - mix deps.get --only prod - MIX_ENV=prod mix release - elif [ -f "Cargo.toml" ]; then - echo "::notice::Detected Rust project (Cargo.toml)" - echo "build_type=cargo" >> "$GITHUB_OUTPUT" - cargo build --release - elif [ -f "build.zig" ]; then - echo "::notice::Detected Zig project (build.zig)" - echo "build_type=zig" >> "$GITHUB_OUTPUT" - zig build -Doptimize=ReleaseSafe - elif [ -f "deno.json" ] || [ -f "deno.jsonc" ]; then - echo "::notice::Detected Deno project (deno.json)" - echo "build_type=deno" >> "$GITHUB_OUTPUT" - deno task build - elif [ -f "gossamer.conf.json" ]; then - echo "::notice::Detected Gossamer project (gossamer.conf.json)" - echo "build_type=gossamer" >> "$GITHUB_OUTPUT" - gossamer build - elif [ -f "gleam.toml" ]; then - echo "::notice::Detected Gleam project (gleam.toml)" - echo "build_type=gleam" >> "$GITHUB_OUTPUT" - gleam build - elif [ -f "rebar.config" ]; then - echo "::notice::Detected Erlang/Rebar project (rebar.config)" - echo "build_type=rebar" >> "$GITHUB_OUTPUT" - rebar3 as prod release - elif [ -f "Justfile" ] || [ -f "justfile" ]; then - echo "::notice::Detected Justfile β€” running 'just build'" - echo "build_type=just" >> "$GITHUB_OUTPUT" - just build - else - echo "::error::No recognised build system found." - echo "Expected one of: mix.exs, Cargo.toml, build.zig, deno.json, gossamer.conf.json, gleam.toml, rebar.config, Justfile" - exit 1 - fi - # TODO: Upload build artifacts if needed - # - uses: actions/upload-artifact@v4 - # with: - # name: release-artifacts - # path: target/release/ - changelog: - name: Generate Changelog - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - outputs: - changelog: ${{ steps.cliff.outputs.content }} - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Zig + uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 with: - fetch-depth: 0 - - name: Extract version from tag - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - - name: Install git-cliff - run: | - curl -sSfL https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-$(uname -m)-unknown-linux-gnu.tar.gz \ - | tar -xz --strip-components=1 -C /usr/local/bin/ git-cliff-*/git-cliff - - name: Generate changelog for this release - id: cliff + version: 0.15.2 + + - name: Typecheck and build release libraries + shell: bash run: | - # Generate changelog for the current tag only - CHANGELOG=$(git cliff --latest --strip header) - # Write to output using delimiter to handle multiline - { - echo "content<> "$GITHUB_OUTPUT" - - name: Update full CHANGELOG.md + set -euo pipefail + idris2 --version | grep -Fx 'Idris 2, version 0.7.0' + idris2 --typecheck abi.ipkg + ( + cd src/interface/ffi + zig build -Doptimize=ReleaseSafe --summary all + zig build test -Doptimize=ReleaseSafe --summary all + ) + + - name: Package libraries and checksums + shell: bash + env: + RELEASE_TAG: ${{ github.ref_name }} run: | - git cliff --output CHANGELOG.md - - name: Upload updated CHANGELOG.md + set -euo pipefail + archive="haec-${RELEASE_TAG}-linux-x86_64.tar.gz" + tar -C src/interface/ffi/zig-out/lib -czf "$archive" libhaec.a libhaec.so + sha256sum "$archive" > "${archive}.sha256" + + - name: Upload release artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: changelog - path: CHANGELOG.md - retention-days: 5 + name: haec-release + path: | + haec-*.tar.gz + haec-*.tar.gz.sha256 + if-no-files-found: error + retention-days: 7 + release: - name: Create GitHub Release - needs: [build, changelog] + name: Publish GitHub release + needs: [build] runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 10 permissions: contents: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - # TODO: Download build artifacts if uploading to the release - # - uses: actions/download-artifact@v4 - # with: - # name: release-artifacts - # path: artifacts/ - - name: Create GitHub Release + - name: Download verified build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 + with: + name: haec-release + path: artifacts + + - name: Publish release uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v2 with: - body: ${{ needs.changelog.outputs.changelog }} draft: false prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} - generate_release_notes: false - # TODO: Add artifact files to the release - # files: | - # artifacts/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - provenance: - name: SLSA Provenance - needs: [build] - permissions: - actions: read - id-token: write - contents: write - # SLSA generator must run in a separate, isolated workflow - # See: https://slsa.dev/spec/v1.0/requirements#build-l3 - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 - with: - base64-subjects: "" - # TODO: Replace with actual artifact hashes - # Generate with: sha256sum artifact | base64 -w0 - # base64-subjects: "${{ needs.build.outputs.hashes }}" + generate_release_notes: true + files: | + artifacts/haec-*.tar.gz + artifacts/haec-*.tar.gz.sha256 diff --git a/.github/workflows/rhodibot.yml b/.github/workflows/rhodibot.yml index bb3d5bf..3bf6197 100644 --- a/.github/workflows/rhodibot.yml +++ b/.github/workflows/rhodibot.yml @@ -10,8 +10,6 @@ name: "βš™ Auto: Rhodibot RSR audit (report-only)" on: - schedule: - - cron: '0 6 * * 1' # Every Monday at 06:00 UTC workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/rsr-antipattern.yml b/.github/workflows/rsr-antipattern.yml deleted file mode 100644 index 93da6de..0000000 --- a/.github/workflows/rsr-antipattern.yml +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# RSR Anti-Pattern Check - Uses reusable workflow from standards - -name: "πŸ”΄ Gate: RSR anti-pattern scan" -on: - push: - branches: [main, master, develop] - pull_request: - branches: [main, master, develop] -jobs: - antipattern-check: - uses: hyperpolymath/standards/.github/workflows/rsr-antipattern-reusable.yml@main diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml deleted file mode 100644 index ad9fe2c..0000000 --- a/.github/workflows/rust-ci.yml +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Rust CI β€” thin wrapper calling the shared estate reusable in -# hyperpolymath/standards. Configure once, propagate everywhere. -# See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards. -name: "🟑 Check: Rust CI (build/test)" -on: - push: - branches: [main, master] - pull_request: -permissions: - contents: read -jobs: - rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@412a7031577112b31ee287cc6060179d638d6500 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 790b9e1..0a6cbb0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -2,15 +2,17 @@ name: "βšͺ Advisory: OpenSSF Scorecard (score)" on: - push: - branches: [main, master] schedule: - - cron: '0 4 * * *' + - cron: '17 4 * * 0' workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: scorecard: uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@d7c22711e830e1f383846472f6e9b99debdb201e diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml deleted file mode 100644 index 863658d..0000000 --- a/.github/workflows/security-policy.yml +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: "πŸ”΄ Gate: Security policy present" -on: - push: - branches: [main, master] - pull_request: - -# Estate guardrail: scope push to default branches so a PR fires once (not -# push+PR), and cancel superseded runs. Safe β€” read-only PR-triggered check. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read -jobs: - check: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Security checks - run: | - FAILED=false - - # Block MD5/SHA1 for security (allow for checksums/caching) - WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) - if [ -n "$WEAK_CRYPTO" ]; then - echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" - echo "$WEAK_CRYPTO" - fi - - # Block HTTP URLs (except localhost) - HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) - if [ -n "$HTTP_URLS" ]; then - echo "⚠️ HTTP URLs found. Use HTTPS:" - echo "$HTTP_URLS" - fi - - # Block hardcoded secrets patterns - SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true) - if [ -n "$SECRETS" ]; then - echo "❌ Potential hardcoded secrets detected!" - FAILED=true - fi - - if [ "$FAILED" = true ]; then - exit 1 - fi - - echo "βœ… Security policy check passed" diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index 2315831..696ff13 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -9,6 +9,10 @@ on: branches: [main, master] permissions: contents: read + security-events: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: # --------------------------------------------------------------------------- # Job 1: panic-attack assail @@ -119,40 +123,43 @@ jobs: with: fetch-depth: 0 - name: Setup Elixir for Hypatia scanner - id: beam - continue-on-error: true uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.18.2 with: elixir-version: '1.19.4' otp-version: '28.3' - - name: Clone and build Hypatia - id: build - continue-on-error: true + - name: Fetch and build pinned Hypatia + env: + HYPATIA_COMMIT: 3d73b66d0f4ada3527b947ddb2c3b9f88d945d45 run: | - git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" 2>/dev/null || true - if [ -f "$HOME/hypatia/mix.exs" ]; then - cd "$HOME/hypatia" - # Build escript if neither hypatia nor hypatia-v2 exists - if [ ! -f hypatia ] && [ ! -f hypatia-v2 ]; then - mix deps.get - mix escript.build - fi - echo "ready=true" >> "$GITHUB_OUTPUT" - else - echo "::notice::Hypatia scanner not available β€” skipping scan" - echo "ready=false" >> "$GITHUB_OUTPUT" - fi + set -euo pipefail + scanner_dir="${RUNNER_TEMP}/hypatia" + git init "$scanner_dir" + git -C "$scanner_dir" remote add origin https://github.com/hyperpolymath/hypatia.git + git -C "$scanner_dir" fetch --depth 1 origin "$HYPATIA_COMMIT" + git -C "$scanner_dir" checkout --detach FETCH_HEAD + test "$(git -C "$scanner_dir" rev-parse HEAD)" = "$HYPATIA_COMMIT" + ( + cd "$scanner_dir" + mix deps.get + mix escript.build + ) - name: Run Hypatia scan id: scan - if: steps.build.outputs.ready == 'true' + env: + GITHUB_TOKEN: ${{ secrets.HYPATIA_SCAN_PAT || secrets.GITHUB_TOKEN }} run: | set +e - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1 + HYPATIA_FORMAT=json "${RUNNER_TEMP}/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json HYP_EXIT=$? set -e - if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then - echo "[]" > hypatia-findings.json + if [ "$HYP_EXIT" -ne 0 ]; then + echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}" + exit "$HYP_EXIT" + fi + if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then + echo "::error::Hypatia did not produce a valid JSON findings array" + exit 1 fi TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) @@ -167,7 +174,6 @@ jobs: echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" echo "low=$LOW" >> "$GITHUB_OUTPUT" - name: Emit check annotations - if: steps.build.outputs.ready == 'true' run: | jq -r '.[] | select(.file != null) | if .severity == "critical" then @@ -179,7 +185,6 @@ jobs: end ' hypatia-findings.json || true - name: Write step summary - if: steps.build.outputs.ready == 'true' run: | cat <> "$GITHUB_STEP_SUMMARY" ## Hypatia Scan Results @@ -192,21 +197,15 @@ jobs: | Low | ${{ steps.scan.outputs.low }} | | **Total**| ${{ steps.scan.outputs.total }} | EOF - - name: Create stub findings (when Hypatia unavailable) - if: steps.build.outputs.ready != 'true' - run: | - echo "[]" > hypatia-findings.json - echo "## Hypatia Scan" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload hypatia findings + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: hypatia-findings path: hypatia-findings.json retention-days: 90 - name: Fail on critical security findings - if: steps.build.outputs.ready == 'true' && steps.scan.outputs.critical > 0 + if: steps.scan.outputs.critical > 0 run: | echo "::error::Hypatia found ${{ steps.scan.outputs.critical }} critical security issue(s) β€” blocking merge" exit 1 @@ -243,8 +242,12 @@ jobs: PB_EXIT=$? set -e - if [ ! -s bridge-report.json ] || ! jq empty bridge-report.json 2>/dev/null; then - echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > bridge-report.json + if [ ! -s bridge-report.json ] || ! jq -e 'type == "object"' bridge-report.json >/dev/null; then + echo "::error::Patch Bridge did not produce a valid JSON report" + exit 1 + fi + if [ "$PB_EXIT" -ne 0 ]; then + echo "::warning::Patch Bridge exited ${PB_EXIT}; preserving its valid report for policy evaluation" fi UNMITIGABLE=$(jq '.unmitigable // 0' bridge-report.json) @@ -256,6 +259,7 @@ jobs: echo "mitigated=$MITIGATED" >> "$GITHUB_OUTPUT" echo "concatenative=$CONCATENATIVE" >> "$GITHUB_OUTPUT" echo "informational=$INFORMATIONAL" >> "$GITHUB_OUTPUT" + echo "exit_code=$PB_EXIT" >> "$GITHUB_OUTPUT" - name: Write step summary if: steps.install.outputs.installed == 'true' run: | diff --git a/.github/workflows/trope-check.yml b/.github/workflows/trope-check.yml index 3a2f35e..e32ee26 100644 --- a/.github/workflows/trope-check.yml +++ b/.github/workflows/trope-check.yml @@ -10,10 +10,13 @@ on: workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: examples-lower-to-valid-ir: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Examples lower to valid Trope IR run: bash tests/check-examples.sh diff --git a/.github/workflows/wellknown-enforcement.yml b/.github/workflows/wellknown-enforcement.yml index 980d60c..208ec96 100644 --- a/.github/workflows/wellknown-enforcement.yml +++ b/.github/workflows/wellknown-enforcement.yml @@ -1,17 +1,9 @@ # SPDX-License-Identifier: MPL-2.0 name: "πŸ”΄ Gate: .well-known / RFC 9116" on: - push: - branches: [main, master] - paths: - - '.well-known/**' - - 'security.txt' - pull_request: - paths: - - '.well-known/**' schedule: # Weekly expiry check - - cron: '0 9 * * *' + - cron: '23 9 * * 0' workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 60f3962..0db1cb1 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -15,6 +15,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: lint-workflows: runs-on: ubuntu-latest diff --git a/Justfile b/Justfile index 4cdfa99..7c6defc 100644 --- a/Justfile +++ b/Justfile @@ -81,26 +81,15 @@ import? "build/just/assess.just" # BUILD & COMPILE # ═══════════════════════════════════════════════════════════════════════════════ -# Build the project (debug mode) +# Typecheck the ABI and build both libhaec variants (debug mode) build *args: - @echo "Building haec (debug)..." - # TODO: Replace with your build command - # Examples: - # cargo build {{args}} # Rust - # mix compile {{args}} # Elixir - # zig build {{args}} # Zig - # deno task build {{args}} # Deno/ReScript - @echo "Build complete" + idris2 --typecheck abi.ipkg + cd src/interface/ffi && zig build {{args}} # Build in release mode with optimizations build-release *args: - @echo "Building haec (release)..." - # TODO: Replace with your release build command - # Examples: - # cargo build --release {{args}} - # MIX_ENV=prod mix compile {{args}} - # zig build -Doptimize=ReleaseFast {{args}} - @echo "Release build complete" + idris2 --typecheck abi.ipkg + cd src/interface/ffi && zig build -Doptimize=ReleaseSafe {{args}} # Build and watch for changes (requires entr or similar) build-watch: @@ -113,9 +102,8 @@ build-watch: # Clean build artifacts [reversible: rebuild with `just build`] clean: - @echo "Cleaning..." - # TODO: Customize for your build system - rm -rf target/ _build/ build/ dist/ out/ obj/ bin/ + # Deliberately limited to generated compiler output. `build/` is source. + rm -rf .zig-cache src/interface/ffi/.zig-cache src/interface/ffi/zig-out build/exec # Deep clean including caches [reversible: rebuild] clean-all: clean @@ -127,47 +115,35 @@ clean-all: clean # Run all tests test *args: - @echo "Running tests..." - # TODO: Replace with your test command - # Examples: - # cargo test {{args}} - # mix test {{args}} - # zig build test {{args}} - # deno test {{args}} - @echo "Tests passed!" + idris2 --typecheck abi.ipkg + cd src/interface/ffi && zig build test {{args}} + bash tests/check-examples.sh + bash tests/aspect_tests.sh + bash scripts/check-root-shape.sh . + bash tests/workflows/validate_workflows_test.sh # Run tests with verbose output test-verbose: - @echo "Running tests (verbose)..." - # TODO: Replace with verbose test command + idris2 --typecheck abi.ipkg + cd src/interface/ffi && zig build test --summary all + bash tests/check-examples.sh + bash tests/aspect_tests.sh + bash scripts/check-root-shape.sh . + bash tests/workflows/validate_workflows_test.sh # Smoke test test-smoke: - @echo "Smoke test..." - # TODO: Add basic sanity checks + idris2 --typecheck abi.ipkg + cd src/interface/ffi && zig build test # Run end-to-end tests (full pipeline: build β†’ run β†’ verify) e2e: - @echo "Running E2E tests..." - # TODO: Replace with your E2E test command. Examples: - # bash tests/e2e.sh # Shell-based E2E - # npx playwright test # Browser E2E - # mix test test/integration/e2e_test.exs # Elixir E2E - # cargo test --test end_to_end # Rust E2E - @echo "E2E tests passed!" + @echo "No Haec end-to-end suite is implemented yet." >&2 + @exit 2 # Run aspect tests (cross-cutting concern validation) aspect: - @echo "Running aspect tests..." - # TODO: Replace with your aspect test command. Examples: - # bash tests/aspect_tests.sh # Shell-based aspect tests - # cargo test --test aspects # Rust aspect tests - # Aspect tests validate architectural invariants: - # - Thread safety (mutex in FFI modules) - # - ABI/FFI contract (declarations match exports) - # - SPDX compliance (all files have license headers) - # - No dangerous patterns (believe_me, assert_total, etc.) - @echo "Aspect tests passed!" + bash tests/aspect_tests.sh # Run benchmarks (performance regression detection) bench: @@ -227,31 +203,16 @@ fix: fmt # Format all source files [reversible: git checkout] fmt: - @echo "Formatting source files..." - # TODO: Replace with your formatter - # Examples: - # cargo fmt - # mix format - # gleam format - # deno fmt + zig fmt src/interface/ffi/build.zig src/interface/ffi/src/main.zig src/interface/ffi/test/integration_test.zig # Check formatting without changes fmt-check: - @echo "Checking formatting..." - # TODO: Replace with your format check - # Examples: - # cargo fmt --check - # mix format --check-formatted - # gleam format --check + zig fmt --check src/interface/ffi/build.zig src/interface/ffi/src/main.zig src/interface/ffi/test/integration_test.zig # Run linter lint: - @echo "Linting source files..." - # TODO: Replace with your linter - # Examples: - # cargo clippy -- -D warnings - # mix credo --strict - # gleam check + find tests scripts -type f -name '*.sh' -print0 | xargs -0 -r bash -n + bash tests/aspect_tests.sh # ═══════════════════════════════════════════════════════════════════════════════ # RUN & EXECUTE diff --git a/docs/status/TEST-NEEDS.adoc b/docs/status/TEST-NEEDS.adoc index fcec70a..ae0741b 100644 --- a/docs/status/TEST-NEEDS.adoc +++ b/docs/status/TEST-NEEDS.adoc @@ -1,118 +1,84 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 // Copyright (c) Jonathan D.A. Jewell -= TEST-NEEDS: rsr-template-repo += TEST-NEEDS: Haec +:toc: -== CRG Grade: C β€” ACHIEVED 2026-04-04 +== Current state (updated 2026-07-19) -== Current State (Updated 2026-04-04) - -[cols="2,1,4", options="header"] +[cols="2,1,4",options="header"] |=== -| Category | Count | Details - -| *Source modules* | 6 | 3 Idris2 ABI (Foreign, Layout, Types), 2 Zig FFI (build, main), 1 Zig integration test template -| *Unit tests* | 0 | None in main source (inline tests in main.zig) -| *Integration tests* | 1 | `test/integration_test.zig` (documented template, 1 placeholder test) -| *E2E tests* | 1 | `tests/e2e/template_instantiation_test.sh` (full instantiation + validation) -| *Workflow tests* | 1 | `tests/workflows/validate_workflows_test.sh` (21 workflows validated) -| *Validation tests* | 1 | `scripts/validate-template.sh` (8-phase comprehensive validation) -| *Benchmarks* | 5 | `benches/template_bench.sh` (validation, Zig build, tests, workflows, instantiation) -| *Fuzz tests* | 0 | `README.adoc` scaffold with harness instructions +|Category |Count |Details + +|Idris2 modules +|3 +|`Abi.Types`, `Abi.Layout`, and `Abi.Foreign`, typechecked together through `abi.ipkg`. + +|Zig unit tests +|3 +|Lifecycle, error handling, and version tests embedded in `src/interface/ffi/src/main.zig`. + +|Zig integration tests +|5 +|Lifecycle, processing, pointer validation, allocated strings, and version/build information exercised through the linked C ABI. + +|Repository contract suites +|3 +|Example/IR structure, cross-cutting aspects, and root-shape validation. + +|End-to-end tests +|0 +|The historical template-instantiation script is not a Haec E2E suite and remains non-gating until repaired or replaced. + +|Fuzz tests +|0 +|`tests/fuzz/README.adoc` contains harness guidance only. |=== -== Completed Work (CRG C - Testing & Benchmarking) - -=== Template Validation Script βœ… - -* [x] `scripts/validate-template.sh` β€” 8-phase validation -** Phase 1: Core repository structure (root files, directories) -** Phase 2: Machine-readable metadata (`.machine_readable/`) -** Phase 3: GitHub Actions workflows (17 required + all present) -** Phase 4: Idris2 ABI and Zig FFI source files -** Phase 5: Placeholder token replacement (skipped in template) -** Phase 6: SPDX license headers (100% coverage, 6/6 files) -** Phase 7: Build system verification (`zig build` + `idris2` syntax check) -** Phase 8: Documentation requirements (TOPOLOGY, ABI-FFI-README, etc) -* Status: *PASSING* (0 errors, 3 warnings about template placeholders) - -=== E2E Template Instantiation Test βœ… - -* [x] `tests/e2e/template_instantiation_test.sh` β€” full workflow -** Clones template to temp directory -** Replaces all `{{PLACEHOLDER}}` tokens with test values -** Validates resulting structure with `scripts/validate-template.sh` -** Verifies Zig build works after instantiation -** Checks no remaining placeholders -** Cleans up temp directory -* Status: *READY TO TEST* (can be verified by CI) - -=== Workflow Validation Test βœ… - -* [x] `tests/workflows/validate_workflows_test.sh` -** Validates all 21 workflows exist and have proper structure -** Checks SPDX headers, `name` field -** Verifies all 15 required workflows present -* Status: *PASSING* (0 errors, 15/15 required workflows found) - -=== Zig FFI Tests βœ… - -* [x] `src/interface/ffi/test/integration_test.zig` β€” template with examples -** Converted from `haec` placeholders to "template" namespace -** Added comprehensive comments for how to instantiate -** Tests grouped by category (lifecycle, operations, strings, errors, version, memory safety, threading) -** Compiles and passes placeholder test -* Status: *PASSING* (1 test: `placeholder_test_implementation_required` passes) - -=== Benchmarks βœ… - -* [x] `benches/template_bench.sh` β€” 5 benchmark suites -** Validation script: ~5.8s average (3 runs) -** Zig build: ~19ms (clean build) -** Zig tests: ~20ms -** Workflow validation: ~117ms -** Template instantiation: ~427ms -* Formats: human, json, csv -* Status: *PASSING* (all benchmarks execute) - -=== Build System βœ… - -* [x] `src/interface/ffi/build.zig` β€” updated for Zig 0.15.2 -** Simplified to test-only configuration -** Supports both unit tests and integration tests -** Works with `zig build` without errors -* Status: *PASSING* (builds successfully) - -== Test Results Summary +== Verified commands +[source,bash] ---- -Validation Script: PASS (0 errors, 3 warnings) -Workflow Validation: PASS (21/21 workflows valid) -Integration Tests: PASS (1/1 placeholder test) -E2E Instantiation: READY (needs CI confirmation) -Benchmarks: PASS (5/5 benchmark suites) -Build System: PASS (zig build succeeds) +idris2 --typecheck abi.ipkg +cd src/interface/ffi +zig build +zig build test --summary all ---- -== CRG C Compliance +With Idris2 0.7.0 and Zig 0.15.2, the build produces both static and shared +`libhaec` artifacts and all eight Zig tests pass. + +From the repository root: + +[source,bash] +---- +bash tests/check-examples.sh +bash tests/aspect_tests.sh +bash scripts/check-root-shape.sh . +---- -* *Coverage*: 6/6 test categories (unit, integration, E2E, workflow, validation, benchmarks) -* *Documentation*: All test files have SPDX headers + inline documentation -* *Author Attribution*: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> -* *License*: MPL-2.0 on all new files -* *Automation*: All scripts executable + working +`tests/check-examples.sh` currently verifies manifest/IR pairing and JSON shape. +Its schema-drift and semantic round-trip portions run only when the sibling +`trope-checker` repository and binary are available, so it must not be described +as full E2E coverage. -== FLAGGED ISSUES - ALL RESOLVED +== CI status -* [.line-through]#*Template repo used by ALL new repos has 0 validation tests*# β†’ FIXED: 4 test suites + validation script -* [.line-through]#*fuzz/placeholder.txt*# β†’ FIXED: replaced with `README.adoc` containing real harness instructions -* [.line-through]#*No E2E tests for template instantiation*# β†’ FIXED: full E2E test suite -* [.line-through]#*Zig FFI integration tests are placeholders*# β†’ FIXED: converted to documented template format +`.github/workflows/ci.yml` is the canonical, bounded core gate. It emits one +stable `CI / required` context and runs Idris2 typechecking, Zig compilation and +tests, shell syntax checks, example/IR structural checks, aspect checks, and the +root-shape contract on one runner. -== Next Steps (Future Sessions) +The former empty E2E workflow, no-op Rust wrapper, and placeholder quality +workflow were removed during the 2026-07-19 consolidation. Domain-specific and +security workflows remain separate until their stronger behavior has been +absorbed into a tested aggregate. -* [ ] Integrate test scripts into CI/CD workflows -* [ ] Generate test coverage reports -* [ ] Add more specialized benchmarks (memory, threading stress) -* [ ] Document test instantiation patterns for new repos +== Remaining test debt -== Priority: P0 (COMPLETE) βœ… +* Implement a real Haec end-to-end scenario across the Idris2-to-Zig boundary. +* Build or fetch a pinned `trope-checker` in CI so schema drift and semantic + verdict round-trips cannot silently skip. +* Repair `tests/e2e/template_instantiation_test.sh` before reusing it; it + currently contains an unbound variable and is template-oriented. +* Add fuzz targets for the actual data-processing boundary. +* Resolve the two Idris2 shadowing warnings before enabling `-Werror`. diff --git a/docs/tech-debt-2026-05-26.adoc b/docs/tech-debt-2026-05-26.adoc new file mode 100644 index 0000000..6c23c72 --- /dev/null +++ b/docs/tech-debt-2026-05-26.adoc @@ -0,0 +1,81 @@ += Tech-Debt Audit β€” rsr-template-repo β€” 2026-05-26 +:toc: + +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) Jonathan D.A. Jewell + +*Source:* estate-wide automated scan 2026-05-26. + +*Companion:* https://github.com/hyperpolymath/standards/tree/main/docs/audits[`hyperpolymath/standards` 2026-05-26-estate-*-debt audits]. + +*Combined severity:* `LOW`. + +This file records the _raw findings_; it does not by itself fix the debt. Each +section ends with a β€œRecommended next move” line. Closing the debt is follow-up +work. + +== 1. Proof debt + +The scanner counted the following markers in proof-bearing files of this repo: + +[source,text] +---- +files= 13 | Coq-Axm/Adm= 0 | Lean-srry/ax= 0 | Agda-pst= 0 | Idr-blv= 6 | Idr-prtl= 0 | Fstr-asm= 0 | TODO= 0 | Unsafe= 0 +---- + +*Total markers:* 6. *Severity:* `>06`. + +*Marker types* (any non-zero counts above): + +* Coq `Axiom`/`Admitted` β€” unconditional proof escapes. +* Lean `sorry`/`axiom` β€” Lean's equivalent. +* Agda `postulate` β€” accepted axiomatically. +* Idris2 `believe_me`/`assert_total` β€” runtime-safe coercion or totality assumption. +* Idris2 top-level `partial` β€” totality check waived. +* F* `assume val`/`admit_p` β€” F* admit. +* `TODO PROOF` / `OWED:` β€” self-documented debt markers. +* `unsafePerformIO`/`unsafeCoerce` β€” soundness-relevant escape hatches in Haskell/Rust source. + +*Recommended next move:* triage each finding into one of: (a) discharge by +proof, (b) cover with property tests plus a documented refutation budget, or +(c) annotate as a known/necessary axiom (for example, `funExt`) in the proof +debt documentation. + +== 2. Licence debt + +[cols="1,1",options="header"] +|=== +|Field |Value +|LICENSE file |`LICENSE` +|SPDX header |`MPL-2.0` +|Manifest licence |`NONE` +|Body classifier |`Palimp-MPL-2.0` +|Severity |`ok` +|=== + +*Recommended next move:* none for licence. + +== 3. Documentation debt + +[cols="1,1",options="header"] +|=== +|Field |Value +|README lines |186 +|`docs/` files |70 +|`docs/` LoC |4218 +|CHANGELOG.md |Y +|CONTRIBUTING.md |N +|CODE_OF_CONDUCT.md |N +|SECURITY.md |N +|Severity |`OK` +|=== + +*Recommended next move:* none for docs. + +== Cross-references + +* Estate proof-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-proof-debt.md` +* Estate licence-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-licence-debt.md` +* Estate documentation-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-documentation-debt.md` + +Generated by the Claude Code estate-wide tech-debt scan on 2026-05-26. This +file is informational; closing the debt is follow-up work owned by the +maintainer. diff --git a/docs/tech-debt-2026-05-26.md b/docs/tech-debt-2026-05-26.md deleted file mode 100644 index 27b8c59..0000000 --- a/docs/tech-debt-2026-05-26.md +++ /dev/null @@ -1,70 +0,0 @@ - -# Tech-Debt Audit β€” rsr-template-repo β€” 2026-05-26 - -**Source:** estate-wide automated scan 2026-05-26. -**Companion:** [`hyperpolymath/standards` 2026-05-26-estate-*-debt audits](https://github.com/hyperpolymath/standards/tree/main/docs/audits). -**Combined severity:** `LOW`. - -This file records the *raw findings* β€” it does not by itself fix the debt. Each section ends with a 'Recommended next move' line; closing the debt is follow-up work. - -## 1. Proof debt - -Scanner counted the following markers in proof-bearing files of this repo: - -``` -files= 13 | Coq-Axm/Adm= 0 | Lean-srry/ax= 0 | Agda-pst= 0 | Idr-blv= 6 | Idr-prtl= 0 | Fstr-asm= 0 | TODO= 0 | Unsafe= 0 -``` - -**Total markers:** 6. **Severity:** `>06`. - -**Marker types** (any non-zero counts above): -- Coq `Axiom`/`Admitted` β€” unconditional proof escapes. -- Lean `sorry`/`axiom` β€” Lean's equivalent. -- Agda `postulate` β€” accepted axiomatically. -- Idris2 `believe_me`/`assert_total` β€” runtime-safe coercion / totality assumption. -- Idris2 top-level `partial` β€” totality-check waived. -- F\* `assume val`/`admit_p` β€” F\* admit. -- `TODO PROOF` / `OWED:` β€” self-documented debt markers. -- `unsafePerformIO`/`unsafeCoerce` β€” soundness-relevant escape hatches in Haskell/Rust source. - -**Recommended next move:** triage each finding into one of: (a) discharge by proof, (b) cover with property-tests + a documented refutation budget, or (c) annotate as a known/necessary axiom (e.g. `funExt`) in `docs/proof-debt.md`. - -## 2. Licence debt - -| Field | Value | -|---|---| -| LICENSE file | `LICENSE` | -| SPDX header | `MPL-2.0` | -| Manifest licence | `NONE` | -| Body classifier | `Palimp-MPL-2.0` | -| Severity | `ok` | - -**Recommended next move:** none for licence. - -## 3. Documentation debt - -| Field | Value | -|---|---| -| README lines | 186 | -| `docs/` files | 70 | -| `docs/` LoC | 4218 | -| CHANGELOG.md | Y | -| CONTRIBUTING.md | N | -| CODE_OF_CONDUCT.md | N | -| SECURITY.md | N | -| Severity | `OK` | - -**Recommended next move:** none for docs. - -## Cross-references - -- Estate proof-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-proof-debt.md` -- Estate licence-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-licence-debt.md` -- Estate documentation-debt audit: `hyperpolymath/standards/docs/audits/2026-05-26-estate-documentation-debt.md` - ---- - -πŸ€– Generated by Claude Code estate-wide tech-debt scan (2026-05-26). This file is informational β€” closing the debt is follow-up work owned by the maintainer. diff --git a/src/interface/Abi/Foreign.idr b/src/interface/Abi/Foreign.idr index ca66b08..072b8c8 100644 --- a/src/interface/Abi/Foreign.idr +++ b/src/interface/Abi/Foreign.idr @@ -17,11 +17,11 @@ import Abi.Layout -------------------------------------------------------------------------------- ||| Raw FFI call to initialize the library -%foreign "C:rsr_init,librsr" +%foreign "C:haec_init,libhaec" prim__init : PrimIO Bits64 ||| Raw FFI call to free library resources -%foreign "C:rsr_free,librsr" +%foreign "C:haec_free,libhaec" prim__free : Bits64 -> PrimIO () ||| Safe wrapper for initialization @@ -41,25 +41,31 @@ free h = primIO (prim__free h.ptr) -------------------------------------------------------------------------------- ||| Raw FFI call for main processing -%foreign "C:rsr_process,librsr" +%foreign "C:haec_process,libhaec" prim__process : Bits64 -> Bits32 -> PrimIO Bits32 ||| Safe wrapper with error handling +resultFromCode : Bits32 -> Result +resultFromCode 0 = Ok +resultFromCode 1 = Error +resultFromCode 2 = InvalidParam +resultFromCode 3 = Busy +resultFromCode _ = Error + +||| Run the main operation and decode the C result code. export -process : Handle -> Bits32 -> IO (Either Result Bits32) +process : Handle -> Bits32 -> IO Result process h input = do result <- primIO (prim__process h.ptr input) - if result == 0 - then pure (Left Error) - else pure (Right result) + pure (resultFromCode result) -------------------------------------------------------------------------------- -- Status and Metrics -------------------------------------------------------------------------------- ||| Get the current error description from the library -%foreign "C:rsr_get_error,librsr" -prim__getError : Bits64 -> PrimIO (Ptr String) +%foreign "C:haec_last_error,libhaec" +prim__getError : PrimIO (Ptr String) ||| Detailed error string helper export @@ -80,4 +86,4 @@ errorDescription Busy = "Library is busy" ||| 4. FFI boundary uses explicitly tagged types from Abi.Types. public export abiSafetyGuarantees : String -abiSafetyGuarantees = "RSR-Template ABI: 4 proven safety properties for FFI integration" +abiSafetyGuarantees = "Haec ABI: 4 proven safety properties for FFI integration" diff --git a/src/interface/ffi/build.zig b/src/interface/ffi/build.zig index 2607c11..a8518f2 100644 --- a/src/interface/ffi/build.zig +++ b/src/interface/ffi/build.zig @@ -1,19 +1,60 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell +// HAEC FFI build graph (Zig 0.15.2+). // -// Template FFI Build Configuration (Zig 0.15.2+) -// Note: This is a minimal build file that demonstrates Zig integration +// `zig build` installs static and shared libhaec artifacts. +// `zig build test` runs unit tests and C-boundary integration tests. const std = @import("std"); pub fn build(b: *std.Build) void { - _ = b.standardTargetOptions(.{}); - _ = b.standardOptimizeOption(.{}); + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); - // 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 shared_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + const shared_library = b.addLibrary(.{ + .name = "haec", + .linkage = .dynamic, + .root_module = shared_module, + }); + b.installArtifact(shared_library); + + const static_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + const static_library = b.addLibrary(.{ + .name = "haec", + .linkage = .static, + .root_module = static_module, + }); + b.installArtifact(static_library); + + const unit_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + const unit_tests = b.addTest(.{ .root_module = unit_module }); + + const integration_module = b.createModule(.{ + .root_source_file = b.path("test/integration_test.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + const integration_tests = b.addTest(.{ .root_module = integration_module }); + integration_tests.linkLibrary(static_library); + + const test_step = b.step("test", "Run unit and C-boundary integration tests"); + test_step.dependOn(&b.addRunArtifact(unit_tests).step); + test_step.dependOn(&b.addRunArtifact(integration_tests).step); } diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig index 3b3707b..f1385c7 100644 --- a/src/interface/ffi/src/main.zig +++ b/src/interface/ffi/src/main.zig @@ -2,7 +2,7 @@ // Copyright (c) Jonathan D.A. Jewell // HAEC FFI Implementation // -// This module implements the C-compatible FFI declared in src/abi/Foreign.idr +// This module implements the C-compatible FFI declared in src/interface/Abi/Foreign.idr // All types and layouts must match the Idris2 ABI definitions. // @@ -34,51 +34,55 @@ pub const Result = enum(c_int) { ok = 0, @"error" = 1, invalid_param = 2, - out_of_memory = 3, - null_pointer = 4, + busy = 3, }; -/// Library handle (opaque to prevent direct access) -pub const Handle = opaque { - // Internal state hidden from C +/// Public C handle. Its representation is deliberately opaque to callers. +pub const Handle = opaque {}; + +/// Private state stored behind the opaque C handle. +const HandleState = struct { allocator: std.mem.Allocator, initialized: bool, - // Add your fields here }; +fn stateFromHandle(handle: *Handle) *HandleState { + return @ptrCast(@alignCast(handle)); +} + //============================================================================== // Library Lifecycle //============================================================================== /// Initialize the library /// Returns a handle, or null on failure -export fn haec_init() ?*Handle { +pub export fn haec_init() ?*Handle { const allocator = std.heap.c_allocator; - const handle = allocator.create(Handle) catch { + const state = allocator.create(HandleState) catch { setError("Failed to allocate handle"); return null; }; - // Initialize handle - handle.* = .{ + state.* = .{ .allocator = allocator, .initialized = true, }; clearError(); - return handle; + return @ptrCast(state); } /// Free the library handle -export fn haec_free(handle: ?*Handle) void { - const h = handle orelse return; - const allocator = h.allocator; +pub export fn haec_free(handle: ?*Handle) void { + const opaque_handle = handle orelse return; + const state = stateFromHandle(opaque_handle); + const allocator = state.allocator; // Clean up resources - h.initialized = false; + state.initialized = false; - allocator.destroy(h); + allocator.destroy(state); clearError(); } @@ -87,13 +91,14 @@ export fn haec_free(handle: ?*Handle) void { //============================================================================== /// Process data (example operation) -export fn haec_process(handle: ?*Handle, input: u32) Result { - const h = handle orelse { +pub export fn haec_process(handle: ?*Handle, input: u32) Result { + const opaque_handle = handle orelse { setError("Null handle"); - return .null_pointer; + return .invalid_param; }; + const state = stateFromHandle(opaque_handle); - if (!h.initialized) { + if (!state.initialized) { setError("Handle not initialized"); return .@"error"; } @@ -111,19 +116,20 @@ export fn haec_process(handle: ?*Handle, input: u32) Result { /// Get a string result (example) /// Caller must free the returned string -export fn haec_get_string(handle: ?*Handle) ?[*:0]const u8 { - const h = handle orelse { +pub export fn haec_get_string(handle: ?*Handle) ?[*:0]const u8 { + const opaque_handle = handle orelse { setError("Null handle"); return null; }; + const state = stateFromHandle(opaque_handle); - if (!h.initialized) { + if (!state.initialized) { setError("Handle not initialized"); return null; } // Example: allocate and return a string - const result = h.allocator.dupeZ(u8, "Example result") catch { + const result = state.allocator.dupeZ(u8, "Example result") catch { setError("Failed to allocate string"); return null; }; @@ -133,7 +139,7 @@ export fn haec_get_string(handle: ?*Handle) ?[*:0]const u8 { } /// Free a string allocated by the library -export fn haec_free_string(str: ?[*:0]const u8) void { +pub export fn haec_free_string(str: ?[*:0]const u8) void { const s = str orelse return; const allocator = std.heap.c_allocator; @@ -146,22 +152,23 @@ export fn haec_free_string(str: ?[*:0]const u8) void { //============================================================================== /// Process an array of data -export fn haec_process_array( +pub export fn haec_process_array( handle: ?*Handle, buffer: ?[*]const u8, len: u32, ) Result { - const h = handle orelse { + const opaque_handle = handle orelse { setError("Null handle"); - return .null_pointer; + return .invalid_param; }; + const state = stateFromHandle(opaque_handle); const buf = buffer orelse { setError("Null buffer"); - return .null_pointer; + return .invalid_param; }; - if (!h.initialized) { + if (!state.initialized) { setError("Handle not initialized"); return .@"error"; } @@ -181,11 +188,11 @@ export fn haec_process_array( //============================================================================== /// Get the last error message -/// Returns null if no error -export fn haec_last_error() ?[*:0]const u8 { +/// Returns null if no error. Free a non-null result with haec_free_string. +pub export fn haec_last_error() ?[*:0]const u8 { const err = last_error orelse return null; - // Return C string (static storage, no need to free) + // Return caller-owned C storage; release it with haec_free_string. const allocator = std.heap.c_allocator; const c_str = allocator.dupeZ(u8, err) catch return null; return c_str.ptr; @@ -196,12 +203,12 @@ export fn haec_last_error() ?[*:0]const u8 { //============================================================================== /// Get the library version -export fn haec_version() [*:0]const u8 { +pub export fn haec_version() [*:0]const u8 { return VERSION.ptr; } /// Get build information -export fn haec_build_info() [*:0]const u8 { +pub export fn haec_build_info() [*:0]const u8 { return BUILD_INFO.ptr; } @@ -210,24 +217,25 @@ export fn haec_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 haec_register_callback( +pub export fn haec_register_callback( handle: ?*Handle, callback: ?Callback, ) Result { - const h = handle orelse { + const opaque_handle = handle orelse { setError("Null handle"); - return .null_pointer; + return .invalid_param; }; + const state = stateFromHandle(opaque_handle); const cb = callback orelse { setError("Null callback"); - return .null_pointer; + return .invalid_param; }; - if (!h.initialized) { + if (!state.initialized) { setError("Handle not initialized"); return .@"error"; } @@ -244,9 +252,10 @@ export fn haec_register_callback( //============================================================================== /// Check if handle is initialized -export fn haec_is_initialized(handle: ?*Handle) u32 { - const h = handle orelse return 0; - return if (h.initialized) 1 else 0; +pub export fn haec_is_initialized(handle: ?*Handle) u32 { + const opaque_handle = handle orelse return 0; + const state = stateFromHandle(opaque_handle); + return if (state.initialized) 1 else 0; } //============================================================================== @@ -262,10 +271,11 @@ test "lifecycle" { test "error handling" { const result = haec_process(null, 0); - try std.testing.expectEqual(Result.null_pointer, result); + try std.testing.expectEqual(Result.invalid_param, result); - const err = haec_last_error(); - try std.testing.expect(err != null); + const err = haec_last_error() orelse return error.MissingError; + defer haec_free_string(err); + try std.testing.expectEqualStrings("Null handle", std.mem.span(err)); } test "version" { diff --git a/src/interface/ffi/test/integration_test.zig b/src/interface/ffi/test/integration_test.zig index 484e156..128ff9f 100644 --- a/src/interface/ffi/test/integration_test.zig +++ b/src/interface/ffi/test/integration_test.zig @@ -1,66 +1,75 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Jonathan D.A. Jewell -// RSR Template FFI Integration Tests +// HAEC FFI integration tests // -// These tests verify that the Zig FFI correctly implements the Idris2 ABI. -// This is a TEMPLATE FILE β€” when instantiating a new project: -// 1. Replace "template" with your project name in lowercase -// 2. Link against your actual FFI implementation library -// 3. Uncomment the test functions below -// -// For now, this file contains documentation of what tests should exist. +// These exercise the public ABI surface through its opaque handle rather than +// duplicating the implementation's private state layout. const std = @import("std"); -// NOTE: When instantiated, declare the actual FFI functions here: -// extern fn mylib_init() ?*Handle; -// extern fn mylib_free(?*Handle) void; -// ... etc +const Handle = opaque {}; +const Result = enum(c_int) { + ok = 0, + @"error" = 1, + invalid_param = 2, + busy = 3, +}; + +extern fn haec_init() ?*Handle; +extern fn haec_free(?*Handle) void; +extern fn haec_process(?*Handle, u32) Result; +extern fn haec_get_string(?*Handle) ?[*:0]const u8; +extern fn haec_free_string(?[*:0]const u8) void; +extern fn haec_process_array(?*Handle, ?[*]const u8, u32) Result; +extern fn haec_last_error() ?[*:0]const u8; +extern fn haec_version() [*:0]const u8; +extern fn haec_build_info() [*:0]const u8; +extern fn haec_is_initialized(?*Handle) u32; -// And define Handle appropriately: -// const Handle = opaque {}; +test "lifecycle creates an initialized opaque handle" { + const handle = haec_init() orelse return error.InitFailed; + defer haec_free(handle); -test "placeholder test - implementation required" { - // This test ensures the file compiles - // Actual tests depend on FFI implementation - try std.testing.expect(true); + try std.testing.expectEqual(@as(u32, 1), haec_is_initialized(handle)); } -// ============================================================================== -// Example tests (uncomment when instantiated with real FFI): -// ============================================================================== -// -// test "lifecycle: create and destroy handle" { -// const handle = mylib_init() orelse return error.InitFailed; -// defer mylib_free(handle); -// } -// -// test "operations: process with valid handle" { -// const handle = mylib_init() orelse return error.InitFailed; -// defer mylib_free(handle); -// -// const result = mylib_process(handle, 42); -// try std.testing.expectEqual(@as(c_int, 0), result); -// } -// -// test "memory safety: double free is safe" { -// const handle = mylib_init() orelse return error.InitFailed; -// mylib_free(handle); -// mylib_free(handle); // Should not crash -// } -// -// test "strings: get string result from handle" { -// const handle = mylib_init() orelse return error.InitFailed; -// defer mylib_free(handle); -// -// const str = mylib_get_string(handle); -// defer if (str) |s| mylib_free_string(s); -// -// try std.testing.expect(str != null); -// } -// -// test "version: returns non-empty version string" { -// const ver = mylib_version(); -// const ver_str = std.mem.span(ver); -// try std.testing.expect(ver_str.len > 0); -// } +test "process accepts a valid handle and rejects a null handle" { + const handle = haec_init() orelse return error.InitFailed; + defer haec_free(handle); + + try std.testing.expectEqual(Result.ok, haec_process(handle, 42)); + try std.testing.expectEqual(Result.invalid_param, haec_process(null, 42)); + + const message = haec_last_error() orelse return error.MissingError; + defer haec_free_string(message); + try std.testing.expectEqualStrings("Null handle", std.mem.span(message)); +} + +test "array processing validates pointers" { + const handle = haec_init() orelse return error.InitFailed; + defer haec_free(handle); + + const input = [_]u8{ 1, 2, 3, 4 }; + try std.testing.expectEqual( + Result.ok, + haec_process_array(handle, input[0..].ptr, input.len), + ); + try std.testing.expectEqual( + Result.invalid_param, + haec_process_array(handle, null, 0), + ); +} + +test "allocated strings cross the public boundary" { + const handle = haec_init() orelse return error.InitFailed; + defer haec_free(handle); + + const result = haec_get_string(handle) orelse return error.MissingString; + defer haec_free_string(result); + try std.testing.expectEqualStrings("Example result", std.mem.span(result)); +} + +test "version and build information are non-empty" { + try std.testing.expectEqualStrings("0.1.0", std.mem.span(haec_version())); + try std.testing.expect(std.mem.span(haec_build_info()).len > 0); +} diff --git a/tests/aspect_tests.sh b/tests/aspect_tests.sh index 69d206b..c54e0bb 100755 --- a/tests/aspect_tests.sh +++ b/tests/aspect_tests.sh @@ -54,7 +54,7 @@ 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/ -type d -name '.zig-cache' -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" @@ -68,7 +68,9 @@ 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) +DANGEROUS_IDRIS=$(grep -rn 'believe_me\|assert_total\|really_believe_me' src/interface/Abi/ verification/proofs/idris2/ 2>/dev/null \ + | grep -vE '^[^:]+:[0-9]+:[[:space:]]*--' \ + | grep -v "^Binary" | grep -v "test" || true) if [ -n "$DANGEROUS_IDRIS" ]; then fail "Dangerous Idris2 patterns found:" echo "$DANGEROUS_IDRIS" | head -5 @@ -76,8 +78,17 @@ else pass "No dangerous Idris2 patterns (believe_me, assert_total)" fi -# 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) +# Language-aware proof-hole checks. Broad text greps used to flag README prose +# and comments that documented the banned words, producing a permanent false +# positive instead of detecting executable proof holes. +DANGEROUS_PROOF=$( + { + grep -rnE '^[[:space:]]*Admitted\.' verification/proofs/coq/ 2>/dev/null || true + grep -rnE '(^|[=:])[[:space:]]*sorry([[:space:]]|$)' verification/proofs/lean4/ 2>/dev/null || true + grep -rnE '\b(unsafeCoerce|Obj\.magic)\b' src/ verification/proofs/ \ + --include='*.idr' --include='*.hs' --include='*.ml' 2>/dev/null || true + } | sort -u +) if [ -n "$DANGEROUS_PROOF" ]; then fail "Dangerous proof patterns found:" echo "$DANGEROUS_PROOF" | head -5 diff --git a/tests/workflows/validate_workflows_test.sh b/tests/workflows/validate_workflows_test.sh index 614bb5a..2e3251b 100755 --- a/tests/workflows/validate_workflows_test.sh +++ b/tests/workflows/validate_workflows_test.sh @@ -81,7 +81,7 @@ while IFS= read -r workflow_file; do done < <(find "$WORKFLOWS_DIR" \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | sort) #============================================================================== -# TEST 2: REQUIRED WORKFLOWS +# TEST 2: REQUIRED AND EXPECTED WORKFLOWS #============================================================================== echo "" @@ -89,21 +89,26 @@ log_info "Checking for required workflows" echo "" REQUIRED_WORKFLOWS=( - "hypatia-scan.yml" + "ci.yml" + "dogfood-gate.yml" + "estate-rules.yml" + "governance.yml" + "openssf-compliance.yml" + "release.yml" + "secret-scanner.yml" + "static-analysis-gate.yml" + "trope-check.yml" + "workflow-linter.yml" +) + +EXPECTED_WORKFLOWS=( "codeql.yml" - "scorecard.yml" - "quality.yml" - "mirror.yml" - "instant-sync.yml" "guix-nix-policy.yml" - "rsr-antipattern.yml" - "security-policy.yml" - "wellknown-enforcement.yml" - "workflow-linter.yml" - "npm-bun-blocker.yml" + "instant-sync.yml" + "rhodibot.yml" + "scorecard.yml" "ts-blocker.yml" - "scorecard-enforcer.yml" - "secret-scanner.yml" + "wellknown-enforcement.yml" ) FOUND_COUNT=0 @@ -112,8 +117,7 @@ for required in "${REQUIRED_WORKFLOWS[@]}"; do log_pass "Found: $required" FOUND_COUNT=$((FOUND_COUNT + 1)) else - log_warning "Missing: $required" - WARNINGS=$((WARNINGS + 1)) + log_error "Missing required workflow: $required" fi done @@ -121,6 +125,14 @@ echo "" echo "Found $FOUND_COUNT/${#REQUIRED_WORKFLOWS[@]} required workflows" echo "" +for expected in "${EXPECTED_WORKFLOWS[@]}"; do + if [ -f "$WORKFLOWS_DIR/$expected" ]; then + log_pass "Found expected advisory/manual workflow: $expected" + else + log_warning "Missing expected advisory/manual workflow: $expected" + fi +done + #============================================================================== # SUMMARY #==============================================================================