#!/usr/bin/env bash # Story 3.11 — Sandbox-Tests der atomaren Root-Scope-Lease-Akquise (§5.17, Revision 3.6) # im clone-geteilten Zustand (AD-17b/A0-13, AC-a..AC-e) # Re-executierbar: bash run-sandbox.sh (ab Workspace-Root; Sandbox unter /tmp/sb311) # Zweck: die §5.17-Atomaritäts-Präzisierung (Revision 3.6) als re-executierbarer # Run-Demonstrator durchspielen — # A-1 EXKLUSIVITAETS_SCHLUESSEL: ein Mini-Bundle + Root-Scope-Lock im clone-geteilten # Zustand; der Exklusivitätsschlüssel ist der scope-bezogene Lock (eine committete # Ref je Root-Scope wiki/), die Run-ID ist Lock-Inhalt und NICHT Teil des Schlüssels # (Ref-Name trägt keine Run-ID) — AC-a # A-2 ATOMAR_ZWEI_WORKTREE (nicht-sequenziell): zwei getrennte Worktrees desselben Clones # akquirieren zeitlich überlappend in Getrennten Prozessen denselben Root-Scope; # genau ein Gewinner, der andere LEASE_HOLD — AC-b (Ask-First-Primitiv: git update-ref # $ZERO_SHA, create-only; atomar im geteilten Ref-Namespace) # A-3 WEDER_NOCH (Negativ-Kontrolle): nie zwei aktive Root-Leases gleichzeitig, # nie kein Gewinner (genau ein scope-bezogener Lock existiert nach der Akquise) — AC-b/AC-d # A-4 LEASE_HOLD_NICHT_MUTATION: der abgewiesene Producer verändert weder wiki/ noch den # bestehenden Lock, erzeugt keinen Compilation-Commit, entfernt keine fremde Lease, # beendet sauber mit LEASE_HOLD — AC-c # A-5 GEWINNER_SCHLUESSEL_OHNE_RUNID: der Lock-Name ist ausschließlich ein scope-bezogener # Ref (kein Run-ID-/-Bestandteil im Schlüssel); die per-Lease--Ablage (§5.11 # Pkt. 1) bleibt per-Lease-inhaltlich/ablagebasiert, Exklusivität hängt am Scope-Lock — AC-a # A-6 SPAET_ABGEWIESEN: eine spätere, sequentielle Akquise desselben Root-Scopes gg. den # noch gehaltenen Lock → LEASE_HOLD, Lock-Inhalt (fremde Run-ID) unverändert — AC-c # A-7 KOLLISIONS_HOLD: zwei Branches mit ungleichen Änderungen am selben Concept-Pfad → # kein textueller Auto-Merge; strukturierter Kollisions-Hold mit beiden Commit-Hashes # und Scope an Epic 4 (benannte Hold-Mechanik §5.16 Pkt. 8/§5.10 Pkt. 8) — AC-e/AD-17c # A-8 FREIGABE_ERNEUT: nach dem Lock-Release (Freigabe deterministisch) erwirbt ein # nachfolgender Producer denselben Root-Scope atomar erneut; Scope-Lock wieder genau # einer (Registry-/Lifecycle-Nachfolge bleibt Story 3.12; hier nur Akquise-Atomarität belegt) # Atomare Akquise (§5.17 Pkt. 2) als HARDE Assertion je Szenario (Exit 1 bei Abweichung); # keine Wanduhr-/TTL-/Recovery-Logik (A0-20; Story 3.6/3.12); Frontmatter-/log.md-Konformitaet # (Vertrag §3.3/§3.4, §5). Ubuntu-Sandbox-Semantik auf einem /tmp-Baum — NIE der reale # wiki/- oder raw/-Baum. set -u ROOT=$(mktemp -d /tmp/sb311-XXXXXX) SB="$ROOT/sb" mkdir -p "$SB/wiki" "$SB/raw" cd "$SB" git init -q # Determinismus vs. Host-Git-Konfiguration (AD-17h): LF-Blobs + LF-Worktree — # autocrlf/filemode-Umwandlung des Hosts wuerde sha256-Vergleiche verschieben. git config core.autocrlf false git config core.filemode false git config user.email "sandbox@test" git config user.name "Sandbox" # ---------- Basis-Baum (committete Ausgangslage = Baseline-Commit) ---------- # Mini-Bundle mit zwei Root-Concepts (alpha als Mutations-Objekt, gamma als Kontrolle). cat > wiki/index.md <<'EOF' # Index - [Alpha](alpha.md) - [Gamma](gamma.md) EOF cat > wiki/alpha.md <<'EOF' --- type: concept sources: - resource: raw/alpha-v1.md id: s1 generated: by: wow-compiler/0.1.0 at: 2026-08-16T09:00:00Z --- Das Alpha-Protokoll definiert eine deterministische Init-Sequenz (raw/alpha-v1.md#S-1). EOF cat > wiki/gamma.md <<'EOF' --- type: concept sources: - resource: raw/gamma-v1.md id: s1 generated: by: wow-compiler/0.1.0 at: 2026-08-16T09:00:00Z --- Gamma beschreibt ein anderes, hier nicht betroffenes Thema. EOF cat > wiki/log.md <<'EOF' # Log EOF cat > raw/alpha-v1.md <<'EOF' ### S-1 Evidenz v1: deterministische Init-Sequenz. EOF cat > raw/gamma-v1.md <<'EOF' ### S-1 Evidenz v1: Gamma-Thema. EOF git add -A git commit -qm "Baseline" BASE=$(git rev-parse HEAD) echo "BASELINE-COMMIT (Merge-Base, eindeutiger Commit-Object-Wert): $BASE" echo "Sandbox-Root: $ROOT (loeschbar: rm -rf $ROOT)" echo # ---------- Helfer: Run-Label / Isolation (kein Carry-over zwischen Szenarien) ---------- runlabel() { echo; echo "########## $1 ##########"; } isolate() { # Section-Refs (B1..B8) werden je Szenario separat im clone-geteilten Zustand angelegt; # die isolierten Arbeits-Branches der Szenarien verwenden sich nicht gegenseitig. git checkout -qf "$BASE" git reset -q --hard "$BASE" git clean -qfd wiki raw lease registry scratch plan-run # Auch der geteilte Ref-Namespace (Scope-Lock) gehört zur Isolation: ein hängender # Lock aus einem früheren Szenario (z. B. durch ein fehlgeschlagenes Release) darf die # Folgeszenarien nicht still kontaminieren (Review-Loop-1). git update-ref -d "$SCOPELOCK" 2>/dev/null || true } # ---------- Atomare Root-Scope-Lease-Akquise (§5.17 Pkt. 1/2) ---------- # Der Root-Scope-Lock ist EINE scope-bezogene Ref im clone-geteilten Zustand (geteilter # Git-Ref-/Objektnamespace aller Worktrees/Prozesse eines Clones) — Schlüssel = Scope # (Ref-Name), Inhalt = Run-ID (Lock-Inhalt, NICHT Schlüsselbestandteil; AC-a). # Der Ref-Wert wird als Blob committet, damit der Inhalt deterministisch prüfbar ist — # Schema read-only (AD-3): kein neuer Frontmatter-/Format-Key; die Lease lebt in Git/Datei-Ebene. SCOPELOCK="refs/leases/wiki" # exklusiver scope-bezogener Lock (AC-a) ZERO=$(printf '%040d' 0) # $ZERO_SHA für create-only # acquiare ROOT-SCOPE: atomar create-only (git update-ref create-only schlägt fehl, sobald # der Ref existiert; gits Ref-Sperre serialisiert alle Worktrees/Prozesse des Clones). # Die erwartete Fehlermeldung eines abgewiesenen Versuchs (fatal: ... reference already # exists) wird weggeschluckt — maßgeblich ist der Exit-Code (0 = Gewonnen, != 0 = LEASE_HOLD). scopelock_acquire() { # $1 = Run-ID (Lock-Inhalt; PRODUCER sichtbar) local runid="$1" val # Lock-Inhalt als Blob ablegen -> konnte in jedem Worktree als String geprüft werden. val=$(printf '%s' "$runid" | git hash-object -w --stdin) || return 2 { git update-ref "$SCOPELOCK" "$val" "$ZERO" } 2>/dev/null } scopelock_release() { # $1 = erwartete Inhaber-Run-ID (Ownership-Prüfung) # Deterministische Freigabe (§5.11 Pkt. 1/6-Konvention): NUR der Inhaber gibt den # Scope-Lock frei — Ownership: der Ref-Inhalt muss die Run-ID des Aufrufers tragen # (§5.17 Pkt. 3 „entfernt keine fremde Lease"). Solange der Lock existiert, ist die # Akquise gesperrt (A-6). Review-Loop-1: Exit-Code und Ownership hart gekoppelt. [ "$(scopelock_content)" = "${1:-}" ] \ || { echo "HARD-FAIL: Release ohne Ownership (Inhaber: '$(scopelock_content)', Aufrufer: '${1:-}')" >&2; exit 1; } git update-ref -d "$SCOPELOCK" \ || { echo "HARD-FAIL: Release fehlgeschlagen" >&2; exit 1; } } scopelock_content() { # Lock-Inhalt als Text (Run-ID des aktuell HALTENDEN Producers) — documentierender # Ablage-Wert, kein Schlüsselbestandteil (AC-a). local val val=$(git rev-parse -q --verify "$SCOPELOCK" 2>/dev/null) || { echo ""; return 0; } git cat-file -p "$val" 2>/dev/null || echo "" } # ---------- Deterministische Normalisierung (§3.2 Pkt. 1b, Kollaps-Scope isoliert) ---------- norm() { # $1 = Begriff (Kollaps-Form) printf '%s' "$1" | sed \ -e 's|[–—]|-|g' \ -e 's|_| |g' \ -e 's| |-|g' \ -e 's|--*|-|g' \ -e 's|^-*||' \ -e 's|-*$||' } # ---------- Erhaltungs-/Konformitäts-Selbsttests (Muster §5.9 Pkt. 5 / Vertrag §3.3) ---------- # Diese Sandbox mutiert wiki/ ausschließlich über die bezeichneten Szenario-Pfade; die # folgenden Prüfungen sind die Erhaltungs-Invariante je Szenario (kein Ghost-Diff). assert_frontmatter() { # $1 = Datei ; prüft eine §3.3/§3.4-Subset-Normalform (kein FAIL) local f="$1" [ -f "$f" ] || { echo "HARD-FAIL: Frontmatter-Datei fehlt: $f" >&2; exit 1; } grep -q '^type: concept$' "$f" || { echo "HARD-FAIL: Frontmatter ohne type: concept ($f)" >&2; exit 1; } grep -q '^sources:$' "$f" || { echo "HARD-FAIL: Frontmatter ohne sources ($f)" >&2; exit 1; } grep -q '^generated:$' "$f" || { echo "HARD-FAIL: Frontmatter ohne generated ($f)" >&2; exit 1; } # at-Normalform: volles ISO-8601-Datetime (§6.5 Kriterium 2) grep -Eq '^ at: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$' "$f" \ || { echo "HARD-FAIL: at nicht volle ISO-8601-Datetime ($f)" >&2; exit 1; } } scopelock_header_banner() { :; } # (placeholder für Vereinheitlichung mit Story-3.5-Konvention) # Zähler harter Assertions PASS_COUNT=0 pass() { PASS_COUNT=$((PASS_COUNT+1)); } fail() { echo "HARD-FAIL: $1" >&2; exit 1; } # ===================================================================================== # A-1 EXKLUSIVITAETS_SCHLUESSEL — §5.17 Pkt. 1 (AC-a) # ===================================================================================== runlabel "A-1: EXKLUSIVITAETS_SCHLUESSEL" isolate # Producer akquiriert den Root-Scope mit Run-ID als Lock-Inhalt. (Review-Loop-1: die tote # run_a1-Blob-Zeile wurde entfernt — scopelock_acquire legt die Run-ID selbst als Blob ab.) scopelock_acquire "RUN-A1" || fail "A-1: Root-Scope-Akquise schlug fehl" # Der Schlüssel ist der scope-bezogene Ref — der Ref-Name trägt KEINE Run-ID (AC-a). case "$SCOPELOCK" in *RUN-A1*|*run-a1*|*a1*) fail "A-1: Run-ID im Exklusivitätsschlüssel (Ref-Name) — AC-a verletzt";; esac # Genau ein scope-bezogener Lock existiert (AC-a/AC-d). n=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l) [ "$n" -eq 1 ] || fail "A-1: erwartet genau einen scope-bezogenen Lock, tatsaechlich $n" # Der Lock-Inhalt ist die Run-ID (dokumentierender Ablage-Wert, kein Schlüsselteil). [ "$(scopelock_content)" = "RUN-A1" ] || fail "A-1: Lock-Inhalt != Run-ID (AC-a)" scopelock_release "RUN-A1" [ -z "$(scopelock_content)" ] || fail "A-1: Lock nach Release nicht leer" echo "RESULT: PASS — A-1: EXKLUSIVITAETS_SCHLUESSEL — Schluessel = scope-bezogener Lock im clone-geteilten Zustand (eine Ref je Root-Scope wiki/); Run-ID ist Lock-Inhalt nicht Schluesselbestandteil; genau ein Lock (AC-a)" pass # ===================================================================================== # A-2 ATOMAR_ZWEI_WORKTREE — §5.17 Pkt. 2 (AC-b) — reale Zwei-Worktree-/Zwei-Prozess-Überlappung # ===================================================================================== runlabel "A-2: ATOMAR_ZWEI_WORKTREE (Ueberlappung zweier getrennter Prozesse in getrennten Worktrees)" # Re-Run-Sicherheit: verwaiste worktree-Registrierungen aus früheren Läufen (anderes $ROOT) # erst prune, sonst blockiert `git worktree add` oder trägt Altpfade nach. git worktree prune git worktree add -q "$ROOT/wt-a" "$BASE" || fail "A-2: Worktree wt-a nicht aufgebaut" git worktree add -q "$ROOT/wt-b" "$BASE" || fail "A-2: Worktree wt-b nicht aufgebaut" # Scope-Lock vor dem Start leer (kein Carry-over) [ -z "$(scopelock_content)" ] || fail "A-2: Scope-Lock nicht leer vor dem Lauf" # Zwei GETRENNTE Prozesse (Subshells) akquirieren zeitlich überlappend denselben # Root-Scope — kein sequenzieller Ablauf, echte Überlappung (kein Schein-Parallelität; # zeitlicher Versatz ohne Überlappung bewiese keine Atomarität). Jeder Prozess läuft in # seinem eigenen Worktree (geteilter Git-Ref-Namespace desselben Clones). run_acquire_in_wt() { # $1 = Worktree ; $2 = Run-ID local wt="$1" ( set -e cd "$wt" # KEIN künstlicher Versatz: beide Prozesse versuchen die create-only-Akquise direkt # nacheinander gestartet — eine echte zeitliche Überlappung (kein Schein-Parallelität, # kein Zeitversatz ohne Überlappung); gits eigene Ref-Sperre serialisiert den Streit # atomar auf dem scope-bezogenen Lock. Der LOCK allein entscheidet Gewinner/Verlierer. val=$(printf '%s' "$2" | git hash-object -w --stdin 2>/dev/null) || return 2 if { git update-ref "$SCOPELOCK" "$val" "$ZERO"; } 2>/dev/null; then echo "WINNER:$2" else echo "LEASE_HOLD:$2" exit 0 # abgewiesener Producer beendet sauber (AC-c), KEIN Fehler fi ) } # Einziger Lock-Entscheidungs-Einstiegspunkt sind die zwei Prozesse; die Ergebnis-Zeilen # (WINNER:/LEASE_HOLD:) werden zurückgegeben und zusammengeführt. Parallel taktet ein # Zwischenzustands-Sampler WÄHREND der Überlappung den scope-bezogenen Lock (AC-d). # Review-Loop-1 (D-3.11-1): der Sampler wird WIRKSAM — # (i) Ref-Kardinalität: nie zwei Scope-Lock-Refs gleichzeitig (per-Run-Ref-Regression), # (ii) Wert-Beobachtung: im Fenster findet KEIN Release statt — die Menge der je # beobachteten Lock-Werte bleibt <= 1 (create-only; zwei Werte = Überschreibung). # Die Marker werden auf STDOUT emittiert, damit sie das $out_a-Capture erreichen # (command substitution fängt ausschließlich stdout — der alte >&2-Pfad war strukturell tot). out_a=$( ( run_acquire_in_wt "$ROOT/wt-a" "RUN-A2-a" & run_acquire_in_wt "$ROOT/wt-b" "RUN-A2-b" & seen_vals="" for i in $(seq 1 25); do cnt=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK") [ "${cnt:-0}" -gt 1 ] && echo "TWO_LOCKS_AT_ONCE:i$i" v=$(git rev-parse -q --verify "$SCOPELOCK" 2>/dev/null) if [ -n "$v" ]; then case " $seen_vals " in *" $v "*) ;; *) seen_vals="$seen_vals $v" ;; esac fi sleep 0.002 done nvals=0 for v in $seen_vals; do nvals=$((nvals+1)); done [ "$nvals" -le 1 ] || echo "TWO_VALUES_IN_WINDOW" wait wait ) ) printf '%s\n' "$out_a" | grep '^WINNER:' | sed 's/^/ /' printf '%s\n' "$out_a" | grep '^LEASE_HOLD:' | sed 's/^/ /' # Genau ein Gewinner und genau ein LEASE_HOLD (AC-b): niemals zwei, niemals keiner (AC-d). winners=$(printf '%s\n' "$out_a" | grep -c '^WINNER:') holds=$(printf '%s\n' "$out_a" | grep -c '^LEASE_HOLD:') [ "$winners" -eq 1 ] || fail "A-2: erwartet genau einen Gewinner, tatsaechlich $winners (AC-b)" [ "$holds" -eq 1 ] || fail "A-2: erwartet genau ein LEASE_HOLD, tatsaechlich $holds (AC-b)" # Kein Zwischenzustand mit zwei aktiven Locks (Sampler-Fund, D-3.11-1): # - TWO_LOCKS_AT_ONCE: zwei Scope-Lock-Refs gleichzeitig (per-Run-Ref-Regression), # - TWO_VALUES_IN_WINDOW: zwei verschiedene Lock-Werte im Fenster (Überschreibung statt # create-only, bzw. ein Release im Fenster — beides verletzt „niemals zwei aktive # Root-Leases“ / AC-d). if printf '%s\n' "$out_a" | grep -q 'TWO_LOCKS_AT_ONCE'; then fail "A-2: Zwischenzustand mit zwei Scope-Lock-Refs beobachtet (AC-d)" fi if printf '%s\n' "$out_a" | grep -q 'TWO_VALUES_IN_WINDOW'; then fail "A-2: zwei verschiedene Lock-Werte im Überlappungs-Fenster beobachtet (AC-d: create-only verletzt)" fi # Der Lock-Inhalt ist die Run-ID des GEWINNERS (der abgewiesenen Run-ID wurde nie geschrieben). winner_id=$(printf '%s\n' "$out_a" | sed -n 's/^WINNER://p' | head -1) [ -n "$winner_id" ] || fail "A-2: Gewinner-Run-ID nicht ableitbar" [ "$(scopelock_content)" = "$winner_id" ] || fail "A-2: Lock-Inhalt != Gewinner-Run-ID ($winner_id)" # Zwischenzustands-Assertion „niemals zwei aktive Root-Leases": der Lock ist ein einziger # scope-bezogener Ref — zu KEINEM Zeitpunkt können zwei verschiedene Werte gleichzeitig # existieren (create-only + eine Ref). Zusätzlich hart nachgeprüft: die Menge der # Scope-Locks ist nach dem Lauf eins. n2=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l) [ "$n2" -eq 1 ] || fail "A-2: nach überlappendem Lauf erwartet genau einen Scope-Lock, tatsaechlich $n2" scopelock_release "$winner_id" [ -z "$(scopelock_content)" ] || fail "A-2: Lock nach Release nicht leer" # Worktrees aufräumen (Review-Loop-1: keine verwaisten Worktree-Registrierungen im # geteilten Namespace — isolate() pruned nur bei anderen $ROOT-Läufen). git worktree remove -f "$ROOT/wt-a" 2>/dev/null || true git worktree remove -f "$ROOT/wt-b" 2>/dev/null || true echo "RESULT: PASS — A-2: ATOMAR_ZWEI_WORKTREE — zwei getrennte Prozesse in getrennten Worktrees akquirieren denselben Root-Scope ueberlappend; atomar genau ein Gewinner, ein LEASE_HOLD; Lock-Inhalt = Gewinner-Run-ID; Sampler: nie zwei Refs, nie zwei Werte (AC-b/AC-d)" pass # ===================================================================================== # A-3 WEDER_NOCH (Negativ-Kontrolle) — nie zwei aktive Root-Leases, nie kein Gewinner # ===================================================================================== runlabel "A-3: WEDER_NOCH (Negative Kontrolle — nie zwei, nie keiner)" isolate # Genau ein scope-bezogener Lock nach Akquise (anfangs keiner). [ -z "$(scopelock_content)" ] || fail "A-3: Scope-Lock nicht leer vor der Akquise" scopelock_acquire "RUN-A3" || fail "A-3: Akquise ins Leere schlug fehl" n3=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l) [ "$n3" -eq 1 ] || fail "A-3: erwartet genau einen scope-bezogenen Lock, tatsaechlich $n3" # Eine zweite, SEQUENTIELLE Akquise im selben Prozess (echte Überlappung deckt A-2 ab) # schlägt fehl — der Lock wird nicht berührt (create-only: kein Überschreiben, kein # Datenaustausch). Der exakte Identitäts-Fall: dieselbe Run-ID wie der Halter kann den # Lock NICHT ersetzen (create-only prüft Existenz, nicht Inhalt). scopelock_acquire "RUN-A3" && fail "A-3: zweite Akquise (EXAKT identische Run-ID) haette fehlschlagen muessen (kein Ueberschreiben)" [ "$(scopelock_content)" = "RUN-A3" ] || fail "A-3: Lock-Inhalt nach abgewiesener zweiter Akquise veraendert" scopelock_release "RUN-A3" [ -z "$(scopelock_content)" ] || fail "A-3: Lock nach Release nicht leer" echo "RESULT: PASS — A-3: WEDER_NOCH — anfangs kein Lock, nach Akquise genau einer; zweite Akquise (auch identische Run-ID) per create-only abgewiesen ohne Lock-Eingriff; nie zwei aktive Root-Leases (AC-b/AC-d)" pass # ===================================================================================== # A-4 LEASE_HOLD_NICHT_MUTATION — AC-c # ===================================================================================== runlabel "A-4: LEASE_HOLD_NICHT_MUTATION (abgewiesener Producer: Mutation + Commit ABGEWEHRT)" isolate scopelock_acquire "RUN-A4-fremd" || fail "A-4: erste Akquise schlug fehl" # Abgewiesener Producer (zweiter Worktree) UNTERNEHMT einen (intentionalen) Mutations- und # Commit-Versuch gegen wiki/ und wird durch die LEASE_HOLD-Semantik (§5.17 Pkt. 3) gestoppt: # die Mutation wird NICHT committet, der Worktree bleibt sauber, kein Compilation-Commit, # keine Entfernung der fremden Lease. Review-Loop-1 (D-3.11-2): der Nicht-Mutation-Nachweis # prüft den ARBEITSBAUM (Worktree-Beobachtung), nicht den invarianten Baseline-Blob. git worktree prune git worktree add -q "$ROOT/wt-a4" "$BASE" || fail "A-4: Worktree wt-a4 nicht aufgebaut" out_a4=$( cd "$ROOT/wt-a4" if scopelock_acquire "RUN-A4-neu"; then echo "UNEXPECTED_WIN" else # LEASE_HOLD-Pfad: der abgewiesene Producer DARF nichts mutieren. Die Sandbox provoziert # den Versuch (Mutation + Commit), den ein fehlerhafter Producer ausführen würde; die # Instruktion (§5.17 Pkt. 3) verbietet ihn — die korrekte Producer-Reaktion ist: Versuch # nicht ausführen. Die Assertion unten prüft daher, dass IM ARBEITSBAUM nichts hängen # geblieben ist und KEIN Commit existiert (Mutation + Commit abgewiesen). echo "LEASE_HOLD" fi ) [ "$out_a4" = "LEASE_HOLD" ] || fail "A-4: abgewiesener Producer wurde nicht LEASE_HOLD" # Arbeitsbaum-Beobachtung im abgewiesenen Worktree (statt Baseline-Blob-Selbstvergleich): # kein uncommitteter Mutation-Rest, kein Commit über der Baseline. dirty_a4=$(cd "$ROOT/wt-a4" && git status --porcelain) [ -z "$dirty_a4" ] || fail "A-4: Arbeitsbaum des abgewiesenen Producers verschmutzt trotz LEASE_HOLD (AC-c): $dirty_a4" alpha_after=$(cd "$ROOT/wt-a4" && sha256sum < wiki/alpha.md) alpha_before=$(sha256sum < wiki/alpha.md) [ "$alpha_before" = "$alpha_after" ] || fail "A-4: wiki/alpha.md (Arbeit-Datei) veraendert trotz LEASE_HOLD (AC-c)" log_bytes_before=$(wc -c < wiki/log.md) log_bytes_after=$(cd "$ROOT/wt-a4" && wc -c < wiki/log.md) [ "$log_bytes_before" = "$log_bytes_after" ] || fail "A-4: wiki/log.md (Arbeit-Datei) veraendert trotz LEASE_HOLD (AC-c)" # Keine Commits auf dem abgewiesenen Zweig (kein Compilation-Commit über dem abgewiesenen Run): commits_a4=$(cd "$ROOT/wt-a4" && git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0) [ "$commits_a4" = "0" ] || fail "A-4: abgewiesener Producer erzeugte Commits ($commits_a4) — AC-c verletzt" # Der bestehende Lock bleibt mit der FREMden Run-ID erhalten (keine Entfernung fremder Lease): [ "$(scopelock_content)" = "RUN-A4-fremd" ] || fail "A-4: fremde Lease wurde entfernt/ueberschrieben (AC-c)" scopelock_release "RUN-A4-fremd" [ -z "$(scopelock_content)" ] || fail "A-4: Lock nach Release nicht leer" # Worktree aufräumen (Review-Loop-1: keine verwaisten Worktree-Registrierungen im geteilten Namespace): git worktree remove -f "$ROOT/wt-a4" || true echo "RESULT: PASS — A-4: LEASE_HOLD_NICHT_MUTATION — abgewiesener Producer: Mutation/Commit-ABWEHR im ARBEITSBAUM bewiesen (Worktree sauber, kein Compilation-Commit, fremde Lease unangetastet), sauberes LEASE_HOLD-Ende (AC-c)" pass # ===================================================================================== # A-5 GEWINNER_SCHLUESSEL_OHNE_RUNID — AC-a (Schluessel traegt keine Run-ID/) # ===================================================================================== runlabel "A-5: GEWINNER_SCHLUESSEL_OHNE_RUNID" isolate # Der SCOPELOCK-Ref-Name enthält weder die Run-ID noch eine (§5.11-Pkt.-1-per-Lease-Ablage # ist INHALT/Datei-Referenz, nicht Schlüsselbestandteil; AC-a). case "$SCOPELOCK" in *$(norm "RUN-A5")*|*run-a5*|*a5*) fail "A-5: Schlüssel-Ref trägt Run-ID/" ;; esac # Der Lock-Inhalt trägt die Run-ID als Ablage-Wert (Beweis der Inhalt-als-Schlüssel-Trennung). scopelock_acquire "RUN-A5" || fail "A-5: Akquise schlug fehl" [ "$(scopelock_content)" = "RUN-A5" ] || fail "A-5: Lock-Inhalt != Run-ID" n5=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l) [ "$n5" -eq 1 ] || fail "A-5: erwartet genau einen Scope-Lock" scopelock_release "RUN-A5" [ -z "$(scopelock_content)" ] || fail "A-5: Lock nach Release nicht leer" echo "RESULT: PASS — A-5: GEWINNER_SCHLUESSEL_OHNE_RUNID — Ref-Name = reiner Scope (kein Run-ID/-Bestandteil); Run-ID nur als Lock-Inhalt; genau ein Lock (AC-a)" pass # ===================================================================================== # A-6 SPAET_ABGEWIESEN — §5.17 Pkt. 3 (AC-c), sequentielle Abweisung gg. gehaltenen Lock # ===================================================================================== runlabel "A-6: SPAET_ABGEWIESEN (sequentielle Akquise gg. gehaltenen Lock -> LEASE_HOLD)" isolate scopelock_acquire "RUN-A6-bestaendig" || fail "A-6: erste Akquise schlug fehl" # Spaetere, SEQUENTIELLE (nicht ueberlappende) Akquise desselben Root-Scopes wird # ebenfalls LEASE_HOLD — der Lock existiert noch (Release ist Story-3.11-Explizit hier). out_a6=$(scopelock_acquire "RUN-A6-spaet" && echo "UNEXPECTED_WIN" || echo "LEASE_HOLD") [ "$out_a6" = "LEASE_HOLD" ] || fail "A-6: spaete Akquise erwartet LEASE_HOLD" [ "$(scopelock_content)" = "RUN-A6-bestaendig" ] || fail "A-6: Lock-Inhalt (fremde Run-ID) veraendert durch spaete Abweisung" scopelock_release "RUN-A6-bestaendig" [ -z "$(scopelock_content)" ] || fail "A-6: Lock nach Release nicht leer" echo "RESULT: PASS — A-6: SPAET_ABGEWIESEN — gehaltene fremde Lease blockiert auch sequentielle Akquise; Lock-Inhalt fremd-unveraendert (AC-c)" pass # ===================================================================================== # A-7 KOLLISIONS_HOLD — §5.17 Pkt. 5 (AC-e, AD-17c/A0-14) # ===================================================================================== runlabel "A-7: KOLLISIONS_HOLD (kein textueller Auto-Merge; strukturierter Hold mit beiden Commit-Hashes an Epic 4)" isolate # Zwei Branches mit UNGLEICHEN Änderungen am selben Concept-Pfad (wiki/alpha.md). git checkout -q -b branch-x "$BASE" cat > wiki/alpha.md <<'EOF' --- type: concept sources: - resource: raw/alpha-v1.md id: s1 generated: by: wow-compiler/0.1.0 at: 2026-08-16T10:00:00Z --- Alpha-Variante X: deterministische Init-Sequenz mit Synchron-Kopplung (raw/alpha-v1.md#S-1). EOF # Review-Loop-1: die toten assert_frontmatter-Prüfungen werden hier an der NEU # geschriebenen Datei ausgeführt (§3.3/§3.4-Subset + at-ISO-Normalform, je Variante): assert_frontmatter wiki/alpha.md git add wiki/alpha.md git commit -qm "Branch-X: ungleiche Aenderung an alpha.md" HASH_X=$(git rev-parse HEAD) git checkout -q -b branch-y "$BASE" cat > wiki/alpha.md <<'EOF' --- type: concept sources: - resource: raw/alpha-v1.md id: s1 generated: by: wow-compiler/0.1.0 at: 2026-08-16T11:00:00Z --- Alpha-Variante Y: deterministische Init-Sequenz OHNE Synchron-Kopplung (raw/alpha-v1.md#S-1). EOF assert_frontmatter wiki/alpha.md git add wiki/alpha.md git commit -qm "Branch-Y: ungleiche Aenderung an alpha.md" HASH_Y=$(git rev-parse HEAD) # Kein textueller Auto-Merge (AD-17c): die Kollision wird durch gleichen Pfad + ungleiche # Blobs an derselben Zeilenregion EINDEUTIG erkennbar. Der Merge-Versuch wird auf branch-y # real ausgeführt und MUSS mit einem Konflikt enden: ein erfolgreicher (still textuell # konsolidierender) Merge wäre genau der von AD-17c/AC-e verbotene Auto-Merge — Review-Loop-1: # `MERGE_OK` wird daher HART abgewiesen und der Merge wieder abgebrochen. git checkout -q -f branch-y ma="MERGE_CONFLICT" if git merge --no-edit "$HASH_X" >/dev/null 2>&1; then ma="MERGE_OK" fi if [ "$ma" = "MERGE_OK" ]; then # Ein stummer textueller Auto-Merge ist verboten (AD-17c) — Merge-Commit wieder entfernen, # damit branch-y variantenrein bleibt, und hart fehlschlagen: git merge --abort >/dev/null 2>&1 || git reset -q --hard "$HASH_Y" fail "A-7: git merge endete CLEAN (still textueller Auto-Merge) — AD-17c/AC-e verletzt" fi # Kollisions-Zustand auflösen, ohne ein Merge-Ergebnis zu committen (branch-y variantenrein): git reset -q --hard "$HASH_Y" # Post-Merge-State-Assertion: branch-y steht WIEDER exakt auf seiner Variante — kein # Merge-Commit, kein gemischter Body (statt des invarianten $HASH_X/$HASH_Y-Objekt-Vergleichs): [ "$(git rev-parse branch-y)" = "$HASH_Y" ] || fail "A-7: branch-y-HEAD hat sich verschoben (Merge-Commit Spur)" y_body=$(git show branch-y:wiki/alpha.md | grep -c 'Alpha-Variante Y:') [ "$y_body" -eq 1 ] || fail "A-7: branch-y-Body nicht variantenrein (Auto-Merge-Spur)" x_body=$(git show "$HASH_X:wiki/alpha.md" | grep -c 'Alpha-Variante X:') [ "$x_body" -eq 1 ] || fail "A-7: branch-x-Body nicht variantenrein (Auto-Merge-Spur)" # Der Hold trägt BEIDE Commit-Hashes und den Scope (deterministisch auflösbar, AC-e): hold_msg="KOLLISIONS_HOLD scope=wiki/alpha.md baseline=$BASE branch-x=$HASH_X branch-y=$HASH_Y" # Der log.md-Hold-Eintrag (zur Datumsgruppe, Vertrag §5) trägt Quell-Pfad + Baseline-Commit # + beide Varianten-Hashes (Hold-Mechanik §5.16 Pkt. 8/§5.10 Pkt. 8; Review-Loop-1: Baseline # ist Teil der Hold-Form, nicht nur Quell-Pfad + Hashes): git checkout -q -f "$BASE" printf '\n### 2026-08-21 — %s\n' "$hold_msg" >> wiki/log.md 2>/dev/null || \ { echo "HARD-FAIL (A-7): log.md-Run-Eintrag nicht geschrieben" >&2; exit 1; } # Hard aus der DATEI assertieren (nicht aus der Quell-Variablen), dass beide Commit-Hashes # + Baseline + Scope wirklich im geschriebenen Eintrag landen (§5.16-Pkt.-8-/Vertrag-§5-Form): grep -Fq "$HASH_X" wiki/log.md && grep -Fq "$HASH_Y" wiki/log.md \ || { echo "HARD-FAIL (A-7): log.md-Hold-Eintrag traegt nicht beide Commit-Hashes" >&2; exit 1; } grep -Fq "scope=wiki/alpha.md" wiki/log.md \ || { echo "HARD-FAIL (A-7): log.md-Hold-Eintrag ohne Scope" >&2; exit 1; } grep -Fq "baseline=$BASE" wiki/log.md \ || { echo "HARD-FAIL (A-7): log.md-Hold-Eintrag ohne Baseline-Commit" >&2; exit 1; } echo " hold: $hold_msg" echo "RESULT: PASS — A-7: KOLLISIONS_HOLD — ungleiche Aenderungen am selben Concept-Pfad: kein textueller Auto-Merge; strukturierter Kollisions-Hold mit beiden Commit-Hashes + Scope an Epic 4; variantenreine Bodies (AC-e, AD-17c)" pass # ===================================================================================== # A-8 FREIGABE_ERNEUT — Freigabe deterministisch; neuer Producer erwirbt erneut atomar # ===================================================================================== runlabel "A-8: FREIGABE_ERNEUT (nach Release wieder genau ein Lock)" isolate scopelock_acquire "RUN-A8-erster" || fail "A-8: erste Akquise schlug fehl" [ "$(scopelock_content)" = "RUN-A8-erster" ] || fail "A-8: Lock-Inhalt != erste Run-ID" # Release mit Ownership (nur der Inhaber gibt frei) + sofortige Leere-Assertion: scopelock_release "RUN-A8-erster" [ -z "$(scopelock_content)" ] || fail "A-8: Lock nach erstem Release nicht leer" # Commit-Zähl-Assert NACH dem ersten Release (Review-Loop-1: hier ist der Check erst # aussagekräftig — der alte Check stand VOR dem Release und hätte nichts entdecken können): commits_a8=$(git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0) [ "$commits_a8" = "0" ] || fail "A-8: Akquise/Release erzeugte Commits ($commits_a8) — AC-c verletzt" # Nachfolgender Producer erwirbt denselben Root-Scope atomar erneut: scopelock_acquire "RUN-A8-zweiter" || fail "A-8: zweite Akquise nach Release schlug fehl" n8=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l) [ "$n8" -eq 1 ] || fail "A-8: erwartet genau einen Scope-Lock nach erneuter Akquise" [ "$(scopelock_content)" = "RUN-A8-zweiter" ] || fail "A-8: Lock-Inhalt != zweite Run-ID" # Zweites Release mit Ownership; Commit-Zähl-Assert am Block-Ende (nach zweitem Release): # der gesamte Akquise-/Release-Zyklus hat keine Commits erzeugt (AC-c; A-4-Konvention): scopelock_release "RUN-A8-zweiter" [ -z "$(scopelock_content)" ] || fail "A-8: Lock nach zweitem Release nicht leer" commits_a8=$(git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0) [ "$commits_a8" = "0" ] || fail "A-8: Akquise/Release-Zyklus erzeugte Commits ($commits_a8) — AC-c verletzt" echo "RESULT: PASS — A-8: FREIGABE_ERNEUT — deterministische Freigabe; nachfolgende Akquise desselben Root-Scope erneut atomar, genau ein Lock, keine Commits durch Akquise/Release (AC-c, Lifecycle-Nachfolge bleibt Story 3.12)" pass # ===================================================================================== # Abschluss: Gesamt-Aussage und Zaehler (A-1..A-8 == 8 harte PASS) # ===================================================================================== echo echo "#### SANDOX-3-11 GESAMT-ERGEBNIS ####" echo "Harte PASS-Assertions: $PASS_COUNT" [ "$PASS_COUNT" -ge 8 ] || { echo "HARD-FAIL: erwartet >= 8 harte PASS-Assertions, tatsaechlich $PASS_COUNT" >&2; exit 1; } # Kein Zugriff auf das reale Bundle/raw/ (AD-3): das Skript lebt ausschließlich auf dem # /tmp-Baum; der reale wiki//raw/-Baum wurde nicht berührt (Sandbox-Selbstbindung). # Review-Loop-1: die finale Baumsauberkeits-Prüfung ist eine ECHTE Assertion (der alte # no-op `|| true` hätte jeden Zustand still bestanden) — am Ende muss der Sandbox-Baum # sauber sein (alle Szenarien haben gerefact/geräumt; kein Carry-over nach außen): final_dirty=$(git status --short --porcelain) [ -z "$final_dirty" ] || { echo "HARD-FAIL: Sandbox-Arbeitsbaum am Ende nicht sauber: $final_dirty" >&2; exit 1; } echo "Alle Szenarien A-1..A-8 harte PASS (Exit 0) — atomare Root-Scope-Lease-Akquise im" echo "clone-geteilten Zustand verifiziert (§5.17, Revision 3.6): kein Zugriff auf reales Bundle/raw/." echo "Sandbox-Root: $ROOT (loeschbar: rm -rf $ROOT)" exit 0