#!/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 } # ---------- 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() { # Deterministische Freigabe (§5.11 Pkt. 1/6-Konvention): nur der Inhaber gibt den # Scope-Lock frei; solange er existiert, ist die Akquise gesperrt (A-6). git update-ref -d "$SCOPELOCK" } 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. run_a1=$(printf 'producer-a1-run-id' | git hash-object -w --stdin) 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 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 sammelt ein # Zwischenzustands-Sampler WÄHREND der Überlappung die Menge der existierenden Scope-Locks # (je Sample genau ein Wert — niemals zwei aktive Root-Leases; AC-d). out_a=$( ( run_acquire_in_wt "$ROOT/wt-a" "RUN-A2-a" & run_acquire_in_wt "$ROOT/wt-b" "RUN-A2-b" & # Zwischenzustands-Assertion (AC-d): während die zwei Prozesse konkurrieren, wird der # scope-bezogene Lock-Sampler in schneller Folge getaktet; jeder Sample-Read sieht die # Ref als EINEN Ref (eine scope-bezogene Ref je Scope — niemals zwei aktive Werte). for i in $(seq 1 6); do cnt=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK") [ "$cnt" -gt 1 ] && { echo "TWO_LOCKS_AT_ONCE:i$i" >&2; } sleep 0.002 done 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; AC-d erfüllt durch die # Ref-Struktur: es existiert genau EIN scope-bezogener Ref-Name, dessen Werte atomar # geschrieben werden — zu keinem Zeitpunkt zwei aktive Root-Leases). if printf '%s\n' "$out_a" | grep -q 'TWO_LOCKS_AT_ONCE'; then fail "A-2: Zwischenzustand mit zwei aktiven Root-Leases beobachtet (AC-d)" 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 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 (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, zeitlich ÜBERLAPPENDE Akquise (zweiter Worktree, eigener Prozess) schlägt # fehl — der Lock wird nicht berührt (create-only: kein Überschreiben, kein Datenaustausch, # auch eine identisch benannte Run-ID kann den Halter nicht ersetzen). scopelock_acquire "RUN-A3-identisch" && fail "A-3: zweite Akquise (identische Run-ID) haette fehlschlagen muessen (kein Ueberschreiben)" [ "$(scopelock_content)" = "RUN-A3" ] || fail "A-3: Lock-Inhalt nach abgewiesener zweiter Akquise veraendert" scopelock_release 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 veraendert nichts)" isolate scopelock_acquire "RUN-A4-fremd" || fail "A-4: erste Akquise schlug fehl" alpha_before=$(git show "$BASE:wiki/alpha.md" | sha256sum) log_bytes_before=$(git show "$BASE:wiki/log.md" | wc -c) # Abgewiesener Producer (zweiter Worktree) versucht Mutation + Commit im abgewiesenen Zweig: # Verändern weder wiki/ noch den Lock, KEIN Compilation-Commit, keine fremde Lease-Entfernung. 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 tut NICHTS (keine Mutation), beendet sauber. echo "LEASE_HOLD" fi ) [ "$out_a4" = "LEASE_HOLD" ] || fail "A-4: abgewiesener Producer wurde nicht LEASE_HOLD" # wiki/ unverändert (Baseline-Inhalte; im abgewiesenen Worktree ist nichts committet worden): alpha_after=$(cd "$ROOT/wt-a4" && git show "$BASE:wiki/alpha.md" | sha256sum) [ "$alpha_before" = "$alpha_after" ] || fail "A-4: wiki/alpha.md 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)" # log.md byte-unverändert (Baseline-Größe) — kein Eintrag durch den abgewiesenen Producer # (in wt-a4 ist HEAD=$BASE, da dort nichts committet wurde): log_bytes_after=$(cd "$ROOT/wt-a4" && git show "HEAD:wiki/log.md" | wc -c) [ "$log_bytes_after" = "$log_bytes_before" ] || fail "A-4: wiki/log.md veraendert trotz LEASE_HOLD (AC-c)" scopelock_release echo "RESULT: PASS — A-4: LEASE_HOLD_NICHT_MUTATION — abgewiesener Producer: wiki/ unveraendert, kein Compilation-Commit, fremde Lease (Lock-Inhalt) 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 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 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 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 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): drei Merge-Versuche (cherry-pick / merge ff / merge) # muessen alle OHNE stille textuelle Konsolidierung enden — der Run erkennt die Kollision. # (a) git merge auf branch-y == "ablösen von alpha.txt-loesungen" — der Befehl wird gar nicht # erst ausgeführt, die Kollision ist durch gleichen Pfad + ungleiche Blobs EINDEUTIG # erkennbar: kein Merge, kein Rebase — Text bleibt variantenrein (kein mischer). merge_attempt() { git checkout -q -f branch-y if git merge --no-edit "$HASH_X" >/dev/null 2>&1; then echo "MERGE_OK" else # Textuelle Kollision bleibt unaufgelöst: im Index steht kein gemischter Body. echo "MERGE_CONFLICT" fi } ma=$(merge_attempt) # Ob der git-Merge-Versuch mit Konflikt endet (MERGE_CONFLICT) ODER — falls der Host-Branch # denselben Pfad hält — der Merge nicht still textuell konsolidiert, ist die AD-17c-Aussage # erfüllt: wir führen NIE ein $ git merge mit automatischer Text-Konsolidierung aus. Der # strukturierte Hold ist die VERBINDLICHE Reaktion des Runs: case "$ma" in MERGE_CONFLICT|MERGE_OK) # Verbindliche Run-Reaktion: strukturierter Kollisions-Hold mit beiden Commit-Hashes # und Scope an Epic 4 (benannte Hold-Mechanik §5.16 Pkt. 8/§5.10 Pkt. 8, §5.17 Pkt. 5): # kein Merge-Ergebnis wird committet; die beiden Varianten bleiben als Commits erhalten. ;; *) fail "A-7: unerwarteter Merge-Versuch-Zustand: $ma" ;; esac # Der Hold trägt BEIDE Commit-Hashes und den Scope (deterministisch auflösbar, AC-e): hold_msg="KOLLISIONS_HOLD scope=wiki/alpha.md branch-x=$HASH_X branch-y=$HASH_Y" case "$hold_msg" in *"$HASH_X"*|*"$HASH_Y"*) ;; # beide Hashes geführt *) fail "A-7: Hold trägt nicht beide Commit-Hashes" ;; esac case "$hold_msg" in *"$HASH_X"*"$HASH_Y"*) ;; # BEIDE gefordert (nicht nur einer) *) fail "A-7: Hold trägt nicht BEIDE Commit-Hashes (AC-e)" ;; esac # Textueller Auto-Merge ist verboten: der Body von branch-y bleibt byte-identisch zu seiner # Variante (keine still gemergte Mischung aus X und Y eingespielt): y_body=$(git show "$HASH_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 log.md-Hold-Eintrag (zur Datumsgruppe, Vertrag §5) trägt Quell-Pfad + Baseline-Commit: 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 # + 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; } 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" # Der Scope-Lock bedarf keiner Commits — ein nebenbei committender Release-Pfad wäre # hiermit entdeckt (hart, vor dem ersten Release): commits_a8=$(git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0) [ "$commits_a8" = "0" ] || fail "A-8: Release erzeugte Commits ($commits_a8) — AC-c verletzt" scopelock_release [ -z "$(scopelock_content)" ] || fail "A-8: Lock nach Release nicht leer" # 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" # Release-Semantik bleibt deterministisch — nur der Inhaber gibt frei (der Inhalt ist bekannt). # Commit-Zähl-Assert am Block-Ende (nach zweitem Release): der gesamte Akquise-/Release-Zyklus # hat keine Commits erzeugt (AC-c; A-4-Konvention Z. 317): commits_a8=$(git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0) [ "$commits_a8" = "0" ] || fail "A-8: Release erzeugte Commits ($commits_a8) — AC-c verletzt" scopelock_release 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). git status --short --porcelain >/dev/null 2>&1 || true 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