From 6235b5d9276dd4c780769d4994c00205c9ef4787 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:20:30 +0100 Subject: [PATCH 1/3] fix(hooks): remove two dead-format gates that deadlocked every commit standards could not accept ANY commit through its own pre-commit hook. Two gates blocked each other, and both validate a format that no longer exists. The registry drift guard demanded every commit stage .machine_readable/REGISTRY.a2ml -- a generated, TOML-shaped artefact, i.e. a specimen of the record dialect the owner ruled SUPERSEDED on 2026-09-08. Staging it then tripped the "A2ML manifests" gate, because validate-a2ml.sh greps manifest syntax (^version:, ^(agent-id|pedigree):) that the live s-expression .deed grammar does not have. Measured: validate-a2ml.sh passes 0 of 222 tracked .a2ml files in this repo. Every commit in this repo's history must therefore have bypassed the hook. Repairing the regex is not the cure. It would turn all 222 files red at once, and it would be repairing a validator for a format the owner has ruled dead: A2ML was abandoned after the ML community objected to the name. .deed and .k9 are the live formats. Note the dates. REGISTRY.a2ml and scripts/build-registry.sh were created 2026-06-03 (#356, #357) -- legitimate work, three months before the DEED rename, simply never migrated. But .githooks/validate-a2ml.sh was created 2026-09-12, NINE DAYS AFTER the rename ruling and four days after the record dialect was killed: a new gate written for a format already declared dead. No hook in this repo knows .deed exists (pre-commit a2ml=4 / deed=0). Removed, per owner ruling R-H3 (2026-09-15): - run_validator "A2ML manifests" "validate-a2ml.sh" "staged" - the registry drift guard block Kept deliberately: - the K9 contracts gate. K9 is live. - registry drift coverage, which CI still enforces at .github/workflows/registry-verify.yml:56 (build-registry.sh --check). - REGISTRY.a2ml itself, byte-for-byte (owner ruling R-H4). It is a GENERATED artefact, so reshaping scripts/build-registry.sh is a separate, deliberate job and is not coupled to unblocking commits. A .deed validator returns to this hook once the dual-accept validate-action lands (owner ruling R-H2). Verification: bash -n clean; the hook now exits 0 against a staged set it previously rejected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/pre-commit | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 475355db..20f4a543 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -65,8 +65,14 @@ default_validator '(^|/)Makefile(\.|$)|\.mk$' "Makefiles not allowed. Use Mustfi default_validator '\.(java|kt|kts)$' "Java/Kotlin files not allowed. Use Rust/Tauri/Dioxus instead." default_validator '\.swift$' "Swift files not allowed. Use Tauri/Dioxus instead." -# A2ML + K9 + SPDX validation -run_validator "A2ML manifests" "validate-a2ml.sh" "staged" +# DEED + K9 + SPDX validation +# NOTE: the "A2ML manifests" gate was REMOVED 2026-09-15 (owner ruling R-H3). +# A2ML no longer exists as a format. validate-a2ml.sh was written 2026-09-12 -- +# nine days AFTER the DEED rename ruling -- and greps manifest syntax +# (^version:, ^(agent-id|pedigree):) that the live s-expression .deed grammar +# does not have. It passed 0 of 222 tracked .a2ml files, so NO commit could be +# made through this hook. Repairing the regex would turn all 222 red at once. +# A .deed validator returns here once the dual-accept action lands (R-H2). run_validator "K9 contracts" "validate-k9.sh" "staged" run_validator "SPDX headers" "validate-spdx.sh" "staged" @@ -77,15 +83,13 @@ run_validator "Workflow permissions" "validate-permissions.sh" "staged" run_validator "CodeQL configuration" "validate-codeql.sh" "staged" run_validator "Bot directives" "validate-bot-directives.sh" "staged" -# Registry drift guard -if [ -f "$REPO_ROOT/scripts/build-registry.sh" ]; then - echo -e "${BLUE}[pre-commit]${NC} Checking registry drift..." - if ! bash "$REPO_ROOT/scripts/build-registry.sh" --check >/dev/null 2>&1; then - echo -e "${RED}[pre-commit] REGISTRY.a2ml / TOPOLOGY.adoc are stale${NC}" >&2 - echo " Fix: bash scripts/build-registry.sh && git add .machine_readable/REGISTRY.a2ml TOPOLOGY.adoc" >&2 - ERRORS=$((ERRORS + 1)) - fi -fi +# Registry drift guard -- REMOVED 2026-09-15 (owner ruling R-H3). +# It demanded that every commit stage .machine_readable/REGISTRY.a2ml, a generated +# TOML-shaped artefact -- i.e. a specimen of the record dialect the owner ruled +# SUPERSEDED on 2026-09-08. Staging it then tripped the A2ML gate above, so the +# two gates deadlocked each other. The registry itself is UNCHANGED (R-H4); +# reshaping scripts/build-registry.sh is a separate, deliberate job. +# Drift is still caught in CI by .github/workflows/registry-verify.yml. # Canonical names guard if [ -f "$REPO_ROOT/scripts/check-canonical-names.sh" ]; then From 3a7971c498e2267d4dab43657ede2fc8bac16f4a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:23:42 +0100 Subject: [PATCH 2/3] fix(hooks): remove the same dead A2ML gate from pre-push The deadlock had a second limb. Removing the A2ML gate from pre-commit was not enough: pre-push line 44 runs the identical dead validator, so a push whose tip commit touched a .a2ml file was still rejected. Found the hard way -- a push whose only change was regenerating .machine_readable/REGISTRY.a2ml with the repo's own generator was refused with "missing agent-id or pedigree" and "missing version", manifest-dialect keys the live s-expression .deed grammar does not have. Same ruling (R-H3), same reasoning as the pre-commit removal in the previous commit. The K9 gate stays: K9 is live. Separately noted, NOT changed here: pre-push computes its file set from HEAD~1..HEAD, so a multi-commit push validates only the TIP commit. That is a latent fake-gate and deserves its own fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/pre-push | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 81f7a3bb..5164991f 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -41,7 +41,13 @@ run() { } # Core validations -run "A2ML manifests" "validate-a2ml.sh" +# NOTE: the "A2ML manifests" gate was REMOVED 2026-09-15 (owner ruling R-H3), +# for the same reason as in pre-commit: A2ML no longer exists as a format, and +# validate-a2ml.sh greps manifest syntax the live s-expression .deed grammar +# does not have. It passed 0 of 222 tracked .a2ml files, so this gate blocked +# every push whose tip commit touched one -- including a push whose ONLY change +# was regenerating .machine_readable/REGISTRY.a2ml with the repo generator. +# A .deed validator returns here once the dual-accept action lands (R-H2). run "K9 contracts" "validate-k9.sh" run "SPDX headers" "validate-spdx.sh" run "Workflow SPDX" "validate-spdx-workflows.sh" From 1c60098e0cffaf2f469e3af094796b513e22636c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:00:37 +0000 Subject: [PATCH 3/3] fix(hooks): make the canonical hookset validator .deed-aware standards/.githooks/validate-a2ml.sh was a 1,399-byte script that knew only the dead A2ML manifest dialect: it required ^(agent-id|pedigree): and ^version: and had no concept of .deed at all. It is the file that propagate-hooks.yml copies into every repo in both estates. Replace it with the dual-accept validator already merged on deed-ecosystem/validate-action main (f9d999b6): DEED s-expression heads (estate-deed, repo-deed, estate-atlas-deed, praxis-deed) dispatched on the first form per DEED-GRAMMAR-SPEC, :canonical-name and :schema-version, with .a2ml retained as legacy so nothing currently passing starts failing. This makes the PROPAGATION SOURCE safe before the vehicle is repaired. propagate-hooks.yml is currently inert -- its last five runs all failed at "Identify repositories with .githooks" with `Invalid format ' ""'`, a multi-line value written to $GITHUB_OUTPUT with no heredoc delimiter, so the propagate job is skipped every time. Repairing that workflow before this commit would have copied the dead-dialect script over 79 repos that already carry a better one. Verified: - bash -n clean - 4/4 deed-ecosystem conformance/valid fixtures accepted, 0 false rejects - 3/5 conformance/invalid rejected; the 2 version cases are warnings by design and the suite's own test 4 asserts strict promotes them - 14/14 sampled vendored repos (725 .a2ml files) give identical exit codes under the old and new script -- no regression - planted positive: a valid .deed passes under the new script; the old one has zero .deed references and cannot see it at all Refs #798 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/validate-a2ml.sh | 434 +++++++++++++++++++++++++++++++++---- 1 file changed, 395 insertions(+), 39 deletions(-) diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh index fb06f810..5020c830 100755 --- a/.githooks/validate-a2ml.sh +++ b/.githooks/validate-a2ml.sh @@ -1,52 +1,408 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 -# A2ML Manifest Validation +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# validate-a2ml.sh — A2ML manifest validation script +# +# 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) +# 4. Section heading syntax ([section] or ## section) +# +# Environment variables: +# INPUT_PATH — Directory to scan (default: .) +# INPUT_STRICT — Promote warnings to errors (default: false) +# +# Exit codes: +# 0 — All files valid (or only warnings in non-strict mode) +# 1 — Validation errors found set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + SCAN_PATH="${INPUT_PATH:-.}" STRICT="${INPUT_STRICT:-false}" -STAGED_FILES="${INPUT_STAGED_FILES:-}" +PATHS_IGNORE_RAW="${INPUT_PATHS_IGNORE:-}" +GITHUB_OUTPUT_FILE="${GITHUB_OUTPUT:-/dev/null}" + +# Parse paths-ignore: newline-separated fragments, blank lines and # comments +# stripped. Each fragment is a substring match against the file path. Pattern +# adopted from hyperpolymath/hypatia#243 — content-pattern validators must +# distinguish a target from a vendored / fixture file that legitimately +# contains the very pattern being checked. +PATHS_IGNORE=() +while IFS= read -r _frag; do + # Strip leading and trailing whitespace (canonical bash idiom). + _frag="${_frag#"${_frag%%[![:space:]]*}"}" + _frag="${_frag%"${_frag##*[![:space:]]}"}" + [[ -z "$_frag" || "$_frag" == \#* ]] && continue + PATHS_IGNORE+=("$_frag") +done <<< "$PATHS_IGNORE_RAW" + +# Returns 0 if path should be skipped (matches any ignore fragment) +path_ignored() { + local p="$1" frag + for frag in "${PATHS_IGNORE[@]}"; do + [[ "$p" == *"$frag"* ]] && return 0 + done + return 1 +} + +# Counters +FILES_SCANNED=0 ERRORS=0 +WARNINGS=0 + +# --------------------------------------------------------------------------- +# Helper: emit GitHub annotation +# --------------------------------------------------------------------------- +# Usage: annotate +# level: error | warning | notice +annotate() { + local level="$1" file="$2" line="$3" message="$4" + echo "::${level} file=${file},line=${line}::${message}" +} + +# --------------------------------------------------------------------------- +# Helper: report issue (respects strict mode) +# --------------------------------------------------------------------------- +# Usage: report_issue +# severity: error | warning +report_issue() { + local severity="$1" file="$2" line="$3" message="$4" + + if [[ "$severity" == "warning" && "$STRICT" == "true" ]]; then + severity="error" + fi + + annotate "$severity" "$file" "$line" "$message" + + if [[ "$severity" == "error" ]]; then + ERRORS=$((ERRORS + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi +} + +# --------------------------------------------------------------------------- +# Validator: check a single .a2ml or .deed file +# --------------------------------------------------------------------------- +validate_a2ml() { + local file="$1" + FILES_SCANNED=$((FILES_SCANNED + 1)) + + # --- Check 1: SPDX header --- + # The SPDX-License-Identifier should appear in the first 10 lines + local has_spdx=false + local line_num=0 + while IFS= read -r line; do + line_num=$((line_num + 1)) + if [[ $line_num -gt 10 ]]; then + break + fi + if [[ "$line" == *"SPDX-License-Identifier"* ]]; then + has_spdx=true + break + fi + done < "$file" + + if [[ "$has_spdx" == "false" ]]; then + report_issue "warning" "$file" 1 \ + "Missing SPDX-License-Identifier in first 10 lines" + fi + + # --- Check 2: Required identity fields --- + # A2ML files must contain either: + # - agent-id = "..." or agent_id = "..." + # - pedigree block with name field + # - name = "..." at top level (for AI manifests) + # - 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 + line_num=$((line_num + 1)) + + # Check for identity fields (various A2ML patterns) + # TOML/kv form: `name = "..."`, `project = "..."`, `agent-id = "..."` + if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project)[[:space:]]*= ]]; then + has_identity=true + fi + # S-expression form: `(name "...")`, `(project "...")`, + # `(agent-id "...")`. Some A2ML dialects (audit registries, + # classification stores) use Lisp-style s-expressions for the + # metadata block instead of TOML. Identity carries the same + # semantics; only the syntax differs. Match at any indent so it + # also picks up entries nested under `(metadata ...)`. + if [[ "$line" =~ ^[[:space:]]*\([[:space:]]*(agent[-_]id|name|project)[[:space:]]+\" ]]; then + has_identity=true + fi + # Colon / brace-block form: `name: "..."`, `id: "..."`, `project: "..."`. + # YAML-ish and brace-block A2ML dialects (e.g. `Trust { name: "..." }`, + # `id: "tsdm-standard"`) carry the same identity semantics; only the + # delimiter (`:` vs `=`) differs. `id` is the brace-block spelling of an + # identity key. + 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 + fi + # Version field — s-expression form + if [[ "$line" =~ ^[[:space:]]*\([[:space:]]*(version|schema_version)[[:space:]]+\" ]]; then + has_version=true + fi + # Version field — colon / brace-block form + 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.) + # use markdown-style headers and free text, so identity check is relaxed + local basename + basename="$(basename "$file")" + local is_manifest=false + # `.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 /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. + case "$basename" in + AGENTIC.a2ml|META.a2ml|NEUROSYM.a2ml|PLAYBOOK.a2ml|AI.a2ml) + # AI.a2ml = free-text "AI Assistant Instructions" manifest, the same + # doc type as 0-AI-MANIFEST.a2ml but with the bare name; identity is + # carried by the enclosing repo/plugin dir, not an in-file field. + is_manifest=true + ;; + # Dockerfile-style top-level typed manifests (Intentfile, Trustfile, …) + # use markdown-flavoured A2ML; identity is carried by the parent repo. + *file.a2ml) + is_manifest=true + ;; + esac + + # Contractile-shape A2ML files use `@directive:` syntax instead of + # TOML `key = value`. Trustfile.a2ml, Intentfile.a2ml, Mustfile.a2ml, + # Adjustfile.a2ml etc. are policy / trust / intent / abstract files + # whose identity is implicit in their @-prefixed directives + # (`@trust-level`, `@intent`, ...) rather than a TOML name/version + # pair. Treating them as manifest-shape produces 100% false positives — + # they're a different A2ML doc type. Detected by the presence of any + # contractile directive in the file body. + local is_contractile_shape=false + if grep -qE '^@(abstract|trust-level|trust-boundary|trust-actions|trust-deny|intent|must|adjust|end)([[:space:]]*:|$)' "$file"; then + is_contractile_shape=true + fi -validate_file() { - local file="$1" - - # Check required fields - if ! grep -qE '^(agent-id|pedigree):' "$file"; then - echo "[validate-a2ml] ERROR: $file missing agent-id or pedigree" >&2 - ERRORS=$((ERRORS + 1)) - fi - - # Check SPDX header - if ! head -5 "$file" | grep -qE '^# SPDX-License-Identifier:'; then - echo "[validate-a2ml] ERROR: $file missing SPDX header" >&2 - ERRORS=$((ERRORS + 1)) - fi - - # Check version - if ! grep -qE '^version:' "$file"; then - echo "[validate-a2ml] ERROR: $file missing version" >&2 - ERRORS=$((ERRORS + 1)) - fi + # 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 + # `*` 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 + report_issue "warning" "$file" 1 \ + "Missing version or schema_version field" + fi + + # --- Check 3: Attestation block structure --- + # If file contains [attestation] or ## ATTESTATION, validate it has + # required sub-fields: proof or signature + local in_attestation=false + local attestation_line=0 + local attestation_has_content=false + line_num=0 + + while IFS= read -r line; do + line_num=$((line_num + 1)) + + # Detect attestation section start + if [[ "$line" =~ ^\[attestation\] ]] || [[ "$line" =~ ^##[[:space:]]+[Aa]ttestation ]] || [[ "$line" =~ ^##[[:space:]]+ATTESTATION ]]; then + in_attestation=true + attestation_line=$line_num + continue + fi + + # Detect next section (ends attestation block) + if [[ "$in_attestation" == "true" ]]; then + if [[ "$line" =~ ^\[.+\] ]] || [[ "$line" =~ ^##[[:space:]] ]]; then + in_attestation=false + continue + fi + # Check for content in attestation block + if [[ "$line" =~ (proof|signature|verified|hash)[[:space:]]*= ]]; then + attestation_has_content=true + fi + fi + done < "$file" + + 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 + + # --- Check 4: Section heading syntax --- + # Validate that [section] headings are well-formed (no unclosed brackets) + line_num=0 + while IFS= read -r line; do + line_num=$((line_num + 1)) + # Lines starting with [ should have a matching ] + if [[ "$line" =~ ^\[ && ! "$line" =~ ^\[.+\] ]]; then + # Exclude markdown-style links and multi-line values + if [[ ! "$line" =~ ^\[.*\]\( && ! "$line" =~ ^\[TODO && ! "$line" =~ ^\[YOUR ]]; then + report_issue "warning" "$file" "$line_num" \ + "Possibly malformed section heading: unclosed bracket" + fi + fi + done < "$file" } -# If STAGED_FILES is provided, only validate those files -if [ -n "$STAGED_FILES" ]; then - while IFS=$'\n' read -r file; do - [ -z "$file" ] && continue - # Only check .a2ml files - [[ "$file" == *.a2ml ]] || continue - # Check if file exists - [ -f "$file" ] || continue - validate_file "$file" - done <<< "$STAGED_FILES" -else - # Scan entire path for .a2ml files - while IFS= read -r file; do - validate_file "$file" - done < <(find "$SCAN_PATH" -path '*/.git/*' -prune -o -name '*.a2ml' -type f -print 2>/dev/null) +# --------------------------------------------------------------------------- +# Main: discover and validate .a2ml and .deed files +# --------------------------------------------------------------------------- + +echo "::group::A2ML Manifest Validation" +echo "Scanning ${SCAN_PATH} for .a2ml and .deed files..." +echo "" + +# 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=() +SKIPPED=0 +for _f in "${a2ml_candidates[@]}"; do + if path_ignored "$_f"; then + SKIPPED=$((SKIPPED + 1)) + continue + fi + a2ml_files+=("$_f") +done + +if [[ $SKIPPED -gt 0 ]]; then + echo "::notice::Skipped ${SKIPPED} file(s) matching paths-ignore" +fi + +if [[ ${#a2ml_files[@]} -eq 0 ]]; then + 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/.deed file(s)" +echo "" + +for file in "${a2ml_files[@]}"; do + echo " Validating: ${file}" + validate_a2ml "$file" +done + +echo "" +echo "────────────────────────────────────────" +echo "Files scanned: ${FILES_SCANNED}" +echo "Errors: ${ERRORS}" +echo "Warnings: ${WARNINGS}" +echo "Strict mode: ${STRICT}" +echo "────────────────────────────────────────" + +# Write outputs for GitHub Actions +{ + echo "files_scanned=${FILES_SCANNED}" + echo "errors=${ERRORS}" + echo "warnings=${WARNINGS}" +} >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true + +echo "::endgroup::" + +# Exit with failure if errors were found +if [[ $ERRORS -gt 0 ]]; then + echo "::error::A2ML validation failed with ${ERRORS} error(s)" + exit 1 fi -[ $ERRORS -gt 0 ] && exit 1 -echo "[validate-a2ml] All A2ML files valid" +echo "A2ML validation passed." exit 0