From dbf95813ee3b033f0b2790c31f253b0d5d627426 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:29:28 +0100 Subject: [PATCH 1/5] fix(actions/validate): see .deed files at all, and accept the deed identity/version forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured by running the validator against four spec-conformant sample deeds, one per ruled head, not by reading it. Three defects: 1. .deed was never scanned. The discovery glob matched only '*.a2ml', so a directory of conformant deeds was reported as having no files of interest and the script printed "A2ML validation passed." with exit 0. Once the estate converts to .deed (#64) this is a total gate bypass. Every source-level survey missed it because greps looked at the regexes and never at the glob. 2. Identity rejected :canonical-name. The identity chain tests the bare words agent[-_]id|name|project|spec_id, plus [metadata]/[scorecard] and @abstract; the deed surface uses a leading-colon keyword. 3. Version rejected :schema-version — the pattern wants version/schema_version, underscore and no leading colon. The identity and version clauses are those prescribed by DEED-GRAMMAR-SPEC <>; the glob is not, because the spec assumed the file was being opened. Per <>, the head symbol itself satisfies the structural half of identity, which is what lets ATLAS.deed — :registry-version and legitimately no :canonical-name — pass on its head alone. This fork's severity model is untouched: identity remains a lint WARNING here, per the header note on standards#435, where a2ml-ecosystem/validate-action treats it as an error. The tests assert that a problem is FLAGGED, not at which severity, so they hold under either policy. Reconciling the two forks is #9. The new workflow is deliberately bash-only so it reports on the shell validator independently of the Idris2 CLI build, which is red on main for unrelated reasons (the pinned Idris2 0.7.0 tarball does not exist). Verified: the four heads pass strict and non-strict; both negatives are flagged; the same tests fail 5 assertions against the unpatched script; and a repo-wide scan is annotation-identical before and after apart from the six new fixtures. Refs #72, #71, #64, #9 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QNjWX2B4FffG7zqMBMui6v --- .github/workflows/deed-conformance.yml | 30 +++++++++ actions/validate/conformance/README.adoc | 44 +++++++++++++ .../invalid/deed-missing-head.deed | 4 ++ .../invalid/deed-missing-version.deed | 4 ++ .../validate/conformance/run-deed-tests.sh | 62 +++++++++++++++++++ actions/validate/conformance/valid/ATLAS.deed | 5 ++ .../conformance/valid/cadastre_praxis.deed | 8 +++ .../conformance/valid/estate_chora.deed | 7 +++ .../conformance/valid/vexometer_chora.deed | 6 ++ actions/validate/validate-a2ml.sh | 36 +++++++---- 10 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/deed-conformance.yml create mode 100644 actions/validate/conformance/README.adoc create mode 100644 actions/validate/conformance/invalid/deed-missing-head.deed create mode 100644 actions/validate/conformance/invalid/deed-missing-version.deed create mode 100755 actions/validate/conformance/run-deed-tests.sh create mode 100644 actions/validate/conformance/valid/ATLAS.deed create mode 100644 actions/validate/conformance/valid/cadastre_praxis.deed create mode 100644 actions/validate/conformance/valid/estate_chora.deed create mode 100644 actions/validate/conformance/valid/vexometer_chora.deed diff --git a/.github/workflows/deed-conformance.yml b/.github/workflows/deed-conformance.yml new file mode 100644 index 0000000..c3659fa --- /dev/null +++ b/.github/workflows/deed-conformance.yml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: MPL-2.0 +# Behavioural test for .deed support in actions/validate/validate-a2ml.sh. +# +# Deliberately bash-only: it must not depend on the Idris2 toolchain, so that +# it reports on the shell validator independently of the CLI build. +name: Deed Conformance + +permissions: read-all + +on: + push: + paths: + - 'actions/validate/**' + - '.github/workflows/deed-conformance.yml' + pull_request: + paths: + - 'actions/validate/**' + - '.github/workflows/deed-conformance.yml' + workflow_dispatch: + +jobs: + deed-conformance: + name: Deed fixtures — all four ruled heads + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + - name: Run deed conformance tests + run: bash actions/validate/conformance/run-deed-tests.sh diff --git a/actions/validate/conformance/README.adoc b/actions/validate/conformance/README.adoc new file mode 100644 index 0000000..2e17cbd --- /dev/null +++ b/actions/validate/conformance/README.adoc @@ -0,0 +1,44 @@ += Deed Conformance Fixtures + +// SPDX-License-Identifier: MPL-2.0 + +Behavioural fixtures for `.deed` support in link:../validate-a2ml.sh[validate-a2ml.sh]. + +== Why these exist + +The gap they guard was invisible to every source-level survey. The discovery +glob matched only `'*.a2ml'`, so `.deed` files were never opened at all: the +validator reported no errors and exited 0 having validated nothing. String +presence of a regex is not behavioural acceptance — only running it settles it. + +link:run-deed-tests.sh[run-deed-tests.sh] therefore asserts on the discovery +COUNT as well as the exit code, because `exit 0` is exactly what the defect +produced. + +== Layout + +* `valid/` — one fixture per ruled deed head (DEED-GRAMMAR-SPEC + `<>`): `estate-deed`, `repo-deed`, `praxis-deed`, + `estate-atlas-deed`. All four must pass, strict and non-strict. ++ +`ATLAS.deed` carries `:registry-version` and deliberately no +`:canonical-name`: per `<>` the head symbol itself satisfies the +structural half of identity, so the atlas form must pass on its head alone. + +* `invalid/` — `deed-missing-head.deed` (no ruled head and no identity keyword) + and `deed-missing-version.deed` (no `:schema-version`). Both are *deliberate* + negatives; a repo-wide scan will report them, which is the intended behaviour. + +== Severity + +This fork treats identity as a lint *warning* (see the script header on +standards#435), where `a2ml-ecosystem/validate-action` treats it as an error. +The tests here assert that a problem is *flagged*, not at which severity, so +they hold under either policy. Reconciling the two forks is a separate matter. + +== Run + +[source,sh] +---- +bash actions/validate/conformance/run-deed-tests.sh +---- diff --git a/actions/validate/conformance/invalid/deed-missing-head.deed b/actions/validate/conformance/invalid/deed-missing-head.deed new file mode 100644 index 0000000..111410f --- /dev/null +++ b/actions/validate/conformance/invalid/deed-missing-head.deed @@ -0,0 +1,4 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: CC-BY-SA-4.0 +(some-other-form + :unrelated "value") diff --git a/actions/validate/conformance/invalid/deed-missing-version.deed b/actions/validate/conformance/invalid/deed-missing-version.deed new file mode 100644 index 0000000..e9562c4 --- /dev/null +++ b/actions/validate/conformance/invalid/deed-missing-version.deed @@ -0,0 +1,4 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: CC-BY-SA-4.0 +(repo-deed + :canonical-name "no-version") diff --git a/actions/validate/conformance/run-deed-tests.sh b/actions/validate/conformance/run-deed-tests.sh new file mode 100755 index 0000000..2127f0a --- /dev/null +++ b/actions/validate/conformance/run-deed-tests.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +# SPDX-License-Identifier: MPL-2.0 +# +# Behavioural test for .deed support in validate-action/validate-a2ml.sh. +# +# This exists because the gap it guards was invisible to every source-level +# survey. The discovery glob matched only '*.a2ml', so .deed files were never +# opened at all: the validator reported "no errors" and exited 0 having +# validated nothing. String presence of a regex is not behavioural acceptance — +# only running it settles it. So this runs the validator, and it asserts on the +# discovery COUNT, because "exit 0" is exactly what the bug produced. +# +# The four valid fixtures are one per ruled deed head (DEED-GRAMMAR-SPEC +# <>): estate-deed, repo-deed, praxis-deed, estate-atlas-deed. +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VALIDATOR="${HERE}/../validate-a2ml.sh" +FAILURES=0 + +ok() { echo " PASS: $1"; } +fail() { echo " FAIL: $1"; FAILURES=$((FAILURES + 1)); } + +# The conformance directories hold .a2ml fixtures too. Isolate the .deed ones +# so the discovery count is exact rather than incidental. +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/valid" "$WORK/invalid" +cp "$HERE"/valid/*.deed "$WORK/valid/" +cp "$HERE"/invalid/*.deed "$WORK/invalid/" + +echo "1. valid deeds — all four ruled heads, non-strict" +out="$(INPUT_PATH="$WORK/valid" bash "$VALIDATOR" 2>&1)"; rc=$? +grep -q 'Found 4 ' <<<"$out" && ok "discovered 4 deed files" \ + || fail "discovery: '$(grep -o 'Found [0-9]* [^ ]* file(s)' <<<"$out")' (expected 4)" +[[ $rc -eq 0 ]] && ok "exit 0" || fail "exit $rc (expected 0)" +grep -q '::error' <<<"$out" && fail "unexpected error annotation" || ok "no errors" + +echo "2. valid deeds — strict" +out="$(INPUT_PATH="$WORK/valid" INPUT_STRICT=true bash "$VALIDATOR" 2>&1)"; rc=$? +[[ $rc -eq 0 ]] && ok "exit 0 under strict" || fail "exit $rc under strict (expected 0)" + +echo "3. deed-missing-head.deed — identity must be flagged" +out="$(INPUT_PATH="$WORK/invalid" bash "$VALIDATOR" 2>&1)" +grep -q 'Found 2 ' <<<"$out" && ok "discovered 2 deed files" || fail "invalid set not discovered" +grep -qi 'deed-missing-head\.deed.*identity' <<<"$out" \ + && ok "identity flagged" || fail "no identity annotation for deed-missing-head.deed" + +echo "4. deed-missing-version.deed — version must be flagged" +grep -q 'deed-missing-version\.deed' <<<"$out" \ + && ok "flagged non-strict" || fail "no annotation for deed-missing-version.deed" +out="$(INPUT_PATH="$WORK/invalid" INPUT_STRICT=true bash "$VALIDATOR" 2>&1)"; rc=$? +[[ $rc -ne 0 ]] && ok "non-zero exit under strict" || fail "exit 0 under strict (expected non-zero)" + +echo +if [[ $FAILURES -eq 0 ]]; then + echo "All deed validator tests passed." +else + echo "${FAILURES} deed validator test(s) failed." + exit 1 +fi diff --git a/actions/validate/conformance/valid/ATLAS.deed b/actions/validate/conformance/valid/ATLAS.deed new file mode 100644 index 0000000..c0b0210 --- /dev/null +++ b/actions/validate/conformance/valid/ATLAS.deed @@ -0,0 +1,5 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: CC-BY-SA-4.0 +(estate-atlas-deed + :schema-version "1.0.0" + :registry-version "3") diff --git a/actions/validate/conformance/valid/cadastre_praxis.deed b/actions/validate/conformance/valid/cadastre_praxis.deed new file mode 100644 index 0000000..ee3514e --- /dev/null +++ b/actions/validate/conformance/valid/cadastre_praxis.deed @@ -0,0 +1,8 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: CC-BY-SA-4.0 +(praxis-deed + :schema-version "1.0.0" + :canonical-name "cadastre" + :beholding-chora #u5"estate/chora" + (rules + (rule :priority 10))) diff --git a/actions/validate/conformance/valid/estate_chora.deed b/actions/validate/conformance/valid/estate_chora.deed new file mode 100644 index 0000000..066792c --- /dev/null +++ b/actions/validate/conformance/valid/estate_chora.deed @@ -0,0 +1,7 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: CC-BY-SA-4.0 +(estate-deed + :schema-version "1.0.0" + :canonical-name "estate" + (vocabulary + (term :name "chora" :gloss "a place with a role"))) diff --git a/actions/validate/conformance/valid/vexometer_chora.deed b/actions/validate/conformance/valid/vexometer_chora.deed new file mode 100644 index 0000000..a6fb80c --- /dev/null +++ b/actions/validate/conformance/valid/vexometer_chora.deed @@ -0,0 +1,6 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: CC-BY-SA-4.0 +(repo-deed + :schema-version "1.0.0" + :canonical-name "vexometer" + :beholding-chora #u5"estate/chora") diff --git a/actions/validate/validate-a2ml.sh b/actions/validate/validate-a2ml.sh index fbdd88c..bd020b7 100755 --- a/actions/validate/validate-a2ml.sh +++ b/actions/validate/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. Identity presence (warning — see below) # 2. SPDX-License-Identifier header presence # 3. Attestation block structure (if present) @@ -88,7 +88,7 @@ report_issue() { } # --------------------------------------------------------------------------- -# Validator: check a single .a2ml file +# Validator: check a single .a2ml or .deed file # --------------------------------------------------------------------------- validate_a2ml() { local file="$1" @@ -129,15 +129,27 @@ validate_a2ml() { while IFS= read -r line; do line_num=$((line_num + 1)) - # Check for identity fields (various A2ML patterns) + # Check for identity fields (various A2ML patterns). + # The last two clauses are the deed surface (DEED-GRAMMAR-SPEC + # <>): the s-expression document head, and the + # leading-colon identity keywords. Per <> the head symbol is + # itself identifying, which is what lets ATLAS.deed — :registry-version + # and legitimately no :canonical-name — pass on its head alone. if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|spec_id)[[:space:]]*= ]] \ || [[ "$line" =~ ^[[:space:]]*name[[:space:]]*: ]] \ || [[ "$line" =~ ^\[(metadata|scorecard)\] ]] \ - || [[ "$line" =~ ^@abstract ]]; then + || [[ "$line" =~ ^@abstract ]] \ + || [[ "$line" =~ ^[[:space:]]*\((estate-deed|repo-deed|estate-atlas-deed|praxis-deed)([[:space:]]|$) ]] \ + || [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then has_identity=true fi - # Check for version field (either separator) - if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*[=:] ]]; then + # Check for version field (either separator). The second clause is the + # deed keyword form: `:schema-version "1.0.0"` — leading colon and a + # hyphen, so the first clause (which spells it schema_version with no + # colon) never matched it. :registry-version is a distinct field, + # optional on the atlas. + if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*[=:] ]] \ + || [[ "$line" =~ ^[[:space:]]*:(schema-version|registry-version)[[:space:]] ]]; then has_version=true fi # Template placeholder marker ({{PROJECT_NAME}}, {{VERSION}}, …) @@ -230,18 +242,18 @@ 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_files < <(find "$SCAN_PATH" -name '*.a2ml' -not -path '*/.git/*' -type f | sort) +# Find all .a2ml and .deed files, excluding .git directory +mapfile -t a2ml_files < <(find "$SCAN_PATH" \( -name '*.a2ml' -o -name '*.deed' \) -not -path '*/.git/*' -type f | sort) 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" 2>/dev/null || true echo "errors=0" >> "$GITHUB_OUTPUT" 2>/dev/null || true echo "warnings=0" >> "$GITHUB_OUTPUT" 2>/dev/null || true @@ -249,7 +261,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 From f4479262c556feb6e9e1c8b5f97423b2435e39b9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:49:27 +0100 Subject: [PATCH 2/5] fix(validate): close three gate bypasses, and assert the diagnostics per fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports to this repo the fixes already made in a2ml-ecosystem#52, plus the per-fixture assertions the review asked for on run-deed-tests.sh:54. Three ways a deed passed validation without meeting the grammar. Each was demonstrated on the parent commit first: the three new fixtures produced rc=0 and ZERO annotations before these changes. 1. `:registry-version` satisfied the version requirement. It is optional atlas metadata; DEED-GRAMMAR-SPEC <> makes `:schema-version` REQUIRED on all four heads. A registry-only atlas head passed carrying no schema version at all. 2. The AI-MANIFEST exemption matched `*AI-MANIFEST*` on any extension, so a .deed named that way was exempted from identity AND version. The exemption is for .a2ml markdown prose; narrowed to it. 3. The document head was matched anywhere in the file, so a nested or trailing `(estate-deed ...)` identified a document whose actual head was something else. The head identifies only as the FIRST form (<>). The tests asserted only that a fixture's NAME appeared in the output, which any annotation satisfied — a broken version check still passed. Now anchored on severity + fixture + message, per fixture, in both modes, with a negative control proving the anchoring does not match across annotation boundaries. Controls run: - 3 new fixtures on the parent commit: rc=0, 0 annotations (bugs live) - the four ruled heads still validate clean under strict (rc=0) - repo-wide annotation SET diff: +4 (exactly the new fixtures), -0 - suite: 20/20 assertions pass, exit 0 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QNjWX2B4FffG7zqMBMui6v --- .../invalid/deed-head-not-first.deed | 8 ++++ .../invalid/deed-registry-version-only.deed | 5 ++ .../invalid/example-AI-MANIFEST.deed | 5 ++ .../validate/conformance/run-deed-tests.sh | 47 +++++++++++++++---- actions/validate/validate-a2ml.sh | 23 +++++++-- 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 actions/validate/conformance/invalid/deed-head-not-first.deed create mode 100644 actions/validate/conformance/invalid/deed-registry-version-only.deed create mode 100644 actions/validate/conformance/invalid/example-AI-MANIFEST.deed diff --git a/actions/validate/conformance/invalid/deed-head-not-first.deed b/actions/validate/conformance/invalid/deed-head-not-first.deed new file mode 100644 index 0000000..48d6b04 --- /dev/null +++ b/actions/validate/conformance/invalid/deed-head-not-first.deed @@ -0,0 +1,8 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: MPL-2.0 + +(some-other-form + :x "y") + +(estate-deed + :schema-version "1.0.0") diff --git a/actions/validate/conformance/invalid/deed-registry-version-only.deed b/actions/validate/conformance/invalid/deed-registry-version-only.deed new file mode 100644 index 0000000..aac7fa6 --- /dev/null +++ b/actions/validate/conformance/invalid/deed-registry-version-only.deed @@ -0,0 +1,5 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: MPL-2.0 + +(estate-atlas-deed + :registry-version "3") diff --git a/actions/validate/conformance/invalid/example-AI-MANIFEST.deed b/actions/validate/conformance/invalid/example-AI-MANIFEST.deed new file mode 100644 index 0000000..a9a5b1a --- /dev/null +++ b/actions/validate/conformance/invalid/example-AI-MANIFEST.deed @@ -0,0 +1,5 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: MPL-2.0 + +(not-a-head + :nothing "here") diff --git a/actions/validate/conformance/run-deed-tests.sh b/actions/validate/conformance/run-deed-tests.sh index 2127f0a..3ddff95 100755 --- a/actions/validate/conformance/run-deed-tests.sh +++ b/actions/validate/conformance/run-deed-tests.sh @@ -41,17 +41,46 @@ echo "2. valid deeds — strict" out="$(INPUT_PATH="$WORK/valid" INPUT_STRICT=true bash "$VALIDATOR" 2>&1)"; rc=$? [[ $rc -eq 0 ]] && ok "exit 0 under strict" || fail "exit $rc under strict (expected 0)" -echo "3. deed-missing-head.deed — identity must be flagged" -out="$(INPUT_PATH="$WORK/invalid" bash "$VALIDATOR" 2>&1)" -grep -q 'Found 2 ' <<<"$out" && ok "discovered 2 deed files" || fail "invalid set not discovered" -grep -qi 'deed-missing-head\.deed.*identity' <<<"$out" \ - && ok "identity flagged" || fail "no identity annotation for deed-missing-head.deed" - -echo "4. deed-missing-version.deed — version must be flagged" -grep -q 'deed-missing-version\.deed' <<<"$out" \ - && ok "flagged non-strict" || fail "no annotation for deed-missing-version.deed" +# assert_ann +# Anchored on the fixture AND the message: "the filename appears somewhere in +# the output" was satisfiable by any annotation at all, so a broken version +# check still passed. `file=[^,]*` keeps the path free but stops the regex +# wandering into the next annotation. +assert_ann() { + grep -qE "::$1 file=[^,]*$2,line=[0-9]+::.*$3" <<<"$out" \ + && ok "$2 -> ::$1 ($3)" || fail "$2: expected ::$1 matching '$3'" +} + +echo "3. invalid deeds — non-strict: every diagnostic, per fixture" +out="$(INPUT_PATH="$WORK/invalid" bash "$VALIDATOR" 2>&1)"; rc=$? +grep -q 'Found 5 ' <<<"$out" && ok "discovered 5 deed files" \ + || fail "discovery: '$(grep -o 'Found [0-9]* [^ ]* file(s)' <<<"$out")' (expected 5)" +[[ $rc -eq 0 ]] && ok "exit 0 (identity and version are warnings here)" \ + || fail "exit $rc non-strict (expected 0)" +assert_ann warning 'deed-missing-head\.deed' 'No identity found' +assert_ann warning 'deed-missing-head\.deed' 'Missing version or schema_version field' +assert_ann warning 'deed-missing-version\.deed' 'Missing version or schema_version field' +# :registry-version is optional atlas metadata, never a schema version. +assert_ann warning 'deed-registry-version-only\.deed' 'Missing version or schema_version field' +# A head that is not the FIRST form does not identify the document. +assert_ann warning 'deed-head-not-first\.deed' 'No identity found' +# The AI-MANIFEST exemption is for .a2ml prose; a .deed is still a deed. +assert_ann warning 'example-AI-MANIFEST\.deed' 'No identity found' +assert_ann warning 'example-AI-MANIFEST\.deed' 'Missing version or schema_version field' +# Negative control: a diagnostic naming a fixture that has none would mean the +# anchoring above is matching across annotation boundaries. +grep -qE '::warning file=[^,]*deed-registry-version-only\.deed,line=[0-9]+::No identity found' <<<"$out" \ + && fail "atlas head wrongly reported identity-less" || ok "atlas head still identifies" + +echo "4. invalid deeds — strict promotes every warning to an error" out="$(INPUT_PATH="$WORK/invalid" INPUT_STRICT=true bash "$VALIDATOR" 2>&1)"; rc=$? [[ $rc -ne 0 ]] && ok "non-zero exit under strict" || fail "exit 0 under strict (expected non-zero)" +assert_ann error 'deed-missing-head\.deed' 'No identity found' +assert_ann error 'deed-missing-version\.deed' 'Missing version or schema_version field' +assert_ann error 'deed-registry-version-only\.deed' 'Missing version or schema_version field' +assert_ann error 'deed-head-not-first\.deed' 'No identity found' +assert_ann error 'example-AI-MANIFEST\.deed' 'Missing version or schema_version field' +grep -q '::warning' <<<"$out" && fail "::warning survived strict" || ok "no ::warning survives strict" echo if [[ $FAILURES -eq 0 ]]; then diff --git a/actions/validate/validate-a2ml.sh b/actions/validate/validate-a2ml.sh index bd020b7..d822ed3 100755 --- a/actions/validate/validate-a2ml.sh +++ b/actions/validate/validate-a2ml.sh @@ -124,6 +124,7 @@ validate_a2ml() { local has_identity=false local has_version=false local has_placeholders=false + local first_form_seen=false line_num=0 while IFS= read -r line; do @@ -139,17 +140,29 @@ validate_a2ml() { || [[ "$line" =~ ^[[:space:]]*name[[:space:]]*: ]] \ || [[ "$line" =~ ^\[(metadata|scorecard)\] ]] \ || [[ "$line" =~ ^@abstract ]] \ - || [[ "$line" =~ ^[[:space:]]*\((estate-deed|repo-deed|estate-atlas-deed|praxis-deed)([[:space:]]|$) ]] \ || [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then has_identity=true fi + # The document head identifies only where the grammar puts it: as the + # FIRST form in the file (DEED-GRAMMAR-SPEC <>). Matched + # anywhere in the file, a nested or trailing `(estate-deed ...)` + # satisfied identity for a document whose actual head was something + # else entirely. + 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 # Check for version field (either separator). The second clause is the # deed keyword form: `:schema-version "1.0.0"` — leading colon and a # hyphen, so the first clause (which spells it schema_version with no - # colon) never matched it. :registry-version is a distinct field, - # optional on the atlas. + # colon) never matched it. :registry-version is a distinct, OPTIONAL + # atlas field and never satisfies the version requirement, which + # <> makes REQUIRED on all four heads. Accepting it let a + # registry-only atlas head pass carrying no schema version at all. if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*[=:] ]] \ - || [[ "$line" =~ ^[[:space:]]*:(schema-version|registry-version)[[:space:]] ]]; then + || [[ "$line" =~ ^[[:space:]]*:schema-version[[:space:]] ]]; then has_version=true fi # Template placeholder marker ({{PROJECT_NAME}}, {{VERSION}}, …) @@ -163,7 +176,7 @@ validate_a2ml() { basename="$(basename "$file")" local identity_exempt=false # AI manifests: markdown prose (0-AI-MANIFEST.a2ml, AI.a2ml, …) - if [[ "$basename" == *"AI-MANIFEST"* || "$basename" == "AI.a2ml" ]]; then + if [[ "$basename" == *"AI-MANIFEST"*.a2ml || "$basename" == "AI.a2ml" ]]; then identity_exempt=true fi # Templates/scaffolds From 021c9374b8ecc0a915decfc7cd67257005834367 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:52:16 +0100 Subject: [PATCH 3/5] test(conformance): name the two diagnostics, and make the helpers analysable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears the SonarCloud code smells this branch introduced (shelldre:S7679 positional parameters, S7682 missing return, S7677 stderr, S1192 repeated literal), none of which failed the quality gate but all of which are new debt from the previous commit. The literal-to-constant change is not only cosmetic. Substituting $NO_IDENTITY into the negative control left it inside SINGLE quotes, where it does not expand, so the assertion could never match and passed vacuously — the exact failure this branch exists to remove. Requoted, then mutation-tested: pointing that grep at a fixture which DOES carry the diagnostic makes it fail, and fail alone (1 failure, not 17). An assertion that cannot fail is not an assertion. Suite: 21/21, exit 0. Note on the remaining red: the SonarCloud quality gate fails on new_security_rating (3 vs required 1), and its sole VULNERABILITY is `.github/workflows/deed-conformance.yml:8` — `permissions: read-all` should be specific (githubactions:S8234). That is a workflow file, so it is blocked by the same missing `workflow` token scope as the two persist-credentials findings. Everything else Sonar reports on this branch is a code smell and the maintainability rating is A. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QNjWX2B4FffG7zqMBMui6v --- .../validate/conformance/run-deed-tests.sh | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/actions/validate/conformance/run-deed-tests.sh b/actions/validate/conformance/run-deed-tests.sh index 3ddff95..fb3d98c 100755 --- a/actions/validate/conformance/run-deed-tests.sh +++ b/actions/validate/conformance/run-deed-tests.sh @@ -19,8 +19,13 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VALIDATOR="${HERE}/../validate-a2ml.sh" FAILURES=0 -ok() { echo " PASS: $1"; } -fail() { echo " FAIL: $1"; FAILURES=$((FAILURES + 1)); } +ok() { local msg="$1"; echo " PASS: $msg"; return 0; } +fail() { local msg="$1"; echo " FAIL: $msg" >&2; FAILURES=$((FAILURES + 1)); return 0; } + +# The two diagnostics under test, named once so a wording change cannot leave +# an assertion quietly matching nothing. +NO_IDENTITY='No identity found' +NO_VERSION='Missing version or schema_version field' # The conformance directories hold .a2ml fixtures too. Isolate the .deed ones # so the discovery count is exact rather than incidental. @@ -47,8 +52,11 @@ out="$(INPUT_PATH="$WORK/valid" INPUT_STRICT=true bash "$VALIDATOR" 2>&1)"; rc=$ # check still passed. `file=[^,]*` keeps the path free but stops the regex # wandering into the next annotation. assert_ann() { - grep -qE "::$1 file=[^,]*$2,line=[0-9]+::.*$3" <<<"$out" \ - && ok "$2 -> ::$1 ($3)" || fail "$2: expected ::$1 matching '$3'" + local severity="$1" fixture="$2" message="$3" + grep -qE "::$severity file=[^,]*$fixture,line=[0-9]+::.*$message" <<<"$out" \ + && ok "$fixture -> ::$severity ($message)" \ + || fail "$fixture: expected ::$severity matching '$message'" + return 0 } echo "3. invalid deeds — non-strict: every diagnostic, per fixture" @@ -57,35 +65,35 @@ grep -q 'Found 5 ' <<<"$out" && ok "discovered 5 deed files" \ || fail "discovery: '$(grep -o 'Found [0-9]* [^ ]* file(s)' <<<"$out")' (expected 5)" [[ $rc -eq 0 ]] && ok "exit 0 (identity and version are warnings here)" \ || fail "exit $rc non-strict (expected 0)" -assert_ann warning 'deed-missing-head\.deed' 'No identity found' -assert_ann warning 'deed-missing-head\.deed' 'Missing version or schema_version field' -assert_ann warning 'deed-missing-version\.deed' 'Missing version or schema_version field' +assert_ann warning 'deed-missing-head\.deed' "$NO_IDENTITY" +assert_ann warning 'deed-missing-head\.deed' "$NO_VERSION" +assert_ann warning 'deed-missing-version\.deed' "$NO_VERSION" # :registry-version is optional atlas metadata, never a schema version. -assert_ann warning 'deed-registry-version-only\.deed' 'Missing version or schema_version field' +assert_ann warning 'deed-registry-version-only\.deed' "$NO_VERSION" # A head that is not the FIRST form does not identify the document. -assert_ann warning 'deed-head-not-first\.deed' 'No identity found' +assert_ann warning 'deed-head-not-first\.deed' "$NO_IDENTITY" # The AI-MANIFEST exemption is for .a2ml prose; a .deed is still a deed. -assert_ann warning 'example-AI-MANIFEST\.deed' 'No identity found' -assert_ann warning 'example-AI-MANIFEST\.deed' 'Missing version or schema_version field' +assert_ann warning 'example-AI-MANIFEST\.deed' "$NO_IDENTITY" +assert_ann warning 'example-AI-MANIFEST\.deed' "$NO_VERSION" # Negative control: a diagnostic naming a fixture that has none would mean the # anchoring above is matching across annotation boundaries. -grep -qE '::warning file=[^,]*deed-registry-version-only\.deed,line=[0-9]+::No identity found' <<<"$out" \ +grep -qE "::warning file=[^,]*deed-registry-version-only\.deed,line=[0-9]+::$NO_IDENTITY" <<<"$out" \ && fail "atlas head wrongly reported identity-less" || ok "atlas head still identifies" echo "4. invalid deeds — strict promotes every warning to an error" out="$(INPUT_PATH="$WORK/invalid" INPUT_STRICT=true bash "$VALIDATOR" 2>&1)"; rc=$? [[ $rc -ne 0 ]] && ok "non-zero exit under strict" || fail "exit 0 under strict (expected non-zero)" -assert_ann error 'deed-missing-head\.deed' 'No identity found' -assert_ann error 'deed-missing-version\.deed' 'Missing version or schema_version field' -assert_ann error 'deed-registry-version-only\.deed' 'Missing version or schema_version field' -assert_ann error 'deed-head-not-first\.deed' 'No identity found' -assert_ann error 'example-AI-MANIFEST\.deed' 'Missing version or schema_version field' +assert_ann error 'deed-missing-head\.deed' "$NO_IDENTITY" +assert_ann error 'deed-missing-version\.deed' "$NO_VERSION" +assert_ann error 'deed-registry-version-only\.deed' "$NO_VERSION" +assert_ann error 'deed-head-not-first\.deed' "$NO_IDENTITY" +assert_ann error 'example-AI-MANIFEST\.deed' "$NO_VERSION" grep -q '::warning' <<<"$out" && fail "::warning survived strict" || ok "no ::warning survives strict" echo if [[ $FAILURES -eq 0 ]]; then echo "All deed validator tests passed." else - echo "${FAILURES} deed validator test(s) failed." + echo "${FAILURES} deed validator test(s) failed." >&2 exit 1 fi From 2d3ec46c9623c185ccbf917752c0b675fa63721b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:12:27 +0100 Subject: [PATCH 4/5] ci(deed-conformance): drop read-all and stop persisting credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Major security findings on this workflow, both quick wins, and together the sole cause of the branch's red SonarCloud gate (new_security_rating 3 vs the required 1 — every other finding on the branch is a code smell and maintainability is already A). `permissions: read-all` (githubactions:S8234) grants every read scope that exists — actions, packages, deployments, security events — to a job that only reads the tree and runs a bash script. Narrowed to `contents: read`, the one scope actually exercised. `actions/checkout` defaults to `persist-credentials: true`, which leaves the token in .git/config for every later step (CWE-522). No step here pushes or uses the token, and the workflow runs on pull_request against repository-controlled code, so the credential is exposure with no upside. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QNjWX2B4FffG7zqMBMui6v --- .github/workflows/deed-conformance.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deed-conformance.yml b/.github/workflows/deed-conformance.yml index c3659fa..0e7914f 100644 --- a/.github/workflows/deed-conformance.yml +++ b/.github/workflows/deed-conformance.yml @@ -5,7 +5,12 @@ # it reports on the shell validator independently of the CLI build. name: Deed Conformance -permissions: read-all +# This workflow only reads the tree and runs a bash test script: it does not +# post statuses, comment, or write to the repository. read-all grants every +# read scope there is (actions, packages, deployments, security events...); +# contents: read is the only one actually exercised. +permissions: + contents: read on: push: @@ -26,5 +31,10 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + with: + # No step here pushes or otherwise uses the token, and this workflow + # runs on pull_request against repository-controlled code, so leaving + # the credential in .git/config is exposure with no upside (CWE-522). + persist-credentials: false - name: Run deed conformance tests run: bash actions/validate/conformance/run-deed-tests.sh From 9140bd2f9cf2df8e0c26559a64141092b42a7341 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:21:44 +0100 Subject: [PATCH 5/5] fix(validate): stop legacy A2ML identity forms satisfying a .deed CodeRabbit flagged `@abstract` being accepted as identity in .deed files (actions/validate/validate-a2ml.sh:142). Verified independently and widened: `@abstract` is one of FOUR legacy A2ML surfaces the identity chain accepted regardless of file extension. The DEED grammar has no `=` production, no `[section]` production and no `@abstract` directive -- its only bracket is `(`. So all four of key = value name: value [metadata]/[scorecard] @abstract let a .deed document satisfy identity without ever presenting one of the four ruled heads. That is a gate bypass of exactly the kind this file already fixes three of. Changes: * `is_deed` flag set from the extension before the scan loop. * Identity OR-chain split into a deed surface (`:canonical-name`, `:estate-authority`, `:agent-id` -- legitimate for both .deed and .a2ml) and a legacy branch gated on `is_deed == false`. * Identity diagnostic split by surface. The generic message named [metadata] and @abstract; told to a deed author, human or bot, that invites them to invent a form the grammar does not have, and an invented form is worse than no message. Deeds now get the four ruled heads and the three `:` fields. Both messages keep the substring "No identity found" so existing assertions still match. Verification: * Negative control on the parent commit proved the bug live: rc=0, "Found 1 .a2ml/.deed file(s)" (non-vacuous discovery), zero diagnostics. * New negative fixture invalid/deed-abstract-identity.deed: @abstract present, head is NOT one of the four ruled heads. * Suite discovery assertion 5 -> 6, two new assertions. PASS=23 FAIL=0, exit 0. * Repo-wide annotation SET diff (not count): zero annotations dropped; the three apparent drops are the same files re-reported under the new deed wording. Net +1 is the new fixture. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QNjWX2B4FffG7zqMBMui6v --- .../invalid/deed-abstract-identity.deed | 8 ++++ .../validate/conformance/run-deed-tests.sh | 6 ++- actions/validate/validate-a2ml.sh | 37 +++++++++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 actions/validate/conformance/invalid/deed-abstract-identity.deed diff --git a/actions/validate/conformance/invalid/deed-abstract-identity.deed b/actions/validate/conformance/invalid/deed-abstract-identity.deed new file mode 100644 index 0000000..7d18e41 --- /dev/null +++ b/actions/validate/conformance/invalid/deed-abstract-identity.deed @@ -0,0 +1,8 @@ +;; SPDX-License-Identifier: MPL-2.0 +;; Negative fixture: @abstract is a contractile Xfile directive, not a deed +;; identity form. The head here is NOT one of the four ruled heads, so this +;; document must be reported as identity-less even though @abstract appears. +(not-a-ruled-head + :schema-version "1.0.0") + +@abstract "this is not a deed identity" diff --git a/actions/validate/conformance/run-deed-tests.sh b/actions/validate/conformance/run-deed-tests.sh index fb3d98c..658517d 100755 --- a/actions/validate/conformance/run-deed-tests.sh +++ b/actions/validate/conformance/run-deed-tests.sh @@ -61,7 +61,7 @@ assert_ann() { echo "3. invalid deeds — non-strict: every diagnostic, per fixture" out="$(INPUT_PATH="$WORK/invalid" bash "$VALIDATOR" 2>&1)"; rc=$? -grep -q 'Found 5 ' <<<"$out" && ok "discovered 5 deed files" \ +grep -q 'Found 6 ' <<<"$out" && ok "discovered 6 deed files" \ || fail "discovery: '$(grep -o 'Found [0-9]* [^ ]* file(s)' <<<"$out")' (expected 5)" [[ $rc -eq 0 ]] && ok "exit 0 (identity and version are warnings here)" \ || fail "exit $rc non-strict (expected 0)" @@ -72,6 +72,9 @@ assert_ann warning 'deed-missing-version\.deed' "$NO_VERSION" assert_ann warning 'deed-registry-version-only\.deed' "$NO_VERSION" # A head that is not the FIRST form does not identify the document. assert_ann warning 'deed-head-not-first\.deed' "$NO_IDENTITY" +# @abstract is a contractile Xfile directive with no deed production. Matched in +# a .deed it satisfied identity for a document with no ruled head at all. +assert_ann warning 'deed-abstract-identity\.deed' "$NO_IDENTITY" # The AI-MANIFEST exemption is for .a2ml prose; a .deed is still a deed. assert_ann warning 'example-AI-MANIFEST\.deed' "$NO_IDENTITY" assert_ann warning 'example-AI-MANIFEST\.deed' "$NO_VERSION" @@ -87,6 +90,7 @@ assert_ann error 'deed-missing-head\.deed' "$NO_IDENTITY" assert_ann error 'deed-missing-version\.deed' "$NO_VERSION" assert_ann error 'deed-registry-version-only\.deed' "$NO_VERSION" assert_ann error 'deed-head-not-first\.deed' "$NO_IDENTITY" +assert_ann error 'deed-abstract-identity\.deed' "$NO_IDENTITY" assert_ann error 'example-AI-MANIFEST\.deed' "$NO_VERSION" grep -q '::warning' <<<"$out" && fail "::warning survived strict" || ok "no ::warning survives strict" diff --git a/actions/validate/validate-a2ml.sh b/actions/validate/validate-a2ml.sh index d822ed3..3348d29 100755 --- a/actions/validate/validate-a2ml.sh +++ b/actions/validate/validate-a2ml.sh @@ -125,6 +125,15 @@ validate_a2ml() { local has_version=false local has_placeholders=false local first_form_seen=false + # The legacy identity forms below (TOML-ish `key =`, `name:`, `[metadata]` + # sections, and the `@abstract` contractile directive) are A2ML surfaces. + # The DEED grammar has NO `=` production, NO `[section]` production and no + # `@abstract` directive — its only bracket is `(`. Accepting them for a + # .deed let any document satisfy identity without ever presenting a ruled + # head, which is a gate bypass of exactly the kind this file already fixes + # three of. + local is_deed=false + [[ "$file" == *.deed ]] && is_deed=true line_num=0 while IFS= read -r line; do @@ -136,11 +145,16 @@ validate_a2ml() { # leading-colon identity keywords. Per <> the head symbol is # itself identifying, which is what lets ATLAS.deed — :registry-version # and legitimately no :canonical-name — pass on its head alone. - if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|spec_id)[[:space:]]*= ]] \ - || [[ "$line" =~ ^[[:space:]]*name[[:space:]]*: ]] \ - || [[ "$line" =~ ^\[(metadata|scorecard)\] ]] \ - || [[ "$line" =~ ^@abstract ]] \ - || [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then + # Deed surface: legitimate for .deed AND .a2ml. + if [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then + has_identity=true + fi + # Legacy A2ML surfaces: never identifying for a .deed (see is_deed above). + if [[ "$is_deed" == "false" ]] \ + && { [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|spec_id)[[:space:]]*= ]] \ + || [[ "$line" =~ ^[[:space:]]*name[[:space:]]*: ]] \ + || [[ "$line" =~ ^\[(metadata|scorecard)\] ]] \ + || [[ "$line" =~ ^@abstract ]]; }; then has_identity=true fi # The document head identifies only where the grammar puts it: as the @@ -193,8 +207,17 @@ validate_a2ml() { done if [[ "$has_identity" == "false" && "$identity_exempt" == "false" ]]; then - report_issue "warning" "$file" 1 \ - "No identity found (agent-id/name/project/spec_id field, [metadata] or [scorecard] section, or @abstract directive)" + # Name the forms that are actually valid FOR THIS SURFACE. The generic + # message listed [metadata] and @abstract, which are A2ML-only: told to + # a deed author (human or bot) it invites them to invent a form the + # grammar does not have, and an invented form is worse than no message. + if [[ "$is_deed" == "true" ]]; then + report_issue "warning" "$file" 1 \ + "No identity found (deed: the first form must head with (estate-deed, (repo-deed, (estate-atlas-deed or (praxis-deed, or the document must carry a :canonical-name, :estate-authority or :agent-id field)" + else + report_issue "warning" "$file" 1 \ + "No identity found (agent-id/name/project/spec_id field, [metadata] or [scorecard] section, or @abstract directive)" + fi fi if [[ "$has_version" == "false" && "$identity_exempt" == "false" ]]; then