From e539d8b79fc6cfa1d0e2764d2a64f233345a0a0e Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 04:22:15 +0100 Subject: [PATCH 1/2] 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 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/validate-a2ml.sh | 106 ++++++++++++++++++++++------- .github/workflows/dogfood-gate.yml | 12 ++-- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh index b053676..5020c83 100755 --- a/.githooks/validate-a2ml.sh +++ b/.githooks/validate-a2ml.sh @@ -4,7 +4,7 @@ # # validate-a2ml.sh — A2ML manifest validation script # -# Scans for .a2ml files and validates: +# Scans for .a2ml and .deed files and validates: # 1. Required fields: agent-id or pedigree name, version # 2. SPDX-License-Identifier header presence # 3. Attestation block structure (if present) @@ -89,7 +89,7 @@ report_issue() { } # --------------------------------------------------------------------------- -# Validator: check a single .a2ml file +# Validator: check a single .a2ml or .deed file # --------------------------------------------------------------------------- validate_a2ml() { local file="$1" @@ -123,6 +123,7 @@ validate_a2ml() { # - project = "..." (for STATE.a2ml) local has_identity=false local has_version=false + local first_form_seen=false line_num=0 while IFS= read -r line; do @@ -150,6 +151,29 @@ validate_a2ml() { if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|id)[[:space:]]*: ]]; then has_identity=true fi + # DEED s-expression head form: `(estate-deed`, `(repo-deed`, + # `(estate-atlas-deed`, `(praxis-deed`. Per DEED-GRAMMAR-SPEC + # <>, a file whose first form is one of the four declared + # heads is a deed of that kind, and the head satisfies the structural + # half of identity. This is what lets ATLAS.deed — which carries + # :registry-version and legitimately no :canonical-name — validate. + # The head is the FIRST form (DEED-GRAMMAR-SPEC <>: + # `Deed ::= Header Sep? Form Sep?` — one form, and it carries the head). + # Checking every line let a malformed file open with some other form and + # then append `(estate-deed ...)` lower down to buy identity. Only the + # first form is eligible. + if [[ "$first_form_seen" == "false" && "$line" =~ ^[[:space:]]*\( ]]; then + first_form_seen=true + if [[ "$line" =~ ^[[:space:]]*\((estate-deed|repo-deed|estate-atlas-deed|praxis-deed)([[:space:]]|$) ]]; then + has_identity=true + fi + fi + # DEED keyword identity form: `:canonical-name "..."` and the two other + # identity keywords the spec names. Note the leading colon: none of the + # three forms above match it, because they test the bare words. + if [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then + has_identity=true + fi # Check for version field — TOML form if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*= ]]; then has_version=true @@ -162,6 +186,19 @@ validate_a2ml() { if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*: ]]; then has_version=true fi + # DEED keyword version form: `:schema-version "1.0.0"` — leading colon, + # hyphenated, REQUIRED on all four deed heads (DEED-GRAMMAR-SPEC + # <>). All three patterns above spell it `schema_version` + # with no leading colon, so a conforming deed matched none of them. + # `:registry-version` is a distinct field, optional on the atlas. + # `:schema-version` ONLY. `:registry-version` is a distinct, optional + # atlas field (see the note above) and never satisfies the version + # requirement, which DEED-GRAMMAR-SPEC <> makes REQUIRED + # on all four heads. Accepting it let a registry-only atlas head pass + # with no schema version at all. + if [[ "$line" =~ ^[[:space:]]*:schema-version[[:space:]] ]]; then + has_version=true + fi done < "$file" # AI manifest files (0-AI-MANIFEST.a2ml, 0.1-AI-MANIFEST.a2ml, etc.) @@ -169,10 +206,14 @@ validate_a2ml() { local basename basename="$(basename "$file")" local is_manifest=false - if [[ "$basename" == *"AI-MANIFEST"* ]]; then + # `.a2ml` ONLY. The exemption exists because AI manifests are markdown-ish + # prose with no in-file identity; it is not a property of the name. Matching + # the bare basename meant `example-AI-MANIFEST.deed` was exempted from BOTH + # the identity and version checks — a deed that skipped the whole gate. + if [[ "$basename" == *"AI-MANIFEST"*.a2ml ]]; then is_manifest=true fi - # Canonical typed manifests under .machine_readable/descriptiles/ — identity comes + # Canonical typed manifests under /descriptiles/ — identity comes # from the enclosing directory + filename, not an in-file field. Sibling # files in the same directory (ECOSYSTEM.a2ml, STATE.a2ml) DO carry their # own $name/project and continue to be validated normally. @@ -203,20 +244,37 @@ validate_a2ml() { is_contractile_shape=true fi - # Canonical structured A2ML tree. Everything under a `.machine_readable/` - # directory is a typed agent-readable doc (CLADE, ANCHOR, STATE, - # ECOSYSTEM, bot_directives/{debt,coverage,methodology}, ai/AI, - # policies/*, integrations/*, …). Per the RSR convention these carry - # identity structurally — owning repo + path + filename — not via an - # in-file `name`/`agent-id`. This generalises the `.machine_readable/descriptiles/` - # rationale above to the whole tree: rsr-template-repo itself ships these - # files without an in-file identity key, so requiring one produces - # estate-wide false positives on every repo built from the canonical - # template. Files outside `.machine_readable/` are still validated. + # The structured A2ML tree. Everything under a repo's machine tree — + # `machine-readable/` canonically, `.machine_readable/` in the legacy + # layout — is a typed agent-readable doc (CLADE, ANCHOR, STATE, ECOSYSTEM, + # bot_directives/{debt,coverage,methodology}, ai/AI, policies/*, + # integrations/*, …). Per the RSR convention these carry identity + # structurally — owning repo + path + filename — not via an in-file + # `name`/`agent-id`. This generalises the `descriptiles/` rationale above + # to the whole tree: rsr-template-repo itself ships these files without an + # in-file identity key, so requiring one produces estate-wide false + # positives on every repo built from the canonical template. Files outside + # the machine tree are still validated. + # + # The machine tree is named `machine-readable/` canonically (un-hidden + # 2026-08); `.machine_readable/` is the LEGACY name. BOTH are matched: the + # canon, scaffoldia, the julia variant and ~300 minted repos still carry the + # dotted form, while rsr-template-repo has moved. Matching only one name + # makes whichever half of the estate has not migrated fail this check with + # 16 spurious "missing identity field" errors -- which is exactly what + # happened when the template renamed its tree and this action, being a + # separate implementation from the template's vendored copy, kept matching + # the old name only. local is_structural_identity=false - if [[ "$file" == *"/.machine_readable/"* || "$file" == "./.machine_readable/"* || "$file" == ".machine_readable/"* ]]; then - is_structural_identity=true - fi + # `*` matches the empty string, so */machine-readable/* already covers the + # ./-prefixed form that `find .` emits; spelling it out separately (as the + # original three-branch test did) is redundant. Verified equivalent across + # ./-prefixed, bare and absolute paths, and on the negative cases. + case "$file" in + */machine-readable/*|machine-readable/*|*/.machine_readable/*|.machine_readable/*) + is_structural_identity=true + ;; + esac if [[ "$has_identity" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$is_structural_identity" == "false" ]]; then report_issue "error" "$file" 1 \ @@ -259,7 +317,7 @@ validate_a2ml() { fi done < "$file" - if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" ]]; then + if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" && "$is_manifest" == "false" ]]; then report_issue "warning" "$file" "$attestation_line" \ "Attestation block found but missing proof/signature/hash fields" fi @@ -281,15 +339,15 @@ validate_a2ml() { } # --------------------------------------------------------------------------- -# Main: discover and validate .a2ml files +# Main: discover and validate .a2ml and .deed files # --------------------------------------------------------------------------- echo "::group::A2ML Manifest Validation" -echo "Scanning ${SCAN_PATH} for .a2ml files..." +echo "Scanning ${SCAN_PATH} for .a2ml and .deed files..." echo "" -# Find all .a2ml files, excluding .git directory -mapfile -t a2ml_candidates < <(find "$SCAN_PATH" -name '*.a2ml' -not -path '*/.git/*' -type f | sort) +# Find all .a2ml and .deed files, excluding .git directory +mapfile -t a2ml_candidates < <(find "$SCAN_PATH" \( -name '*.a2ml' -o -name '*.deed' \) -not -path '*/.git/*' -type f | sort) # Apply paths-ignore filter a2ml_files=() @@ -307,7 +365,7 @@ if [[ $SKIPPED -gt 0 ]]; then fi if [[ ${#a2ml_files[@]} -eq 0 ]]; then - echo "::notice::No .a2ml files found in ${SCAN_PATH}" + echo "::notice::No .a2ml or .deed files found in ${SCAN_PATH}" echo "files_scanned=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true echo "errors=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true echo "warnings=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true @@ -315,7 +373,7 @@ if [[ ${#a2ml_files[@]} -eq 0 ]]; then exit 0 fi -echo "Found ${#a2ml_files[@]} .a2ml file(s)" +echo "Found ${#a2ml_files[@]} .a2ml/.deed file(s)" echo "" for file in "${a2ml_files[@]}"; do diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 650f2fc..fc2d039 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -33,10 +33,10 @@ jobs: - name: Check for A2ML files id: detect run: | - COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l) + 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 manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml" + echo "::warning::No .deed manifest files found. Every RSR repo should have 0-AI-MANIFEST.deed (.a2ml is legacy and no longer authored)" fi - name: Validate A2ML manifests @@ -49,9 +49,9 @@ jobs: cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" ## A2ML Validation - :warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`. + :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.) - Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). + Copy one from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). EOF else echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" @@ -249,7 +249,7 @@ jobs: MAX=5 # A2ML manifest present? - if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then + if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then SCORE=$((SCORE + 1)) A2ML_STATUS=":white_check_mark:" else @@ -295,7 +295,7 @@ jobs: | Tool/Format | Status | Notes | |-------------|--------|-------| - | A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos | + | DEED manifest (0-AI-MANIFEST.deed) | ${A2ML_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 | From 742dcce1c2fe5be17b4cf38ec187bdb107b0f4aa Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:18:41 +0100 Subject: [PATCH 2/2] fix(ci): address review - align the gate's labels with what it counts CodeRabbit raised three findings on the first commit. All three are correct. 1. The step summary reported the combined .a2ml+.deed count as ".a2ml file(s)", and the scorecard row was renamed to "DEED manifest" while its status still came from a find matching .a2ml too - so a repo holding only legacy manifests scored a green DEED row. That is a label asserting one thing while its check asks another, which is the exact defect class this branch exists to cure. Fixed by aligning the LABELS to the check rather than narrowing the check: narrowing to .deed only would red-score essentially every repo in the estate today (19,477 tracked .a2ml against 15 .deed), and this branch is behaviour-neutral by design. 2. Both find sites now pass -type f. A directory named *.deed is not a manifest. 3. The validator's structural-identity exemption no longer waives the schema version for .deed. DEED is the live, grammar-bearing format; legacy .a2ml keeps the exemption. Proved by control: two files in one machine-readable directory differing only by extension now get different verdicts. Severity is unchanged (warning, and the gate invokes strict:false), so no repo can turn red on this. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/validate-a2ml.sh | 7 ++++++- .github/workflows/dogfood-gate.yml | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh index 5020c83..b16fa85 100755 --- a/.githooks/validate-a2ml.sh +++ b/.githooks/validate-a2ml.sh @@ -281,7 +281,12 @@ validate_a2ml() { "Missing required identity field (agent-id, name, or project)" fi - if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$is_structural_identity" == "false" ]]; then + # DEED is the live, grammar-bearing format: its schema version is required even + # under a machine-readable tree. The structural-identity exemption below stays + # scoped to legacy *.a2ml, which is no longer authored. (CodeRabbit, PR review.) + local version_exempt_structural="$is_structural_identity" + case "$file" in *.deed) version_exempt_structural=false ;; esac + if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$version_exempt_structural" == "false" ]]; then report_issue "warning" "$file" 1 \ "Missing version or schema_version field" fi diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index fc2d039..b772a40 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -33,7 +33,7 @@ jobs: - name: Check for A2ML files id: detect run: | - COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) + COUNT=$(find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) echo "count=$COUNT" >> "$GITHUB_OUTPUT" if [ "$COUNT" -eq 0 ]; then echo "::warning::No .deed manifest files found. Every RSR repo should have 0-AI-MANIFEST.deed (.a2ml is legacy and no longer authored)" @@ -56,7 +56,7 @@ jobs: else echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${A2ML_COUNT}** .a2ml file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" + echo "Scanned **${A2ML_COUNT}** manifest file(s) (.deed, or legacy .a2ml). See step output for details." >> "$GITHUB_STEP_SUMMARY" fi # --------------------------------------------------------------------------- @@ -249,7 +249,7 @@ jobs: MAX=5 # A2ML manifest present? - if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then + if find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then SCORE=$((SCORE + 1)) A2ML_STATUS=":white_check_mark:" else @@ -295,7 +295,7 @@ jobs: | Tool/Format | Status | Notes | |-------------|--------|-------| - | DEED manifest (0-AI-MANIFEST.deed) | ${A2ML_STATUS} | Required for all RSR repos | + | AI manifest (0-AI-MANIFEST.deed, or legacy .a2ml) | ${A2ML_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 |