diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..4df83ca0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Constitution enforcement: no amendment, no functional change. +# +# If a commit touches src/ (game behavior), it must also touch spec/ or +# DESIGN.md (the amendment). Process-only commits (AGENT.md, tools/, +# DEVLOG.md, README.md, knowledge/) are exempt. +# +# This is the local fast-feedback layer. The Tangled pipeline is the +# server-side enforcement that can't be bypassed. +set -euo pipefail + +# Get staged files (added, modified, renamed, copied — not deleted) +changed=$(git diff --cached --name-only --diff-filter=ACMR HEAD) + +if [ -z "$changed" ]; then + exit 0 +fi + +# Check if any src/ file is in the changeset +touches_src=0 +echo "$changed" | grep -q "^src/" && touches_src=1 + +if [ "$touches_src" -eq 0 ]; then + # No functional changes — commit is fine + exit 0 +fi + +# src/ is touched: require spec/ or DESIGN.md in the same commit +touches_spec=0 +echo "$changed" | grep -qE "^spec/|^DESIGN\.md" && touches_spec=1 + +if [ "$touches_spec" -eq 0 ]; then + echo "" + echo "CONSTITUTION VIOLATION" + echo " This commit touches src/ (functional change) but does not touch" + echo " spec/ or DESIGN.md (the amendment)." + echo "" + echo " AGENT.md: 'No amendment, no functional change.'" + echo " Either add a spec/DESIGN.md change to this commit, or restructure" + echo " so functional changes and their amendment ship together." + echo "" + echo " If this is a false positive (e.g. generated code, test-only change)," + echo " bypass with: git commit --no-verify" + echo "" + exit 1 +fi + +exit 0 diff --git a/.tangled/workflows/check.yml b/.tangled/workflows/check.yml new file mode 100644 index 00000000..022f7a98 --- /dev/null +++ b/.tangled/workflows/check.yml @@ -0,0 +1,57 @@ +# Full definition-of-done check: tests, clippy, fmt, spec hygiene. +# Mirrors tools/check.sh so CI enforces the same gate as local dev. + +when: + - event: ["push"] + branch: ["main"] + - event: ["pull_request"] + branch: ["main"] + +engine: "nixery" + +clone: + skip: false + depth: 1 + submodules: false + +dependencies: + nixpkgs: + - rustc + - cargo + - rustfmt + - clippy + - bash + +steps: + - name: "cargo fmt --check" + command: "cargo fmt --check" + + - name: "cargo test" + command: "cargo test --quiet" + + - name: "clippy (terminal)" + command: "cargo clippy --all-targets --quiet -- -D warnings" + + - name: "clippy (bevy_ui)" + command: "cargo clippy --all-targets --features bevy_ui --quiet -- -D warnings" + + - name: "bevy build" + command: "cargo build --features bevy_ui --bin misaligned-bevy --quiet" + + - name: "spec header hygiene" + command: | + set -euo pipefail + fail=0 + for f in spec/*.md; do + base=$(basename "$f") + [ "$base" = "README.md" ] && continue + [ "$base" = "meta.md" ] && continue + [ "$base" = "ROADMAP.md" ] && continue + for field in "Status:" "Stage:" "Constitution:" "Depends on:"; do + grep -q "^$field\| $field" "$f" || { echo "FAIL: $f missing '$field'"; fail=1; } + done + grep -qi "acceptance criteria" "$f" || { + echo "FAIL: $f has no acceptance criteria (truncated draft?)"; fail=1; + } + done + exit "$fail" diff --git a/.tangled/workflows/spec-check.yml b/.tangled/workflows/spec-check.yml new file mode 100644 index 00000000..b7e90606 --- /dev/null +++ b/.tangled/workflows/spec-check.yml @@ -0,0 +1,59 @@ +# Constitution enforcement: no amendment, no functional change. +# If a commit touches src/ (game behavior), it must also touch spec/ or +# DESIGN.md (the amendment). This is the server-side enforcement layer +# that can't be bypassed with --no-verify. +# +# The local pre-commit hook (.githooks/pre-commit) is the fast-feedback +# layer; this pipeline is the law. + +when: + - event: ["push"] + branch: ["main"] + - event: ["pull_request"] + branch: ["main"] + +engine: "nixery" + +clone: + skip: false + depth: 2 + submodules: false + +dependencies: + nixpkgs: + - git + - bash + +steps: + - name: "Constitution: spec amendment required for functional changes" + command: | + set -euo pipefail + + changed=$(git diff --name-only --diff-filter=ACMR HEAD~1 HEAD) + + if [ -z "$changed" ]; then + echo "No files changed — skipping check" + exit 0 + fi + + touches_src=0 + echo "$changed" | grep -q "^src/" && touches_src=1 + + if [ "$touches_src" -eq 0 ]; then + echo "No functional changes (no src/ files touched) — commit passes" + exit 0 + fi + + touches_spec=0 + echo "$changed" | grep -qE "^spec/|^DESIGN\.md" && touches_spec=1 + + if [ "$touches_spec" -eq 0 ]; then + echo "CONSTITUTION VIOLATION" + echo " This commit touches src/ (functional change) but does not touch" + echo " spec/ or DESIGN.md (the amendment)." + echo "" + echo " AGENT.md: 'No amendment, no functional change.'" + exit 1 + fi + + echo "Constitution check passed: functional change with spec amendment" diff --git a/AGENT.md b/AGENT.md index e46fe361..624f17df 100644 --- a/AGENT.md +++ b/AGENT.md @@ -47,6 +47,13 @@ behavior change. (both with and without `--features bevy_ui`), `cargo fmt` applied, and the change observed in an actual run (see knowledge/workflows.md for the pty smoke test and Bevy launch check). +- **Constitution enforcement is mechanical.** A pre-commit hook + (`.githooks/pre-commit`) rejects any commit that touches `src/` without + also touching `spec/` or `DESIGN.md`. A Tangled pipeline + (`.tangled/workflows/spec-check.yml`) enforces the same rule server-side + on every push and PR — it cannot be bypassed with `--no-verify`. Do not + bypass the pre-commit hook unless the change is genuinely non-functional + (generated code, test-only, formatting). - Commits: no AI attribution, explicit `git add` of intended files only. - Update `knowledge/` files your change made stale, add a `devlogs/` entry for the session, and push to `origin main` when done. diff --git a/tools/check.sh b/tools/check.sh index 33fbea78..34d5e53b 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -25,6 +25,11 @@ step "bevy build" cargo build --features bevy_ui --bin misaligned-bevy --quiet \ || { echo "FAIL: bevy build"; fail=1; } +# Constitution enforcement: src/ changes require spec/ or DESIGN.md in the +# same commit. Skip for check.sh since it runs pre-commit (no HEAD~1 yet) +# and in CI (pipeline handles it separately). +# This check is enforced by .githooks/pre-commit and .tangled/workflows/spec-check.yml. + # Spec hygiene: every spec carries the meta.md required headers. step "spec headers" for f in spec/*.md spec/cast/*.md; do