-
-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ci): make the dogfood gate .deed-aware #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| # <<identity>>, 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 <<concrete-syntax>>: | ||
| # `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,17 +186,34 @@ 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 | ||
| # <<version-field>>). 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 <<version-field>> 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.) | ||
| # use markdown-style headers and free text, so identity check is relaxed | ||
| 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 <machine tree>/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,27 +244,49 @@ 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 \ | ||
| "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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not let contractile exemptions bypass DEED validation.
Restrict 🤖 Prompt for AI Agents🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Require
Track a DEED-specific schema flag and use it for 🤖 Prompt for AI Agents |
||
| report_issue "warning" "$file" 1 \ | ||
| "Missing version or schema_version field" | ||
| fi | ||
|
|
@@ -259,7 +322,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 +344,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,15 +370,15 @@ 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 | ||
| echo "::endgroup::" | ||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,10 +33,10 @@ jobs: | |
| - name: Check for A2ML files | ||
| id: detect | ||
| run: | | ||
| COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l) | ||
| COUNT=$(find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Add The workflow accepts a repository containing only 🤖 Prompt for AI Agents |
||
| 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,14 +49,14 @@ 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). | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| EOF | ||
| 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' -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 | | ||
| |-------------|--------|-------| | ||
| | A2ML manifest (0-AI-MANIFEST.a2ml) | ${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 | | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.