feat: Story 3.11 — Review-Loop-1-Abschluss committen (20 Patches, D-3.11-1/2, Sandbox A-1..A-8 8/8)

Abschluss des in der vorigen Session erarbeiteten, nach Commit e02cf84
liegengebliebenen Review-Loop-1 von Story 3.11 (Root-Scope-Leasing atomar):

- wiki/log.md: Eintrag „Story 3.11 → Review-Loop-1-Abschluss + done"
  (D-3.11-1 Konstruktiv+härten, D-3.11-2 Intentionaler Mutationsversuch;
   20 angewendete Patches; kein Loopback)
- sprint-status.yaml: Key 3-11-… → done (finaler Step-05-Flip), last_updated 15:10
- schema/compiler.md: §5.17-Pkt.-1/4/5-Berichtigungen + §8-Revision-3.6-Nachführung
- spec-3-11 + sandbox-3-11: 20 Loop-1-Patches; deferred-work.md: 5 Defer-Einträge
  + KORREKTUR-Teileintrag (Zeilen-Drift-Defer widerlegt)

Erhaltungs-Invariante §5.9 Pkt. 5: git status --porcelain -- wiki/ zeigt nur
wiki/log.md; AD-3 read-only; kein Standalone, keine neue §7-Klasse.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Tamse
2026-08-21 20:00:21 +02:00
co-authored by Claude
parent e02cf84501
commit 80480af3ec
6 changed files with 220 additions and 119 deletions
@@ -102,6 +102,10 @@ isolate() {
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) ----------
@@ -125,10 +129,15 @@ scopelock_acquire() { # $1 = Run-ID (Lock-Inhalt; PRODUCER sichtbar)
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_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
@@ -175,8 +184,8 @@ fail() { echo "HARD-FAIL: $1" >&2; exit 1; }
# =====================================================================================
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)
# 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
@@ -185,7 +194,8 @@ 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
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
@@ -223,21 +233,34 @@ run_acquire_in_wt() { # $1 = Worktree ; $2 = Run-ID
)
}
# 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).
# (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" &
# 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
seen_vals=""
for i in $(seq 1 25); do
cnt=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK")
[ "$cnt" -gt 1 ] && { echo "TWO_LOCKS_AT_ONCE:i$i" >&2; }
[ "${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
)
@@ -249,11 +272,16 @@ 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).
# 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 aktiven Root-Leases beobachtet (AC-d)"
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)
@@ -265,8 +293,13 @@ winner_id=$(printf '%s\n' "$out_a" | sed -n 's/^WINNER://p' | head -1)
# 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)"
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
# =====================================================================================
@@ -279,51 +312,64 @@ isolate
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)"
# 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
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 veraendert nichts)"
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"
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.
# 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";
echo "UNEXPECTED_WIN"
else
# LEASE_HOLD-Pfad: der abgewiesene Producer tut NICHTS (keine Mutation), beendet sauber.
# 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"
# 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)"
# 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)"
# 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)"
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
# =====================================================================================
@@ -339,7 +385,8 @@ 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
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/<id>-Bestandteil); Run-ID nur als Lock-Inhalt; genau ein Lock (AC-a)"
pass
@@ -354,7 +401,8 @@ scopelock_acquire "RUN-A6-bestaendig" || fail "A-6: erste Akquise schlug fehl"
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
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
@@ -377,6 +425,9 @@ generated:
---
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)
@@ -394,63 +445,52 @@ generated:
---
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): 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:')
# 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 log.md-Hold-Eintrag (zur Datumsgruppe, Vertrag §5) trägt Quell-Pfad + Baseline-Commit:
# 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
# + Scope wirklich im geschriebenen Eintrag landen (§5.16-Pkt.-8-/Vertrag-§5-Form):
# 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
@@ -462,23 +502,24 @@ 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):
# 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: Release erzeugte Commits ($commits_a8) — AC-c verletzt"
scopelock_release
[ -z "$(scopelock_content)" ] || fail "A-8: Lock nach Release nicht leer"
[ "$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"
# 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):
# 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: Release erzeugte Commits ($commits_a8) — AC-c verletzt"
scopelock_release
[ "$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
@@ -491,7 +532,11 @@ 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
# 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)"