Skip to content

Commit 66e76a9

Browse files
fix(ci): make the dogfood gate .deed-aware (#379)
## Why This repo's Dogfood Gate enforced **A2ML, a format that no longer exists.** A2ML was retired after a name clash with the ML community; the live s-expression format is **`.deed`**, which has a grammar (`DEED-GRAMMAR-SPEC`) and is bound for IANA. There are **two** defects here, and fixing only the first would have been a fake cure. ### 1. The validator was frozen before the rename `.githooks/validate-a2ml.sh` was vendored at 13,737 bytes by commit `d5f229d` (2026-07-27, *"ci: vendor validation scripts and remove remote action pins"*) — six weeks **before** the 09-03 DEED rename. It contains **zero** `.deed` references. Replaced with the canonical dual-accept body from `hyperpolymath/deed-ecosystem/validate-action` @ `f9d999b6` (17,544 b), which dispatches on the DEED s-expression head — `(estate-deed|repo-deed|estate-atlas-deed|praxis-deed)` on the first form, per `DEED-GRAMMAR-SPEC <<concrete-syntax>>` — and **keeps `.a2ml` passing as legacy**. This repo's own `SPDX-License-Identifier` line is preserved, not overwritten. ### 2. 🚨 The step that *gates* that validator counted only `*.a2ml` ```yaml COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l) - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 # <- the actual gate run: bash .githooks/validate-a2ml.sh ``` 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**. Upgrading the validator alone would have been invisible. The selector now admits both extensions — **the same shape the K9 job in this very file already uses** (`find . \( -name '*.k9' -o -name '*.k9.ncl' \)`). ### 3. The gate was instructing maintainers to author the dead format The warning and summary told every RSR repo to create `0-AI-MANIFEST.a2ml` and to run `a2mliser init`. Reworded to name `.deed` as the format to author, `.a2ml` as legacy. ## Measured, one denominator Census across the **75** repos carrying a vendored `.githooks/validate-a2ml.sh` (75 unique repos; an earlier figure of 79 was a *row* count — 4 slugs were duplicated by two local checkouts reaching one remote): | property | count | |---|---| | detect step counts only `*.a2ml` | **75 / 75** | | workflow mentions `deed` at all | **0 / 75** | | carry any `.deed` file at `origin/main` | **0 / 75** | | validator at 13,737 b / md5 `8f61f8da` / MPL-2.0 | 74 | | `echidna` — 13,747 b / `c1769fc4` / **AGPL-3.0-or-later** | 1 | ## Why edit 2 is safe **It is behaviour-neutral today.** `find A -o B` is a strict superset of `find A`, and **0 of 75 repos carry a `.deed` file**, so `COUNT` is unchanged on every repo in the population. The change is purely forward-correct: it stops the gate going silently green the day a repo migrates. ## Verification - `bash -n` on the new validator: clean. - `python3 -c 'yaml.safe_load(...)'` on the edited workflow: parses. - The sweep **asserts its own precondition** per repo — exact size **and** md5 **and** SPDX before any write — and refuses (skips + flags) on any mismatch rather than overwriting an unrecognised file. `echidna` is handled by the same assert, which is what prevented a blanket copy from silently relicensing it AGPL-3.0-or-later → MPL-2.0. - Post-edit asserts: the restored SPDX line is present, the widened selector is present, and **no a2ml-only `find` survived**. - Upstream regression evidence: the 17,544-byte body is already merged on `origin/main` in both `a2ml-ecosystem` and `deed-ecosystem`; 14 repos / 725 `.a2ml` files produced identical exit codes under old and new validators, with a planted positive and negative. ## Not changed here - `strict` stays `false`. Flipping it is a separate, measured decision: 8 of 10 sampled repos go rc=0 → rc=1. - No `.a2ml` file is renamed. Extension migration is explicitly held pending a dry-run manifest (19,477 tracked `.a2ml` vs 15 `.deed`). - The canonical validator's own line 5 still reads `# validate-a2ml.sh — A2ML manifest validation script`. Left byte-identical to upstream on purpose, so *"is this repo at canonical?"* stays answerable by md5. **Flagged for an upstream fix in `deed-ecosystem`**, where it corrects once for every consumer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 6e9cacf commit 66e76a9

2 files changed

Lines changed: 95 additions & 32 deletions

File tree

‎.githooks/validate-a2ml.sh‎

Lines changed: 88 additions & 25 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,27 +244,49 @@ 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 \
223281
"Missing required identity field (agent-id, name, or project)"
224282
fi
225283

226-
if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$is_structural_identity" == "false" ]]; then
284+
# DEED is the live, grammar-bearing format: its schema version is required even
285+
# under a machine-readable tree. The structural-identity exemption below stays
286+
# scoped to legacy *.a2ml, which is no longer authored. (CodeRabbit, PR review.)
287+
local version_exempt_structural="$is_structural_identity"
288+
case "$file" in *.deed) version_exempt_structural=false ;; esac
289+
if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$version_exempt_structural" == "false" ]]; then
227290
report_issue "warning" "$file" 1 \
228291
"Missing version or schema_version field"
229292
fi
@@ -259,7 +322,7 @@ validate_a2ml() {
259322
fi
260323
done < "$file"
261324

262-
if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" ]]; then
325+
if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" && "$is_manifest" == "false" ]]; then
263326
report_issue "warning" "$file" "$attestation_line" \
264327
"Attestation block found but missing proof/signature/hash fields"
265328
fi
@@ -281,15 +344,15 @@ validate_a2ml() {
281344
}
282345

283346
# ---------------------------------------------------------------------------
284-
# Main: discover and validate .a2ml files
347+
# Main: discover and validate .a2ml and .deed files
285348
# ---------------------------------------------------------------------------
286349

287350
echo "::group::A2ML Manifest Validation"
288-
echo "Scanning ${SCAN_PATH} for .a2ml files..."
351+
echo "Scanning ${SCAN_PATH} for .a2ml and .deed files..."
289352
echo ""
290353

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

294357
# Apply paths-ignore filter
295358
a2ml_files=()
@@ -307,15 +370,15 @@ if [[ $SKIPPED -gt 0 ]]; then
307370
fi
308371

309372
if [[ ${#a2ml_files[@]} -eq 0 ]]; then
310-
echo "::notice::No .a2ml files found in ${SCAN_PATH}"
373+
echo "::notice::No .a2ml or .deed files found in ${SCAN_PATH}"
311374
echo "files_scanned=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true
312375
echo "errors=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true
313376
echo "warnings=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true
314377
echo "::endgroup::"
315378
exit 0
316379
fi
317380

318-
echo "Found ${#a2ml_files[@]} .a2ml file(s)"
381+
echo "Found ${#a2ml_files[@]} .a2ml/.deed file(s)"
319382
echo ""
320383

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

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ jobs:
4040
- name: Check for A2ML files
4141
id: detect
4242
run: |
43-
COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l)
43+
COUNT=$(find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l)
4444
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
4545
if [ "$COUNT" -eq 0 ]; then
46-
echo "::warning::No .a2ml manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml"
46+
echo "::warning::No .deed manifest files found. Every RSR repo should have 0-AI-MANIFEST.deed (.a2ml is legacy and no longer authored)"
4747
fi
4848
4949
- name: Validate A2ML manifests
@@ -56,14 +56,14 @@ jobs:
5656
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
5757
## A2ML Validation
5858
59-
:warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`.
59+
: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.)
6060
61-
Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo).
61+
Copy one from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo).
6262
EOF
6363
else
6464
echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY"
6565
echo "" >> "$GITHUB_STEP_SUMMARY"
66-
echo "Scanned **${A2ML_COUNT}** .a2ml file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY"
66+
echo "Scanned **${A2ML_COUNT}** manifest file(s) (.deed, or legacy .a2ml). See step output for details." >> "$GITHUB_STEP_SUMMARY"
6767
fi
6868
6969
# ---------------------------------------------------------------------------
@@ -326,7 +326,7 @@ jobs:
326326
MAX=6
327327
328328
# A2ML manifest present?
329-
if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then
329+
if find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then
330330
SCORE=$((SCORE + 1))
331331
A2ML_STATUS=":white_check_mark:"
332332
else
@@ -380,7 +380,7 @@ jobs:
380380
381381
| Tool/Format | Status | Notes |
382382
|-------------|--------|-------|
383-
| A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos |
383+
| AI manifest (0-AI-MANIFEST.deed, or legacy .a2ml) | ${A2ML_STATUS} | Required for all RSR repos |
384384
| K9 contracts | ${K9_STATUS} | Required for repos with config files |
385385
| .editorconfig | ${EC_STATUS} | Required for all repos |
386386
| Groove endpoint | ${GROOVE_STATUS} | Required for service repos |

0 commit comments

Comments
 (0)