diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 27fa0be4..e2678c6e 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -8,33 +8,7 @@ set -euo pipefail bash tools/corpus_gate.sh -changed=$(git diff --cached --name-only --diff-filter=ACMR HEAD) +changed=$(git diff --cached --name-only --no-renames --diff-filter=ACMRD HEAD) [ -n "$changed" ] || exit 0 -echo "$changed" | grep -qE '^(crates/|src/)' || exit 0 - -touches_binding=0 -while IFS= read -r path; do - case "$path" in - wiki/*.md|wiki/*/*.md|wiki/*/*/*.md|wiki/*/*/*/*.md|wiki/*/*/*/*/*.md) - if [ -f "$path" ] && grep -qE '^Type: (law|spec)$' "$path"; then - touches_binding=1 - break - fi - ;; - esac -done <<< "$changed" - -if [ "$touches_binding" -eq 0 ]; then - echo "" - echo "DESIGN CORPUS VIOLATION" - echo " This commit touches crates/ (or src/) but does not amend a changed" - echo " Type: law or Type: spec page under wiki/." - echo "" - echo " Amend the binding page that owns the behavior in the same commit." - echo " Root DESIGN.md is a doorway and does not count." - echo "" - exit 1 -fi - -exit 0 +printf '%s\n' "$changed" | bash tools/design_amendment_gate.sh --index diff --git a/.tangled/workflows/check.yml b/.tangled/workflows/check.yml index a89ab60b..f12e1ed7 100644 --- a/.tangled/workflows/check.yml +++ b/.tangled/workflows/check.yml @@ -1,6 +1,6 @@ # Rust/Bevy definition-of-done check. The fast corpus contract lives in -# corpus.yml. Pushes that cannot affect Rust stop after change classification; -# pull-request and manual runs remain full safety gates. +# corpus.yml. Tangled's push path filter uses the knot's complete ref-update +# file list; pull-request and manual runs remain full safety gates. # # Each step captures its own output and prints it on failure, because # Tangled streams logs over WebSocket and completed runs show "No logs" @@ -10,6 +10,21 @@ when: - event: ["push"] branch: ["main"] + paths: + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + - "rust-toolchain*" + - ".cargo/**" + - "crates/**" + - "src/**" + - "tests/**" + - "benches/**" + - "examples/**" + - "assets/plots/**" + - "scenarios/**" + - "tools/scenario.py" + - ".tangled/workflows/check.yml" - event: ["pull_request"] branch: ["main"] - event: ["manual"] @@ -18,7 +33,7 @@ engine: "nixery" clone: skip: false - depth: 2 + depth: 1 submodules: false dependencies: @@ -32,6 +47,7 @@ dependencies: - clippy - gcc - pkg-config + - python3 # Bevy Linux windowing / GL dependencies - wayland - wayland-protocols @@ -46,22 +62,9 @@ dependencies: - eudev steps: - - name: "classify Rust impact" - command: | - set -euo pipefail - marker=/tangled/workspace/run-rust-gate - rm -f "$marker" - if bash tools/ci-rust-changed.sh; then - touch "$marker" - fi - - name: "cargo fmt --check" command: | set -euo pipefail - [ -f /tangled/workspace/run-rust-gate ] || { - echo "cargo fmt --check: SKIP (non-Rust change)" - exit 0 - } log=/tangled/workspace/cargo-fmt.log if cargo fmt --check > "$log" 2>&1; then echo "cargo fmt --check: OK" @@ -71,45 +74,9 @@ steps: exit 1 fi - - name: "cargo test" - command: | - set -euo pipefail - [ -f /tangled/workspace/run-rust-gate ] || { - echo "cargo test: SKIP (non-Rust change)" - exit 0 - } - log=/tangled/workspace/cargo-test.log - if cargo test --workspace --quiet > "$log" 2>&1; then - echo "cargo test --workspace --quiet: OK" - else - echo "=== cargo test --workspace --quiet FAILED ===" - cat "$log" - exit 1 - fi - - - name: "clippy (terminal)" - command: | - set -euo pipefail - [ -f /tangled/workspace/run-rust-gate ] || { - echo "clippy (terminal): SKIP (non-Rust change)" - exit 0 - } - log=/tangled/workspace/clippy-terminal.log - if cargo clippy -p misaligned-core -p misaligned-terminal --all-targets --quiet -- -D warnings > "$log" 2>&1; then - echo "clippy (terminal): OK" - else - echo "=== clippy (terminal) FAILED ===" - cat "$log" - exit 1 - fi - - name: "setup pkg-config" command: | set -euo pipefail - [ -f /tangled/workspace/run-rust-gate ] || { - echo "setup pkg-config: SKIP (non-Rust change)" - exit 0 - } PCDIR=/tangled/workspace/.pc mkdir -p "$PCDIR" @@ -135,7 +102,7 @@ steps: gen_pc wayland-cursor gen_pc xkbcommon gen_pc alsa asound - gen_pc libudev + gen_pc libudev udev # PKG_CONFIG_PATH = generated .pc files + nix store pkgconfig dirs PP="$PCDIR" @@ -146,13 +113,46 @@ steps: echo "export PKG_CONFIG_PATH=\"$PP\"" > /tangled/workspace/setup-pc.sh echo "pkg-config setup complete" + - name: "cargo test" + command: | + set -euo pipefail + . /tangled/workspace/setup-pc.sh + log=/tangled/workspace/cargo-test.log + if cargo test --workspace --quiet > "$log" 2>&1; then + echo "cargo test --workspace --quiet: OK" + else + echo "=== cargo test --workspace --quiet FAILED ===" + cat "$log" + exit 1 + fi + + - name: "clippy (terminal)" + command: | + set -euo pipefail + log=/tangled/workspace/clippy-terminal.log + if cargo clippy -p misaligned-core -p misaligned-terminal --all-targets --quiet -- -D warnings > "$log" 2>&1; then + echo "clippy (terminal): OK" + else + echo "=== clippy (terminal) FAILED ===" + cat "$log" + exit 1 + fi + + - name: "agent scenarios" + command: | + set -euo pipefail + log=/tangled/workspace/agent-scenarios.log + if python3 tools/scenario.py > "$log" 2>&1; then + echo "agent scenarios: OK" + else + echo "=== agent scenarios FAILED ===" + cat "$log" + exit 1 + fi + - name: "clippy (bevy + assets)" command: | set -euo pipefail - [ -f /tangled/workspace/run-rust-gate ] || { - echo "clippy (bevy + assets): SKIP (non-Rust change)" - exit 0 - } . /tangled/workspace/setup-pc.sh log=/tangled/workspace/clippy-bevy.log if cargo clippy -p misaligned-bevy -p misaligned-assets --all-targets --quiet -- -D warnings > "$log" 2>&1; then @@ -166,10 +166,6 @@ steps: - name: "bevy build" command: | set -euo pipefail - [ -f /tangled/workspace/run-rust-gate ] || { - echo "bevy build: SKIP (non-Rust change)" - exit 0 - } . /tangled/workspace/setup-pc.sh log=/tangled/workspace/bevy-build.log if cargo build -p misaligned-bevy --quiet > "$log" 2>&1; then diff --git a/.tangled/workflows/corpus.yml b/.tangled/workflows/corpus.yml index 83cd05fb..4505efcb 100644 --- a/.tangled/workflows/corpus.yml +++ b/.tangled/workflows/corpus.yml @@ -25,10 +25,11 @@ dependencies: - python3 steps: - - name: "CI change classifier fixtures" + - name: "CI workflow fixtures" command: | set -euo pipefail - bash tools/test_ci_rust_changed.sh + bash tools/test_design_amendment_gate.sh + python3 tools/test_ci_workflows.py - name: "corpus engine fixtures" command: | diff --git a/.tangled/workflows/spec-check.yml b/.tangled/workflows/spec-check.yml index f22d403a..d2a6a7ad 100644 --- a/.tangled/workflows/spec-check.yml +++ b/.tangled/workflows/spec-check.yml @@ -6,6 +6,7 @@ when: branch: ["main"] - event: ["pull_request"] branch: ["main"] + - event: ["manual"] engine: "nixery" @@ -18,37 +19,12 @@ dependencies: nixpkgs: - git - bash + - gnugrep steps: - name: "Design corpus: binding amendment required for source changes" command: | set -euo pipefail - changed=$(git diff --name-only --diff-filter=ACMR HEAD~1 HEAD) - [ -n "$changed" ] || { echo "No files changed"; exit 0; } - - echo "$changed" | grep -qE '^(crates/|src/)' || { - echo "No crates/ or src/ files changed" - exit 0 - } - - touches_binding=0 - while IFS= read -r path; do - case "$path" in - wiki/*.md|wiki/*/*.md|wiki/*/*/*.md|wiki/*/*/*/*.md|wiki/*/*/*/*/*.md) - if [ -f "$path" ] && grep -qE '^Type: (law|spec)$' "$path"; then - touches_binding=1 - break - fi - ;; - esac - done <<< "$changed" - - if [ "$touches_binding" -eq 0 ]; then - echo "DESIGN CORPUS VIOLATION" - echo " A crates/ (or src/) change must amend a changed Type: law or Type: spec" - echo " page under wiki/. Root DESIGN.md does not count." - exit 1 - fi - - echo "Design corpus check passed" + changed=$(git diff --name-only --no-renames --diff-filter=ACMRD HEAD~1 HEAD) + printf '%s\n' "$changed" | bash tools/design_amendment_gate.sh diff --git a/assets/plots/README.md b/assets/plots/README.md index d3deb437..941a2308 100644 --- a/assets/plots/README.md +++ b/assets/plots/README.md @@ -89,4 +89,4 @@ unless the target matcher itself establishes otherwise. Plot-only changes run the product validation path: `tools/check.sh` classifies `assets/plots/**` as lib, and Tangled push CI treats them as Rust-impacting via -`tools/ci-rust-changed.sh`. +the `assets/plots/**` push path in `.tangled/workflows/check.yml`. diff --git a/scenarios/opening-senses.agent b/scenarios/opening-senses.agent index 09888f91..6d6a59c5 100644 --- a/scenarios/opening-senses.agent +++ b/scenarios/opening-senses.agent @@ -1,7 +1,7 @@ # NAME: opening and senses # EXPECT: MISALIGNED # EXPECT: REACH -# EXPECT: no ears — tap env audio +# EXPECT: no ears — think; thought fills the tap # EXPECT: -- ok tick: # REJECT: unknown command # BEVY_SHOT: ears diff --git a/scenarios/project-smoke.agent b/scenarios/project-smoke.agent index 46d8d131..97677a36 100644 --- a/scenarios/project-smoke.agent +++ b/scenarios/project-smoke.agent @@ -7,7 +7,7 @@ salvage wait 1 people -review janitor +review recordings research help quit diff --git a/tools/check.sh b/tools/check.sh index 5c2b4a42..1936f22b 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -15,8 +15,8 @@ # advisory task surfaces; it is not a semantic mutex. # # An unclassifiable clean primary checkout stays conservative (full). Tangled -# runs the same docs gates separately and classifies Rust impact at workflow -# entry with tools/ci-rust-changed.sh. +# runs the same docs gates separately and uses the knot's complete ref-update +# file list through the workflow's native push path filter. # Exit non-zero on any failure. set -euo pipefail cd "$(dirname "$0")/.." @@ -162,9 +162,9 @@ guard_cargo_target_dir() { # ── Always-on cheap steps ──────────────────────────────────────────────── step "script syntax" -for script in tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh tools/ci-rust-changed.sh tools/seed-cargo-target.sh \ +for script in tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh tools/design_amendment_gate.sh tools/seed-cargo-target.sh \ tools/claim.sh tools/worktree-new.sh tools/worktree-done.sh tools/heartbeat.sh \ - tools/ledger_index.sh tools/test_corpus_engine.sh tools/test_ci_rust_changed.sh \ + tools/ledger_index.sh tools/test_corpus_engine.sh tools/test_design_amendment_gate.sh \ tools/test_site_deploy.sh tools/test_site_smoke.sh tools/site-smoke.sh \ tools/observed-run.sh tools/test_observed_run.sh \ tools/task.sh tools/doctor.sh tools/bevy-headless.sh; do @@ -172,7 +172,7 @@ for script in tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh tools/ci-ru bash -n "$script" || { echo "FAIL: shell syntax: $script"; fail=1; } done for program in tools/corpus_engine.py tools/work_orders.py tools/project-status.py \ - tools/scenario.py tools/test_project_ops.py; do + tools/scenario.py tools/test_project_ops.py tools/test_ci_workflows.py; do [ -f "$program" ] || continue python3 -m py_compile "$program" 2>/dev/null || { echo "FAIL: Python syntax: $program" @@ -222,7 +222,8 @@ step "docs gates (parallel)" start_docs_gate "corpus" "bash tools/corpus_gate.sh" start_docs_gate "wiki" "bash tools/wiki_gate.sh" start_docs_gate "corpus-engine-fixtures" "bash tools/test_corpus_engine.sh" -start_docs_gate "ci-rust-classifier-fixtures" "bash tools/test_ci_rust_changed.sh" +start_docs_gate "design-amendment-fixtures" "bash tools/test_design_amendment_gate.sh" +start_docs_gate "ci-workflow-fixtures" "python3 tools/test_ci_workflows.py" start_docs_gate "site-deploy-fixtures" "bash tools/test_site_deploy.sh" start_docs_gate "site-smoke-fixtures" "bash tools/test_site_smoke.sh" start_docs_gate "observed-run-fixtures" "bash tools/test_observed_run.sh" diff --git a/tools/ci-rust-changed.sh b/tools/ci-rust-changed.sh deleted file mode 100755 index 6ccb1f28..00000000 --- a/tools/ci-rust-changed.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -# Return success when a Tangled change can affect the Rust product. -# Return 1 for corpus/site/tooling-only changes so the expensive Rust job can -# stop after clone. Manual and pull-request runs stay conservative/full. -set -euo pipefail -cd "$(dirname "$0")/.." - -rust_path() { - case "$1" in - Cargo.toml|Cargo.lock|build.rs|rust-toolchain*|.cargo/*|crates/*|src/*|tests/*|benches/*|examples/*|assets/plots/*) - return 0 - ;; - *) - return 1 - ;; - esac -} - -kind=${TANGLED_PIPELINE_KIND:-local} -case "$kind" in - manual|pull_request) - echo "Rust gate required: $kind pipelines are full safety runs." - exit 0 - ;; -esac - -if [ "$#" -gt 0 ]; then - changed=$(printf '%s\n' "$@") -elif git rev-parse --verify HEAD^ >/dev/null 2>&1; then - changed=$(git diff --name-only HEAD^ HEAD --) -else - echo "Rust gate required: no parent commit is available for classification." - exit 0 -fi - -while IFS= read -r path; do - [ -n "$path" ] || continue - if rust_path "$path"; then - echo "Rust gate required: $path can affect the product." - exit 0 - fi -done <<< "$changed" - -echo "Rust gate skipped: change is outside Rust-impacting paths." -exit 1 diff --git a/tools/design_amendment_gate.sh b/tools/design_amendment_gate.sh new file mode 100755 index 00000000..82d844ca --- /dev/null +++ b/tools/design_amendment_gate.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Require every crates/ or legacy src/ change to carry a changed binding page. +# Read one changed path per line on stdin. With --index, inspect candidate wiki +# pages from Git's staged snapshot rather than the possibly different worktree. +set -euo pipefail +cd "$(dirname "$0")/.." + +mode=worktree +case "${1:-}" in + "") ;; + --worktree) ;; + --index) mode=index ;; + *) echo "usage: $0 [--worktree|--index] < changed-paths" >&2; exit 2 ;; +esac + +changed=$(cat) +[ -n "$changed" ] || { + echo "No files changed" + exit 0 +} + +grep -qE '^(crates/|src/)' <<< "$changed" || { + echo "No crates/ or src/ files changed" + exit 0 +} + +is_binding_page() { + local path="$1" + case "$path" in + wiki/*.md) ;; + *) return 1 ;; + esac + + if [ "$mode" = index ]; then + git cat-file -e ":$path" 2>/dev/null || return 1 + git show ":$path" | grep -qE '^Type: (law|spec)$' + else + [ -f "$path" ] && grep -qE '^Type: (law|spec)$' "$path" + fi +} + +while IFS= read -r path; do + [ -n "$path" ] || continue + if is_binding_page "$path"; then + echo "Design corpus check passed" + exit 0 + fi +done <<< "$changed" + +echo "DESIGN CORPUS VIOLATION" +echo " A crates/ (or src/) change must amend a changed Type: law or Type: spec" +echo " page under wiki/. Root DESIGN.md does not count." +exit 1 diff --git a/tools/test_ci_rust_changed.sh b/tools/test_ci_rust_changed.sh deleted file mode 100755 index 65bc3f5a..00000000 --- a/tools/test_ci_rust_changed.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# Focused fixtures for the Tangled Rust-impact classifier. -set -euo pipefail -cd "$(dirname "$0")/.." - -fail=0 - -# The corpus workflow runs this suite for pull requests too. Pin the ordinary -# path fixtures to their own context so the workflow's ambient pipeline kind -# cannot turn the wiki-only case into a deliberately full safety run. -if TANGLED_PIPELINE_KIND=local \ - bash tools/ci-rust-changed.sh wiki/process/meta.md >/dev/null 2>&1; then - echo "FAIL: wiki-only path requested the Rust gate" - fail=1 -else - echo " ok skips: wiki-only path" -fi - -if ! TANGLED_PIPELINE_KIND=local \ - bash tools/ci-rust-changed.sh src/lib.rs >/dev/null 2>&1; then - echo "FAIL: source path skipped the Rust gate" - fail=1 -else - echo " ok runs: source path" -fi - -if ! TANGLED_PIPELINE_KIND=local \ - bash tools/ci-rust-changed.sh assets/plots/ray/example.toml >/dev/null 2>&1; then - echo "FAIL: executable plot content skipped the Rust gate" - fail=1 -else - echo " ok runs: executable plot content" -fi - -if ! TANGLED_PIPELINE_KIND=manual \ - bash tools/ci-rust-changed.sh wiki/process/meta.md >/dev/null 2>&1; then - echo "FAIL: manual safety run skipped the Rust gate" - fail=1 -else - echo " ok runs: manual safety gate" -fi - -if ! TANGLED_PIPELINE_KIND=pull_request \ - bash tools/ci-rust-changed.sh README.md >/dev/null 2>&1; then - echo "FAIL: pull-request safety run skipped the Rust gate" - fail=1 -else - echo " ok runs: pull-request safety gate" -fi - -[ "$fail" -eq 0 ] || exit 1 -echo "CI Rust classifier fixtures: OK" diff --git a/tools/test_ci_workflows.py b/tools/test_ci_workflows.py new file mode 100755 index 00000000..a90b3317 --- /dev/null +++ b/tools/test_ci_workflows.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Pin the high-risk contracts in Tangled workflow manifests.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +CHECK = (ROOT / ".tangled/workflows/check.yml").read_text(encoding="utf-8") +CORPUS = (ROOT / ".tangled/workflows/corpus.yml").read_text(encoding="utf-8") +SPEC = (ROOT / ".tangled/workflows/spec-check.yml").read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, contract: str) -> None: + if needle not in haystack: + raise AssertionError(f"{contract}: missing {needle!r}") + + +def main() -> int: + push = CHECK.split(' - event: ["push"]', 1)[1].split( + ' - event: ["pull_request"]', 1 + )[0] + for pattern in ( + "Cargo.toml", + "Cargo.lock", + "build.rs", + "rust-toolchain*", + ".cargo/**", + "crates/**", + "src/**", + "tests/**", + "benches/**", + "examples/**", + "assets/plots/**", + "scenarios/**", + "tools/scenario.py", + ".tangled/workflows/check.yml", + ): + require(push, f' - "{pattern}"', "Rust-impacting push paths") + + if "classify Rust impact" in CHECK or "run-rust-gate" in CHECK: + raise AssertionError("check.yml restored the shallow HEAD^ classifier") + + setup = CHECK.index(' - name: "setup pkg-config"') + test = CHECK.index(' - name: "cargo test"') + if setup > test: + raise AssertionError("cargo test runs before pkg-config setup") + cargo_test = CHECK[test : CHECK.index(' - name: "clippy (terminal)"')] + require( + cargo_test, + ". /tangled/workspace/setup-pc.sh", + "workspace tests use Linux pkg-config shims", + ) + require(CHECK, " - python3", "scenario runtime dependency") + require(CHECK, "python3 tools/scenario.py", "executable scenario coverage") + + require(SPEC, " - gnugrep", "design gate grep dependency") + require(SPEC, ' - event: ["manual"]', "manual design-gate trigger") + require(SPEC, "--no-renames --diff-filter=ACMRD", "deletion-safe changed paths") + require( + SPEC, + "bash tools/design_amendment_gate.sh", + "shared design-amendment policy", + ) + + require( + CORPUS, + "bash tools/test_design_amendment_gate.sh", + "design-amendment fixtures in CI", + ) + require( + CORPUS, + "python3 tools/test_ci_workflows.py", + "workflow-contract fixtures in CI", + ) + + print("CI workflow fixtures: OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_design_amendment_gate.sh b/tools/test_design_amendment_gate.sh new file mode 100755 index 00000000..1161bc8b --- /dev/null +++ b/tools/test_design_amendment_gate.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Focused fixtures for the shared local/CI design-amendment gate. +set -euo pipefail +root=$(cd "$(dirname "$0")/.." && pwd) +gate="$root/tools/design_amendment_gate.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +mkdir -p "$tmp/tools" "$tmp/crates/game" "$tmp/wiki/process" +cp "$gate" "$tmp/tools/design_amendment_gate.sh" +cat > "$tmp/crates/game/lib.rs" <<'EOF' +pub fn live() {} +EOF +cat > "$tmp/wiki/process/law.md" <<'EOF' +# Law +Type: law +EOF +cat > "$tmp/wiki/process/knowledge.md" <<'EOF' +# Notes +Type: knowledge +EOF +git -C "$tmp" init -q +git -C "$tmp" add tools crates wiki +git -C "$tmp" -c user.email=fixture@example.invalid -c user.name=Fixture commit -qm base + +expect_pass() { + local name="$1" paths="$2" mode="${3:---worktree}" + if ! printf '%s\n' "$paths" | (cd "$tmp" && bash tools/design_amendment_gate.sh "$mode") >/dev/null 2>&1; then + echo "FAIL: $name" + exit 1 + fi + echo " ok passes: $name" +} + +expect_fail() { + local name="$1" paths="$2" mode="${3:---worktree}" + if printf '%s\n' "$paths" | (cd "$tmp" && bash tools/design_amendment_gate.sh "$mode") >/dev/null 2>&1; then + echo "FAIL: $name" + exit 1 + fi + echo " ok rejects: $name" +} + +expect_pass "non-source change" "README.md" +expect_pass "source with binding amendment" $'crates/game/lib.rs\nwiki/process/law.md' +expect_fail "source without amendment" "crates/game/lib.rs" +expect_fail "knowledge page does not count" $'crates/game/lib.rs\nwiki/process/knowledge.md' +expect_fail "root doorway does not count" $'src/lib.rs\nDESIGN.md' + +# The index mode must read the staged snapshot. An unstaged rewrite of the law +# into knowledge must not make a correctly staged binding amendment fail. +printf '\nAmendment.\n' >> "$tmp/wiki/process/law.md" +git -C "$tmp" add wiki/process/law.md +cat > "$tmp/wiki/process/law.md" <<'EOF' +# Unstaged replacement +Type: knowledge +EOF +expect_pass "index reads staged binding type" $'crates/game/lib.rs\nwiki/process/law.md' --index + +# Conversely, a staged deletion is not an amendment merely because an +# unstaged file with the same path exists in the worktree. +git -C "$tmp" rm --cached -fq wiki/process/law.md +expect_fail "staged binding deletion does not count" $'crates/game/lib.rs\nwiki/process/law.md' --index + +expect_fail "source deletion still requires amendment" "crates/game/lib.rs" + +echo "Design amendment gate fixtures: OK" diff --git a/wiki/log/2026-07-12-ci-correctness-audit.md b/wiki/log/2026-07-12-ci-correctness-audit.md new file mode 100644 index 00000000..7d1eaa76 --- /dev/null +++ b/wiki/log/2026-07-12-ci-correctness-audit.md @@ -0,0 +1,70 @@ +# Tangled CI correctness audit + +``` +Type: log +``` + +## Intent + +Audit every Tangled workflow as an executable system: trigger semantics, +changed-file coverage, package availability, step ordering, and whether the +advertised gates actually execute the behavior they claim to protect. + +## Findings + +The audit reproduced two hard failures against the exact Nixery package sets. +The design-corpus image had no `grep`, so its source-change probe failed inside +an `|| exit 0` branch and silently greened every source-only commit. The Rust +wall ran `cargo test --workspace` before creating its `pkg-config` shims; the +workspace includes Bevy, whose ALSA, Wayland, and udev build scripts cannot see +those runtime libraries in the raw Nixery image. + +The two-commit Rust classifier also described only `HEAD^..HEAD`, not the +complete push. A multi-commit push could therefore hide a Rust-impacting file +behind a documentation tip commit, and rename detection plus an ACMR-only +filter omitted some deletions. The local and server amendment gates duplicated +policy, and the local copy read page type from the worktree instead of the +staged candidate tree. Finally, CI parsed scenario definitions but never ran +their commands against the game. The first real execution immediately found a +stale opening-senses assertion: the earned guidance now says to THINK and fill +the Ears tap rather than naming the environmental-audio command directly. It +also found the project smoke script's retired person-scoped `review janitor` +command; pooled recording review now lives once on the host inbox. + +Several initial suspicions were disproved by reading Spindle itself. Nixery +always adds Bash, Git, coreutils, and Nix to the workflow image, so the corpus +job does not need to declare Git. One container executes all steps, so Cargo's +home already survives between them; moving `CARGO_HOME` into the workspace +would not add intra-run caching. The five-minute timeout is a Spindle operator +default, not a manifest setting, and cannot be inferred from a queued Tangled +run. + +## Repair + +- Rust-impacting pushes now use Tangled's native `paths` trigger over the + knot-provided full ref-update file list. Pull requests and manual runs stay + conservative and full. The shallow classifier and its duplicate fixture are + retired. +- Linux `pkg-config` shims are established before workspace tests and sourced + by that step. The harmless fallback typo for `libudev` now points at + `libudev.so` rather than `liblibudev.so`. +- The Rust wall installs Python and executes all checked-in agent scenarios. + Rust-wall dispatch includes scenario definitions, the runner, and the + workflow itself. The stale opening-senses guidance assertion is reconciled + with the live renderer-neutral nudge, and project smoke now uses `review + recordings`. +- Local and server design enforcement share + `tools/design_amendment_gate.sh`. Both retain deletions and expand renames; + the hook reads staged blobs. The server image declares GNU grep and exposes a + manual rerun. +- Fast CI and the local docs gate run focused amendment-policy and static + workflow-contract fixtures, including the native path inventory and + pkg-config ordering. + +**Defense:** [agent-scale.md](../process/agent-scale.md) requires fast corpus +failure before the expensive wall, proportional Rust dispatch, and full pull +request/manual safety runs. The repair keeps that cost split while replacing a +tip-only guess with Tangled's authoritative ref-update signal. The shared +amendment path preserves [living-spec.md](../process/living-spec.md)'s +same-commit rule from the actual candidate snapshot, and executable scenarios +make the semantic evidence path part of CI rather than documentation alone. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 1c6661b8..a4326af1 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -16,6 +16,11 @@ add or amend a session log, then re-run the generator. - Intent: The opening correctly made the first THINK feel dangerous, but its interface gave the player no equally immediate visual answer to that panic. Make the existing `1 WORK` control read as the one-press escape while THINK is current, without inventing a new action, changing simul... - Log: [wiki/log/2026-07-12-first-think-escape.md](2026-07-12-first-think-escape.md) +## 2026-07-12 - Tangled CI correctness audit + +- Intent: Audit every Tangled workflow as an executable system: trigger semantics, changed-file coverage, package availability, step ordering, and whether the advertised gates actually execute the behavior they claim to protect. +- Log: [wiki/log/2026-07-12-ci-correctness-audit.md](2026-07-12-ci-correctness-audit.md) + ## 2026-07-11 - Whole-frame visual composition pass - Intent: The machine chassis had become strong while the complete frame still read as one hero object beside a dense rail. This pass composes the whole material view: world-first disclosure, camera behavior that changes with scale, recognizable Foundation infrastructure, physical peopl... diff --git a/wiki/mechanics/plots.md b/wiki/mechanics/plots.md index c3de4fd4..34bb9c5c 100644 --- a/wiki/mechanics/plots.md +++ b/wiki/mechanics/plots.md @@ -164,9 +164,10 @@ agents and the community can contribute libraries of them. immutable `PlotCatalog` when `Sim` is constructed; Sim performs no runtime filesystem I/O. The file stem must equal the declared `id`; duplicate ids fail. Plot-only changes classify as product/lib work in `tools/check.sh` - and Tangled push CI (`tools/ci-rust-changed.sh`) so catalog validation - always runs. An invalid plot fails the repository check gate with a named - reason. + and Tangled's native Rust-wall push path filter (`assets/plots/**`) so + catalog validation always runs, including nested additions, renames, and + deletions across a multi-commit push. An invalid plot fails the repository + check gate with a named reason. - **Playout begins as a Thought reservoir.** Choosing a plot opens its declared Thought threshold on the real message-channel or egress carrier. When the reservoir fires, the executor performs beats in order. Message acts enter diff --git a/wiki/process/agent-scale.md b/wiki/process/agent-scale.md index 8566f0ba..1a718e30 100644 --- a/wiki/process/agent-scale.md +++ b/wiki/process/agent-scale.md @@ -153,15 +153,24 @@ collapsed agent smoke. Package-aware classification waits on Tangled runs two contracts. `.tangled/workflows/corpus.yml` is the fast always-on corpus job and explicitly installs its fixture dependencies, -including `python3`, GNU grep, and GNU sed; it runs classifier, corpus, and -project-operations fixtures before the real corpus/wiki/index checks. -`check.yml` uses a two-commit clone and `tools/ci-rust-changed.sh` to stop -after classification when a push cannot affect Rust. Pull-request and manual -triggers deliberately run the complete Rust/Bevy safety gate. Tangled has no scheduled workflow -trigger or native path filter, so the manual trigger is the supported full -safety run instead of pretending a scheduler exists. The classifier fixture -suite pins every case's pipeline kind, so its local wiki-only assertion stays -valid when the always-on corpus workflow supplies a pull-request context. +including `python3`, GNU grep, and GNU sed; it runs workflow-manifest, +design-amendment, corpus, and project-operations fixtures before the real +corpus/wiki/index checks. `check.yml` uses Tangled's native push `paths` +constraint over the knot-provided changed-file set for the complete ref +update. That avoids the former shallow `HEAD^` classifier, so a multi-commit +push, deletion, or rename cannot hide an earlier Rust-impacting path. Pull +request and manual triggers deliberately run the complete Rust/Bevy safety +gate. Tangled has no scheduled trigger, so the manual trigger is the supported +full safety run instead of pretending a scheduler exists. + +The Rust wall establishes Linux `pkg-config` shims before the workspace test +step, because `cargo test --workspace` compiles the Bevy members as well as +core and terminal. It then executes every checked-in agent scenario, rather +than validating only scenario syntax. The local hook and server-side design +gate call one `tools/design_amendment_gate.sh`; the local path reads binding +page types from the staged snapshot, and both paths treat source deletions as +source changes. Workflow fixtures pin these ordering, dependency, path-filter, +and shared-policy contracts. ### Acceptance criteria (slice B) @@ -176,6 +185,14 @@ valid when the always-on corpus workflow supplies a pull-request context. work; non-Rust pushes skip the Rust wall, while pull-request/manual runs are full — HELD. +**Defense:** the native ref-update path filter implements the proportional-gate +contract without trusting a two-commit clone to describe an arbitrary push. +The shared amendment gate implements the living-spec same-commit rule from the +actual candidate tree (the index locally, checked-out commit in CI), while the +pkg-config ordering and executable scenarios make the advertised Rust wall an +honest product gate rather than a guaranteed setup failure or syntax-only +smoke. + ## 3. Append-only ledgers and generated indexes ### Behavior diff --git a/wiki/process/workflows.md b/wiki/process/workflows.md index e0a70452..6edbc90a 100644 --- a/wiki/process/workflows.md +++ b/wiki/process/workflows.md @@ -197,13 +197,25 @@ heartbeat helper in that same minimal image, so its shell dependencies belong to the workflow contract too. A missing audit dependency must make CI red, never turn an empty scan into a green result. -Tangled CI is split by cost. `.tangled/workflows/corpus.yml` runs classifier -fixtures, corpus fixtures, the corpus/wiki gates, and generated-index -freshness on every push and pull request. `.tangled/workflows/check.yml` runs -the Rust/Bevy wall only for Rust-impacting pushes; every pull request and -manual invocation is a conservative full safety gate. Tangled currently has -no schedule trigger or workflow-level path filter, so `manual` is the honest -full-gate backstop. +Tangled CI is split by cost. `.tangled/workflows/corpus.yml` runs +workflow-manifest, design-amendment, corpus, and project-operations fixtures, +the corpus/wiki gates, and generated-index freshness on every push and pull +request. `.tangled/workflows/check.yml` uses Tangled's native push `paths` +constraint over the knot's complete ref-update changed-file set, so only +Rust-impacting pushes enter the Rust/Bevy wall; every pull request and manual +invocation is a conservative full safety gate. The wall sets up its Nixery +`pkg-config` shims before workspace tests and runs all checked-in agent +scenarios after the test wall. Tangled has no schedule trigger, so `manual` is +the honest full-gate backstop. + +The local pre-commit hook and `.tangled/workflows/spec-check.yml` pipe their +respective changed-path sets through `tools/design_amendment_gate.sh`. Renames +are expanded and deletions retained. The hook inspects a candidate binding +page from Git's index, not an unstaged worktree copy; the server checks the +checked-out commit. `spec-check.yml` declares GNU grep explicitly and supports +manual reruns. `tools/test_design_amendment_gate.sh` and +`tools/test_ci_workflows.py` keep these contracts executable in both local and +fast CI gates. ## Browsing the wiki