From 1109012c43d100e282d78e1e564994cdbdea0367 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:12:26 +0100 Subject: [PATCH 1/3] fix(governance): make policy validation fail closed --- .github/workflows/governance-reusable.yml | 18 +++++ scripts/tests/policy-gates-test.sh | 80 +++++++++++++++++++++++ tools/policy/check-language-policy.sh | 79 ++++++++++++++++++++++ tools/policy/check-workflows-parse.sh | 65 ++++++++++++++++++ 4 files changed, 242 insertions(+) create mode 100755 scripts/tests/policy-gates-test.sh create mode 100755 tools/policy/check-language-policy.sh create mode 100755 tools/policy/check-workflows-parse.sh diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index decd2d64..6b08cc43 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -257,6 +257,7 @@ jobs: path: .standards-checkout sparse-checkout: | scripts + tools/policy/check-language-policy.sh sparse-checkout-cone-mode: false - name: Run Hypatia scan (Baseline validation) @@ -384,6 +385,9 @@ jobs: # the .ts is a separate follow-up after the dual-target window. run: deno run --allow-read --no-lock .standards-checkout/scripts/check-ts-allowlist.deno.js + - name: Check language-policy invariants + run: bash .standards-checkout/tools/policy/check-language-policy.sh + - name: check-ts-allowlist source/compile drift (informational) # Non-blocking — informational until the AffineScript compiler # output is hash-pinned per compiler version. The compiler header @@ -1109,6 +1113,7 @@ jobs: sparse-checkout: | scripts/check-workflow-duplicate-keys.sh scripts/update-actions-lock.sh + tools/policy/check-workflows-parse.sh sparse-checkout-cone-mode: false # ⚠ Not fatal if the file is absent. This checkout is pinned to # standards@main, so during a rename of the script the fetch finds @@ -1118,6 +1123,19 @@ jobs: # fallback in the next step. continue-on-error: true + - name: Parse every tracked workflow + run: | + SCRIPT=".standards-dupkey/tools/policy/check-workflows-parse.sh" + if [ ! -f "$SCRIPT" ] && [ -f tools/policy/check-workflows-parse.sh ]; then + SCRIPT="tools/policy/check-workflows-parse.sh" + echo "Using this repository's own copy (standards self-lint)." + fi + if [ ! -f "$SCRIPT" ]; then + echo "::error::workflow parser gate not found in standards@main or locally" + exit 1 + fi + bash "$SCRIPT" + - name: Duplicate YAML keys in workflows run: | # GitHub Actions REJECTS a workflow with duplicate keys: the run is diff --git a/scripts/tests/policy-gates-test.sh b/scripts/tests/policy-gates-test.sh new file mode 100755 index 00000000..70a37c83 --- /dev/null +++ b/scripts/tests/policy-gates-test.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(git rev-parse --show-toplevel) +language_gate="$repo_root/tools/policy/check-language-policy.sh" +workflow_gate="$repo_root/tools/policy/check-workflows-parse.sh" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT + +init_fixture() { + local target=$1 + mkdir -p "$target" + git -C "$target" init -q + git -C "$target" config user.email tests@example.invalid + git -C "$target" config user.name 'Policy gate tests' +} + +write_policy() { + local target=$1 extra=${2-} + mkdir -p "$target/.claude" + { + printf '%s\n' '### ALLOWED' '| **Bun** | JS runtime |' + printf '%s\n' '### BANNED' '| **Deno** | **Bun** |' + printf '%s\n' "$extra" + } > "$target/.claude/CLAUDE.md" + git -C "$target" add .claude/CLAUDE.md +} + +expect_pass() { "$@" >/dev/null; } +expect_fail() { if "$@" >/dev/null 2>&1; then echo "expected failure: $*" >&2; exit 1; fi; } + +quoted="$fixture/quoted" +init_fixture "$quoted" +write_policy "$quoted" 'History: "Supports TypeScript" and “JS/TS runtime”.' +(cd "$quoted" && expect_pass "$language_gate") + +mixed="$fixture/mixed" +init_fixture "$mixed" +write_policy "$mixed" 'History: "Supports TypeScript"; live policy Supports TypeScript.' +(cd "$mixed" && expect_fail "$language_gate") + +blockquote="$fixture/blockquote" +init_fixture "$blockquote" +write_policy "$blockquote" '> Historical policy Supports TypeScript.' +(cd "$blockquote" && expect_pass "$language_gate") + +blank="$fixture/blank" +init_fixture "$blank" +write_policy "$blank" '| | replacement |' +(cd "$blank" && expect_fail "$language_gate") + +no_workflows="$fixture/no-workflows" +init_fixture "$no_workflows" +mkdir -p "$no_workflows/bin" +ln -s "$(command -v git)" "$no_workflows/bin/git" +(cd "$no_workflows" && PATH="$no_workflows/bin" expect_pass /bin/bash "$workflow_gate") + +without_parser="$fixture/without-parser" +init_fixture "$without_parser" +mkdir -p "$without_parser/.github/workflows" "$without_parser/bin" +printf '%s\n' 'name: test' 'on: push' 'jobs: {}' > "$without_parser/.github/workflows/test.yml" +git -C "$without_parser" add .github/workflows/test.yml +ln -s "$(command -v git)" "$without_parser/bin/git" +(cd "$without_parser" && PATH="$without_parser/bin" expect_fail /bin/bash "$workflow_gate") + +valid="$fixture/valid" +init_fixture "$valid" +mkdir -p "$valid/.github/workflows" +printf '%s\n' 'name: test' 'on: push' 'jobs: {}' > "$valid/.github/workflows/test.yml" +git -C "$valid" add .github/workflows/test.yml +(cd "$valid" && expect_pass "$workflow_gate") + +invalid="$fixture/invalid" +init_fixture "$invalid" +mkdir -p "$invalid/.github/workflows" +printf '%s\n' 'name: test' 'jobs: [' > "$invalid/.github/workflows/test.yml" +git -C "$invalid" add .github/workflows/test.yml +(cd "$invalid" && expect_fail "$workflow_gate") + +echo 'policy gate controls passed' diff --git a/tools/policy/check-language-policy.sh b/tools/policy/check-language-policy.sh new file mode 100755 index 00000000..4c37e690 --- /dev/null +++ b/tools/policy/check-language-policy.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Assert the invariant parts of the estate language policy in tracked CLAUDE.md files. +set -uo pipefail + +status=0 +declare -a files=() +mapfile -d '' -t files < <(git ls-files -z -- '*CLAUDE.md') + +if [ "${#files[@]}" -eq 0 ]; then + echo "no CLAUDE.md tracked - nothing to check" + exit 0 +fi + +fail() { + printf ' FAIL %s\n %s\n' "$1" "$2" + status=1 +} + +# Historical policy text is often retained in Markdown quotes. Remove blockquotes and +# quoted substrings while preserving the rest of each line, so a live violation after a +# historical quotation is still visible. +live_lines() { + sed -E '/^[[:space:]]*>/d; s/"[^"]*"//g; s/“[^”]*”//g' "$1" +} + +for file in "${files[@]}"; do + case "$file" in + node_modules/*|*/node_modules/*) continue ;; + esac + + echo "checking $file" + live=$(live_lines "$file") + + if grep -nF -- '| Bun | Deno |' "$file" >/dev/null; then + fail "$file" 'Bun is listed as banned with Deno as its replacement.' + fi + if grep -F 'No package.json for runtime deps' <<<"$live" >/dev/null; then + fail "$file" 'Policy forbids the dependency manifest that Bun requires.' + fi + if grep -F 'deno.json imports' <<<"$live" >/dev/null; then + fail "$file" 'Policy directs runtime dependencies into deno.json.' + fi + + typescript_runtime='Executes .\.ts. directly|JS/TS runtime|[Ss]upports? TypeScript|[Rr]uns? [^[:alnum:][:space:]]*\.ts[^[:alnum:][:space:]]* files?' + if grep -E "$typescript_runtime" <<<"$live" >/dev/null; then + fail "$file" 'Policy advertises TypeScript execution.' + fi + + if awk -F'|' 'NF >= 4 && $2 ~ /^[[:space:]]*$/ { found=1; exit } END { exit !found }' "$file"; then + fail "$file" 'Policy table contains an empty first cell (a blanking scar).' + fi + if grep -F '| **** |' "$file" >/dev/null; then + fail "$file" 'Policy table contains an empty bold cell.' + fi + if grep -E '\*\*No new +files\*\*|Only where +cannot' "$file" >/dev/null; then + fail "$file" 'Enforcement text contains a blanked language name.' + fi + if grep -E '^\|[[:space:]]*AffineScript[[:space:]]*\|[[:space:]]*AffineScript[[:space:]]*\|' "$file" >/dev/null; then + fail "$file" 'The banned table maps AffineScript to itself.' + fi + + if grep -qE '^### (ALLOWED|BANNED)' "$file"; then + if ! grep -qE '^\|[[:space:]]*\*\*Bun\*\*[[:space:]]*\|' "$file" && + ! grep -qiE '^[-*][[:space:]]+\*{0,2}Bun\*{0,2}([[:space:]]|$)' "$file"; then + fail "$file" 'No Bun entry appears in the allowed policy.' + fi + if ! grep -qE '^\|[[:space:]]*\*{0,2}Deno\*{0,2}[[:space:]]*\|[[:space:]]*\*{0,2}Bun\*{0,2}[[:space:]]*\|' "$file" && + ! grep -qiE '^[-*][[:space:]]+Deno[[:space:]]*\(use Bun\)' "$file"; then + fail "$file" 'Deno is not listed as banned with Bun as its replacement.' + fi + fi +done + +if [ "$status" -eq 0 ]; then + echo "language policy OK" +else + echo "Language-policy drift detected. Fix the local copy; do not weaken this gate." +fi +exit "$status" diff --git a/tools/policy/check-workflows-parse.sh b/tools/policy/check-workflows-parse.sh new file mode 100755 index 00000000..12fe5c05 --- /dev/null +++ b/tools/policy/check-workflows-parse.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Fail if any tracked GitHub Actions workflow does not parse as YAML. +set -uo pipefail + +if ! command -v git >/dev/null 2>&1; then + echo '::error::git is required to enumerate tracked workflows' + exit 1 +fi + +declare -a workflows=() +mapfile -d '' -t workflows < <( + git ls-files -z -- '.github/workflows/*.yml' '.github/workflows/*.yaml' \ + '**/.github/workflows/*.yml' '**/.github/workflows/*.yaml' +) + +if [ "${#workflows[@]}" -eq 0 ]; then + echo "no workflows tracked - nothing to check" + exit 0 +fi + +parser='' +if command -v yq >/dev/null 2>&1; then + parser=yq +elif command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' >/dev/null 2>&1; then + parser=python +elif command -v ruby >/dev/null 2>&1; then + parser=ruby +else + echo "::error::no YAML parser available (yq, python3+pyyaml, or ruby)" + exit 1 +fi + +parse_ok() { + case "$parser" in + yq) yq '.' "$1" >/dev/null 2>&1 ;; + python) python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1], encoding="utf-8"))' "$1" >/dev/null 2>&1 ;; + ruby) ruby -ryaml -e 'YAML.safe_load(File.read(ARGV[0]), aliases: true)' "$1" >/dev/null 2>&1 ;; + esac +} + +has_forbidden_control() { + od -An -v -tu1 "$1" | awk ' + { for (i=1; i<=NF; i++) if (($i < 9) || ($i > 10 && $i < 13) || ($i > 13 && $i < 32)) exit 0 } + END { exit 1 } + ' +} + +status=0 +for file in "${workflows[@]}"; do + [ -f "$file" ] || continue + if ! parse_ok "$file"; then + status=1 + printf '::error file=%s::workflow does not parse; an unloaded workflow produces no check run\n' "$file" + if has_forbidden_control "$file"; then + echo ' contains a YAML-forbidden control character' + fi + fi +done + +if [ "$status" -eq 0 ]; then + echo "all ${#workflows[@]} workflow(s) parse" +else + echo 'At least one workflow cannot load. Fix the YAML; do not delete the check.' +fi +exit "$status" From 05c16a56aad062e0a616daa7b2f1cb65f3c69357 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:23:31 +0100 Subject: [PATCH 2/3] fix(governance): self-host policy gate on pull requests --- .github/workflows/governance-reusable.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 6b08cc43..94e3a161 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -386,7 +386,17 @@ jobs: run: deno run --allow-read --no-lock .standards-checkout/scripts/check-ts-allowlist.deno.js - name: Check language-policy invariants - run: bash .standards-checkout/tools/policy/check-language-policy.sh + run: | + SCRIPT=".standards-checkout/tools/policy/check-language-policy.sh" + if [ ! -f "$SCRIPT" ] && [ -f tools/policy/check-language-policy.sh ]; then + SCRIPT="tools/policy/check-language-policy.sh" + echo "Using this repository's own copy (standards self-check)." + fi + if [ ! -f "$SCRIPT" ]; then + echo "::error::language-policy gate not found in standards@main or locally" + exit 1 + fi + bash "$SCRIPT" - name: check-ts-allowlist source/compile drift (informational) # Non-blocking — informational until the AffineScript compiler From d29b83c62d192453a325d7d3773dbad9b837487a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:28:13 +0100 Subject: [PATCH 3/3] fix(governance): close reviewer-identified gate gaps --- .github/workflows/governance-reusable.yml | 3 ++- scripts/tests/policy-gates-test.sh | 29 +++++++++++++++++++++++ tools/policy/check-language-policy.sh | 20 ++++++++-------- tools/policy/check-workflows-parse.sh | 4 ++-- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 94e3a161..6ed47ae5 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -365,9 +365,10 @@ jobs: repository: hyperpolymath/standards ref: main path: .standards-checkout - # Sparse-checkout only the scripts dir to keep this fast. + # Include the shared policy gate as well as the scripts it complements. sparse-checkout: | scripts + tools/policy/check-language-policy.sh sparse-checkout-cone-mode: false - name: Check for TypeScript diff --git a/scripts/tests/policy-gates-test.sh b/scripts/tests/policy-gates-test.sh index 70a37c83..86170e9d 100755 --- a/scripts/tests/policy-gates-test.sh +++ b/scripts/tests/policy-gates-test.sh @@ -44,11 +44,32 @@ init_fixture "$blockquote" write_policy "$blockquote" '> Historical policy Supports TypeScript.' (cd "$blockquote" && expect_pass "$language_gate") +quoted_invariants="$fixture/quoted-invariants" +init_fixture "$quoted_invariants" +write_policy "$quoted_invariants" '> | | historical blank |' +printf '%s\n' '> **No new files**' >> "$quoted_invariants/.claude/CLAUDE.md" +git -C "$quoted_invariants" add .claude/CLAUDE.md +(cd "$quoted_invariants" && expect_pass "$language_gate") + blank="$fixture/blank" init_fixture "$blank" write_policy "$blank" '| | replacement |' (cd "$blank" && expect_fail "$language_gate") +missing_bun="$fixture/missing-bun" +init_fixture "$missing_bun" +mkdir -p "$missing_bun/.claude" +printf '%s\n' '### ALLOWED' '### BANNED' '| **Deno** | **Bun** |' > "$missing_bun/.claude/CLAUDE.md" +git -C "$missing_bun" add .claude/CLAUDE.md +(cd "$missing_bun" && expect_fail "$language_gate") + +missing_deno="$fixture/missing-deno" +init_fixture "$missing_deno" +mkdir -p "$missing_deno/.claude" +printf '%s\n' '### ALLOWED' '| **Bun** | JS runtime |' '### BANNED' > "$missing_deno/.claude/CLAUDE.md" +git -C "$missing_deno" add .claude/CLAUDE.md +(cd "$missing_deno" && expect_fail "$language_gate") + no_workflows="$fixture/no-workflows" init_fixture "$no_workflows" mkdir -p "$no_workflows/bin" @@ -77,4 +98,12 @@ printf '%s\n' 'name: test' 'jobs: [' > "$invalid/.github/workflows/test.yml" git -C "$invalid" add .github/workflows/test.yml (cd "$invalid" && expect_fail "$workflow_gate") +control="$fixture/control" +init_fixture "$control" +mkdir -p "$control/.github/workflows" +printf 'name: test\001\non: push\njobs: {}\n' > "$control/.github/workflows/test.yml" +git -C "$control" add .github/workflows/test.yml +control_output=$(cd "$control" && "$workflow_gate" 2>&1 || true) +grep -q 'contains a YAML-forbidden control character' <<<"$control_output" + echo 'policy gate controls passed' diff --git a/tools/policy/check-language-policy.sh b/tools/policy/check-language-policy.sh index 4c37e690..be6d702b 100755 --- a/tools/policy/check-language-policy.sh +++ b/tools/policy/check-language-policy.sh @@ -31,7 +31,7 @@ for file in "${files[@]}"; do echo "checking $file" live=$(live_lines "$file") - if grep -nF -- '| Bun | Deno |' "$file" >/dev/null; then + if grep -nF -- '| Bun | Deno |' <<<"$live" >/dev/null; then fail "$file" 'Bun is listed as banned with Deno as its replacement.' fi if grep -F 'No package.json for runtime deps' <<<"$live" >/dev/null; then @@ -46,26 +46,26 @@ for file in "${files[@]}"; do fail "$file" 'Policy advertises TypeScript execution.' fi - if awk -F'|' 'NF >= 4 && $2 ~ /^[[:space:]]*$/ { found=1; exit } END { exit !found }' "$file"; then + if awk -F'|' 'NF >= 4 && $2 ~ /^[[:space:]]*$/ { found=1 } END { exit !found }' <<<"$live"; then fail "$file" 'Policy table contains an empty first cell (a blanking scar).' fi - if grep -F '| **** |' "$file" >/dev/null; then + if grep -F '| **** |' <<<"$live" >/dev/null; then fail "$file" 'Policy table contains an empty bold cell.' fi - if grep -E '\*\*No new +files\*\*|Only where +cannot' "$file" >/dev/null; then + if grep -E '\*\*No new +files\*\*|Only where +cannot' <<<"$live" >/dev/null; then fail "$file" 'Enforcement text contains a blanked language name.' fi - if grep -E '^\|[[:space:]]*AffineScript[[:space:]]*\|[[:space:]]*AffineScript[[:space:]]*\|' "$file" >/dev/null; then + if grep -E '^\|[[:space:]]*AffineScript[[:space:]]*\|[[:space:]]*AffineScript[[:space:]]*\|' <<<"$live" >/dev/null; then fail "$file" 'The banned table maps AffineScript to itself.' fi - if grep -qE '^### (ALLOWED|BANNED)' "$file"; then - if ! grep -qE '^\|[[:space:]]*\*\*Bun\*\*[[:space:]]*\|' "$file" && - ! grep -qiE '^[-*][[:space:]]+\*{0,2}Bun\*{0,2}([[:space:]]|$)' "$file"; then + if grep -qE '^### (ALLOWED|BANNED)' <<<"$live"; then + if ! grep -qE '^\|[[:space:]]*\*\*Bun\*\*[[:space:]]*\|' <<<"$live" && + ! grep -qiE '^[-*][[:space:]]+\*{0,2}Bun\*{0,2}([[:space:]]|$)' <<<"$live"; then fail "$file" 'No Bun entry appears in the allowed policy.' fi - if ! grep -qE '^\|[[:space:]]*\*{0,2}Deno\*{0,2}[[:space:]]*\|[[:space:]]*\*{0,2}Bun\*{0,2}[[:space:]]*\|' "$file" && - ! grep -qiE '^[-*][[:space:]]+Deno[[:space:]]*\(use Bun\)' "$file"; then + if ! grep -qE '^\|[[:space:]]*\*{0,2}Deno\*{0,2}[[:space:]]*\|[[:space:]]*\*{0,2}Bun\*{0,2}[[:space:]]*\|' <<<"$live" && + ! grep -qiE '^[-*][[:space:]]+Deno[[:space:]]*\(use Bun\)' <<<"$live"; then fail "$file" 'Deno is not listed as banned with Bun as its replacement.' fi fi diff --git a/tools/policy/check-workflows-parse.sh b/tools/policy/check-workflows-parse.sh index 12fe5c05..fcf18829 100755 --- a/tools/policy/check-workflows-parse.sh +++ b/tools/policy/check-workflows-parse.sh @@ -40,8 +40,8 @@ parse_ok() { has_forbidden_control() { od -An -v -tu1 "$1" | awk ' - { for (i=1; i<=NF; i++) if (($i < 9) || ($i > 10 && $i < 13) || ($i > 13 && $i < 32)) exit 0 } - END { exit 1 } + { for (i=1; i<=NF; i++) if (($i < 9) || ($i > 10 && $i < 13) || ($i > 13 && $i < 32)) found=1 } + END { exit !found } ' }