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@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@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
1+ @/tmp/n_007.yml
0 commit comments