feat: Story 3.11 — Root-Scope-Leasing atomar und worktree-übergreifend akquirieren (Revision 3.6, Verankerung §5.17)

This commit is contained in:
Michael Tamse
2026-08-21 13:40:50 +02:00
parent a8b486d0f0
commit e02cf84501
7 changed files with 654 additions and 4 deletions
@@ -0,0 +1,498 @@
#!/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
# <ref> <wert> $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-/<id>-Bestandteil im Schlüssel); die per-Lease-<id>-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/<id>)
# =====================================================================================
runlabel "A-5: GEWINNER_SCHLUESSEL_OHNE_RUNID"
isolate
# Der SCOPELOCK-Ref-Name enthält weder die Run-ID noch eine <id> (§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/<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/<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