|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-License-Identifier: MPL-2.0 |
| 3 | +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) |
| 4 | +# |
| 5 | +# check-contract.sh — the estate layering-contract gate (generic; identical in |
| 6 | +# every repo). Enforces the inter-project invariants that keep |
| 7 | +# systemet <- anytype <- AffineScript -> typed-wasm <- ephapax |
| 8 | +# composable, so independent work in one repo cannot silently DETACH it. |
| 9 | +# Behaviour comes from contract.config.sh at the repo root. See CONTRACT.adoc. |
| 10 | +# |
| 11 | +# I1 Dependency direction one-way -> BLOCKING |
| 12 | +# I3 Shared ABI is a multi-producer contract -> BLOCKING (content-anchored) |
| 13 | +# I4 Role purity -> ADVISORY |
| 14 | +# Presence: CONTRACT.adoc + back-link -> BLOCKING (presence) |
| 15 | +# |
| 16 | +# Usage: tools/check-contract.sh # check |
| 17 | +# tools/check-contract.sh --reseal # rewrite contract.abi.sha256 |
| 18 | +# # (ONLY with an abi_version bump + ADR) |
| 19 | +set -uo pipefail |
| 20 | +cd "$(dirname "$0")/.." |
| 21 | +RESEAL=0; [ "${1:-}" = "--reseal" ] && RESEAL=1 |
| 22 | +RED=$'\033[31m'; GRN=$'\033[32m'; YEL=$'\033[33m'; DIM=$'\033[2m'; RST=$'\033[0m' |
| 23 | +fail=0; warn=0 |
| 24 | +err() { printf '%sERROR%s %s\n' "$RED" "$RST" "$1"; fail=1; } |
| 25 | +note() { printf '%swarn%s %s\n' "$YEL" "$RST" "$1"; warn=$((warn+1)); } |
| 26 | +ok() { printf '%sok%s %s\n' "$GRN" "$RST" "$1"; } |
| 27 | + |
| 28 | +[ -f contract.config.sh ] || { err "contract.config.sh missing"; exit 1; } |
| 29 | +# shellcheck disable=SC1091 |
| 30 | +. ./contract.config.sh |
| 31 | +: "${CONTRACT_ROLE:?contract.config.sh must set CONTRACT_ROLE}" |
| 32 | +: "${CONTRACT_ABI_VERSION:=none}" |
| 33 | +DIGEST_FILE="contract.abi.sha256" |
| 34 | + |
| 35 | +# Presence |
| 36 | +if [ -f CONTRACT.adoc ]; then ok "CONTRACT.adoc present (role: ${CONTRACT_ROLE})" |
| 37 | +else err "CONTRACT.adoc missing — the rules for this repo are not written down"; fi |
| 38 | +if grep -rql 'CONTRACT\.adoc' README* .claude/CLAUDE.md 2>/dev/null; then ok "back-link to CONTRACT.adoc present" |
| 39 | +else note "no README/CLAUDE back-link to CONTRACT.adoc"; fi |
| 40 | + |
| 41 | +# I1 — dependency direction (manifests + .gitmodules only; never prose) |
| 42 | +i1=0 |
| 43 | +for name in ${CONTRACT_FORBIDDEN_DEPS:-}; do |
| 44 | + for m in ${CONTRACT_MANIFESTS:-} .gitmodules; do |
| 45 | + [ -f "$m" ] || continue |
| 46 | + if grep -nE "(hyperpolymath/${name}([./\"' ]|\$)|^[[:space:]]*\"?${name}\"?[[:space:]]*=)" "$m" \ |
| 47 | + | grep -vE '^\s*#|SPDX' >/dev/null 2>&1; then |
| 48 | + err "I1 violation: '${name}' referenced as a dependency in ${m} (must not depend on downstream)"; i1=$((i1+1)) |
| 49 | + fi |
| 50 | + done |
| 51 | +done |
| 52 | +[ "$i1" -eq 0 ] && ok "I1 dependency direction clean (forbidden deps: ${CONTRACT_FORBIDDEN_DEPS:-none})" |
| 53 | + |
| 54 | +# I3 — shared-ABI drift (content-anchored regions) |
| 55 | +extract_region() { awk -v m="$2" ' |
| 56 | + $0 ~ (">>> CONTRACT-ABI-ANCHOR " m "($|[^A-Za-z0-9_-])") {on=1; next} |
| 57 | + $0 ~ ("<<< CONTRACT-ABI-ANCHOR " m "($|[^A-Za-z0-9_-])") {on=0} |
| 58 | + on {print}' "$1"; } |
| 59 | +compute() { for a in ${CONTRACT_ABI_ANCHORS:-}; do |
| 60 | + f="${a%%::*}"; mk="${a##*::}" |
| 61 | + [ -f "$f" ] || { echo "MISSING $a"; continue; } |
| 62 | + reg="$(extract_region "$f" "$mk")" |
| 63 | + [ -z "$reg" ] && { echo "NOMARKER $a"; continue; } |
| 64 | + printf '%s %s\n' "$(printf '%s' "$reg" | sha256sum | cut -d' ' -f1)" "$a" |
| 65 | + done; } |
| 66 | +if [ -n "${CONTRACT_ABI_ANCHORS:-}" ]; then |
| 67 | + if [ "$RESEAL" -eq 1 ]; then |
| 68 | + { echo "# ABI-anchor digests (abi_version=${CONTRACT_ABI_VERSION}). DO NOT hand-edit." |
| 69 | + echo "# Re-seal ONLY with an abi_version bump + an ADR. tools/check-contract.sh --reseal" |
| 70 | + compute; } > "$DIGEST_FILE" |
| 71 | + ok "re-sealed ${DIGEST_FILE} at abi_version=${CONTRACT_ABI_VERSION}" |
| 72 | + else |
| 73 | + [ -f "$DIGEST_FILE" ] || err "I3: ${DIGEST_FILE} missing — run tools/check-contract.sh --reseal" |
| 74 | + cur="$(compute)" |
| 75 | + if echo "$cur" | grep -qE '^(MISSING|NOMARKER)'; then |
| 76 | + echo "$cur" | grep -E '^(MISSING|NOMARKER)' | while read -r k a; do err "I3: anchor ${a}: ${k}"; done; fail=1 |
| 77 | + fi |
| 78 | + exp="$(grep -vE '^\s*#' "$DIGEST_FILE" 2>/dev/null)" |
| 79 | + if [ "$(printf '%s' "$cur" | sort)" = "$(printf '%s' "$exp" | sort)" ]; then |
| 80 | + ok "I3 shared-ABI anchors unchanged (abi_version=${CONTRACT_ABI_VERSION})" |
| 81 | + else |
| 82 | + err "I3 violation: a shared-ABI anchor changed — this is a MULTI-PRODUCER ABI (${CONTRACT_FORBIDDEN_DEPS:-producers})." |
| 83 | + printf ' %sTo change it: bump CONTRACT_ABI_VERSION, reference a coordinated ADR, --reseal, update every producer.%s\n' "$DIM" "$RST" |
| 84 | + printf ' %sDo NOT re-seal to make this pass in isolation.%s\n' "$DIM" "$RST" |
| 85 | + diff <(printf '%s\n' "$exp"|sort) <(printf '%s\n' "$cur"|sort) | sed 's/^/ /' || true |
| 86 | + fi |
| 87 | + fi |
| 88 | +else ok "I3 not applicable (no shared-ABI anchors for role ${CONTRACT_ROLE})"; fi |
| 89 | + |
| 90 | +# I4 — role purity (advisory) |
| 91 | +if [ -n "${CONTRACT_ROLE_DENY:-}" ]; then |
| 92 | + while IFS='|' read -r rx msg; do |
| 93 | + [ -z "$rx" ] && continue |
| 94 | + hits="$(grep -rInE "$rx" ${CONTRACT_SRC_DIRS:-src lib} 2>/dev/null | grep -vE 'CONTRACT|contract\.' | head -3)" |
| 95 | + [ -n "$hits" ] && { note "I4 (role purity): ${msg}"; printf '%s\n' "$hits" | sed 's/^/ /'; } |
| 96 | + done <<< "${CONTRACT_ROLE_DENY}" |
| 97 | +else ok "I4 no role-purity denials declared"; fi |
| 98 | + |
| 99 | +echo |
| 100 | +if [ "$fail" -ne 0 ]; then |
| 101 | + printf '%sCONTRACT GATE FAILED%s — this change would detach the repo from the stack. See CONTRACT.adoc.\n' "$RED" "$RST"; exit 1; fi |
| 102 | +printf '%sOK%s: contract gate — I1 + I3 hold%s\n' "$GRN" "$RST" "$([ "$warn" -gt 0 ] && echo " (${warn} advisory warning(s))")" |
0 commit comments