From 708ba42579e8d581ec9a65534e659024185355af Mon Sep 17 00:00:00 2001 From: Timothy Tschampel Date: Wed, 5 Aug 2026 14:41:33 -0700 Subject: [PATCH 1/5] feat: versioned shell installer with binary download setup-rebar.sh now accepts a version positional arg and installs to ~/.rebar/versions// with a 'current' symlink. - Release tags (vX.Y.Z): download prebuilt binary from GitHub Releases, fall back to source build if no binary for this platform - Branches / dev refs: clone repo and build from source with version injected via ldflags - Multiple versions coexist; projects pin via .rebar-version - curl pipe: bash -s -- v3.0.0-beta.2 --- setup-rebar.sh | 234 +++++-- .../scripts/_rebar-config.sh | 64 -- .../project-bootstrap/scripts/agent-health.sh | 83 --- .../scripts/check-bypass-flags.sh | 142 ---- .../scripts/check-compliance.sh | 438 ------------ .../scripts/check-contract-headers.sh | 84 --- .../scripts/check-contract-refs.sh | 56 -- .../scripts/check-decay-patterns.sh | 232 ------- .../scripts/check-doc-refs.sh | 220 ------ .../scripts/check-fix-commit.sh | 126 ---- .../scripts/check-freshness.sh | 102 --- .../scripts/check-ground-truth.sh | 106 --- .../scripts/check-jtbd-presence.sh | 174 ----- .../scripts/check-prefix-uniqueness.sh | 135 ---- .../scripts/check-registry.sh | 92 --- .../project-bootstrap/scripts/check-todos.sh | 68 -- .../scripts/check-version-bump.sh | 108 --- .../scripts/compute-registry.sh | 309 --------- .../scripts/flush-notifications.sh | 240 ------- .../project-bootstrap/scripts/inbox-watch.sh | 288 -------- .../project-bootstrap/scripts/rebar-doc.sh | 191 ------ .../scripts/refresh-context.sh | 184 ----- .../scripts/scan-consumers.sh | 119 ---- .../project-bootstrap/scripts/steward.sh | 645 ------------------ 24 files changed, 169 insertions(+), 4271 deletions(-) delete mode 100644 templates/project-bootstrap/scripts/_rebar-config.sh delete mode 100755 templates/project-bootstrap/scripts/agent-health.sh delete mode 100755 templates/project-bootstrap/scripts/check-bypass-flags.sh delete mode 100755 templates/project-bootstrap/scripts/check-compliance.sh delete mode 100755 templates/project-bootstrap/scripts/check-contract-headers.sh delete mode 100755 templates/project-bootstrap/scripts/check-contract-refs.sh delete mode 100755 templates/project-bootstrap/scripts/check-decay-patterns.sh delete mode 100755 templates/project-bootstrap/scripts/check-doc-refs.sh delete mode 100755 templates/project-bootstrap/scripts/check-fix-commit.sh delete mode 100755 templates/project-bootstrap/scripts/check-freshness.sh delete mode 100755 templates/project-bootstrap/scripts/check-ground-truth.sh delete mode 100755 templates/project-bootstrap/scripts/check-jtbd-presence.sh delete mode 100755 templates/project-bootstrap/scripts/check-prefix-uniqueness.sh delete mode 100755 templates/project-bootstrap/scripts/check-registry.sh delete mode 100755 templates/project-bootstrap/scripts/check-todos.sh delete mode 100755 templates/project-bootstrap/scripts/check-version-bump.sh delete mode 100755 templates/project-bootstrap/scripts/compute-registry.sh delete mode 100755 templates/project-bootstrap/scripts/flush-notifications.sh delete mode 100755 templates/project-bootstrap/scripts/inbox-watch.sh delete mode 100755 templates/project-bootstrap/scripts/rebar-doc.sh delete mode 100755 templates/project-bootstrap/scripts/refresh-context.sh delete mode 100755 templates/project-bootstrap/scripts/scan-consumers.sh delete mode 100755 templates/project-bootstrap/scripts/steward.sh diff --git a/setup-rebar.sh b/setup-rebar.sh index 3221325..1c4e6bf 100755 --- a/setup-rebar.sh +++ b/setup-rebar.sh @@ -1,27 +1,30 @@ #!/usr/bin/env bash -# setup-rebar.sh — One-line installer for rebar. +# setup-rebar.sh — Versioned one-line installer for rebar. # -# Clones rebar to $REBAR_DIR (default ~/.rebar), runs the canonical -# `bin/install` to add ASK + rebar to your PATH, and exits pointing -# you at `rebar new` / `rebar adopt` for actual project bootstrap. +# Release tags (vX.Y.Z) — download prebuilt binary from GitHub Releases +# Branches / dev refs — clone repo and build from source (requires Go) # -# This is intentionally a thin shim over rebar's existing tooling -# (`bin/install`, `rebar new`, `rebar adopt`) — not a parallel -# bootstrap. Anything beyond clone + PATH wiring belongs in the -# Go CLI so it stays in sync with the rest of the project. +# Usage — curl pipe (most common): +# curl -fsSL https://raw.githubusercontent.com/willackerly/rebar/main/setup-rebar.sh \ +# | bash -s -- v3.0.0-beta.2 # -# Usage (curl pipe): -# curl -fsSL https://raw.githubusercontent.com/willackerly/rebar/v3.0.0-beta.2/setup-rebar.sh | bash +# Usage — local: +# ./setup-rebar.sh [] [--server HOST:PORT] [--help] # -# Usage (local): -# ./setup-rebar.sh [--server HOST:PORT] [--dir PATH] +# Multiple versions coexist under ~/.rebar/versions//. +# The 'current' symlink points at the active one; bin/rebar resolves it. +# +# Env overrides: +# REBAR_GITHUB owner/repo (default: willackerly/rebar) +# REBAR_BASE install base dir (default: ~/.rebar) +# ASK_SERVER remote ASK server set -euo pipefail -REBAR_REPO="${REBAR_REPO:-https://github.com/willackerly/rebar.git}" -REBAR_REF="${REBAR_REF:-v3.0.0-beta.2}" -REBAR_DIR="${REBAR_DIR:-$HOME/.rebar}" +REBAR_GITHUB="${REBAR_GITHUB:-willackerly/rebar}" +REBAR_BASE="${REBAR_BASE:-$HOME/.rebar}" ASK_SERVER="${ASK_SERVER:-}" +VERSION="" if [[ -t 1 ]]; then RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m' @@ -32,90 +35,191 @@ fi log() { printf '%s[rebar]%s %s\n' "$BLUE" "$NC" "$*"; } warn() { printf '%s[rebar]%s %s\n' "$YELLOW" "$NC" "$*" >&2; } -err() { printf '%s[rebar]%s %s\n' "$RED" "$NC" "$*" >&2; } +err() { printf '%s[rebar]%s %s\n' "$RED" "$NC" "$*" >&2; exit 1; } ok() { printf '%s[rebar]%s %s\n' "$GREEN" "$NC" "$*"; } usage() { cat <] [--server HOST:PORT] $0 --help -Env vars (override flags): - REBAR_DIR target install dir (default: ~/.rebar) - REBAR_REPO git remote to clone from (default: github.com/willackerly/rebar) - REBAR_REF branch/tag to check out (default: v3.0.0-beta.2) - ASK_SERVER remote ASK server, written to your shell RC by bin/install + release tag (v3.0.0) or branch name (default: latest release tag) + +Examples: + curl -fsSL .../setup-rebar.sh | bash -s -- v3.0.0-beta.2 + ./setup-rebar.sh main --server localhost:8080 -Next steps after install: - rebar new my-project -d "what it does" # create a new rebar project - rebar adopt # adopt rebar in an existing repo +Env overrides: + REBAR_GITHUB GitHub owner/repo (default: willackerly/rebar) + REBAR_BASE install base dir (default: ~/.rebar) + ASK_SERVER remote ASK server EOF } +# ── Argument parsing ────────────────────────────────────────────────────────── + while [[ $# -gt 0 ]]; do case "$1" in --server) ASK_SERVER="$2"; shift 2 ;; --server=*) ASK_SERVER="${1#--server=}"; shift ;; - --dir) REBAR_DIR="$2"; shift 2 ;; - --dir=*) REBAR_DIR="${1#--dir=}"; shift ;; - --ref) REBAR_REF="$2"; shift 2 ;; - --ref=*) REBAR_REF="${1#--ref=}"; shift ;; -h|--help) usage; exit 0 ;; - *) err "unknown option: $1"; usage >&2; exit 2 ;; + -*) err "Unknown option: $1" ;; + *) VERSION="$1"; shift ;; esac done -if ! command -v git >/dev/null 2>&1; then - err "git is required. Install it and re-run." - exit 1 +# ── Resolve version ─────────────────────────────────────────────────────────── + +is_release_tag() { + [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9] ]] +} + +if [[ -z "$VERSION" ]]; then + log "Resolving latest release tag from GitHub..." + if command -v curl &>/dev/null; then + VERSION=$(curl -fsSL "https://api.github.com/repos/${REBAR_GITHUB}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + elif command -v wget &>/dev/null; then + VERSION=$(wget -qO- "https://api.github.com/repos/${REBAR_GITHUB}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + else + err "curl or wget required. Install either and re-run, or pass a version explicitly." + fi + [[ -z "$VERSION" ]] && err "Could not resolve latest release tag. Pass a version explicitly: $0 v3.0.0" + log "Latest: $VERSION" fi -if [[ -d "$REBAR_DIR/.git" ]]; then - log "Updating existing rebar checkout at $REBAR_DIR" - if ! git -C "$REBAR_DIR" diff --quiet || ! git -C "$REBAR_DIR" diff --cached --quiet; then - warn "$REBAR_DIR has uncommitted changes — skipping fetch/checkout" +INSTALL_DIR="${REBAR_BASE}/versions/${VERSION}" +CURRENT_LINK="${REBAR_BASE}/current" + +log "Installing rebar ${VERSION} → ${INSTALL_DIR}" + +mkdir -p "$INSTALL_DIR" + +# ── Download or build ───────────────────────────────────────────────────────── + +detect_platform() { + local os arch + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + case "$arch" in + x86_64|amd64) arch="x86_64" ;; + arm64|aarch64) arch="arm64" ;; + *) arch="$arch" ;; + esac + echo "${os}_${arch}" +} + +install_from_release() { + local platform + platform="$(detect_platform)" + local asset="rebar_${VERSION#v}_${platform}.tar.gz" + local url="https://github.com/${REBAR_GITHUB}/releases/download/${VERSION}/${asset}" + + log "Downloading prebuilt binary: $asset" + + local tmp + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + if command -v curl &>/dev/null; then + curl -fsSL --output "${tmp}/${asset}" "$url" \ + || { warn "Prebuilt binary not found for $platform — falling back to source build"; return 1; } else - git -C "$REBAR_DIR" fetch --tags origin "$REBAR_REF" - git -C "$REBAR_DIR" checkout "$REBAR_REF" - git -C "$REBAR_DIR" pull --ff-only origin "$REBAR_REF" || true + wget -qO "${tmp}/${asset}" "$url" \ + || { warn "Prebuilt binary not found for $platform — falling back to source build"; return 1; } fi -elif [[ -e "$REBAR_DIR" ]]; then - err "$REBAR_DIR exists but is not a git checkout." - err "Move it aside or set REBAR_DIR= and re-run." - exit 1 + + tar -xzf "${tmp}/${asset}" -C "$INSTALL_DIR" --strip-components=0 + chmod +x "${INSTALL_DIR}/bin/rebar" 2>/dev/null || true + return 0 +} + +install_from_source() { + local ref="$1" + log "Building from source (ref: $ref)..." + command -v go &>/dev/null || err "Go is required to build from source. Install Go and re-run." + command -v git &>/dev/null || err "git is required. Install it and re-run." + + local src_dir="${INSTALL_DIR}/.src" + if [[ -d "${src_dir}/.git" ]]; then + log "Updating existing checkout..." + git -C "$src_dir" fetch --tags origin + git -C "$src_dir" checkout "$ref" + git -C "$src_dir" pull --ff-only origin "$ref" 2>/dev/null || true + else + log "Cloning https://github.com/${REBAR_GITHUB}.git (ref: $ref)..." + git clone --branch "$ref" --depth=1 \ + "https://github.com/${REBAR_GITHUB}.git" "$src_dir" + fi + + # Copy framework files + cp -r "${src_dir}/." "${INSTALL_DIR}/" + + # Build binary with version injected + log "Building rebar binary..." + mkdir -p "${INSTALL_DIR}/bin" + (cd "$src_dir/cli" && go build \ + -ldflags="-s -w -X github.com/willackerly/rebar/cli/cmd.Version=${VERSION}" \ + -o "${INSTALL_DIR}/bin/rebar" .) + chmod +x "${INSTALL_DIR}/bin/rebar" +} + +if is_release_tag "$VERSION"; then + install_from_release || install_from_source "$VERSION" else - log "Cloning $REBAR_REPO ($REBAR_REF) → $REBAR_DIR" - git clone --branch "$REBAR_REF" --depth=1 "$REBAR_REPO" "$REBAR_DIR" + install_from_source "$VERSION" fi -if [[ ! -x "$REBAR_DIR/bin/install" ]]; then - err "$REBAR_DIR/bin/install not found or not executable." - err "The clone may have failed or the ref does not contain bin/install." - exit 1 +# ── Verify binary ───────────────────────────────────────────────────────────── + +[[ -x "${INSTALL_DIR}/bin/rebar" ]] \ + || err "Installation failed — ${INSTALL_DIR}/bin/rebar not found." + +# ── Update 'current' symlink ────────────────────────────────────────────────── + +mkdir -p "${REBAR_BASE}/bin" +ln -sfn "${INSTALL_DIR}" "${CURRENT_LINK}" + +# ── Wire PATH via bin/install ───────────────────────────────────────────────── + +INSTALL_BIN="${INSTALL_DIR}/bin/install" +if [[ -x "$INSTALL_BIN" ]]; then + install_args=() + [[ -n "$ASK_SERVER" ]] && install_args+=(--server "$ASK_SERVER") + log "Running bin/install..." + "$INSTALL_BIN" "${install_args[@]}" +else + # Fallback: write PATH entry manually + warn "bin/install not found — writing PATH entry manually" + SHELL_RC="" + [[ -f "$HOME/.zshrc" ]] && SHELL_RC="$HOME/.zshrc" + [[ -f "$HOME/.bashrc" ]] && SHELL_RC="${SHELL_RC:-$HOME/.bashrc}" + [[ -f "$HOME/.profile" ]] && SHELL_RC="${SHELL_RC:-$HOME/.profile}" + if [[ -n "$SHELL_RC" ]]; then + if ! grep -q 'rebar/current/bin' "$SHELL_RC" 2>/dev/null; then + printf '\n# rebar\nexport PATH="%s/current/bin:$PATH"\n' "${REBAR_BASE}" >> "$SHELL_RC" + log "Added ${REBAR_BASE}/current/bin to PATH in $SHELL_RC" + fi + fi fi -install_args=() -[[ -n "$ASK_SERVER" ]] && install_args+=(--server "$ASK_SERVER") -log "Running $REBAR_DIR/bin/install ${install_args[*]:-}" -"$REBAR_DIR/bin/install" "${install_args[@]}" +# ── Done ────────────────────────────────────────────────────────────────────── -ok "rebar installed at $REBAR_DIR (ref: $REBAR_REF)" +ok "rebar ${VERSION} installed" cat < .rebarrc file > default (3) - -_rebar_tier() { - # 1. Environment variable (highest priority) - if [ -n "${REBAR_TIER:-}" ]; then - echo "$REBAR_TIER" - return - fi - - # 2. .rebarrc file (project-level config) - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local project_root - project_root="$(cd "$script_dir/.." && pwd)" - - local rc_file="$project_root/.rebarrc" - if [ -f "$rc_file" ]; then - local tier - tier=$(grep '^tier' "$rc_file" 2>/dev/null | head -1 | sed 's/.*=[[:space:]]*//' | tr -d ' ') - if [ -n "$tier" ] && [[ "$tier" =~ ^[123]$ ]]; then - echo "$tier" - return - fi - fi - - # 3. Default: full enforcement - echo "3" -} - -# Check if a script should run based on its minimum required tier -# Usage: _rebar_skip 2 && exit 0 # skip if tier < 2 -_rebar_skip() { - local min_tier="$1" - local current_tier - current_tier=$(_rebar_tier) - - if [ "$current_tier" -lt "$min_tier" ]; then - echo "SKIP: tier $current_tier < required tier $min_tier (set REBAR_TIER or .rebarrc to change)" - return 0 # true = should skip - fi - return 1 # false = should run -} - -# Read the rebar-scripts version from a script file -_rebar_script_version() { - local file="$1" - grep '^# rebar-scripts:' "$file" 2>/dev/null | head -1 | sed 's/^# rebar-scripts:[[:space:]]*//' -} diff --git a/templates/project-bootstrap/scripts/agent-health.sh b/templates/project-bootstrap/scripts/agent-health.sh deleted file mode 100755 index f696ca8..0000000 --- a/templates/project-bootstrap/scripts/agent-health.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# agent-health.sh — Source-able health primitives for worktree agents -# rebar-scripts: 2026.03.21 -# -# Usage: source this file in agent prompts or test-stack scripts. -# -# source scripts/agent-health.sh -# agent_checkpoint "fix: correct font ascender metric" -# agent_heartbeat -# agent_metric "rmse_delta" "-0.045" -# -# All output goes to /tmp/agent-.* files. The parent can poll these -# or the shared progress file to monitor the swarm. - -set -euo pipefail - -# Agent identity — derived from worktree name or PID -AGENT_ID="${AGENT_ID:-$(basename "$(git rev-parse --show-toplevel 2>/dev/null || echo "agent-$$")")}" -PROGRESS_FILE="${AGENT_PROGRESS_FILE:-agent-progress.jsonl}" - -_agent_ts() { - date -u +%Y-%m-%dT%H:%M:%SZ -} - -# --- Checkpoint: stage tracked files and commit --- -# Commits only tracked files (git add -u), never untracked. This prevents -# agents from accidentally committing generated artifacts. -agent_checkpoint() { - local msg="${1:?Usage: agent_checkpoint \"commit message\"}" - git add -u - if git diff --cached --quiet; then - echo "[agent-health] nothing to commit" - return 0 - fi - git commit -m "$msg" - local hash - hash=$(git rev-parse --short HEAD) - echo "[agent-health] checkpoint: $hash $msg" - - # Append to shared progress file if it exists - if [ -n "$PROGRESS_FILE" ]; then - local entry - entry=$(printf '{"agent":"%s","ts":"%s","type":"checkpoint","commit":"%s","message":"%s"}\n' \ - "$AGENT_ID" "$(_agent_ts)" "$hash" "$msg") - echo "$entry" >> "$PROGRESS_FILE" 2>/dev/null || true - fi -} - -# --- Heartbeat: signal that agent is alive --- -agent_heartbeat() { - local hb_file="/tmp/agent-${AGENT_ID}.heartbeat" - _agent_ts > "$hb_file" -} - -# --- Metric: record a key-value measurement --- -agent_metric() { - local key="${1:?Usage: agent_metric \"key\" \"value\"}" - local value="${2:?Usage: agent_metric \"key\" \"value\"}" - local metrics_file="/tmp/agent-${AGENT_ID}.metrics.jsonl" - printf '{"agent":"%s","ts":"%s","key":"%s","value":%s}\n' \ - "$AGENT_ID" "$(_agent_ts)" "$key" "$value" >> "$metrics_file" - - # Also append to shared progress - if [ -n "$PROGRESS_FILE" ]; then - printf '{"agent":"%s","ts":"%s","type":"metric","key":"%s","value":%s}\n' \ - "$AGENT_ID" "$(_agent_ts)" "$key" "$value" >> "$PROGRESS_FILE" 2>/dev/null || true - fi -} - -# --- RMSE delta: specialized metric for fidelity work --- -agent_rmse() { - local doc="${1:?Usage: agent_rmse \"doc\" before after}" - local before="${2:?}" - local after="${3:?}" - agent_metric "rmse" "$(printf '{"doc":"%s","before":%s,"after":%s}' "$doc" "$before" "$after")" - - if [ -n "$PROGRESS_FILE" ]; then - printf '{"agent":"%s","ts":"%s","type":"rmse","doc":"%s","rmse_before":%s,"rmse_after":%s}\n' \ - "$AGENT_ID" "$(_agent_ts)" "$doc" "$before" "$after" >> "$PROGRESS_FILE" 2>/dev/null || true - fi -} - -echo "[agent-health] loaded for agent=$AGENT_ID" diff --git a/templates/project-bootstrap/scripts/check-bypass-flags.sh b/templates/project-bootstrap/scripts/check-bypass-flags.sh deleted file mode 100755 index 685d37f..0000000 --- a/templates/project-bootstrap/scripts/check-bypass-flags.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -# check-bypass-flags.sh — Gate I enforcement: bypass flags require ticket refs. -# -# Source: feedback/processed/2026-04-24-process-gates-G-through-L.md §Gate I. -# -# Rule: any commit whose body mentions a test-bypass flag (--skip-stress, -# --skip-tests, --no-verify, --force, SKIP_TESTS=1, etc.) MUST contain a -# `Bypass tickets:` line listing the broken-test IDs being deferred. Without -# this, the bypass is a quiet escape hatch that ships regressions in -# regression-fix PRs. -# -# The rule covers the COMMIT MESSAGE — it's the audit trail for why a bypass -# was chosen. The actual flag invocation (e.g., wrapping promote-to-prod) is -# project-specific and lives in your project's deploy scripts. -# -# Modes: -# ./scripts/check-bypass-flags.sh # check HEAD's commit msg -# ./scripts/check-bypass-flags.sh # check given file (commit-msg hook usage) -# ./scripts/check-bypass-flags.sh --range .. # audit a range of commits -# ./scripts/check-bypass-flags.sh --warn # flag but don't fail -# -# Exit code: 0 if all qualifying commits are clean, 1 if any violations. -# -# Bash 3.2 compatible. - -set -uo pipefail - -WARN_ONLY=0 -MODE="head" -ARG="" - -while [ $# -gt 0 ]; do - case "$1" in - --warn) WARN_ONLY=1; shift ;; - --range) MODE="range"; ARG="$2"; shift 2 ;; - -h|--help) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' - exit 0 - ;; - -*) - echo "check-bypass-flags: unknown flag '$1'" >&2; exit 2 ;; - *) - MODE="file"; ARG="$1"; shift ;; - esac -done - -# Pattern set covering the most common bypass flags. If your project has -# others (e.g., domain-specific --skip-X), extend BYPASS_REGEX in your local -# copy. -# -# Note: we look in the FULL commit body, not the diff. Authors who use a -# bypass flag should mention WHY in the commit body — that's what creates the -# audit trail. If they used the flag silently, that's a separate problem -# (project-specific shell-history tooling). -BYPASS_REGEX='(--skip-(stress|tests|all|e2e|smoke|ci|build|fix|preflight)|--no-verify|--no-gpg-sign|--force[[:space:]]|--force$|SKIP_TESTS=|SKIP_E2E=|SKIP_STRESS=|BYPASS_=)' - -# A commit satisfies Gate I if every bypass mention is paired with a -# `Bypass tickets:` line. -satisfies() { - local body="$1" - echo "$body" | grep -qE '^Bypass[[:space:]]tickets:[[:space:]]*\S' -} - -mentions_bypass() { - local body="$1" - echo "$body" | grep -qE "$BYPASS_REGEX" -} - -check_one() { - local subject="$1" body="$2" label="$3" - if ! mentions_bypass "$body"; then - return 0 # no bypass mentioned - fi - # Documentation/meta commits frequently mention bypass flags as content - # (e.g., "the script catches --skip-stress / --no-verify"). Honor an - # explicit `Bypass-flags-meta: ` opt-out so authors can mark a - # commit as referring to bypass flags rather than invoking them. - if echo "$body" | grep -qE '^Bypass-flags-meta:[[:space:]]*\S'; then - return 0 - fi - if satisfies "$body"; then - return 0 # gate satisfied - fi - echo "" - echo "check-bypass-flags: VIOLATION ($label)" - echo " subject: $subject" - echo "" - echo " Body mentions a test-bypass flag (matched: $(echo "$body" | grep -oE "$BYPASS_REGEX" | head -3 | tr '\n' ' '))" - echo " Gate I requires a line of the form:" - echo " Bypass tickets: TICKET-1, TICKET-2 (one ticket per broken test)" - echo "" - echo " And a justification per ticket. Without this, bypassing a test" - echo " ships an unowned regression. Source: practices/regression-fix-protocol.md §Gate I" - return 1 -} - -violations=0 - -case "$MODE" in - head) - subject="$(git log -1 --format='%s' HEAD 2>/dev/null || true)" - body="$(git log -1 --format='%B' HEAD 2>/dev/null || true)" - if [ -z "$subject" ]; then - echo "check-bypass-flags: no commit found at HEAD" - exit 0 - fi - check_one "$subject" "$body" "HEAD" || violations=$((violations + 1)) - ;; - - file) - if [ ! -f "$ARG" ]; then - echo "check-bypass-flags: file not found: $ARG" >&2 - exit 2 - fi - subject="$(grep -vE '^#|^$' "$ARG" | head -1)" - body="$(cat "$ARG")" - check_one "$subject" "$body" "$ARG" || violations=$((violations + 1)) - ;; - - range) - while IFS= read -r sha; do - [ -z "$sha" ] && continue - subject="$(git log -1 --format='%s' "$sha")" - body="$(git log -1 --format='%B' "$sha")" - check_one "$subject" "$body" "$sha" || violations=$((violations + 1)) - done < <(git rev-list "$ARG" 2>/dev/null) - ;; -esac - -if [ "$violations" -eq 0 ]; then - echo "check-bypass-flags: OK — Gate I satisfied (no bypass mentions without Bypass tickets:)" - exit 0 -fi - -echo "" -echo "check-bypass-flags: $violations violation(s)" -echo "Wire as commit-msg hook: ln -sf ../../scripts/check-bypass-flags.sh .git/hooks/commit-msg" - -if [ "$WARN_ONLY" -eq 1 ]; then - exit 0 -fi -exit 1 diff --git a/templates/project-bootstrap/scripts/check-compliance.sh b/templates/project-bootstrap/scripts/check-compliance.sh deleted file mode 100755 index 7de0718..0000000 --- a/templates/project-bootstrap/scripts/check-compliance.sh +++ /dev/null @@ -1,438 +0,0 @@ -#!/usr/bin/env bash -# check-compliance.sh — Verify rebar compliance: version, tier, README badge, AGENTS.md -# rebar-scripts: 2026.03.20 -# -# Checks: -# 1. .rebar-version file exists and contains a valid semver tag -# 2. .rebarrc file exists and declares a tier (1, 2, or 3) -# 3. README.md has a well-formed rebar badge on the first content line after the title -# 4. Badge version matches .rebar-version -# 5. Badge tier matches .rebarrc tier -# 6. AGENTS.md has required load-bearing sections (Tier 2+) -# 7. AGENTS.md mentions ASK CLI (Tier 2+) -# 8. Federation drift-check wired when CONSUMES.md declares dependencies -# 9. Contract maturity — reads DECLARED `**Status:**` fields from -# architecture/CONTRACT-*.md and weights the badge (v3, Cluster 1): -# <33% stub-or-draft → tier stands as declared -# 33–66% → tier annotated "— IN PROGRESS" (advisory) -# >66% → badge demoted one tier (compliance failure) -# Zero Status: fields anywhere → pre-v3 repo: no penalty, one advisory. -# -# Badge format (must be a blockquote, first line after # Title): -# > **rebar vX.Y.Z** | **Tier N: LEVEL** -# -# Where LEVEL is: PARTIAL (1), ADOPTED (2), or ENFORCED (3) -# -# Usage: ./scripts/check-compliance.sh -# Exit code: 0 = compliant, 1 = non-compliant - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -README="$PROJECT_ROOT/README.md" -AGENTS="$PROJECT_ROOT/AGENTS.md" -VERSION_FILE="$PROJECT_ROOT/.rebar-version" -RC_FILE="$PROJECT_ROOT/.rebarrc" - -errors=0 - -tier_label() { - case "$1" in - 1) echo "PARTIAL" ;; - 2) echo "ADOPTED" ;; - 3) echo "ENFORCED" ;; - *) echo "UNKNOWN" ;; - esac -} - -# ─── Check 1: .rebar-version file ──────────────────────────────────────── - -echo "=== Rebar compliance check ===" -echo "" - -if [ ! -f "$VERSION_FILE" ]; then - echo "FAIL: .rebar-version file not found" - echo " Create it with: echo 'v3.0.0-beta.2' > .rebar-version" - errors=$((errors + 1)) - declared_version="" -else - declared_version=$(cat "$VERSION_FILE" | tr -d '[:space:]') - # Semver with optional pre-release suffix (v3.0.0-beta.2, v3.1.0-rc.1) - if [[ "$declared_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - echo "OK: .rebar-version = $declared_version" - else - echo "FAIL: .rebar-version contains '$declared_version' — expected format: vX.Y.Z or vX.Y.Z-prerelease" - errors=$((errors + 1)) - fi -fi - -# ─── Check 2: .rebarrc tier ────────────────────────────────────────────── - -if [ ! -f "$RC_FILE" ]; then - echo "FAIL: .rebarrc file not found" - echo " Create it from .rebarrc.template" - errors=$((errors + 1)) - declared_tier="" -else - # '|| true' — under errexit a .rebarrc with no tier line must reach the - # FAIL message below, not kill the script mid-pipeline with no diagnostic. - declared_tier=$(grep '^tier' "$RC_FILE" 2>/dev/null | head -1 | sed 's/.*=[[:space:]]*//' | tr -d ' ' || true) - if [[ "$declared_tier" =~ ^[123]$ ]]; then - echo "OK: .rebarrc tier = $declared_tier ($(tier_label "$declared_tier"))" - else - echo "FAIL: .rebarrc tier is '$declared_tier' — expected 1, 2, or 3" - errors=$((errors + 1)) - fi -fi - -# ─── Check 3: README.md rebar badge ────────────────────────────────────── - -if [ ! -f "$README" ]; then - echo "FAIL: README.md not found" - errors=$((errors + 1)) -else - # Find the badge line: first blockquote line containing "rebar v" after the title - badge_line=$(grep -n '^\s*>\s*\*\*rebar v' "$README" | head -1 || true) - - if [ -z "$badge_line" ]; then - echo "FAIL: README.md has no rebar badge" - echo " Add this as the first line after your # Title:" - echo ' > **rebar v3.0.0-beta.2** | **Tier 2: ADOPTED**' - errors=$((errors + 1)) - else - line_num=$(echo "$badge_line" | cut -d: -f1) - line_content=$(echo "$badge_line" | cut -d: -f2-) - - # Validate format: > **rebar vX.Y.Z[-prerelease]** | **Tier N: LEVEL** - if [[ "$line_content" =~ \*\*rebar\ (v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?)\*\*.*\*\*Tier\ ([0-9]+):\ ([A-Z]+)\*\* ]]; then - badge_version="${BASH_REMATCH[1]}" - badge_tier="${BASH_REMATCH[3]}" - badge_level="${BASH_REMATCH[4]}" - - echo "OK: README.md badge found on line $line_num" - echo " Version: $badge_version | Tier: $badge_tier ($badge_level)" - - # Check badge is near the top (within first 5 lines) - if [ "$line_num" -gt 5 ]; then - echo "WARN: Badge is on line $line_num — should be within the first 3 lines (right after # Title)" - fi - - # ─── Check 4: Badge version matches .rebar-version ────────── - if [ -n "$declared_version" ] && [ "$badge_version" != "$declared_version" ]; then - echo "FAIL: Badge says $badge_version but .rebar-version says $declared_version" - errors=$((errors + 1)) - fi - - # ─── Check 5: Badge tier matches .rebarrc tier ────────────── - if [ -n "$declared_tier" ] && [ "$badge_tier" != "$declared_tier" ]; then - echo "FAIL: Badge says Tier $badge_tier but .rebarrc says tier = $declared_tier" - errors=$((errors + 1)) - fi - - # Check level matches tier number - expected_level=$(tier_label "$badge_tier") - if [ "$badge_level" != "$expected_level" ]; then - echo "FAIL: Tier $badge_tier should be '$expected_level', not '$badge_level'" - errors=$((errors + 1)) - fi - else - echo "FAIL: README.md badge is malformed on line $line_num" - echo " Found: $line_content" - echo ' Expected: > **rebar vX.Y.Z** | **Tier N: LEVEL**' - echo ' Where LEVEL is PARTIAL (1), ADOPTED (2), or ENFORCED (3)' - errors=$((errors + 1)) - fi - fi -fi - -# ─── Check 6: AGENTS.md load-bearing sections (Tier 2+) ────────────────── - -# Source tier config if available -if [ -f "$SCRIPT_DIR/_rebar-config.sh" ]; then - source "$SCRIPT_DIR/_rebar-config.sh" - current_tier=$(_rebar_tier) -else - current_tier="${declared_tier:-3}" -fi - -if [ "$current_tier" -ge 2 ] && [ -f "$AGENTS" ]; then - echo "" - echo "=== AGENTS.md required sections ===" - - # These are the load-bearing walls. Without them, agents don't know - # about contracts, testing discipline, or the TODO system. - required_sections=( - "Cold Start\|Read Before Coding:Cold Start / Read Before Coding — agents must know the reading order" - "Contract-Driven\|Contract.Driven:Contract-Driven Development — the 4 rules that make contracts operational" - "Testing Cascade\|Testing Expectations\|Scout Rule:Testing expectations — cascade tiers or scout rule" - "TODO Tracking:TODO Tracking — two-tag system prevents invisible debt" - ) - - for entry in "${required_sections[@]}"; do - pattern="${entry%%:*}" - description="${entry##*:}" - - if grep -qi "$pattern" "$AGENTS" 2>/dev/null; then - echo "OK: $description" - else - echo "FAIL: AGENTS.md missing required section: $description" - errors=$((errors + 1)) - fi - done - - # Check 7: ASK CLI awareness — agents need to know they can query role-based agents - if grep -qi '\bask\b\|ASK CLI\|ask architect\|ask product\|ask steward\|ask englead' "$AGENTS" 2>/dev/null; then - echo "OK: AGENTS.md mentions ASK CLI" - else - echo "FAIL: AGENTS.md does not mention ASK CLI" - echo " Agents need to know they can use 'ask \"question\"' for focused queries." - echo " Add a reference in the Cold Start or Reference section." - errors=$((errors + 1)) - fi - -elif [ "$current_tier" -ge 2 ] && [ ! -f "$AGENTS" ]; then - echo "" - echo "FAIL: AGENTS.md not found (required at Tier 2+)" - errors=$((errors + 1)) -fi - -# ─── Check 8: Federation — drift-check wired when CONSUMES.md exists ───── -# -# CHARTER §1.6 makes federation opt-in but compliance-gated: once a repo -# adds a CONSUMES.md (declaring cross-repo dependencies), it MUST run -# `rebar contract drift-check` in CI so consumers don't silently age out -# while owner contracts evolve. Adopters self-select by adding the file. - -CONSUMES_FILE="$PROJECT_ROOT/CONSUMES.md" -if [ -f "$CONSUMES_FILE" ]; then - echo "" - echo "=== Federation (CONSUMES.md present) ===" - - # Has at least one real entry (## owner/contract.version section)? - # Count REAL declarations only — a fenced code block documenting the format - # is not a dependency. The bootstrap's own CONSUMES.md carries five worked - # examples inside a ```markdown fence; a line-anchored grep counted all five, - # so every freshly-bootstrapped project failed this check out of the box - # ("declares dependencies but drift-check is not wired") without ever having - # declared anything. sch-repos named this class exactly, in another context, - # the same day: an anchored regex cannot distinguish a real declaration from - # a document quoting one. Toggling on ``` fences is the cheap correct fix. - consumes_entries=$(awk ' - /^```/ { fence = !fence; next } - !fence && /^## [A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/ { n++ } - END { print n + 0 } - ' "$CONSUMES_FILE" 2>/dev/null | tr -d '[:space:]' || echo 0) - if [ "${consumes_entries:-0}" = "0" ]; then - echo "OK: CONSUMES.md present but no entries declared (federation opt-in not yet active)" - else - echo "OK: CONSUMES.md declares $consumes_entries cross-repo dependency(ies)" - - # drift-check must be wired into a CI-relevant script. Check the - # standard rebar surfaces in priority order. An adopter can override - # by adding their own framework (Makefile, GitHub Actions, etc.) — - # we look across the obvious places. - drift_wired=0 - for f in "$PROJECT_ROOT/scripts/ci-check.sh" "$PROJECT_ROOT/scripts/pre-commit.sh" "$PROJECT_ROOT/Makefile" "$PROJECT_ROOT/.github/workflows"/*; do - [ -e "$f" ] || continue - if [ -d "$f" ]; then - if grep -rq "drift-check" "$f" 2>/dev/null; then - drift_wired=1 - break - fi - elif grep -q "drift-check" "$f" 2>/dev/null; then - drift_wired=1 - break - fi - done - - if [ "$drift_wired" -eq 1 ]; then - echo "OK: drift-check is wired into CI" - else - echo "FAIL: CONSUMES.md declares dependencies but \`rebar contract drift-check\` is not wired into CI" - echo " Add to scripts/ci-check.sh (or your CI of choice):" - echo " rebar contract drift-check" - echo " Without this, consumed contracts can silently age out as upstream evolves (CHARTER §1.6)." - errors=$((errors + 1)) - fi - fi -fi - -# ─── Check 9: Contract maturity (declared Status: fields) ──────────────── -# -# v3 maturity honesty (docs/v3-beta-plan.md Cluster 1): contracts DECLARE -# maturity in a `**Status:** ` header line — stub / draft / -# in-progress / active / verified. This is the human/agent honesty marker, -# distinct from the Steward's COMPUTED lifecycle -# (draft/active/testing/impl-present), which this check never reads. -# The badge is weighted by how much of the live contract set is still -# stub-or-draft, so a tier can't sit on top of placeholder contracts. - -ARCH_DIR="$PROJECT_ROOT/architecture" - -if [ -d "$ARCH_DIR" ]; then - echo "" - echo "=== Contract maturity (declared Status:) ===" - - status_total=0 # Status: fields found across all contract files - live_total=0 # live (non-superseded) contracts considered - live_declared=0 # live contracts with a recognized maturity value - stub_draft=0 # live contracts declared stub or draft - missing_status="" # newline-joined names of live contracts without Status: - - for contract in "$ARCH_DIR"/CONTRACT-*.md; do - [ -f "$contract" ] || continue - cbase="$(basename "$contract")" - - # Skip templates, the generated registry, and companion files — - # same exclusions as compute-registry.sh. - case "$cbase" in - CONTRACT-TEMPLATE.md|CONTRACT-SEAM-TEMPLATE.md|CONTRACT-REGISTRY.md|CONTRACT-REGISTRY.template.md|CONTRACT-GAPS.md) - continue ;; - *.impl.md) - continue ;; - esac - - # Tolerant parse, identical to cold-start-checks.sh: bolded or bare - # 'Status:' line, first word, case-folded. The canonical form stays - # '**Status:** value' (conventions.md) but parsers of record must agree — - # and conventions.md now publishes THIS regex verbatim so prose cannot - # drift from the parser again. - # - # The strip is a single asterisk-tolerant expression rather than two - # fixed alternatives. The old chain matched '^\*{0,2}Status:' but only - # stripped the two-asterisk and bare forms, so '*Status:* active' fell - # through with its marker intact, awk yielded '*Status:*', and the value - # landed on the '*)' arm — silently counted as stub-or-draft. A - # formatting slip became a badge penalty explained only by a WARN. - # See feedback/2026-08-02-sch-repos-canonical-status-grep-*.md - cstatus_raw="$(grep -m1 -E '^\*{0,2}Status:' "$contract" 2>/dev/null || true)" - cstatus="$(printf '%s' "$cstatus_raw" \ - | sed -E 's/^\*{0,2}Status:\*{0,2}//' \ - | awk '{print $1}' | tr -d '*' | tr '[:upper:]' '[:lower:]' || true)" - - if [ -n "$cstatus" ]; then - status_total=$((status_total + 1)) - fi - - # Terminal states are out of the live maturity mix — a superseded - # contract kept around for its migration window shouldn't drag (or - # inflate) the badge. - case "$cstatus" in - superseded|deprecated|retired) continue ;; - esac - - live_total=$((live_total + 1)) - - case "$cstatus" in - "") - # Warned about later — only when the repo has *some* Status: fields - # (partially migrated). A pre-v3 repo gets one advisory, not N warns. - # Once any contract declares, undeclared live contracts COUNT AS - # stub-or-draft: selective declaration must not bypass demotion. - missing_status="${missing_status}${cbase} -" - ;; - stub|draft) - live_declared=$((live_declared + 1)) - stub_draft=$((stub_draft + 1)) - ;; - in-progress|active|verified) - live_declared=$((live_declared + 1)) - ;; - *) - # Report the RAW line, not just the parsed token. For a near-miss - # format the parsed value is mangled, and a WARN naming the mangled - # value is not actionable — the author cannot see what to change. - echo "WARN: $cbase declares Status: '$cstatus' — not in the maturity vocabulary (stub|draft|in-progress|active|verified); counted as stub-or-draft" - echo " source line: $(printf '%s' "$cstatus_raw" | head -c 120)" - stub_draft=$((stub_draft + 1)) - ;; - esac - done - - if [ "$status_total" -eq 0 ]; then - if [ "$live_total" -gt 0 ]; then - echo "ADVISORY: no contract declares a Status: field — treating as a pre-v3 repo (no maturity penalty)." - echo " Add '**Status:** ' to each contract header" - echo " (see architecture/CONTRACT-TEMPLATE.md) so the badge can reflect real maturity." - else - echo "OK: no contracts found — maturity weighting not applicable" - fi - elif [ -n "$missing_status" ]; then - # Partially migrated: some contracts declare Status:, these don't. - printf '%s' "$missing_status" | while IFS= read -r mname; do - echo "WARN: $mname has no Status: line — add one (see architecture/CONTRACT-TEMPLATE.md header)" - done - fi - - if [ "$status_total" -gt 0 ] && [ "$live_declared" -eq 0 ] && [ "$live_total" -eq 0 ]; then - echo "OK: no live contracts declare maturity (all terminal) — no maturity weighting" - elif [ "$status_total" -gt 0 ] && [ "$live_total" -gt 0 ]; then - # Weight over ALL live contracts: undeclared ones already counted as - # stub-or-draft above (selective declaration must not launder a badge). - undeclared=$((live_total - live_declared)) - if [ "$undeclared" -gt 0 ]; then - stub_draft=$((stub_draft + undeclared)) - fi - pct_tenths=$((stub_draft * 1000 / live_total)) - pct="$((pct_tenths / 10)).$((pct_tenths % 10))" - echo "OK: $live_declared of $live_total live contract(s) declare maturity — $stub_draft stub-or-draft-or-undeclared (${pct}%)" - - if [[ "$declared_tier" =~ ^[123]$ ]]; then - # Product comparisons — integer division floor must not soften the - # documented thresholds (<33% ok, 33-66% annotate, >66% demote). - # - # DEGENERATE POPULATION GUARD (sch-repos, 2026-08-02). At live_total=1 - # a percentage quantizes to exactly 0% or 100%: the middle NOTE band is - # mathematically unreachable, so the only honest downgrade available to - # a one-contract repo jumps straight from OK to FAIL + tier demotion. - # That made dishonesty the green path — declaring your single contract - # `draft` (the honest value while it drifts from its implementation) - # failed CI, while leaving it `active` passed. Exactly the inversion v3 - # exists to remove, and it lands on new/small adopters — the on-ramp. - # - # Threshold is -lt 2 because n=1 is precisely the degenerate case; from - # n=2 the NOTE band is reachable and the formula behaves as documented. - # 100%-stub still fails at every n>=2, so anti-laundering is intact. - # Gameable in principle by deleting contracts to drop under it — but - # shrinking your governance surface to dodge a badge is self-defeating, - # visible in the diff, and leaves the repo in the state the advisory - # already prints. - # See feedback/2026-08-02-sch-repos-check-compliance-maturity-*.md - if [ "$live_total" -lt 2 ]; then - echo "NOTE: only $live_total live contract(s) — maturity is advisory at this population" - echo " (a percentage over n=1 quantizes to 0% or 100%; badge not weighted)" - elif [ $((stub_draft * 100)) -lt $((live_total * 33)) ]; then - echo "OK: maturity supports the declared badge (Tier $declared_tier: $(tier_label "$declared_tier"))" - elif [ $((stub_draft * 100)) -le $((live_total * 66)) ]; then - echo "NOTE: ${pct}% of live contracts are stub-or-draft — badge reads as" - echo " 'Tier $declared_tier: $(tier_label "$declared_tier") — IN PROGRESS' until the set matures" - else - demoted_tier=$((declared_tier - 1)) - echo "FAIL: ${pct}% of live contracts are stub-or-draft (>66%) — badge demoted one tier" - if [ "$demoted_tier" -ge 1 ]; then - echo " Declared: Tier $declared_tier: $(tier_label "$declared_tier") — Effective: Tier $demoted_tier: $(tier_label "$demoted_tier")" - else - echo " Declared: Tier $declared_tier: $(tier_label "$declared_tier") — Effective: below Tier 1 (adoption not yet real)" - fi - echo " Reason: a tier claimed on top of a mostly stub/draft contract set overstates adoption." - echo " Fix: mature the contracts (or lower the badge) until <=66% are stub-or-draft." - errors=$((errors + 1)) - fi - else - echo "NOTE: no valid tier declared — maturity computed but badge weighting skipped" - fi - fi -fi - -# ─── Summary ────────────────────────────────────────────────────────────── - -echo "" -if [ "$errors" -gt 0 ]; then - echo "FAIL: $errors compliance issue(s) found." - exit 1 -else - echo "OK: Rebar compliance verified." - exit 0 -fi diff --git a/templates/project-bootstrap/scripts/check-contract-headers.sh b/templates/project-bootstrap/scripts/check-contract-headers.sh deleted file mode 100755 index 627e47c..0000000 --- a/templates/project-bootstrap/scripts/check-contract-headers.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# check-contract-headers.sh — Verify every source file has a CONTRACT: or Architecture: header -# rebar-scripts: 2026.03.20 -# -# Usage: ./scripts/check-contract-headers.sh [directories...] -# Default: scans src/ internal/ cmd/ client/ packages/ lib/ app/ -# -# Exit code: 0 = all files have headers, 1 = missing headers found - -set -euo pipefail - -# Tier gate: contract headers are Tier 2+ (skip for Tier 1 / partial adoption) -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -[ -f "$SCRIPT_DIR/_rebar-config.sh" ] && source "$SCRIPT_DIR/_rebar-config.sh" && _rebar_skip 2 && exit 0 - -# Configurable: file extensions to check -EXTENSIONS="${CONTRACT_EXTENSIONS:-.go .ts .tsx .js .jsx .py .rs}" - -# Configurable: directories to scan (override with args) -if [ $# -gt 0 ]; then - DIRS=("$@") -else - DIRS=() - for d in src internal cmd client packages lib app; do - [ -d "$d" ] && DIRS+=("$d") - done -fi - -if [ ${#DIRS[@]} -eq 0 ]; then - echo "No source directories found. Pass directories as arguments." - exit 0 -fi - -# Build find expression for extensions -FIND_ARGS=() -first=true -for ext in $EXTENSIONS; do - if $first; then - FIND_ARGS+=(-name "*${ext}") - first=false - else - FIND_ARGS+=(-o -name "*${ext}") - fi -done - -missing=0 -total=0 - -while IFS= read -r file; do - # Skip test files, generated files, vendor, node_modules - case "$file" in - *_test.go|*.test.ts|*.test.tsx|*.test.js|*.spec.ts|*.spec.tsx|*.spec.js) continue ;; - */vendor/*|*/node_modules/*|*/dist/*|*/build/*|*/.git/*) continue ;; - *_generated*|*.gen.*|*.pb.go|*.pb.ts) continue ;; - esac - - total=$((total + 1)) - - # Check first 15 lines for CONTRACT: or Architecture: - if ! head -15 "$file" | grep -q "CONTRACT:\|Architecture:"; then - echo "MISSING: $file" - missing=$((missing + 1)) - fi -done < <(find "${DIRS[@]}" \( "${FIND_ARGS[@]}" \) -type f 2>/dev/null) - -echo "" -echo "Scanned $total source files, $missing missing contract headers." - -if [ "$missing" -gt 0 ]; then - # NOTE: literal CONTRACT prefix split via variable so the shadow-ref - # detector in compute-registry.sh doesn't false-positive on these example - # strings. See feedback/processed/2026-04-25-bootstrap-template-script-drift-and-bash3.2.md - # for the related cli/cmd/context.go fix. - P="CONTRACT:" - echo "" - echo "Every source file must declare which contract it implements:" - echo " // ${P}C1-BLOBSTORE.2.1" - echo " // Architecture: ${P}S2-API-GATEWAY.1.0" - echo "" - echo "See architecture/README.md for the full convention." - exit 1 -fi - -exit 0 diff --git a/templates/project-bootstrap/scripts/check-contract-refs.sh b/templates/project-bootstrap/scripts/check-contract-refs.sh deleted file mode 100755 index 8cacaaf..0000000 --- a/templates/project-bootstrap/scripts/check-contract-refs.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# check-contract-refs.sh — Verify every CONTRACT: reference points to a real file -# rebar-scripts: 2026.03.20 -# -# Usage: ./scripts/check-contract-refs.sh [architecture-dir] -# Default: architecture/ -# -# Exit code: 0 = all refs valid, 1 = broken refs found - -set -euo pipefail - -ARCH_DIR="${1:-architecture}" - -if [ ! -d "$ARCH_DIR" ]; then - echo "Architecture directory '$ARCH_DIR' not found." - exit 1 -fi - -broken=0 -total=0 - -# Find all CONTRACT: references in source files -while IFS= read -r line; do - file=$(echo "$line" | cut -d: -f1) - lineno=$(echo "$line" | cut -d: -f2) - - # Extract the contract ID (e.g., C1-BLOBSTORE.2.1) - ref=$(echo "$line" | grep -o 'CONTRACT:[A-Za-z0-9_-]*\.[0-9]*\.[0-9]*' | head -1 | sed 's/CONTRACT://') - - [ -z "$ref" ] && continue - total=$((total + 1)) - - # Check if the contract file exists - expected="${ARCH_DIR}/CONTRACT-${ref}.md" - if [ ! -f "$expected" ]; then - echo "BROKEN: $file:$lineno references CONTRACT:$ref" - echo " Expected: $expected" - broken=$((broken + 1)) - fi -done < <(grep -rn "CONTRACT:[A-Za-z0-9_-]*\.[0-9]*\.[0-9]*" \ - --include="*.go" --include="*.ts" --include="*.tsx" --include="*.js" \ - --include="*.py" --include="*.rs" --include="*.jsx" \ - . 2>/dev/null | grep -v "node_modules\|vendor\|dist\|\.git") - -echo "" -echo "Checked $total contract references, $broken broken." - -if [ "$broken" -gt 0 ]; then - echo "" - echo "Fix by either:" - echo " 1. Creating the missing contract in $ARCH_DIR/" - echo " 2. Updating the code reference to the correct contract version" - exit 1 -fi - -exit 0 diff --git a/templates/project-bootstrap/scripts/check-decay-patterns.sh b/templates/project-bootstrap/scripts/check-decay-patterns.sh deleted file mode 100755 index e9e5737..0000000 --- a/templates/project-bootstrap/scripts/check-decay-patterns.sh +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env bash -# check-decay-patterns.sh — Flag soft-hardening patterns that look like -# done work but ship a longer-fuse failure mode. -# -# Sources: rebar/feedback/2026-04-24-fidelity-decay-soft-hardening-patterns.md -# rebar/feedback/processed/2026-04-27-e2e-test-bypass-closed-loop-verification-drift.md (P8) -# -# An author cannot reliably self-audit hardening work because the author's -# context (right now, with the test fresh) matches the consumer's context -# (six months from now, scanning a dashboard). This script applies external -# state — a structural lens — at commit time. -# -# Eight named patterns from the fidelity-decay feedback. The grep-detectable -# subset is implemented; the semantic ones (hermeticity, env-name -# plausibility) are left for the author's self-audit prompt linked from -# AGENTS.template.md. P8 (demo-spec bypass) is the banned-pattern gate from -# the e2e-bypass feedback: demo-claiming and uaks-declared specs must drive -# the surface a user touches (see practices/test-fidelity.md, Gate 2). -# -# Modes: -# ./scripts/check-decay-patterns.sh # scan staged files -# ./scripts/check-decay-patterns.sh --all # scan everything tracked -# ./scripts/check-decay-patterns.sh --paths a b # scan specific paths -# ./scripts/check-decay-patterns.sh --warn # flag but don't fail -# -# Exit code: 0 if no findings (or --warn), 1 if findings in default mode. -# -# Bash 3.2 compatible (macOS default). - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -ALLOWLIST="$PROJECT_ROOT/.rebar/decay-patterns-allow.txt" - -MODE="staged" -WARN_ONLY=0 -declare -a EXPLICIT_PATHS=() - -while [ $# -gt 0 ]; do - case "$1" in - --all) MODE="all"; shift ;; - --paths) MODE="explicit"; shift; while [ $# -gt 0 ] && [ "${1:0:2}" != "--" ]; do EXPLICIT_PATHS+=("$1"); shift; done ;; - --warn) WARN_ONLY=1; shift ;; - -h|--help) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' - exit 0 - ;; - *) echo "check-decay-patterns: unknown arg '$1'" >&2; exit 2 ;; - esac -done - -cd "$PROJECT_ROOT" - -# Resolve the file set to scan. -declare -a FILES=() -case "$MODE" in - staged) - while IFS= read -r f; do FILES+=("$f"); done < <(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null) - if [ ${#FILES[@]} -eq 0 ]; then - # Fall back to working-tree changes (useful when running outside a hook). - while IFS= read -r f; do FILES+=("$f"); done < <(git diff --name-only --diff-filter=ACMR 2>/dev/null) - fi - ;; - all) - while IFS= read -r f; do FILES+=("$f"); done < <(git ls-files) - ;; - explicit) - FILES=("${EXPLICIT_PATHS[@]+"${EXPLICIT_PATHS[@]}"}") - ;; -esac - -if [ ${#FILES[@]} -eq 0 ]; then - echo "check-decay-patterns: no files to scan." - exit 0 -fi - -# Filter to spec/test files + workflow files. Other files don't have these -# patterns and would just produce noise. -declare -a SCAN_FILES=() -for f in "${FILES[@]+"${FILES[@]}"}"; do - [ -f "$f" ] || continue - case "$f" in - *.spec.ts|*.spec.tsx|*.spec.js|*.spec.jsx|*.spec.mjs) SCAN_FILES+=("$f") ;; - *.test.ts|*.test.tsx|*.test.js|*.test.jsx|*.test.mjs) SCAN_FILES+=("$f") ;; - *_test.go|*_spec.rb|*_test.py|*test_*.py) SCAN_FILES+=("$f") ;; - *playwright*.config.*|*vitest*.config.*|*jest*.config.*) SCAN_FILES+=("$f") ;; - .github/workflows/*.yml|.github/workflows/*.yaml) SCAN_FILES+=("$f") ;; - esac -done - -if [ ${#SCAN_FILES[@]} -eq 0 ]; then - echo "check-decay-patterns: no spec/config/workflow files in scan set." - exit 0 -fi - -# Findings: ::: -findings_tmp="$(mktemp)" -trap 'rm -f "$findings_tmp"' EXIT -: > "$findings_tmp" - -# Helper: scan with a regex, emit findings. -scan() { - local pattern_id="$1" regex="$2" message="$3" file - shift 3 - for file in "$@"; do - [ -f "$file" ] || continue - # Use grep -nE; allow pattern to fail gracefully on no-match. - while IFS=: read -r lineno _; do - [ -z "$lineno" ] && continue - # Allowlist check by file:line:pattern triple. - if [ -f "$ALLOWLIST" ] && grep -Fxq -- "${file}:${lineno}:${pattern_id}" "$ALLOWLIST" 2>/dev/null; then - continue - fi - echo "${file}:${lineno}:${pattern_id}:${message}" >> "$findings_tmp" - done < <(grep -nEi "$regex" "$file" 2>/dev/null || true) - done -} - -# ------------------------------------------------------------------------- -# Pattern 1 — Silenced failures -# testInfo.fail() / test.fail() / it.fail() / expect.fail() / .skip() with TODO -# ------------------------------------------------------------------------- -scan "P1-silenced-failure" \ - '(testInfo\.fail|test\.fail|it\.fail|describe\.fail|expect\.fail)\(' \ - "Pattern 1 (silenced failure): use a real failing assertion + CI continue-on-error instead — silencing the test makes the bug invisible to anyone glancing at the dashboard." \ - "${SCAN_FILES[@]}" - -# ------------------------------------------------------------------------- -# Pattern 2 — Inverted assertion (heuristic — flags for review) -# `.toBe(null)` / `.toBeNull()` / `.toEqual([])` / `.not.toContain` in security/audit specs -# ------------------------------------------------------------------------- -declare -a SEC_FILES=() -for f in "${SCAN_FILES[@]+"${SCAN_FILES[@]}"}"; do - case "$f" in - *security*|*audit*|*regression*|*detector*) SEC_FILES+=("$f") ;; - esac -done -if [ ${#SEC_FILES[@]} -gt 0 ]; then - scan "P2-inverted-assertion" \ - '\.(toBe\(null\)|toBeNull\(\)|toEqual\(\[\]\)|not\.toContain)' \ - "Pattern 2 (inverted assertion): assertion passes when the thing being tested is broken — a future engineer 'fixing' this test will silently elide the signal. Restate positively + use expected-fail at the workflow level. Or pair with a negative-control test that stages the violation." \ - "${SEC_FILES[@]}" -fi - -# ------------------------------------------------------------------------- -# Pattern 4 — Hand-copied "keep in sync" data -# Comments containing `keep .* in sync` / `update when` / `mirrors` -# ------------------------------------------------------------------------- -scan "P4-keep-in-sync" \ - '(keep[[:space:]]+(this|in|them)[[:space:]]+(in[[:space:]]+)?sync|update[[:space:]]+when[[:space:]]+upgrading|mirrors[[:space:]]+the[[:space:]]+upstream)' \ - "Pattern 4 (silent drift on upgrade): a comment saying 'keep in sync' is a wish. Read the source of truth at runtime and throw on parse failure, instead of hand-copying." \ - "${SCAN_FILES[@]}" - -# ------------------------------------------------------------------------- -# Pattern 5 — Magic-string project/name gating -# `testInfo.project.name === 'literal'` or `project.name == 'literal'` -# ------------------------------------------------------------------------- -scan "P5-magic-string-gating" \ - "testInfo\.project\.name[[:space:]]*[!=]==?[[:space:]]*['\"]" \ - "Pattern 5 (magic-string gating): renaming the project silently breaks the gate, no compile error. Use testInfo.project.metadata. instead — moving metadata with the project definition makes the coupling explicit." \ - "${SCAN_FILES[@]}" - -# ------------------------------------------------------------------------- -# Pattern 7 — Plausible test-only env var names (heuristic) -# VITE_ALLOW_*, *_ALLOW_HOST, *_DISABLE_AUTH — generic-sounding test loosening -# ------------------------------------------------------------------------- -scan "P7-plausible-env-name" \ - '(VITE_ALLOW_ALL|ALLOW_ALL_HOSTS|DISABLE_AUTH|SKIP_VERIFY|BYPASS_(AUTH|TLS|CORS))' \ - "Pattern 7 (too-plausible-to-refuse): this env var name reads like normal infrastructure config — a deploy script copy-paste won't be flagged. Make the name long and obviously test-only (e.g., FOO_PLAYWRIGHT_REAL_CHROME_LOOSENING) and require a second test-only env var to activate." \ - "${SCAN_FILES[@]}" - -# ------------------------------------------------------------------------- -# Pattern 8 — Demo/UAKS spec bypass (silenced failure by state injection) -# Demo-claiming specs (demo-*.spec.*, ui-demo-*.spec.*) and specs declaring -# `fidelity: uaks` must drive the surface a user touches. Auth/state -# injection, network mocks, or API-direct user actions in those files make -# a green run a costume, not evidence — the login page can be frozen while -# the suite reports 8/8. Doctrine: practices/test-fidelity.md (Gate 2). -# ------------------------------------------------------------------------- -declare -a DEMO_FILES=() -for f in "${SCAN_FILES[@]+"${SCAN_FILES[@]}"}"; do - case "$f" in - *.config.*|.github/workflows/*) continue ;; - esac - demo_base="${f##*/}" - case "$demo_base" in - demo-*|ui-demo-*|*uaks*|*user-at-keyboard*) - DEMO_FILES+=("$f") ;; - *) - # A `fidelity: uaks` declaration opts the file in wherever it lives. - if grep -qE '(//|#|--)[[:space:]]*fidelity:[[:space:]]*uaks' "$f" 2>/dev/null; then - DEMO_FILES+=("$f") - fi - ;; - esac -done -if [ ${#DEMO_FILES[@]} -gt 0 ]; then - scan "P8-demo-bypass" \ - '(loginAs[[:space:]]*\(|installSessionCookie|userManager\.storeUser|sessionStorage\.setItem|localStorage\.setItem|addInitScript|addCookies[[:space:]]*\(|page\.route[[:space:]]*\(|route\.fulfill|page\.evaluate|request\.(post|get|put|delete)[[:space:]]*\()' \ - "Pattern 8 (demo-spec bypass): a demo-claiming or uaks-declared spec must use only what a user at a keyboard could do — no state injection, no network mocks, no API-direct user actions. Move the shortcut to a lower-fidelity spec (and name the helper STUB_/INJECT_/BYPASS_), or drop the demo claim. See practices/test-fidelity.md." \ - "${DEMO_FILES[@]}" -fi - -# ------------------------------------------------------------------------- -# Report -# ------------------------------------------------------------------------- -count="$(wc -l < "$findings_tmp" | tr -d ' ')" - -if [ "$count" -eq 0 ]; then - echo "check-decay-patterns: OK — no soft-hardening patterns detected in scan set." - exit 0 -fi - -echo "check-decay-patterns: $count finding(s) in scan set" -echo "" -echo "Each line: :::" -echo "" -cat "$findings_tmp" -echo "" -echo "If a finding is intentional + reviewed, allowlist by adding" -echo " ::" -echo "to .rebar/decay-patterns-allow.txt (one per line, # for comments)." -echo "" -echo "References: feedback/2026-04-24-fidelity-decay-soft-hardening-patterns.md" -echo " feedback/processed/2026-04-27-e2e-test-bypass-closed-loop-verification-drift.md (P8)" -echo " practices/test-fidelity.md (fidelity ladder + gates)" - -if [ "$WARN_ONLY" -eq 1 ]; then - exit 0 -fi -exit 1 diff --git a/templates/project-bootstrap/scripts/check-doc-refs.sh b/templates/project-bootstrap/scripts/check-doc-refs.sh deleted file mode 100755 index a24670f..0000000 --- a/templates/project-bootstrap/scripts/check-doc-refs.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env bash -# check-doc-refs.sh — Verify that every file referenced from a tracked *.md -# is itself tracked in git. -# -# Catches a class of drift Tier 2 ci-check.sh misses: a load-bearing doc -# (e.g., FEDERATION-STORIES-DRAFT.md) is added to the working tree, cited -# from another doc, but never `git add`-ed. The current repo passes; a -# fresh clone fails to resolve the link. -# -# Source: rebar/feedback/2026-04-21-filedag-cross-ref-and-federation-coord.md §1 -# -# Usage: -# ./scripts/check-doc-refs.sh # report broken refs, exit 1 if any -# ./scripts/check-doc-refs.sh --quiet # only print failures -# ./scripts/check-doc-refs.sh --json # JSON summary on stdout -# -# Heuristics: -# - Walks every tracked *.md file (`git ls-files '*.md'`). -# - Extracts markdown-link targets: `[text](TARGET)`. -# - Skips: external URLs (http*, mailto, tel, ftp), anchor-only refs (#x), -# home-relative refs (~/...), template placeholders ({...}), allowlisted -# paths from .rebar/doc-refs-allow.txt (one path per line, # for comments). -# - Resolves remaining targets relative to the source file's directory. -# - Fails if resolved target is not in `git ls-files`. -# -# Bash 3.2 compatible (macOS default). - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -ALLOWLIST="$PROJECT_ROOT/.rebar/doc-refs-allow.txt" - -MODE="text" -case "${1:-}" in - --quiet) MODE="quiet" ;; - --json) MODE="json" ;; - -h|--help) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' - exit 0 - ;; -esac - -if ! command -v git >/dev/null 2>&1; then - echo "check-doc-refs: git not found" >&2 - exit 2 -fi - -cd "$PROJECT_ROOT" - -# Load allowlist into a sorted file for grep -Fx checks. Empty if missing. -allowlist_tmp="$(mktemp)" -trap 'rm -f "$allowlist_tmp" "$tracked_tmp" "$findings_tmp"' EXIT -if [ -f "$ALLOWLIST" ]; then - grep -v '^#' "$ALLOWLIST" 2>/dev/null | grep -v '^$' | sort -u > "$allowlist_tmp" || true -fi - -# Snapshot all tracked files once; lookups become a sorted-file grep. -tracked_tmp="$(mktemp)" -git ls-files | sort -u > "$tracked_tmp" - -# Findings accumulator: one line per broken ref. -# Format: :: -findings_tmp="$(mktemp)" -: > "$findings_tmp" - -# Iterate every tracked *.md. -total_refs=0 -broken_refs=0 - -while IFS= read -r src; do - # Skip files that no longer exist on disk (stale worktrees, deletions - # not yet committed). git ls-files reports tracked entries even if the - # working-tree copy is missing. - [ -f "$src" ] || continue - - # Skip generated files. - if head -3 "$src" 2>/dev/null | grep -qE 'AUTO-GENERATED| markers -# and flags any that are older than the threshold. -# -# Exit code: 0 = all fresh, 1 = stale docs found - -set -euo pipefail - -# Tier gate: freshness checks are Tier 2+ (skip for Tier 1 / partial adoption) -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -[ -f "$SCRIPT_DIR/_rebar-config.sh" ] && source "$SCRIPT_DIR/_rebar-config.sh" && _rebar_skip 2 && exit 0 - -MAX_AGE_DAYS="${1:-14}" -TODAY=$(date +%s) -stale=0 -total=0 -unmarked=0 - -while IFS= read -r file; do - # Skip template files — their freshness markers are placeholders that - # adopters update on copy. Holding them to the freshness threshold - # produces noise, not signal. - case "$file" in - ./templates/*) continue ;; - esac - - # Honor an explicit "freshness: archived" opt-out for historical - # retrospectives, decided design docs, and other intentionally-static - # content. The literal token is searched before the date pattern. - if grep -q 'freshness: archived' "$file" 2>/dev/null; then - continue - fi - - # Extract the currency date. TWO vocabularies are in the wild and both - # are honored: `freshness:` (the original) and `last-synced:` (used by - # QUICKCONTEXT and CONSUMES). Parsing only the first silently exempted - # the most-read doc in every rebar repo — rebar's own QUICKCONTEXT - # drifted 29 days while cold-start reported freshness all-green. - # See feedback/2026-08-02-freshness-check-parses-one-vocabulary-*.md. - # `freshness:` wins when a file carries both. - # - # `grep -oE` exits 1 when no marker is present (most files have none). - # Under `set -o pipefail` that aborts the whole pipeline, so disable - # pipefail just for this lookup. - set +o pipefail - marker="freshness" - date_str=$(grep -o 'freshness: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' "$file" 2>/dev/null | head -1 | sed 's/freshness: //') - if [ -z "$date_str" ]; then - marker="last-synced" - date_str=$(grep -o 'last-synced: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' "$file" 2>/dev/null | head -1 | sed 's/last-synced: //') - fi - set -o pipefail - - if [ -z "$date_str" ]; then - unmarked=$((unmarked + 1)) - continue - fi - - # Skip placeholder dates - case "$date_str" in - YYYY-MM-DD|0000-00-00) continue ;; - esac - - total=$((total + 1)) - - # Calculate age (portable: works on macOS and Linux) - if date --version >/dev/null 2>&1; then - # GNU date (Linux) - doc_epoch=$(date -d "$date_str" +%s 2>/dev/null || echo 0) - else - # BSD date (macOS) - doc_epoch=$(date -j -f "%Y-%m-%d" "$date_str" +%s 2>/dev/null || echo 0) - fi - - [ "$doc_epoch" -eq 0 ] && continue - - age_days=$(( (TODAY - doc_epoch) / 86400 )) - - if [ "$age_days" -gt "$MAX_AGE_DAYS" ]; then - echo "STALE ($age_days days): $file — $marker: $date_str" - stale=$((stale + 1)) - fi -done < <(find . -name "*.md" -not -path "./.git/*" -not -path "*/node_modules/*" -type f) - -echo "" -echo "Checked $total docs with currency markers (freshness:/last-synced:), $stale stale (>${MAX_AGE_DAYS} days)." -echo "$unmarked .md file(s) carry no currency marker at all — unchecked, not verified." - -if [ "$stale" -gt 0 ]; then - echo "" - echo "Stale docs may contain outdated status claims." - echo "Review and update the freshness date after verifying content." - exit 1 -fi - -exit 0 diff --git a/templates/project-bootstrap/scripts/check-ground-truth.sh b/templates/project-bootstrap/scripts/check-ground-truth.sh deleted file mode 100755 index 78d4abb..0000000 --- a/templates/project-bootstrap/scripts/check-ground-truth.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# check-ground-truth.sh — Verify METRICS file matches codebase reality. -# rebar-scripts: 2026.03.20 -# -# Computes project metrics from code and compares against claims in the -# METRICS file. Catches "silent success" drift where everything works but -# documented numbers describe a different reality. -# -# CUSTOMIZATION REQUIRED: Define your project's metrics in compute_metrics(). -# -# Usage: ./scripts/check-ground-truth.sh -# Exit: 0 = all claims match, 1 = drift detected - -set -euo pipefail - -# Tier gate: ground truth is Tier 3 only (skip for Tier 1-2) -SCRIPT_DIR_GT="$(cd "$(dirname "$0")" && pwd)" -[ -f "$SCRIPT_DIR_GT/_rebar-config.sh" ] && source "$SCRIPT_DIR_GT/_rebar-config.sh" && _rebar_skip 3 && exit 0 - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -# METRICS or METRICS.md — the template ships .md and `rebar audit` -# accepts both, so this checker must too. -METRICS_FILE="$REPO_ROOT/METRICS" -[ ! -f "$METRICS_FILE" ] && [ -f "$REPO_ROOT/METRICS.md" ] && METRICS_FILE="$REPO_ROOT/METRICS.md" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' -exit_code=0 - -# ── CUSTOMIZE: Define your project's metrics ── -# -# Each metric is a key=value pair echoed to stdout. -# Keys must match entries in the METRICS file. -# -# By convention, these locations are reliable and countable: -# tests/ — all test files live here -# architecture/ — all contracts live here -# src/ — source code with CONTRACT: headers -# -compute_metrics() { - # Examples (uncomment and adjust for your project): - # - # echo "unit_test_files=$(find tests/ -name '*.test.ts' -o -name '*.test.tsx' 2>/dev/null | wc -l | tr -d ' ')" - # echo "e2e_spec_files=$(find tests/e2e/ -name '*.spec.ts' 2>/dev/null | wc -l | tr -d ' ')" - # echo "contracts=$(ls architecture/CONTRACT-*.md 2>/dev/null | wc -l | tr -d ' ')" - # echo "api_route_modules=$(find src/routes/ -name '*.ts' -not -name 'index.ts' -not -name '*.test.ts' 2>/dev/null | wc -l | tr -d ' ')" - - : # no-op — remove this line when adding metrics -} - -# ── Verification engine (do not modify below this line) ── - -verify() { - if [ ! -f "$METRICS_FILE" ]; then - echo -e "${YELLOW}SKIP${NC}: No METRICS file found" - echo " Create a METRICS file with key = value pairs." - echo " See scripts/check-ground-truth.sh for examples." - return 0 - fi - - local computed - computed=$(compute_metrics) - - if [ -z "$computed" ]; then - echo "No metrics defined. Customize compute_metrics() in this script." - return 0 - fi - - while IFS='=' read -r key value; do - # Skip empty lines and comments - [ -z "$key" ] && continue - echo "$key" | grep -q '^[[:space:]]*#' && continue - - key=$(echo "$key" | xargs) - value=$(echo "$value" | xargs) - - # Look up key in METRICS file - local documented - documented=$(grep "^[[:space:]]*${key}[[:space:]]*=" "$METRICS_FILE" 2>/dev/null \ - | head -1 | cut -d'=' -f2 | xargs) || true - - if [ -z "$documented" ]; then - echo -e "${YELLOW}NEW${NC}: $key = $value (not in METRICS file)" - elif [ "$value" = "$documented" ]; then - echo -e "${GREEN}OK${NC}: $key = $value" - else - echo -e "${RED}DRIFT${NC}: $key — METRICS says $documented, code says $value" - exit_code=1 - fi - done <<< "$computed" -} - -echo "=== Ground Truth Verification ===" -echo "" -verify -echo "" - -if [ $exit_code -eq 0 ]; then - echo -e "${GREEN}All documented metrics match codebase reality${NC}" -else - echo -e "${RED}Metric drift detected — update METRICS to match reality${NC}" -fi - -exit $exit_code diff --git a/templates/project-bootstrap/scripts/check-jtbd-presence.sh b/templates/project-bootstrap/scripts/check-jtbd-presence.sh deleted file mode 100755 index 0425416..0000000 --- a/templates/project-bootstrap/scripts/check-jtbd-presence.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env bash -# check-jtbd-presence.sh — Enforce JTBD framing sections on contracts -# rebar-scripts: 2026.07.04 -# -# Usage: ./scripts/check-jtbd-presence.sh -# -# Every latest-version contract in architecture/ MUST have non-empty -# "## Why this exists", "## Who needs this", and "## Scenarios" sections — -# the JTBD framing required by CONTRACT-TEMPLATE.md. Catches the -# "interface description without motivation" anti-pattern: a contract that -# reads like a header file with no callers. -# -# Skipped: -# - Template / registry / companion files (CONTRACT-TEMPLATE.md, -# CONTRACT-SEAM-TEMPLATE.md, CONTRACT-REGISTRY*, CONTRACT-GAPS.md, -# *.impl.md) -# - Older versions of a contract ID — only the latest version on disk is -# held to the requirement -# - Contracts marked "SUPERSEDED BY:" — covers the window where the newer -# version lives on another branch or lands in a concurrent commit -# - SKIP_JTBD=1 skips the whole check -# -# Tier behavior: below Tier 2 = skip; Tier 2 = warn only (exit 0); -# Tier 3 = blocking (exit 1 on missing sections). -# -# Source: feedback/processed/2026-04-24-contract-discipline-and-jtbd-framing.md §E -# See: practices/spike-first-contracts.md for the JTBD framing rationale -# -# Bash 3.2 compatible (macOS default). -# -# Exit code: 0 = all present (or advisory tier), 1 = missing sections at Tier 3 - -set -uo pipefail - -# Honor the explicit skip flag (house style: ci-check.sh SKIP_* family) -if [ "${SKIP_JTBD:-0}" = "1" ]; then - echo "SKIP: check-jtbd-presence (SKIP_JTBD=1)" - exit 0 -fi - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -ARCH_DIR="$PROJECT_ROOT/architecture" - -# Tier gate: JTBD presence is Tier 2+ (warning) and blocks at Tier 3 -TIER=3 -if [ -f "$SCRIPT_DIR/_rebar-config.sh" ]; then - source "$SCRIPT_DIR/_rebar-config.sh" - _rebar_skip 2 && exit 0 - TIER="$(_rebar_tier)" -fi - -if [ ! -d "$ARCH_DIR" ]; then - echo "SKIP: no architecture/ directory found." - exit 0 -fi - -index_tmp="$(mktemp)" -latest_tmp="$(mktemp)" -trap 'rm -f "$index_tmp" "$latest_tmp"' EXIT - -# ─── Index contracts: ID, major, minor, filename ──────────────────────────── - -for contract in "$ARCH_DIR"/CONTRACT-*.md; do - [ -f "$contract" ] || continue - base="$(basename "$contract")" - - # Skip templates, registry files, gap tracker, and companions - case "$base" in - CONTRACT-TEMPLATE.md|CONTRACT-SEAM-TEMPLATE.md|CONTRACT-REGISTRY*|CONTRACT-GAPS.md) - continue ;; - *.impl.md) - continue ;; - esac - - # Parse ID + version from filename (same logic as compute-registry.sh) - stem="${base#CONTRACT-}" - stem="${stem%.md}" - if [[ "$stem" =~ ^(.+)\.([0-9]+)\.([0-9]+)$ ]]; then - id="${BASH_REMATCH[1]}"; major="${BASH_REMATCH[2]}"; minor="${BASH_REMATCH[3]}" - elif [[ "$stem" =~ ^(.+)\.([0-9]+)$ ]]; then - id="${BASH_REMATCH[1]}"; major="${BASH_REMATCH[2]}"; minor=0 - else - id="$stem"; major=1; minor=0 - fi - - printf '%s\t%s\t%s\t%s\n' "$id" "$major" "$minor" "$base" >> "$index_tmp" -done - -if [ ! -s "$index_tmp" ]; then - echo "OK: no contracts found in architecture/ — nothing to check." - exit 0 -fi - -# Keep only the latest version per contract ID (sort by ID, then version -# numerically; last row per ID wins). awk arrays keep this bash 3.2 safe — -# no associative arrays in the shell itself. -sort -t"$(printf '\t')" -k1,1 -k2,2n -k3,3n "$index_tmp" \ - | awk -F'\t' '{ latest[$1] = $4 } END { for (id in latest) print latest[id] }' \ - | sort > "$latest_tmp" - -# ─── Section presence ──────────────────────────────────────────────────────── - -# section_present -# Returns 0 when the section exists AND has non-comment, non-whitespace -# content before the next same-or-higher-level heading. Subsections -# (### Scenario 1 — ...) belong to the section and count as content. -# HTML comments (the template's inline guidance) don't count as content. -section_present() { - awk -v pat="$2" ' - BEGIN { insect = 0; incom = 0; content = 0 } - insect == 0 && $0 ~ pat { insect = 1; next } - insect == 1 && /^##?[[:space:]]/ { exit } - insect == 1 { - line = $0 - if (incom) { - if (sub(/^.*-->/, "", line)) incom = 0 - else next - } - gsub(//, "", line) - if (index(line, " - -Auto-generated by `scripts/check-version-bump.sh` (post-commit hook). -Owner runs `scripts/flush-notifications.sh` to deliver pending entries -via the `ask__featurerequest` gate. - -Each entry: contract id, old → new version, detected timestamp, -commit, status (pending | sent | dropped). `flush-notifications.sh` -auto-classifies severity (breaking | additive | patch) from the -semver delta unless --severity overrides. - -EOF -fi - -now=$(date -u +%Y-%m-%dT%H:%M:%SZ) -short_sha=$(git rev-parse --short HEAD) -appended=0 - -# IFS pipe-separated, parsed on the way in. -echo "$renames" | while IFS='|' read -r old new; do - old_iv=$(parse_contract_path "$old") - new_iv=$(parse_contract_path "$new") - old_id="${old_iv%%|*}" - old_ver="${old_iv#*|}" - new_id="${new_iv%%|*}" - new_ver="${new_iv#*|}" - # Skip if id changed (rename to a different contract, not a version bump) - [ "$old_id" = "$new_id" ] || continue - [ -z "$new_id" ] && continue - - cat >> "$OUTBOX" </dev/null | tr -d ' \n' || echo 0) -if [ "$pending" != "0" ] && [ "$pending" != "" ]; then - echo "rebar: contract version bump recorded → outbox has $pending pending notification(s); flush with scripts/flush-notifications.sh" >&2 -fi diff --git a/templates/project-bootstrap/scripts/compute-registry.sh b/templates/project-bootstrap/scripts/compute-registry.sh deleted file mode 100755 index a7d5d25..0000000 --- a/templates/project-bootstrap/scripts/compute-registry.sh +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env bash -# compute-registry.sh — Generate CONTRACT-REGISTRY.md from contract files on disk -# rebar-scripts: 2026.03.20 -# -# Usage: -# ./scripts/compute-registry.sh Generate architecture/CONTRACT-REGISTRY.md -# ./scripts/compute-registry.sh --check Exit 0 if current, 1 if stale -# ./scripts/compute-registry.sh --stdout Print to stdout instead of writing file -# -# The contract filesystem IS the registry. This script just renders it. -# -# Exit code: 0 = success/current, 1 = stale (--check mode) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -ARCH_DIR="$PROJECT_ROOT/architecture" -REGISTRY="$ARCH_DIR/CONTRACT-REGISTRY.md" -MODE="${1:-generate}" - -# ─── Helpers ──────────────────────────────────────────────────────────────── - -# Parse contract filename → ID + version (reused logic from steward.sh) -parse_contract_file() { - local filename - filename="$(basename "$1")" - - local stem="${filename#CONTRACT-}" - stem="${stem%.md}" - - if [[ "$stem" =~ ^(.+)\.([0-9]+)\.([0-9]+)$ ]]; then - CONTRACT_ID="${BASH_REMATCH[1]}" - CONTRACT_VERSION="${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" - elif [[ "$stem" =~ ^(.+)\.([0-9]+)$ ]]; then - CONTRACT_ID="${BASH_REMATCH[1]}" - CONTRACT_VERSION="${BASH_REMATCH[2]}.0" - else - CONTRACT_ID="$stem" - CONTRACT_VERSION="1.0" - fi -} - -# Extract status from contract file (looks for **Status:** or Status: line) -extract_status() { - local file="$1" - local status - status=$(grep -i '^\*\*Status:\*\*\|^Status:' "$file" 2>/dev/null | head -1 | sed 's/.*:\s*//' | tr -d '*' | xargs) - echo "${status:-draft}" -} - -# Extract purpose (first non-empty line after ## Purpose) -extract_purpose() { - local file="$1" - awk '/^## Purpose/{found=1; next} found && /^[^#]/ && NF{print; exit}' "$file" 2>/dev/null | head -c 80 -} - -# Classify by prefix -classify_prefix() { - local id="$1" - case "$id" in - S*) echo "Services" ;; - C*) echo "Components" ;; - I*) echo "Interfaces" ;; - P*) echo "Protocols" ;; - *) echo "Other" ;; - esac -} - -# Count implementing files for a contract. -# `grep` returns 1 when there are no matches; under `set -o pipefail` that -# would abort the whole script for any orphan contract. The greps in this -# pipeline legitimately return 1 on empty input, so we run with pipefail -# temporarily disabled. -# -# Two passes, deduplicated by file path: -# 1. Source files matching the standard extension whitelist -# 2. Files anywhere under `bin/` (CLI entry points often have no extension — -# `bin/ask`, `bin/ask-mcp-server`, `bin/rebar`) -count_implementations() { - local id="$1" - set +o pipefail - { - grep -rln "CONTRACT:${id}" "$PROJECT_ROOT" \ - --include='*.go' --include='*.ts' --include='*.tsx' --include='*.js' \ - --include='*.py' --include='*.rs' --include='*.java' --include='*.rb' \ - --include='*.jsx' --include='*.sh' 2>/dev/null - if [ -d "$PROJECT_ROOT/bin" ]; then - grep -rln "CONTRACT:${id}" "$PROJECT_ROOT/bin" 2>/dev/null - fi - } \ - | grep -v "node_modules\|vendor\|dist\|\.git\|architecture/" \ - | sort -u \ - | wc -l | tr -d ' ' - set -o pipefail -} - -# ─── Collect Contracts ────────────────────────────────────────────────────── - -declare -a services=() components=() interfaces=() protocols=() other=() - -for contract in "$ARCH_DIR"/CONTRACT-*.md; do - [ ! -f "$contract" ] && continue - - basename=$(basename "$contract") - - # Skip non-contract files - case "$basename" in - CONTRACT-TEMPLATE.md|CONTRACT-SEAM-TEMPLATE.md|CONTRACT-REGISTRY.md|CONTRACT-REGISTRY.template.md|CONTRACT-GAPS.md) - continue ;; - *.impl.md) - continue ;; - esac - - parse_contract_file "$contract" - local_status=$(extract_status "$contract") - local_purpose=$(extract_purpose "$contract") - impl_count=$(count_implementations "$CONTRACT_ID") - - entry="| $CONTRACT_ID | $CONTRACT_VERSION | $local_status | $impl_count | ${local_purpose:-—} |" - - category=$(classify_prefix "$CONTRACT_ID") - case "$category" in - Services) services+=("$entry") ;; - Components) components+=("$entry") ;; - Interfaces) interfaces+=("$entry") ;; - Protocols) protocols+=("$entry") ;; - *) other+=("$entry") ;; - esac -done - -# ─── Generate Output ──────────────────────────────────────────────────────── - -GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - -generate_registry() { - cat <
- - - -HEADER - - # bash 3.2-compatible: pass array entries through a function instead of - # using `local -n` namerefs (bash 4.3+). macOS ships bash 3.2 by default. - emit_category() { - local category="$1"; shift - [ $# -eq 0 ] && return - echo "## $category" - echo "" - echo "| ID | Version | Status | Impl Files | Purpose |" - echo "|----|---------|--------|------------|---------|" - local entry - for entry in "$@"; do - echo "$entry" - done - echo "" - } - - emit_category "Services" ${services[@]+"${services[@]}"} - emit_category "Components" ${components[@]+"${components[@]}"} - emit_category "Interfaces" ${interfaces[@]+"${interfaces[@]}"} - emit_category "Protocols" ${protocols[@]+"${protocols[@]}"} - emit_category "Other" ${other[@]+"${other[@]}"} - - # Contract files section (for tooling compatibility) - echo "## Contract Files" - echo "" - echo "" - for contract in "$ARCH_DIR"/CONTRACT-*.md; do - [ ! -f "$contract" ] && continue - basename=$(basename "$contract") - case "$basename" in - CONTRACT-TEMPLATE.md|CONTRACT-SEAM-TEMPLATE.md|CONTRACT-REGISTRY.md|CONTRACT-REGISTRY.template.md|CONTRACT-GAPS.md) - continue ;; - *.impl.md) - continue ;; - esac - echo "- $basename" - done - echo "" - - # ─── Contract Health: zombie + shadow detection ────────────────────────── - # zombie — contract file exists but no CONTRACT: headers in source - # (deletion candidate unless tracked in TODO.md) - # shadow — CONTRACT: reference in source but no architecture/.md - # file (typo, deleted contract, or not-yet-written spec) - - local zombies=() - local known_ids=" " # space-padded list for easy substring matching - for contract in "$ARCH_DIR"/CONTRACT-*.md; do - [ ! -f "$contract" ] && continue - basename=$(basename "$contract") - case "$basename" in - CONTRACT-TEMPLATE.md|CONTRACT-SEAM-TEMPLATE.md|CONTRACT-REGISTRY.md|CONTRACT-REGISTRY.template.md|CONTRACT-GAPS.md) - continue ;; - *.impl.md) - continue ;; - esac - - parse_contract_file "$contract" - known_ids="$known_ids$CONTRACT_ID " - - local count - count=$(count_implementations "$CONTRACT_ID") - if [ "$count" -eq 0 ]; then - if [ -f "$PROJECT_ROOT/TODO.md" ] && grep -q "$CONTRACT_ID" "$PROJECT_ROOT/TODO.md" 2>/dev/null; then - zombies+=("- **$CONTRACT_ID** — tracked in TODO.md") - else - zombies+=("- **$CONTRACT_ID** — ⚠ untracked, 0 implementing files") - fi - fi - done - - # Shadow detection: every CONTRACT:. reference in source, - # filtered to IDs not in known_ids. Same extension whitelist + bin/ - # second-pass as count_implementations() so we don't miss CLI scripts. - local shadows_tmp - shadows_tmp="$(mktemp)" - set +o pipefail - { - grep -rEhno 'CONTRACT:[A-Za-z0-9_-]+\.[0-9]+\.[0-9]+' "$PROJECT_ROOT" \ - --include='*.go' --include='*.ts' --include='*.tsx' --include='*.js' \ - --include='*.py' --include='*.rs' --include='*.java' --include='*.rb' \ - --include='*.jsx' --include='*.sh' 2>/dev/null - if [ -d "$PROJECT_ROOT/bin" ]; then - grep -rEhno 'CONTRACT:[A-Za-z0-9_-]+\.[0-9]+\.[0-9]+' "$PROJECT_ROOT/bin" 2>/dev/null - fi - } \ - | grep -v "node_modules\|vendor\|dist\|\.git\|architecture/" \ - | sed -E 's/^[^:]*:[^:]*://; s/^CONTRACT://; s/\.[0-9]+\.[0-9]+$//' \ - | sort -u > "$shadows_tmp" - set -o pipefail - - local shadows=() - while IFS= read -r ref_id; do - [ -z "$ref_id" ] && continue - case "$known_ids" in - *" $ref_id "*) ;; # known — not a shadow - *) shadows+=("- **$ref_id** — referenced in source, no architecture/CONTRACT-${ref_id}.*.md found") ;; - esac - done < "$shadows_tmp" - rm -f "$shadows_tmp" - - if [ ${#zombies[@]} -gt 0 ] || [ ${#shadows[@]} -gt 0 ]; then - echo "## Contract Health" - echo "" - - if [ ${#zombies[@]} -gt 0 ]; then - echo "### Zombies — contract files with no implementing code" - echo "" - echo "(Deletion candidates unless intentionally pre-impl. Add a TODO.md entry" - echo "to suppress this warning while implementation is in flight.)" - echo "" - for z in "${zombies[@]}"; do - echo "$z" - done - echo "" - fi - - if [ ${#shadows[@]} -gt 0 ]; then - echo "### Shadow refs — code references to nonexistent contracts" - echo "" - echo "(Either write the missing contract file or fix the typo in source.)" - echo "" - for s in "${shadows[@]}"; do - echo "$s" - done - echo "" - fi - fi -} - -# ─── Execute Mode ─────────────────────────────────────────────────────────── - -case "$MODE" in - --check) - if [ ! -f "$REGISTRY" ]; then - echo "FAIL: $REGISTRY does not exist. Run scripts/compute-registry.sh to generate." - exit 1 - fi - - # Generate to temp and compare - tmpfile=$(mktemp) - generate_registry > "$tmpfile" - - # Compare ignoring the timestamp line - if diff <(grep -v "^" - echo "" - echo "" - echo "## Summary" - echo "" - echo "| Metric | Value |" - echo "|--------|-------|" - echo "| Contracts | $total total ($draft draft, $active active, $testing testing, $impl_present impl-present) |" - echo "| Open Discoveries | $open_disc |" - echo "| Enforcement | $enf_pass/$enf_total passing |" - echo "" - - # Contract Status - echo "## Contract Status" - echo "" - echo "| Contract | Version | Lifecycle | Spec Gate | Impl Files | Test Files | Discoveries |" - echo "|----------|---------|-----------|-----------|------------|------------|-------------|" - - if [ "$total" -eq 0 ]; then - echo "| _(none)_ | | | | | | |" - else - echo "$report_json" | jq -r '.contracts[] | "| \(.contract_id) | \(.version) | \(.lifecycle) | \(.spec_gate.completeness) | \(.impl_gate.implementing_count) | \(.impl_gate.test_count) | \(.discoveries | length) |"' - fi - - echo "" - - # Action Items - echo "## Action Items" - echo "" - - echo "### Architect" - local arch_count - arch_count="$(echo "$report_json" | jq '.action_items.architect | length')" - if [ "$arch_count" -eq 0 ]; then - echo "- _(no items)_" - else - echo "$report_json" | jq -r '.action_items.architect[] | "- \(.)"' - fi - echo "" - - echo "### Engineering Lead" - local eng_count - eng_count="$(echo "$report_json" | jq '.action_items.englead | length')" - if [ "$eng_count" -eq 0 ]; then - echo "- _(no items)_" - else - echo "$report_json" | jq -r '.action_items.englead[] | "- \(.)"' - fi - echo "" - - echo "### Product" - local prod_count - prod_count="$(echo "$report_json" | jq '.action_items.product | length')" - if [ "$prod_count" -eq 0 ]; then - echo "- _(no items)_" - else - echo "$report_json" | jq -r '.action_items.product[] | "- \(.)"' - fi - echo "" - - echo "### Developer" - local dev_count - dev_count="$(echo "$report_json" | jq '.action_items.dev | length')" - if [ "$dev_count" -eq 0 ]; then - echo "- _(no items)_" - else - echo "$report_json" | jq -r '.action_items.dev[] | "- \(.)"' - fi - echo "" - - # Open Discoveries - echo "## Open Discoveries" - echo "" - echo "_(See TODO.md Discoveries section for details)_" - echo "" - echo "| Type | Contract | Description |" - echo "|------|----------|-------------|" - - local disc_count - disc_count="$(echo "$report_json" | jq '[.contracts[].discoveries[]] | length')" - if [ "$disc_count" -eq 0 ]; then - echo "| _(none)_ | | |" - else - echo "$report_json" | jq -r '.contracts[] as $c | $c.discoveries[] | "| \(.type) | \($c.contract_id) | \(.description) |"' - fi - - echo "" - - # Enforcement Results - echo "## Enforcement Results" - echo "" - echo "| Check | Result |" - echo "|-------|--------|" - - local check_names=( - "contract_headers:Contract Headers" - "contract_refs:Contract References" - "todo_tracking:TODO Tracking" - "doc_freshness:Doc Freshness" - "registry:Registry Consistency" - "ground_truth:Ground Truth" - "compliance:Rebar Compliance" - ) - - for entry in "${check_names[@]}"; do - local key="${entry%%:*}" - local label="${entry##*:}" - local result - result="$(echo "$report_json" | jq -r ".enforcement.${key} // \"_(not run)_\"")" - echo "| $label | $result |" - done - - echo "" - echo "---" - echo "" - echo "_Generated at ${GENERATED_AT} by \`scripts/steward.sh\`_" - } > "$output" -} - -# ─── Main ─────────────────────────────────────────────────────────────────── - -# Globals set by run_enforcement / generate_action_items -ENFORCEMENT_JSON='{}' -ENFORCEMENT_PASSING=0 -ENFORCEMENT_TOTAL=0 -ACTION_ARCHITECT='[]' -ACTION_ENGLEAD='[]' -ACTION_PRODUCT='[]' -ACTION_DEV='[]' - -main() { - local mode="${1:-full}" - local check_id="${2:-}" - - # Single contract check - if [ "$mode" = "--check" ]; then - if [ -z "$check_id" ]; then - echo "Usage: steward.sh --check " >&2 - exit 1 - fi - # Find contract file - local contract_file - contract_file="$(ls "$ARCH_DIR"/CONTRACT-"${check_id}"*.md 2>/dev/null | head -1 || true)" - if [ -z "$contract_file" ]; then - echo "Contract not found: $check_id" >&2 - echo "Valid IDs:" >&2 - ls "$ARCH_DIR"/CONTRACT-*.md 2>/dev/null \ - | sed -e 's|.*/CONTRACT-||' -e 's|\.[0-9][0-9.]*\.md$||' \ - | grep -v -E 'TEMPLATE|REGISTRY|GAPS|\.impl$' | sort -u | sed 's/^/ /' >&2 - exit 1 - fi - scan_contract "$contract_file" - cat "$STATE_DIR/${check_id}."*.json 2>/dev/null | jq . - exit 0 - fi - - # Full scan: collect all contracts - local all_contracts='[]' - local contract_files=() - - while IFS= read -r f; do - contract_files+=("$f") - done < <(ls "$ARCH_DIR"/CONTRACT-*.md 2>/dev/null | grep -v TEMPLATE | grep -v REGISTRY || true) - - for contract_file in "${contract_files[@]+"${contract_files[@]}"}"; do - local state_file - state_file="$(scan_contract "$contract_file")" - local contract_json - contract_json="$(cat "$state_file")" - all_contracts="$(echo "$all_contracts" | jq --argjson c "$contract_json" '. + [$c]')" - done - - # Run enforcement checks - run_enforcement - - # Generate action items - generate_action_items "$all_contracts" - - # Build aggregate report - local report - report="$(build_aggregate "$all_contracts")" - - # Write aggregate JSON - echo "$report" | jq . > "$STATE_DIR/steward-report.json" - - case "$mode" in - --json) - echo "$report" | jq . - ;; - --summary) - local total draft active testing impl_present open_disc enf_pass enf_total - total="$(echo "$report" | jq '.summary.contracts.total')" - draft="$(echo "$report" | jq '.summary.contracts.draft')" - active="$(echo "$report" | jq '.summary.contracts.active')" - testing="$(echo "$report" | jq '.summary.contracts.testing')" - impl_present="$(echo "$report" | jq '.summary.contracts.impl_present')" - open_disc="$(echo "$report" | jq '.summary.open_discoveries')" - enf_pass="$(echo "$report" | jq '.summary.enforcement.passing')" - enf_total="$(echo "$report" | jq '.summary.enforcement.total')" - echo "Steward: ${total} contracts (${draft}d/${active}a/${testing}t/${impl_present}ip), ${open_disc} discoveries, ${enf_pass}/${enf_total} enforcement passing" - ;; - full|*) - # Generate markdown report - generate_markdown "$report" - echo "Steward scan complete." - echo " JSON: $STATE_DIR/steward-report.json" - echo " Markdown: $PROJECT_ROOT/STEWARD_REPORT.md" - echo "" - # Print summary line - local total - total="$(echo "$report" | jq '.summary.contracts.total')" - local enf_pass enf_total - enf_pass="$(echo "$report" | jq '.summary.enforcement.passing')" - enf_total="$(echo "$report" | jq '.summary.enforcement.total')" - local open_disc - open_disc="$(echo "$report" | jq '.summary.open_discoveries')" - echo " ${total} contracts, ${open_disc} discoveries, ${enf_pass}/${enf_total} enforcement passing" - ;; - esac -} - -main "$@" From 8c66877dc28bc85b139eb04e0622c72c97c11971 Mon Sep 17 00:00:00 2001 From: Timothy Tschampel Date: Wed, 5 Aug 2026 14:41:55 -0700 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20adoption=20scripts=20are=20thin=20w?= =?UTF-8?q?rappers=20=E2=80=94=20call=20rebar=20CLI,=20no=20embedded=20log?= =?UTF-8?q?ic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pre-commit.sh calls `rebar check --pre-commit`. ci-check.sh calls `rebar audit`. All enforcement logic lives in the rebar binary (sourced from the rebar installation), not in scripts copied into adopting repos. Adopting repos no longer carry embedded copies of rebar's check internals; upgrading rebar upgrades enforcement automatically without any script sync. Removed 23 scripts from templates/project-bootstrap/scripts/ that were duplicating logic from the rebar install. The two remaining entry-point scripts (pre-commit.sh, ci-check.sh) are the only surface adopting repos need to carry. --- templates/project-bootstrap/scripts/README.md | 98 +++++--------- .../project-bootstrap/scripts/ci-check.sh | 120 +++--------------- .../project-bootstrap/scripts/pre-commit.sh | 62 ++------- 3 files changed, 64 insertions(+), 216 deletions(-) diff --git a/templates/project-bootstrap/scripts/README.md b/templates/project-bootstrap/scripts/README.md index 137ed83..fa1a9a9 100644 --- a/templates/project-bootstrap/scripts/README.md +++ b/templates/project-bootstrap/scripts/README.md @@ -1,81 +1,49 @@ -# Scripts +# scripts/ -Enforcement scripts and quality scanning for the contract-driven methodology. +Thin wrappers that delegate enforcement to the **rebar CLI**. The logic lives +in the rebar installation — adopting repos carry only these entry points, not +copies of rebar's internals. -See the [root README](../README.md) for how scripts fit into the overall system. +## Files -## Quality Scanner +| File | Purpose | +|------|---------| +| `pre-commit.sh` | Git pre-commit hook → `rebar check --pre-commit` | +| `ci-check.sh` | CI entrypoint → `rebar audit` | -| Script | What It Does | Invocation | -|--------|-------------|-----------| -| `steward.sh` | Full quality scan — contract lifecycle, enforcement, discoveries | `ask steward` or `./scripts/steward.sh` | -| `steward.sh --json` | Aggregate JSON to stdout | `ask steward json` | -| `steward.sh --summary` | One-line health summary | `ask steward summary` | -| `steward.sh --check ` | Single contract scan | `ask steward check C1` | +## Wire the pre-commit hook -Output goes to `architecture/.state/` (JSON) and `STEWARD_REPORT.md` (human-readable). - -## Enforcement Checks - -Each script is standalone, runs in <5 seconds, and exits 0 (pass) or 1 (fail). +```bash +ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit +``` -| Script | What It Checks | -|--------|---------------| -| `check-contract-headers.sh` | Every source file has a `CONTRACT:` header | -| `check-contract-refs.sh` | Every `CONTRACT:` ref points to a real contract file | -| `check-todos.sh` | No untracked `TODO:` comments (two-tag system) | -| `check-freshness.sh` | Doc freshness dates aren't stale (>14 days) | -| `check-registry.sh` | Contract registry matches actual files | -| `check-ground-truth.sh` | METRICS file matches codebase reality | +Or use `rebar init` — it wires the hook automatically. -## Composite Runners +## How it works -| Script | When to Run | -|--------|-------------| -| `ci-check.sh` | CI pipeline — runs all checks including steward | -| `pre-commit.sh` | Git hook — fast checks (TODOs + contract refs) | +`pre-commit.sh` and `ci-check.sh` call the `rebar` binary in `$PATH`. +All enforcement logic (contract reference checks, TODO tracking, freshness, +registry consistency, steward scan, etc.) lives inside the rebar binary — +sourced from the rebar installation, not from this directory. -## Installation +This means: +- Upgrading rebar upgrades enforcement automatically — no scripts to sync. +- Adopting repos stay clean: only entry points here, no duplicated logic. +- CI and local pre-commit use the same `rebar` command, same logic, same output. -```bash -# Pre-commit hook (pick one) -cp scripts/pre-commit.sh .git/hooks/pre-commit # copy -ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit # symlink +## Extending -# Make all scripts executable -chmod +x scripts/*.sh +Add project-specific checks by creating additional scripts alongside these +that call `rebar check ` or `rebar audit --only `. Project-specific +logic that cannot live in rebar belongs in a separate tool, not in copies of +rebar's internals. -# CI pipeline (GitHub Actions example) -# - run: ./scripts/ci-check.sh --strict -``` +## Suppress individual checks in CI -## Configuration +`rebar audit` respects `SKIP_*` env vars: -| Environment Variable | Default | Purpose | -|---------------------|---------|---------| -| `CONTRACT_EXTENSIONS` | `.go .ts .tsx .js .jsx .py .rs` | File extensions to scan | -| `SKIP_CONTRACT_HEADERS` | `0` | Skip header check | -| `SKIP_CONTRACT_REFS` | `0` | Skip ref check | -| `SKIP_TODOS` | `0` | Skip TODO check | -| `SKIP_FRESHNESS` | `0` | Skip freshness check | -| `SKIP_REGISTRY` | `0` | Skip registry check | -| `SKIP_GROUND_TRUTH` | `0` | Skip ground truth check | -| `SKIP_STEWARD` | `0` | Skip steward scan | - -## Automation Hierarchy - -``` -Pre-commit (fast, <5s) CI (thorough, <30s) Full scan (comprehensive) -├── check-todos.sh ├── all pre-commit checks ├── all CI checks -└── check-contract-refs.sh ├── check-contract-headers ├── steward.sh (lifecycle, - ├── check-freshness discoveries, action items) - ├── check-registry └── STEWARD_REPORT.md - ├── check-ground-truth - └── steward.sh +```bash +SKIP_FRESHNESS=1 ./scripts/ci-check.sh ``` -## Dependencies - -- **bash** — all scripts are bash -- **jq** — required by steward.sh and ground truth verification -- **grep, find** — standard Unix tools +See `rebar audit --help` for the full list. diff --git a/templates/project-bootstrap/scripts/ci-check.sh b/templates/project-bootstrap/scripts/ci-check.sh index ad9a6c0..6cfe041 100755 --- a/templates/project-bootstrap/scripts/ci-check.sh +++ b/templates/project-bootstrap/scripts/ci-check.sh @@ -1,110 +1,30 @@ #!/usr/bin/env bash -# ci-check.sh — Atomic CI entrypoint that runs all contract/doc checks -# rebar-scripts: 2026.03.20 +# ci-check.sh — CI entrypoint: delegates to the rebar CLI. # -# Usage: ./scripts/ci-check.sh [--strict] +# Usage: +# ./scripts/ci-check.sh +# ./scripts/ci-check.sh --no-fail # report but always exit 0 # -# Runs each check script and reports a summary. In strict mode (default), -# any failure fails the CI job. In non-strict mode, failures are warnings. +# The rebar CLI runs the full enforcement and audit suite. Logic lives in +# the rebar install — this script is intentionally a thin wrapper so +# adopting repos do not carry a copy of rebar's internals. # -# Individual checks can be skipped with environment variables: -# SKIP_CONTRACT_HEADERS=1 — skip contract header check -# SKIP_CONTRACT_REFS=1 — skip contract reference check -# SKIP_TODOS=1 — skip TODO tracking check -# SKIP_FRESHNESS=1 — skip freshness check -# SKIP_REGISTRY=1 — skip registry consistency check -# SKIP_JTBD=1 — skip JTBD-presence contract check -# SKIP_PREFIX_UNIQUENESS=1 — skip contract prefix-number uniqueness check -# SKIP_GROUND_TRUTH=1 — skip ground truth metric verification -# SKIP_COMPLIANCE=1 — skip rebar compliance check -# SKIP_STEWARD=1 — skip steward health scan -# SKIP_DOC_REFS=1 — skip cross-doc reference check -# SKIP_DECAY_PATTERNS=1 — skip soft-hardening decay pattern check -# SKIP_BOOTSTRAP_SYNC=1 — skip templates/project-bootstrap/scripts drift check -# SKIP_FIX_COMMIT=1 — skip Gate G (HEAD's fix:/regression: must have Reproduced on:) -# SKIP_BYPASS_FLAGS=1 — skip Gate I (HEAD's bypass mentions must have Bypass tickets:) -# -# Exit code: 0 = all pass, 1 = failures in strict mode - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -STRICT="${1:---strict}" - -passed=0 -failed=0 -skipped=0 -results=() - -run_check() { - local name="$1" - local skip_var="$2" - local script="$3" - shift 3 - - # Check skip flag - if [ "${!skip_var:-0}" = "1" ]; then - results+=("SKIP $name") - skipped=$((skipped + 1)) - return - fi +# Suppress individual checks via env vars understood by `rebar audit`: +# SKIP_CONTRACT_HEADERS=1 SKIP_TODOS=1 SKIP_FRESHNESS=1 etc. - # Check script exists - if [ ! -x "$script" ]; then - results+=("SKIP $name (script not found or not executable)") - skipped=$((skipped + 1)) - return - fi +set -euo pipefail - echo "" - echo "━━━ $name ━━━" - if "$script" "$@"; then - results+=("PASS $name") - passed=$((passed + 1)) - else - results+=("FAIL $name") - failed=$((failed + 1)) - fi -} +NO_FAIL=0 +[[ "${1:-}" == "--no-fail" ]] && NO_FAIL=1 -echo "Running contract and documentation checks..." - -run_check "Contract Headers" SKIP_CONTRACT_HEADERS "$SCRIPT_DIR/check-contract-headers.sh" -run_check "Contract References" SKIP_CONTRACT_REFS "$SCRIPT_DIR/check-contract-refs.sh" -run_check "Doc References" SKIP_DOC_REFS "$SCRIPT_DIR/check-doc-refs.sh" -run_check "TODO Tracking" SKIP_TODOS "$SCRIPT_DIR/check-todos.sh" -run_check "Doc Freshness" SKIP_FRESHNESS "$SCRIPT_DIR/check-freshness.sh" -run_check "Registry Consistency" SKIP_REGISTRY "$SCRIPT_DIR/compute-registry.sh" --check -run_check "JTBD Presence" SKIP_JTBD "$SCRIPT_DIR/check-jtbd-presence.sh" -run_check "Prefix Uniqueness" SKIP_PREFIX_UNIQUENESS "$SCRIPT_DIR/check-prefix-uniqueness.sh" -run_check "Ground Truth" SKIP_GROUND_TRUTH "$SCRIPT_DIR/check-ground-truth.sh" -run_check "Rebar Compliance" SKIP_COMPLIANCE "$SCRIPT_DIR/check-compliance.sh" -run_check "Decay Patterns" SKIP_DECAY_PATTERNS "$SCRIPT_DIR/check-decay-patterns.sh" -run_check "Bootstrap Sync" SKIP_BOOTSTRAP_SYNC "$SCRIPT_DIR/sync-bootstrap.sh" --check -run_check "Fix Commit Gate" SKIP_FIX_COMMIT "$SCRIPT_DIR/check-fix-commit.sh" -run_check "Bypass Flags Gate" SKIP_BYPASS_FLAGS "$SCRIPT_DIR/check-bypass-flags.sh" -run_check "Steward" SKIP_STEWARD "$SCRIPT_DIR/steward.sh" - -# NOTE: Tag-to-CI coverage check (Node.js, project-specific) lives at -# templates/scripts/check-tag-ci-coverage.mjs. Adopters with Playwright -# or @-tagged spec files should copy it into their own scripts/ and wire -# it into their CI workflow directly, not via this universal ci-check. - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "SUMMARY" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -for result in "${results[@]}"; do - echo " $result" -done -echo "" -echo " $passed passed, $failed failed, $skipped skipped" - -if [ "$failed" -gt 0 ] && [ "$STRICT" = "--strict" ]; then - echo "" - echo "CI check failed. Fix the issues above or skip individual checks" - echo "with environment variables (e.g., SKIP_FRESHNESS=1)." +if ! command -v rebar &>/dev/null; then + echo "ci-check: rebar not found in PATH" >&2 + echo "Install rebar: https://github.com/willackerly/rebar#installation" >&2 exit 1 fi -exit 0 +if [[ "$NO_FAIL" -eq 1 ]]; then + rebar audit || true +else + exec rebar audit +fi diff --git a/templates/project-bootstrap/scripts/pre-commit.sh b/templates/project-bootstrap/scripts/pre-commit.sh index 2c83761..a338127 100755 --- a/templates/project-bootstrap/scripts/pre-commit.sh +++ b/templates/project-bootstrap/scripts/pre-commit.sh @@ -1,61 +1,21 @@ #!/usr/bin/env bash -# pre-commit.sh — Pre-commit hook that runs fast contract/TODO checks -# rebar-scripts: 2026.03.20 +# pre-commit.sh — Pre-commit hook: delegates to the rebar CLI. # -# Install: -# cp scripts/pre-commit.sh .git/hooks/pre-commit -# chmod +x .git/hooks/pre-commit -# -# Or symlink: +# Install as a git hook: # ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit # -# This runs only the fast checks suitable for pre-commit (<5s total). -# The full suite (including freshness, registry) runs in CI via ci-check.sh. +# The rebar CLI runs the fast enforcement checks appropriate for pre-commit. +# Logic lives in the rebar install — this script is intentionally a thin +# wrapper so adopting repos do not carry a copy of rebar's internals. # -# Skip with: git commit --no-verify (use sparingly) - -set -uo pipefail +# Skip (use sparingly): git commit --no-verify -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +set -euo pipefail -# If installed as a git hook, scripts are at ../../scripts/ -# If run directly, they're in the same directory -if [ -f "$SCRIPT_DIR/check-todos.sh" ]; then - BASE="$SCRIPT_DIR" -elif [ -f "$SCRIPT_DIR/../../scripts/check-todos.sh" ]; then - BASE="$SCRIPT_DIR/../../scripts" -else - echo "Warning: Cannot find check scripts. Skipping pre-commit checks." +if ! command -v rebar &>/dev/null; then + echo "pre-commit: rebar not found in PATH — skipping checks" >&2 + echo "Install rebar: https://github.com/willackerly/rebar#installation" >&2 exit 0 fi -failed=0 - -echo "Pre-commit: checking TODO tracking..." -if [ -x "$BASE/check-todos.sh" ]; then - "$BASE/check-todos.sh" || failed=$((failed + 1)) -fi - -echo "" -echo "Pre-commit: checking contract references..." -if [ -x "$BASE/check-contract-refs.sh" ]; then - "$BASE/check-contract-refs.sh" || failed=$((failed + 1)) -fi - -# Optional: verify numeric claims haven't drifted (fast — just find/grep/wc) -if [ -x "$BASE/check-ground-truth.sh" ]; then - echo "" - echo "Pre-commit: checking ground truth metrics..." - "$BASE/check-ground-truth.sh" || failed=$((failed + 1)) -fi - -if [ "$failed" -gt 0 ]; then - echo "" - echo "Pre-commit checks failed. Fix the issues above before committing." - echo "To skip (not recommended): git commit --no-verify" - exit 1 -fi - -echo "" -echo "Pre-commit checks passed." -exit 0 +exec rebar check --pre-commit From b9ea3ae297dfbcb33ab4d9062c4acd59b9eb00e1 Mon Sep 17 00:00:00 2001 From: Timothy Tschampel Date: Wed, 5 Aug 2026 14:41:55 -0700 Subject: [PATCH 3/5] feat: contract_namespace in .rebarrc + namespace-aware steward scan config.go: add ContractNamespace field, read from contract_namespace key in .rebarrc. Existing repos without the key are unaffected (zero value). steward.sh: replace literal CONTRACT: grep with an extended-regex pattern that matches both legacy (CONTRACT:.) and namespaced (CONTRACT::.) references. Repos in transition between forms are scanned correctly without any changes to source files. .rebarrc template: add contract_namespace as a commented-out example with an explanation of the legacy vs namespaced forms and when to use it. No migration required. Both reference forms remain valid indefinitely. --- cli/internal/config/config.go | 49 ++++++++++++++++++---------- scripts/steward.sh | 8 +++-- templates/project-bootstrap/.rebarrc | 14 ++++++-- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 279a529..a352889 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -10,13 +10,14 @@ import ( ) type Config struct { - RepoRoot string - RebarDir string // .rebar/ - Tier int // 1, 2, or 3 - Version string // from .rebar-version - ScriptsDir string // scripts/ - AgentsDir string // agents/ - BinDir string // bin/ + RepoRoot string + RebarDir string // .rebar/ + Tier int // 1, 2, or 3 + Version string // from .rebar-version + ScriptsDir string // scripts/ + AgentsDir string // agents/ + BinDir string // bin/ + ContractNamespace string // contract_namespace from .rebarrc (e.g. github.com/org/repo) } // Load reads configuration from .rebarrc and .rebar-version, respecting @@ -39,9 +40,12 @@ func Load(repoRoot string) (*Config, error) { } } else { // Read from .rebarrc - tier, err := readRebarRC(filepath.Join(repoRoot, ".rebarrc")) - if err == nil && tier >= 1 && tier <= 3 { - c.Tier = tier + rc, err := readRebarRC(filepath.Join(repoRoot, ".rebarrc")) + if err == nil { + if rc.tier >= 1 && rc.tier <= 3 { + c.Tier = rc.tier + } + c.ContractNamespace = rc.namespace } } @@ -81,15 +85,20 @@ func FindRepoRoot(dir string) (string, error) { } } -// readRebarRC parses a .rebarrc file for the tier setting. -// Format: key = value lines, comments with #. -func readRebarRC(path string) (int, error) { +type rebarRC struct { + tier int + namespace string +} + +// readRebarRC parses a .rebarrc file. Format: key = value lines, # comments. +func readRebarRC(path string) (rebarRC, error) { f, err := os.Open(path) if err != nil { - return 0, err + return rebarRC{}, err } defer f.Close() + var rc rebarRC scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -102,11 +111,17 @@ func readRebarRC(path string) (int, error) { } key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) - if strings.EqualFold(key, "tier") || strings.EqualFold(key, "rebar_tier") { - return strconv.Atoi(val) + switch { + case strings.EqualFold(key, "tier") || strings.EqualFold(key, "rebar_tier"): + rc.tier, _ = strconv.Atoi(val) + case strings.EqualFold(key, "contract_namespace"): + rc.namespace = val } } - return 0, fmt.Errorf("tier not found in .rebarrc") + if rc.tier == 0 { + return rc, fmt.Errorf("tier not found in .rebarrc") + } + return rc, nil } // EnsureRebarDir creates the .rebar/ directory structure. diff --git a/scripts/steward.sh b/scripts/steward.sh index eef3b50..388ed67 100755 --- a/scripts/steward.sh +++ b/scripts/steward.sh @@ -85,15 +85,19 @@ scan_contract() { completeness="fail" fi - # Implementing files: grep for CONTRACT: across the project + # Implementing files: grep for CONTRACT: across the project. + # Matches both legacy (CONTRACT:.) and namespaced + # (CONTRACT::.) references so repos in transition + # are scanned correctly regardless of which form they use. local impl_files=() local test_files=() + local impl_pattern="CONTRACT:([a-zA-Z0-9][a-zA-Z0-9_./-]+:)?${id}\\." while IFS= read -r line; do local filepath filepath="$(echo "$line" | cut -d: -f1)" impl_files+=("$filepath") - done < <(grep -rn "CONTRACT:${id}" "$PROJECT_ROOT" \ + done < <(grep -rEn "$impl_pattern" "$PROJECT_ROOT" \ --include='*.go' --include='*.ts' --include='*.js' --include='*.py' \ --include='*.rs' --include='*.java' --include='*.rb' --include='*.c' \ --include='*.cpp' --include='*.h' --include='*.cs' --include='*.swift' \ diff --git a/templates/project-bootstrap/.rebarrc b/templates/project-bootstrap/.rebarrc index fb23d67..56386cf 100644 --- a/templates/project-bootstrap/.rebarrc +++ b/templates/project-bootstrap/.rebarrc @@ -6,8 +6,18 @@ # 3 = Enforced — + ground truth, strict steward (department / production) tier = 1 -# (legacy alias, kept for older scripts that read REBAR_TIER) -REBAR_TIER=1 + +# Contract namespace — used to scope CONTRACT: references to this repo. +# Format: host/org/repo (Go module style, no trailing slash). +# Example: contract_namespace = github.com/myorg/myrepo +# +# When set, source files may reference contracts as either: +# CONTRACT:.. (legacy / local) +# CONTRACT::.. (namespaced / cross-repo) +# Both forms are recognised by rebar's enforcement and steward scan. +# +# Leave unset for single-repo projects or until you need cross-repo references. +# contract_namespace = github.com/myorg/myrepo # Version compatibility REBAR_VERSION=3.0.0-beta From eab6c5654fa74a841d081c7b7ba24005ff6cc1a5 Mon Sep 17 00:00:00 2001 From: Timothy Tschampel Date: Wed, 5 Aug 2026 14:47:22 -0700 Subject: [PATCH 4/5] feat: add namespace to CONTRACT-TEMPLATE title, refs, and grep patterns Both architecture/CONTRACT-TEMPLATE.md and templates/project-bootstrap/architecture/CONTRACT-TEMPLATE.md updated: - Title: CONTRACT-{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} - Naming comment: explains filename (unnamespaced) vs title vs in-source reference, and when to use legacy vs namespaced form - VERSIONING comment: superseded-by/supersedes use namespaced form - Dependencies: example CONTRACT: ref uses {NAMESPACE}:{NAME} - Implementing Files: adds in-source header comment example; grep uses extended-regex pattern matching both legacy and namespaced forms - Retirement: predecessor reference uses NAME + namespace-aware regex - Companion File: filename example drops legacy C1-prefix --- architecture/CONTRACT-TEMPLATE.md | 37 +++++++++++++------ .../architecture/CONTRACT-TEMPLATE.md | 37 +++++++++++++------ 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/architecture/CONTRACT-TEMPLATE.md b/architecture/CONTRACT-TEMPLATE.md index a37674a..fa7fc59 100644 --- a/architecture/CONTRACT-TEMPLATE.md +++ b/architecture/CONTRACT-TEMPLATE.md @@ -1,13 +1,25 @@ -# CONTRACT-{NAME}.{MAJOR}.{MINOR} +# CONTRACT-{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} + Remove these HTML comments when done. + + Filename: CONTRACT-{NAME}.{MAJOR}.{MINOR}.md ← filenames stay unnamespaced + Title: # CONTRACT-{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} + In-source: CONTRACT:{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} + + {NAMESPACE} = your repo's namespace from .rebarrc contract_namespace + (e.g. github.com/myorg/myrepo) + {NAME} = contract ID (e.g. AUTH, BLOBSTORE, KEY-EXCHANGE) + + Legacy form CONTRACT:{NAME}.{v} is still valid — both are recognised + by rebar's enforcement and steward scan. Use namespaced form when this + contract may be referenced from other repos. --> **Version:** {MAJOR}.{MINOR} @@ -112,7 +124,7 @@ type BlobStore interface { -- Depends on: `CONTRACT:I2-KEY-EXCHANGE.1.0` for encryption keys +- Depends on: `CONTRACT:{NAMESPACE}:KEY-EXCHANGE.1.0` for encryption keys - Configuration: `BLOBSTORE_PATH` environment variable - External: none (self-contained) @@ -142,15 +154,18 @@ type BlobStore interface { OPTIONAL otherwise. Without a deadline, superseded contracts accumulate indefinite-state lag (see filedag's C9-ABAC retirement-lag finding). --> -- **Predecessor:** `CONTRACT-.` — retirement criterion: `grep -rn ""` returns zero +- **Predecessor:** `CONTRACT-.` — retirement criterion: `grep -rEn "CONTRACT:([^:]+:)?\."` returns zero - **Migration deadline:** YYYY-MM-DD or named phase boundary - **Migration owner:** [team or person responsible for the cutover] ## Implementing Files - `internal/blobstore/file.go` — file-backed implementation @@ -203,8 +218,8 @@ type BlobStore interface { the companion without bumping the contract version. The companion lives alongside the contract in architecture/: - architecture/CONTRACT-C1-BLOBSTORE.2.1.md ← the contract - architecture/CONTRACT-C1-BLOBSTORE.impl.md ← the companion + architecture/CONTRACT-BLOBSTORE.2.1.md ← the contract (filename unnamespaced) + architecture/CONTRACT-BLOBSTORE.impl.md ← the companion --> ## Change History diff --git a/templates/project-bootstrap/architecture/CONTRACT-TEMPLATE.md b/templates/project-bootstrap/architecture/CONTRACT-TEMPLATE.md index a37674a..fa7fc59 100644 --- a/templates/project-bootstrap/architecture/CONTRACT-TEMPLATE.md +++ b/templates/project-bootstrap/architecture/CONTRACT-TEMPLATE.md @@ -1,13 +1,25 @@ -# CONTRACT-{NAME}.{MAJOR}.{MINOR} +# CONTRACT-{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} + Remove these HTML comments when done. + + Filename: CONTRACT-{NAME}.{MAJOR}.{MINOR}.md ← filenames stay unnamespaced + Title: # CONTRACT-{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} + In-source: CONTRACT:{NAMESPACE}:{NAME}.{MAJOR}.{MINOR} + + {NAMESPACE} = your repo's namespace from .rebarrc contract_namespace + (e.g. github.com/myorg/myrepo) + {NAME} = contract ID (e.g. AUTH, BLOBSTORE, KEY-EXCHANGE) + + Legacy form CONTRACT:{NAME}.{v} is still valid — both are recognised + by rebar's enforcement and steward scan. Use namespaced form when this + contract may be referenced from other repos. --> **Version:** {MAJOR}.{MINOR} @@ -112,7 +124,7 @@ type BlobStore interface { -- Depends on: `CONTRACT:I2-KEY-EXCHANGE.1.0` for encryption keys +- Depends on: `CONTRACT:{NAMESPACE}:KEY-EXCHANGE.1.0` for encryption keys - Configuration: `BLOBSTORE_PATH` environment variable - External: none (self-contained) @@ -142,15 +154,18 @@ type BlobStore interface { OPTIONAL otherwise. Without a deadline, superseded contracts accumulate indefinite-state lag (see filedag's C9-ABAC retirement-lag finding). --> -- **Predecessor:** `CONTRACT-.` — retirement criterion: `grep -rn ""` returns zero +- **Predecessor:** `CONTRACT-.` — retirement criterion: `grep -rEn "CONTRACT:([^:]+:)?\."` returns zero - **Migration deadline:** YYYY-MM-DD or named phase boundary - **Migration owner:** [team or person responsible for the cutover] ## Implementing Files - `internal/blobstore/file.go` — file-backed implementation @@ -203,8 +218,8 @@ type BlobStore interface { the companion without bumping the contract version. The companion lives alongside the contract in architecture/: - architecture/CONTRACT-C1-BLOBSTORE.2.1.md ← the contract - architecture/CONTRACT-C1-BLOBSTORE.impl.md ← the companion + architecture/CONTRACT-BLOBSTORE.2.1.md ← the contract (filename unnamespaced) + architecture/CONTRACT-BLOBSTORE.impl.md ← the companion --> ## Change History From 7b413a26a204dfb9ae577d9260f76359546bf233 Mon Sep 17 00:00:00 2001 From: Timothy Tschampel Date: Wed, 5 Aug 2026 15:48:20 -0700 Subject: [PATCH 5/5] fix: pre-commit runs from rebar install, not project scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check --pre-commit now resolves the rebar installation via findRebarRoot() and runs check-todos.sh + check-contract-refs.sh from there directly. Project scripts/pre-commit.sh is a 7-line entry point only. This eliminates: - Circular chain: pre-commit.sh → rebar check → pre-commit.sh → ∞ - Enforcement logic in adoption repos (only entry point lives there) - Stale logic when rebar upgrades (rebar home scripts update automatically) Also fixes: - setup-rebar.sh: REBAR_REPO env now used as clone URL in source builds - init.go: findRebarRoot() marker changed to setup-rebar.sh (steward.sh was removed from templates/project-bootstrap/scripts/ in thin-scripts refactor, breaking framework detection for source installs) - init.go: bootstrapV2Files() log message reflects thin-scripts count --- cli/cmd/check.go | 64 +++++++++++++++++-- cli/cmd/init.go | 6 +- setup-rebar.sh | 6 +- .../project-bootstrap/scripts/ci-check.sh | 2 +- .../project-bootstrap/scripts/pre-commit.sh | 21 ++---- 5 files changed, 70 insertions(+), 29 deletions(-) diff --git a/cli/cmd/check.go b/cli/cmd/check.go index 6dd751f..84c1c50 100644 --- a/cli/cmd/check.go +++ b/cli/cmd/check.go @@ -3,37 +3,89 @@ package cmd import ( "fmt" "os" + "path/filepath" "github.com/spf13/cobra" "github.com/willackerly/rebar/cli/internal/scripts" ) var checkStrict bool +var checkPreCommit bool var checkCmd = &cobra.Command{ Use: "check", - Short: "Run all enforcement checks", - Long: `Runs steward and CI checks via scripts/ci-check.sh.`, - RunE: runCheck, + Short: "Run enforcement checks", + Long: `Run enforcement checks against the current project. + +Without flags: runs the full check suite via scripts/ci-check.sh. +--pre-commit: runs fast pre-commit checks (TODOs, contract refs) directly + from the rebar installation — no project scripts involved. + Called by the .git/hooks/pre-commit hook.`, + RunE: runCheck, } func init() { - checkCmd.Flags().BoolVar(&checkStrict, "strict", true, "exit 1 on any failure") + checkCmd.Flags().BoolVar(&checkStrict, "strict", true, "exit 1 on any failure (full check only)") + checkCmd.Flags().BoolVar(&checkPreCommit, "pre-commit", false, "run fast pre-commit checks from rebar install") } func runCheck(cmd *cobra.Command, args []string) error { + if checkPreCommit { + return runPreCommitChecks() + } + scriptArgs := []string{} if checkStrict { scriptArgs = append(scriptArgs, "--strict") } - exitCode, err := scripts.RunPassthrough(cfg.ScriptsDir, "ci-check.sh", scriptArgs...) if err != nil { return fmt.Errorf("running checks: %w", err) } - if exitCode != 0 { os.Exit(exitCode) } return nil } + +// runPreCommitChecks runs the fast enforcement subset from the rebar +// installation's scripts/ directory — not from the project's scripts/. +// This avoids the circular chain: +// pre-commit.sh → rebar check --pre-commit → rebar home scripts +// The project's scripts/pre-commit.sh is a 3-line entry point only. +func runPreCommitChecks() error { + rebarRoot := findRebarRoot() + if rebarRoot == "" { + fmt.Fprintln(os.Stderr, "pre-commit: rebar installation not found — skipping checks") + fmt.Fprintln(os.Stderr, " Set REBAR_ROOT or ensure rebar is installed at ~/.rebar") + return nil // fail-open: don't block commits on missing rebar + } + + rebarScripts := filepath.Join(rebarRoot, "scripts") + + // Fast checks appropriate for pre-commit (<5s total). + // These run from the rebar install, not the project's scripts/. + fastChecks := []string{ + "check-todos.sh", + "check-contract-refs.sh", + } + + failed := 0 + for _, name := range fastChecks { + if _, err := os.Stat(filepath.Join(rebarScripts, name)); err != nil { + continue // script not present in this rebar version — skip + } + fmt.Printf(" checking: %s\n", name) + exitCode, err := scripts.RunPassthrough(rebarScripts, name) + if err != nil || exitCode != 0 { + failed++ + } + } + + if failed > 0 { + fmt.Fprintf(os.Stderr, "\n%d pre-commit check(s) failed. Fix above or skip with --no-verify.\n", failed) + os.Exit(1) + } + fmt.Println(" pre-commit checks passed.") + return nil +} diff --git a/cli/cmd/init.go b/cli/cmd/init.go index cbbd9b8..cad841e 100644 --- a/cli/cmd/init.go +++ b/cli/cmd/init.go @@ -278,7 +278,7 @@ _None currently._ } } if copiedScripts > 0 { - fmt.Printf(" Created scripts/ (%d scripts incl. cold-start-checks.sh, ci-check.sh, inbox-watch.sh)\n", copiedScripts) + fmt.Printf(" Created scripts/ (%d scripts: pre-commit.sh, ci-check.sh — thin wrappers calling rebar)\n", copiedScripts) created++ } } @@ -493,7 +493,9 @@ func findRebarRoot() string { if dir == "" { return false } - _, err := os.Stat(filepath.Join(dir, "templates", "project-bootstrap", "scripts", "steward.sh")) + // setup-rebar.sh exists in both installed framework dirs and source checkouts; + // steward.sh was removed from project-bootstrap/scripts/ in the thin-scripts refactor. + _, err := os.Stat(filepath.Join(dir, "setup-rebar.sh")) return err == nil } diff --git a/setup-rebar.sh b/setup-rebar.sh index 1c4e6bf..87d8d33 100755 --- a/setup-rebar.sh +++ b/setup-rebar.sh @@ -145,15 +145,15 @@ install_from_source() { command -v git &>/dev/null || err "git is required. Install it and re-run." local src_dir="${INSTALL_DIR}/.src" + local clone_url="${REBAR_REPO:-https://github.com/${REBAR_GITHUB}.git}" if [[ -d "${src_dir}/.git" ]]; then log "Updating existing checkout..." git -C "$src_dir" fetch --tags origin git -C "$src_dir" checkout "$ref" git -C "$src_dir" pull --ff-only origin "$ref" 2>/dev/null || true else - log "Cloning https://github.com/${REBAR_GITHUB}.git (ref: $ref)..." - git clone --branch "$ref" --depth=1 \ - "https://github.com/${REBAR_GITHUB}.git" "$src_dir" + log "Cloning ${clone_url} (ref: $ref)..." + git clone --branch "$ref" --depth=1 "$clone_url" "$src_dir" fi # Copy framework files diff --git a/templates/project-bootstrap/scripts/ci-check.sh b/templates/project-bootstrap/scripts/ci-check.sh index 6cfe041..32c50a8 100755 --- a/templates/project-bootstrap/scripts/ci-check.sh +++ b/templates/project-bootstrap/scripts/ci-check.sh @@ -9,7 +9,7 @@ # the rebar install — this script is intentionally a thin wrapper so # adopting repos do not carry a copy of rebar's internals. # -# Suppress individual checks via env vars understood by `rebar audit`: +# Suppress individual checks via SKIP_* env vars understood by rebar audit: # SKIP_CONTRACT_HEADERS=1 SKIP_TODOS=1 SKIP_FRESHNESS=1 etc. set -euo pipefail diff --git a/templates/project-bootstrap/scripts/pre-commit.sh b/templates/project-bootstrap/scripts/pre-commit.sh index a338127..bedddfc 100755 --- a/templates/project-bootstrap/scripts/pre-commit.sh +++ b/templates/project-bootstrap/scripts/pre-commit.sh @@ -1,21 +1,8 @@ #!/usr/bin/env bash -# pre-commit.sh — Pre-commit hook: delegates to the rebar CLI. -# -# Install as a git hook: -# ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit -# -# The rebar CLI runs the fast enforcement checks appropriate for pre-commit. -# Logic lives in the rebar install — this script is intentionally a thin -# wrapper so adopting repos do not carry a copy of rebar's internals. -# -# Skip (use sparingly): git commit --no-verify +# pre-commit.sh — Pre-commit hook entry point. +# Fast enforcement checks run from the rebar installation, not from this file. +# Skip (sparingly): git commit --no-verify set -euo pipefail - -if ! command -v rebar &>/dev/null; then - echo "pre-commit: rebar not found in PATH — skipping checks" >&2 - echo "Install rebar: https://github.com/willackerly/rebar#installation" >&2 - exit 0 -fi - +command -v rebar &>/dev/null || { echo "pre-commit: rebar not in PATH — skipping"; exit 0; } exec rebar check --pre-commit