Skip to content

Commit c52ceaf

Browse files
hyperpolymathclaude
andcommitted
fix(ci): make the dogfood gate .deed-aware
The dogfood gate enforced a format that no longer exists. Two defects, and fixing only the first would have been a fake cure. 1. .githooks/validate-a2ml.sh was frozen at a pre-DEED revision (13,737 b) with zero .deed references. Replaced with the canonical dual-accept body from deed-ecosystem/validate-action (17,544 b), which dispatches on the DEED s-expression head and keeps .a2ml passing as legacy. This repo's own SPDX-License-Identifier line is preserved. 2. The detect step that GATES that validator counted only *.a2ml: COUNT=$(find . -name '*.a2ml' ...) - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 So on a repo that had migrated to .deed, COUNT would be 0, the validate step would be SKIPPED, and the job would report green having validated nothing. The selector now admits both extensions -- the same shape the K9 job in this file already uses. Edit 2 is behaviour-neutral today: this repo carries no .deed file, so COUNT is unchanged. It is purely forward-correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx
1 parent c488538 commit c52ceaf

2 files changed

Lines changed: 88 additions & 30 deletions

File tree

‎.githooks/validate-a2ml.sh‎

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
# validate-a2ml.sh — A2ML manifest validation script
66
#
7-
# Scans for .a2ml files and validates:
7+
# Scans for .a2ml and .deed files and validates:
88
# 1. Required fields: agent-id or pedigree name, version
99
# 2. SPDX-License-Identifier header presence
1010
# 3. Attestation block structure (if present)
@@ -89,7 +89,7 @@ report_issue() {
8989
}
9090

9191
# ---------------------------------------------------------------------------
92-
# Validator: check a single .a2ml file
92+
# Validator: check a single .a2ml or .deed file
9393
# ---------------------------------------------------------------------------
9494
validate_a2ml() {
9595
local file="$1"
@@ -123,6 +123,7 @@ validate_a2ml() {
123123
# - project = "..." (for STATE.a2ml)
124124
local has_identity=false
125125
local has_version=false
126+
local first_form_seen=false
126127
line_num=0
127128

128129
while IFS= read -r line; do
@@ -150,6 +151,29 @@ validate_a2ml() {
150151
if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|id)[[:space:]]*: ]]; then
151152
has_identity=true
152153
fi
154+
# DEED s-expression head form: `(estate-deed`, `(repo-deed`,
155+
# `(estate-atlas-deed`, `(praxis-deed`. Per DEED-GRAMMAR-SPEC
156+
# <<identity>>, a file whose first form is one of the four declared
157+
# heads is a deed of that kind, and the head satisfies the structural
158+
# half of identity. This is what lets ATLAS.deed — which carries
159+
# :registry-version and legitimately no :canonical-name — validate.
160+
# The head is the FIRST form (DEED-GRAMMAR-SPEC <<concrete-syntax>>:
161+
# `Deed ::= Header Sep? Form Sep?` — one form, and it carries the head).
162+
# Checking every line let a malformed file open with some other form and
163+
# then append `(estate-deed ...)` lower down to buy identity. Only the
164+
# first form is eligible.
165+
if [[ "$first_form_seen" == "false" && "$line" =~ ^[[:space:]]*\( ]]; then
166+
first_form_seen=true
167+
if [[ "$line" =~ ^[[:space:]]*\((estate-deed|repo-deed|estate-atlas-deed|praxis-deed)([[:space:]]|$) ]]; then
168+
has_identity=true
169+
fi
170+
fi
171+
# DEED keyword identity form: `:canonical-name "..."` and the two other
172+
# identity keywords the spec names. Note the leading colon: none of the
173+
# three forms above match it, because they test the bare words.
174+
if [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then
175+
has_identity=true
176+
fi
153177
# Check for version field — TOML form
154178
if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*= ]]; then
155179
has_version=true
@@ -162,17 +186,34 @@ validate_a2ml() {
162186
if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*: ]]; then
163187
has_version=true
164188
fi
189+
# DEED keyword version form: `:schema-version "1.0.0"` — leading colon,
190+
# hyphenated, REQUIRED on all four deed heads (DEED-GRAMMAR-SPEC
191+
# <<version-field>>). All three patterns above spell it `schema_version`
192+
# with no leading colon, so a conforming deed matched none of them.
193+
# `:registry-version` is a distinct field, optional on the atlas.
194+
# `:schema-version` ONLY. `:registry-version` is a distinct, optional
195+
# atlas field (see the note above) and never satisfies the version
196+
# requirement, which DEED-GRAMMAR-SPEC <<version-field>> makes REQUIRED
197+
# on all four heads. Accepting it let a registry-only atlas head pass
198+
# with no schema version at all.
199+
if [[ "$line" =~ ^[[:space:]]*:schema-version[[:space:]] ]]; then
200+
has_version=true
201+
fi
165202
done < "$file"
166203

167204
# AI manifest files (0-AI-MANIFEST.a2ml, 0.1-AI-MANIFEST.a2ml, etc.)
168205
# use markdown-style headers and free text, so identity check is relaxed
169206
local basename
170207
basename="$(basename "$file")"
171208
local is_manifest=false
172-
if [[ "$basename" == *"AI-MANIFEST"* ]]; then
209+
# `.a2ml` ONLY. The exemption exists because AI manifests are markdown-ish
210+
# prose with no in-file identity; it is not a property of the name. Matching
211+
# the bare basename meant `example-AI-MANIFEST.deed` was exempted from BOTH
212+
# the identity and version checks — a deed that skipped the whole gate.
213+
if [[ "$basename" == *"AI-MANIFEST"*.a2ml ]]; then
173214
is_manifest=true
174215
fi
175-
# Canonical typed manifests under .machine_readable/descriptiles/ — identity comes
216+
# Canonical typed manifests under <machine tree>/descriptiles/ — identity comes
176217
# from the enclosing directory + filename, not an in-file field. Sibling
177218
# files in the same directory (ECOSYSTEM.a2ml, STATE.a2ml) DO carry their
178219
# own $name/project and continue to be validated normally.
@@ -203,20 +244,37 @@ validate_a2ml() {
203244
is_contractile_shape=true
204245
fi
205246

206-
# Canonical structured A2ML tree. Everything under a `.machine_readable/`
207-
# directory is a typed agent-readable doc (CLADE, ANCHOR, STATE,
208-
# ECOSYSTEM, bot_directives/{debt,coverage,methodology}, ai/AI,
209-
# policies/*, integrations/*, …). Per the RSR convention these carry
210-
# identity structurally — owning repo + path + filename — not via an
211-
# in-file `name`/`agent-id`. This generalises the `.machine_readable/descriptiles/`
212-
# rationale above to the whole tree: rsr-template-repo itself ships these
213-
# files without an in-file identity key, so requiring one produces
214-
# estate-wide false positives on every repo built from the canonical
215-
# template. Files outside `.machine_readable/` are still validated.
247+
# The structured A2ML tree. Everything under a repo's machine tree —
248+
# `machine-readable/` canonically, `.machine_readable/` in the legacy
249+
# layout — is a typed agent-readable doc (CLADE, ANCHOR, STATE, ECOSYSTEM,
250+
# bot_directives/{debt,coverage,methodology}, ai/AI, policies/*,
251+
# integrations/*, …). Per the RSR convention these carry identity
252+
# structurally — owning repo + path + filename — not via an in-file
253+
# `name`/`agent-id`. This generalises the `descriptiles/` rationale above
254+
# to the whole tree: rsr-template-repo itself ships these files without an
255+
# in-file identity key, so requiring one produces estate-wide false
256+
# positives on every repo built from the canonical template. Files outside
257+
# the machine tree are still validated.
258+
#
259+
# The machine tree is named `machine-readable/` canonically (un-hidden
260+
# 2026-08); `.machine_readable/` is the LEGACY name. BOTH are matched: the
261+
# canon, scaffoldia, the julia variant and ~300 minted repos still carry the
262+
# dotted form, while rsr-template-repo has moved. Matching only one name
263+
# makes whichever half of the estate has not migrated fail this check with
264+
# 16 spurious "missing identity field" errors -- which is exactly what
265+
# happened when the template renamed its tree and this action, being a
266+
# separate implementation from the template's vendored copy, kept matching
267+
# the old name only.
216268
local is_structural_identity=false
217-
if [[ "$file" == *"/.machine_readable/"* || "$file" == "./.machine_readable/"* || "$file" == ".machine_readable/"* ]]; then
218-
is_structural_identity=true
219-
fi
269+
# `*` matches the empty string, so */machine-readable/* already covers the
270+
# ./-prefixed form that `find .` emits; spelling it out separately (as the
271+
# original three-branch test did) is redundant. Verified equivalent across
272+
# ./-prefixed, bare and absolute paths, and on the negative cases.
273+
case "$file" in
274+
*/machine-readable/*|machine-readable/*|*/.machine_readable/*|.machine_readable/*)
275+
is_structural_identity=true
276+
;;
277+
esac
220278

221279
if [[ "$has_identity" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$is_structural_identity" == "false" ]]; then
222280
report_issue "error" "$file" 1 \
@@ -259,7 +317,7 @@ validate_a2ml() {
259317
fi
260318
done < "$file"
261319

262-
if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" ]]; then
320+
if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" && "$is_manifest" == "false" ]]; then
263321
report_issue "warning" "$file" "$attestation_line" \
264322
"Attestation block found but missing proof/signature/hash fields"
265323
fi
@@ -281,15 +339,15 @@ validate_a2ml() {
281339
}
282340

283341
# ---------------------------------------------------------------------------
284-
# Main: discover and validate .a2ml files
342+
# Main: discover and validate .a2ml and .deed files
285343
# ---------------------------------------------------------------------------
286344

287345
echo "::group::A2ML Manifest Validation"
288-
echo "Scanning ${SCAN_PATH} for .a2ml files..."
346+
echo "Scanning ${SCAN_PATH} for .a2ml and .deed files..."
289347
echo ""
290348

291-
# Find all .a2ml files, excluding .git directory
292-
mapfile -t a2ml_candidates < <(find "$SCAN_PATH" -name '*.a2ml' -not -path '*/.git/*' -type f | sort)
349+
# Find all .a2ml and .deed files, excluding .git directory
350+
mapfile -t a2ml_candidates < <(find "$SCAN_PATH" \( -name '*.a2ml' -o -name '*.deed' \) -not -path '*/.git/*' -type f | sort)
293351

294352
# Apply paths-ignore filter
295353
a2ml_files=()
@@ -307,15 +365,15 @@ if [[ $SKIPPED -gt 0 ]]; then
307365
fi
308366

309367
if [[ ${#a2ml_files[@]} -eq 0 ]]; then
310-
echo "::notice::No .a2ml files found in ${SCAN_PATH}"
368+
echo "::notice::No .a2ml or .deed files found in ${SCAN_PATH}"
311369
echo "files_scanned=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true
312370
echo "errors=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true
313371
echo "warnings=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true
314372
echo "::endgroup::"
315373
exit 0
316374
fi
317375

318-
echo "Found ${#a2ml_files[@]} .a2ml file(s)"
376+
echo "Found ${#a2ml_files[@]} .a2ml/.deed file(s)"
319377
echo ""
320378

321379
for file in "${a2ml_files[@]}"; do

‎.github/workflows/dogfood-gate.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ jobs:
3333
- name: Check for A2ML files
3434
id: detect
3535
run: |
36-
COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l)
36+
COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l)
3737
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
3838
if [ "$COUNT" -eq 0 ]; then
39-
echo "::warning::No .a2ml manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml"
39+
echo "::warning::No .deed manifest files found. Every RSR repo should have 0-AI-MANIFEST.deed (.a2ml is legacy and no longer authored)"
4040
fi
4141
4242
- name: Validate A2ML manifests
@@ -49,9 +49,9 @@ jobs:
4949
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
5050
## A2ML Validation
5151
52-
:warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`.
52+
:warning: **No manifest found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.deed`. (`.a2ml` still validates as legacy but is no longer authored.)
5353
54-
Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo).
54+
Copy one from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo).
5555
EOF
5656
else
5757
echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY"
@@ -245,7 +245,7 @@ jobs:
245245
MAX=5
246246
247247
# A2ML manifest present?
248-
if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then
248+
if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then
249249
SCORE=$((SCORE + 1))
250250
A2ML_STATUS=":white_check_mark:"
251251
else
@@ -291,7 +291,7 @@ jobs:
291291
292292
| Tool/Format | Status | Notes |
293293
|-------------|--------|-------|
294-
| A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos |
294+
| DEED manifest (0-AI-MANIFEST.deed) | ${A2ML_STATUS} | Required for all RSR repos |
295295
| K9 contracts | ${K9_STATUS} | Required for repos with config files |
296296
| .editorconfig | ${EC_STATUS} | Required for all repos |
297297
| Groove endpoint | ${GROOVE_STATUS} | Required for service repos |

0 commit comments

Comments
 (0)