1- @/tmp/n_007.yml
1+ # SPDX-License-Identifier: PMPL-1.0-or-later
2+ # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+ #
4+ # dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate
5+ # Validates that the repo uses hyperpolymath's own formats and tools.
6+ # Companion to static-analysis-gate.yml (security) — this is for format compliance.
7+ name : Dogfood Gate
8+
9+ on :
10+ pull_request :
11+ branches : ['**']
12+ push :
13+ branches : [main, master]
14+
15+ permissions :
16+ contents : read
17+
18+ jobs :
19+ # ---------------------------------------------------------------------------
20+ # Job 1: A2ML manifest validation
21+ # ---------------------------------------------------------------------------
22+ a2ml-validate :
23+ name : Validate A2ML manifests
24+ runs-on : ubuntu-latest
25+
26+ steps :
27+ - name : Checkout repository
28+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
30+ - name : Check for A2ML files
31+ id : detect
32+ run : |
33+ COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l)
34+ echo "count=$COUNT" >> "$GITHUB_OUTPUT"
35+ if [ "$COUNT" -eq 0 ]; then
36+ echo "::warning::No .a2ml manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml"
37+ fi
38+
39+ - name : Validate A2ML manifests
40+ if : steps.detect.outputs.count > 0
41+ uses : hyperpolymath/a2ml-validate-action@59145c7d1039fa3059b3ecacdb50ee23d7505898 # main
42+ with :
43+ path : ' .'
44+ strict : ' false'
45+
46+ - name : Write summary
47+ run : |
48+ A2ML_COUNT="${{ steps.detect.outputs.count }}"
49+ if [ "$A2ML_COUNT" -eq 0 ]; then
50+ cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
51+ ## A2ML Validation
52+
53+ :warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`.
54+
55+ Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo).
56+ EOF
57+ else
58+ echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY"
59+ echo "" >> "$GITHUB_STEP_SUMMARY"
60+ echo "Scanned **${A2ML_COUNT}** .a2ml file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY"
61+ fi
62+
63+ # ---------------------------------------------------------------------------
64+ # Job 2: K9 contract validation
65+ # ---------------------------------------------------------------------------
66+ k9-validate :
67+ name : Validate K9 contracts
68+ runs-on : ubuntu-latest
69+
70+ steps :
71+ - name : Checkout repository
72+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
73+
74+ - name : Check for K9 files
75+ id : detect
76+ run : |
77+ COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l)
78+ CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \
79+ -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \
80+ -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l)
81+ echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT"
82+ echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT"
83+ if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then
84+ echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts."
85+ fi
86+
87+ - name : Validate K9 contracts
88+ if : steps.detect.outputs.k9_count > 0
89+ uses : hyperpolymath/k9-validate-action@2d96f43c538964b097d159ed3a56ba5b5ceca227 # main
90+ with :
91+ path : ' .'
92+ strict : ' false'
93+
94+ - name : Write summary
95+ run : |
96+ K9_COUNT="${{ steps.detect.outputs.k9_count }}"
97+ CFG_COUNT="${{ steps.detect.outputs.config_count }}"
98+ if [ "$K9_COUNT" -eq 0 ]; then
99+ cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
100+ ## K9 Contract Validation
101+
102+ :warning: **No K9 contract files found.** Repos with configuration files should have K9 contracts.
103+
104+ Generate contracts with: `k9iser generate .`
105+ EOF
106+ else
107+ echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY"
108+ echo "" >> "$GITHUB_STEP_SUMMARY"
109+ echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY"
110+ fi
111+
112+ # ---------------------------------------------------------------------------
113+ # Job 3: Empty-linter — invisible character detection
114+ # ---------------------------------------------------------------------------
115+ empty-lint :
116+ name : Empty-linter (invisible characters)
117+ runs-on : ubuntu-latest
118+
119+ steps :
120+ - name : Checkout repository
121+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
122+
123+ - name : Scan for invisible characters
124+ id : lint
125+ run : |
126+ # Inline invisible character detection (from empty-linter's core patterns).
127+ # Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens,
128+ # non-breaking spaces, null bytes, and other invisible Unicode in source files.
129+ set +e
130+ 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'
131+ find "$GITHUB_WORKSPACE" \
132+ -not -path '*/.git/*' -not -path '*/node_modules/*' \
133+ -not -path '*/.deno/*' -not -path '*/target/*' \
134+ -not -path '*/_build/*' -not -path '*/deps/*' \
135+ -not -path '*/external_corpora/*' -not -path '*/.lake/*' \
136+ -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \
137+ -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \
138+ -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \
139+ -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \
140+ -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \
141+ -exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null
142+ EL_EXIT=$?
143+ set -e
144+
145+ FINDINGS=$(wc -l < /tmp/empty-lint-results.txt 2>/dev/null || echo 0)
146+ echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT"
147+ echo "exit_code=$EL_EXIT" >> "$GITHUB_OUTPUT"
148+ echo "ready=true" >> "$GITHUB_OUTPUT"
149+
150+ # Emit annotations for each file with invisible chars
151+ while IFS= read -r filepath; do
152+ [ -z "$filepath" ] && continue
153+ REL_PATH="${filepath#$GITHUB_WORKSPACE/}"
154+ echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)"
155+ done < /tmp/empty-lint-results.txt
156+
157+ - name : Write summary
158+ run : |
159+ if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then
160+ FINDINGS="${{ steps.lint.outputs.findings }}"
161+ if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then
162+ echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
163+ echo "" >> "$GITHUB_STEP_SUMMARY"
164+ echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY"
165+ else
166+ echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
167+ echo "" >> "$GITHUB_STEP_SUMMARY"
168+ echo ":white_check_mark: No invisible character issues found." >> "$GITHUB_STEP_SUMMARY"
169+ fi
170+ else
171+ echo "## Empty-Linter" >> "$GITHUB_STEP_SUMMARY"
172+ echo "" >> "$GITHUB_STEP_SUMMARY"
173+ echo "Skipped: empty-linter not available." >> "$GITHUB_STEP_SUMMARY"
174+ fi
175+
176+ # ---------------------------------------------------------------------------
177+ # Job 4: Groove manifest check (for repos that should expose services)
178+ # ---------------------------------------------------------------------------
179+ groove-check :
180+ name : Groove manifest check
181+ runs-on : ubuntu-latest
182+
183+ steps :
184+ - name : Checkout repository
185+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
186+
187+ - name : Check for Groove manifest
188+ id : groove
189+ run : |
190+ # Check for static or dynamic Groove endpoints
191+ HAS_MANIFEST="false"
192+ HAS_GROOVE_CODE="false"
193+
194+ if [ -f ".well-known/groove/manifest.json" ]; then
195+ HAS_MANIFEST="true"
196+ # Validate the manifest JSON
197+ if ! jq empty .well-known/groove/manifest.json 2>/dev/null; then
198+ echo "::error file=.well-known/groove/manifest.json::Invalid JSON in Groove manifest"
199+ else
200+ SVC_ID=$(jq -r '.service_id // "unknown"' .well-known/groove/manifest.json)
201+ echo "service_id=$SVC_ID" >> "$GITHUB_OUTPUT"
202+ fi
203+ fi
204+
205+ # Check for Groove endpoint code (Rust, Elixir, Zig, V)
206+ if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then
207+ HAS_GROOVE_CODE="true"
208+ fi
209+
210+ # Check if this repo likely serves HTTP (has server/listener code)
211+ HAS_SERVER="false"
212+ 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
213+ HAS_SERVER="true"
214+ fi
215+
216+ echo "has_manifest=$HAS_MANIFEST" >> "$GITHUB_OUTPUT"
217+ echo "has_groove_code=$HAS_GROOVE_CODE" >> "$GITHUB_OUTPUT"
218+ echo "has_server=$HAS_SERVER" >> "$GITHUB_OUTPUT"
219+
220+ if [ "$HAS_SERVER" = "true" ] && [ "$HAS_MANIFEST" = "false" ] && [ "$HAS_GROOVE_CODE" = "false" ]; then
221+ echo "::warning::This repo has server code but no Groove endpoint. Add .well-known/groove/manifest.json for service discovery."
222+ fi
223+
224+ - name : Write summary
225+ run : |
226+ echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY"
227+ echo "" >> "$GITHUB_STEP_SUMMARY"
228+ echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY"
229+ echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
230+ echo "| Static manifest (.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY"
231+ echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY"
232+ echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY"
233+
234+ # ---------------------------------------------------------------------------
235+ # Job 5: eclexiaiser manifest validation
236+ # ---------------------------------------------------------------------------
237+ eclexiaiser-validate :
238+ name : Validate eclexiaiser manifest
239+ runs-on : ubuntu-latest
240+
241+ steps :
242+ - name : Checkout repository
243+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
244+
245+ - name : Check and validate eclexiaiser manifest
246+ id : eclex
247+ run : |
248+ if [ ! -f "eclexiaiser.toml" ]; then
249+ # Check if repo has a Containerfile — if so, recommend eclexiaiser
250+ if [ -f "Containerfile" ]; then
251+ echo "::warning::Containerfile present but no eclexiaiser.toml. Run \`eclexiaiser init\` to scaffold energy/carbon budgets."
252+ fi
253+ echo "has_manifest=false" >> "$GITHUB_OUTPUT"
254+ exit 0
255+ fi
256+
257+ echo "has_manifest=true" >> "$GITHUB_OUTPUT"
258+
259+ # Validate TOML structure using Python 3.11+ tomllib.
260+ # Note: quoted heredoc delimiter ('PYEOF') passes the body verbatim
261+ # to Python stdin; YAML literal-block strips the leading 10 spaces,
262+ # so Python sees unindented top-level code.
263+ python3 - <<'PYEOF' || {
264+ import tomllib, sys
265+ with open('eclexiaiser.toml', 'rb') as f:
266+ data = tomllib.load(f)
267+ project = data.get('project', {})
268+ if not project.get('name', '').strip():
269+ print('ERROR: project.name is required', file=sys.stderr)
270+ sys.exit(1)
271+ functions = data.get('functions', [])
272+ if not functions:
273+ print('ERROR: at least one [[functions]] entry is required', file=sys.stderr)
274+ sys.exit(1)
275+ for fn in functions:
276+ if not fn.get('name', '').strip():
277+ print('ERROR: function name cannot be empty', file=sys.stderr)
278+ sys.exit(1)
279+ if not fn.get('source', '').strip():
280+ print(f'ERROR: function {fn["name"]} has no source path', file=sys.stderr)
281+ sys.exit(1)
282+ print(f'Valid: {project["name"]} ({len(functions)} function(s))')
283+ PYEOF
284+ echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml — see step output for details"
285+ exit 1
286+ }
287+
288+ - name : Write summary
289+ run : |
290+ if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then
291+ echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY"
292+ echo "" >> "$GITHUB_STEP_SUMMARY"
293+ echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY"
294+ else
295+ echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY"
296+ echo "" >> "$GITHUB_STEP_SUMMARY"
297+ echo ":ballot_box_with_check: No eclexiaiser.toml. Add one with \`eclexiaiser init\` for energy/carbon tracking." >> "$GITHUB_STEP_SUMMARY"
298+ fi
299+
300+ # ---------------------------------------------------------------------------
301+ # Job 6: Dogfooding summary
302+ # ---------------------------------------------------------------------------
303+ dogfood-summary :
304+ name : Dogfooding compliance summary
305+ runs-on : ubuntu-latest
306+ needs : [a2ml-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate]
307+ if : always()
308+
309+ steps :
310+ - name : Checkout repository
311+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
312+
313+ - name : Generate dogfooding scorecard
314+ run : |
315+ SCORE=0
316+ MAX=6
317+
318+ # A2ML manifest present?
319+ if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then
320+ SCORE=$((SCORE + 1))
321+ A2ML_STATUS=":white_check_mark:"
322+ else
323+ A2ML_STATUS=":x:"
324+ fi
325+
326+ # K9 contracts present?
327+ if find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | head -1 | grep -q .; then
328+ SCORE=$((SCORE + 1))
329+ K9_STATUS=":white_check_mark:"
330+ else
331+ K9_STATUS=":x:"
332+ fi
333+
334+ # .editorconfig present?
335+ if [ -f ".editorconfig" ]; then
336+ SCORE=$((SCORE + 1))
337+ EC_STATUS=":white_check_mark:"
338+ else
339+ EC_STATUS=":x:"
340+ fi
341+
342+ # Groove manifest or code?
343+ 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
344+ SCORE=$((SCORE + 1))
345+ GROOVE_STATUS=":white_check_mark:"
346+ else
347+ GROOVE_STATUS=":ballot_box_with_check:"
348+ fi
349+
350+ # VeriSimDB integration?
351+ 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
352+ SCORE=$((SCORE + 1))
353+ VSDB_STATUS=":white_check_mark:"
354+ else
355+ VSDB_STATUS=":ballot_box_with_check:"
356+ fi
357+
358+ # eclexiaiser energy tracking?
359+ if [ -f "eclexiaiser.toml" ]; then
360+ SCORE=$((SCORE + 1))
361+ ECLEX_STATUS=":white_check_mark:"
362+ else
363+ ECLEX_STATUS=":ballot_box_with_check:"
364+ fi
365+
366+ cat <<EOF >> "$GITHUB_STEP_SUMMARY"
367+ ## Dogfooding Scorecard
368+
369+ **Score: ${SCORE}/${MAX}**
370+
371+ | Tool/Format | Status | Notes |
372+ |-------------|--------|-------|
373+ | A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos |
374+ | K9 contracts | ${K9_STATUS} | Required for repos with config files |
375+ | .editorconfig | ${EC_STATUS} | Required for all repos |
376+ | Groove endpoint | ${GROOVE_STATUS} | Required for service repos |
377+ | VeriSimDB integration | ${VSDB_STATUS} | Required for stateful repos |
378+ | eclexiaiser | ${ECLEX_STATUS} | Energy/carbon budgets for container services |
379+
380+ ---
381+ *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.*
382+ *Dogfooding is guinea pig fooding — we test our tools on ourselves.*
383+ EOF
0 commit comments