fix(gates): gate on the DEED grammar, per the template and standards#… #192
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-License-Identifier: MPL-2.0 | |
| # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> | |
| # | |
| # 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. | |
| name: Dogfood Gate | |
| on: | |
| pull_request: | |
| branches: ['**'] | |
| push: | |
| branches: [main, master] | |
| permissions: | |
| contents: read | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Job 1: DEED manifest validation | |
| # --------------------------------------------------------------------------- | |
| deed-validate: | |
| name: Validate DEED manifests | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Check for manifest files (.a2ml/.deed) | |
| id: detect | |
| run: | | |
| COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) | |
| echo "count=$COUNT" >> "$GITHUB_OUTPUT" | |
| if [ "$COUNT" -eq 0 ]; then | |
| echo "::warning::No .a2ml/.deed manifest files found. Every RSR repo should have a repo deed (<reponame>_chora.deed); legacy 0-AI-MANIFEST.a2ml accepted mid-migration — standards #837" | |
| fi | |
| - name: Validate DEED manifests | |
| if: steps.detect.outputs.count > 0 | |
| uses: hyperpolymath/deed-ecosystem/validate-action@aa4b836bd969df2bc58128cb8e3d20bbc88d5e79 # main | |
| with: | |
| path: '.' | |
| strict: 'false' | |
| - name: Write summary | |
| run: | | |
| MANIFEST_COUNT="${{ steps.detect.outputs.count }}" | |
| if [ "$MANIFEST_COUNT" -eq 0 ]; then | |
| cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" | |
| ## DEED Manifest Validation | |
| :warning: **No .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`<reponame>_chora.deed`) at its root. | |
| Copy it from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). Manifests are validated against the DEED grammar (standards `1-formats/deed/`); the `.a2ml` → `.deed` extension migration is tracked in standards #837 — the deed validator scans both. | |
| EOF | |
| else | |
| echo "## DEED Manifest Validation" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Scanned **${MANIFEST_COUNT}** .a2ml/.deed manifest(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Job 2: K9 contract validation | |
| # --------------------------------------------------------------------------- | |
| k9-validate: | |
| name: Validate K9 contracts | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - 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-ecosystem/validate-action@89f3c2702f4f650a92aa7411502f38da06abd562 # main | |
| 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 .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`<reponame>_chora.deed`) at its root. | |
| 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-24.04 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Scan for invisible characters | |
| id: lint | |
| 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='(*UTF)[\x00-\x08\x0B\x0C\x0E-\x1F\x{a0}\x{ad}\x{200b}-\x{200f}\x{202a}-\x{202f}\x{2060}\x{2066}-\x{2069}\x{feff}]' | |
| find "$GITHUB_WORKSPACE" \ | |
| -not -path '*/.git/*' -not -path '*/node_modules/*' \ | |
| -not -path '*/.deno/*' -not -path '*/target/*' \ | |
| -not -path '*/_build/*' -not -path '*/deps/*' \ | |
| -not -path '*/external_corpora/*' -not -path '*/.lake/*' \ | |
| -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \ | |
| -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \ | |
| -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 -aPl "$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" | |
| # Blocking subset: C0 controls and NUL only (owner ruling 2026-08-28). | |
| # Invisible Unicode (NBSP/BOM/zero-width) stays ADVISORY - about 2,100 | |
| # estate files carry it as legitimate typography in prose. | |
| blocking=0 | |
| while IFS= read -r bf; do | |
| [ -z "$bf" ] && continue | |
| if grep -qaP '\x00|[\x01-\x08\x0B\x0C\x0E-\x1F]' "$bf"; then | |
| blocking=$((blocking+1)) | |
| echo "::error file=${bf#$GITHUB_WORKSPACE/}::C0 control characters or NUL bytes - file corruption, blocks the gate" | |
| fi | |
| done < /tmp/empty-lint-results.txt | |
| echo "blocking=$blocking" >> "$GITHUB_OUTPUT" | |
| # Emit annotations for each file with invisible chars | |
| 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 | |
| # Enforce (owner ruling 2026-08-28): C0/NUL corruption BLOCKS; other | |
| # invisible Unicode stays advisory. Enforcement lives inside this step | |
| # so a crash above fails the job directly - counts can never arrive | |
| # empty into a separate check that then passes silently. | |
| if [ "$EL_EXIT" -ne 0 ]; then | |
| echo "::warning::invisible-character scan exited $EL_EXIT - results may be incomplete" | |
| fi | |
| if [ "${blocking:-0}" -gt 0 ]; then | |
| echo "## Empty-linter: BLOCKED - $blocking file(s) with C0/NUL corruption" >> "$GITHUB_STEP_SUMMARY" | |
| echo "::error::$blocking file(s) contain C0 control characters or NUL bytes - corruption, not typography. See file annotations." | |
| exit 1 | |
| elif [ "${FINDINGS:-0}" -gt 0 ]; then | |
| echo "::notice::$FINDINGS file(s) carry invisible Unicode (NBSP/BOM/zero-width) - advisory only" | |
| fi | |
| - 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 | |
| # --------------------------------------------------------------------------- | |
| # Job 4: Groove manifest check (for repos that should expose services) | |
| # --------------------------------------------------------------------------- | |
| groove-check: | |
| name: Groove manifest check | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Check for Groove manifest | |
| id: groove | |
| 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 | |
| if ! jq empty .well-known/groove/manifest.json 2>/dev/null; then | |
| 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" | |
| 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" | |
| 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." | |
| fi | |
| - 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-24.04 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Check and validate eclexiaiser manifest | |
| id: eclex | |
| 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." | |
| fi | |
| echo "has_manifest=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "has_manifest=true" >> "$GITHUB_OUTPUT" | |
| # Validate TOML structure using pre-installed yq | |
| ( | |
| PROJECT_NAME=$(yq -e '.project.name // ""' eclexiaiser.toml) | |
| if [ -z "$PROJECT_NAME" ]; then | |
| echo "ERROR: project.name is required" >&2 | |
| exit 1 | |
| fi | |
| FUNCTIONS_LEN=$(yq -e '.functions | length' eclexiaiser.toml) | |
| if [ -z "$FUNCTIONS_LEN" ] || [ "$FUNCTIONS_LEN" -eq 0 ]; then | |
| echo "ERROR: at least one [[functions]] entry is required" >&2 | |
| exit 1 | |
| fi | |
| for i in $(seq 0 $((FUNCTIONS_LEN - 1))); do | |
| FN_NAME=$(yq -e ".functions[$i].name // \"\"" eclexiaiser.toml) | |
| if [ -z "$FN_NAME" ]; then | |
| echo "ERROR: function name cannot be empty" >&2 | |
| exit 1 | |
| fi | |
| FN_SRC=$(yq -e ".functions[$i].source // \"\"" eclexiaiser.toml) | |
| if [ -z "$FN_SRC" ]; then | |
| echo "ERROR: function $FN_NAME has no source path" >&2 | |
| exit 1 | |
| fi | |
| done | |
| echo "Valid: $PROJECT_NAME ($FUNCTIONS_LEN function(s))" | |
| ) || { | |
| echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml — see step output for details" | |
| exit 1 | |
| } | |
| - 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-24.04 | |
| needs: [deed-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate] | |
| if: always() | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Generate dogfooding scorecard | |
| run: | | |
| SCORE=0 | |
| MAX=6 | |
| # DEED manifest present? | |
| if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then | |
| SCORE=$((SCORE + 1)) | |
| DEED_STATUS=":white_check_mark:" | |
| else | |
| DEED_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 | |
| cat <<EOF >> "$GITHUB_STEP_SUMMARY" | |
| ## Dogfooding Scorecard | |
| **Score: ${SCORE}/${MAX}** | |
| | Tool/Format | Status | Notes | | |
| |-------------|--------|-------| | |
| | DEED repo deed (`<reponame>_chora.deed`) | ${DEED_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.* | |
| EOF | |