feat: Story 3.12 — Lease-Lifecycle und Commit-Abschluss transaktional schließen (Revision 3.7, §5.18; Sandbox L-1..L-9 9/9 harte PASS/Exit 0)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Tamse
2026-08-21 20:49:50 +02:00
co-authored by Claude
parent 80480af3ec
commit 7f839febab
7 changed files with 860 additions and 4 deletions
@@ -0,0 +1,687 @@
#!/usr/bin/env bash
# Story 3.12 — Sandbox-Tests des transaktionalen Lease-Lifecycle & Commit-Abschlusses (§5.18, Revision 3.7)
# Re-executierbar: bash run-sandbox.sh (ab Workspace-Root; Sandbox unter /tmp/sb312)
# Zweck: die §5.18-Lifecycle-Klammer (Story 3.12) als re-executierbarer Run-Demonstrator
# durchspielen — Akquise (§5.11/§5.17) -> Preflight/Protect (§5.11 Pkt. 3) -> Mutation
# -> Rollback (§5.13) -> durable Release -> Clean-Input-Guard (AC-1..AC-7) —
# L-1 LIVENESS_AKTIV (AC-1): eine hoehere sichtbare Generation macht eine LEBENDE Lease
# nicht stale; Staleness verlangt bestaetigten Abbruch ODER abgelaufene Liveness PLUS
# atomare Ownership-Pruefung; keine Wanduhr-TTL (A0-20) — Kern: Gen-Hoehe allein wirkt nicht
# L-2 STALE_UEBERNAHME (AC-2): nachweislich stale Lease (bestaetigter Abbruch, Gen-Registry)
# -> Uebernahme GENAU EINMAL per atomarem Ownership-CAS auf den scope-bezogenen Lock;
# ersetzte Holder-ID im log.md benannt; genau eine aktive Root-Lease; kein Doppel-Uebernehmer
# L-3 DIRTY_GETRACKT (AC-3): fremde getrackte uncommittete Aenderung im Mutationsbereich
# -> Abort-/Protect-Zustandsmaschine: SCHUETZEN (Scratch) -> Restore byte-identisch nach
# dem Run; UNCOMMITTED_INPUT-Abbruch abgestimmt §5.11 Pkt. 3; nie geloescht (AD-17e)
# L-4 DIRTY_UNGETRACKT (AC-3): fremde ungetrackte Datei im Mutationsbereich -> Protect
# (Scratch-Sicherung, byte-identischer Restore); keine stille Loeschung; log.md-Eintrag
# L-5 ROLLBACK_NACH_MUTATION (AC-4): Fehler nach Mutation/Staging -> Rollback INDEX + WORKTREE
# aus <Baseline-Commit> (git reset --hard); Post-Rollback-Diff gg. Baseline leer;
# Kill-Punkt nach Mutation: konsistenter Endzustand
# L-6 ROLLBACK_NACH_COMMIT_VOR_RELEASE (AC-4/AC-5): Fehler nach Commit, vor Release
# -> Kill-Punkt vor Commit/vor Release: Zustand == Baseline oder valide committet,
# kein Teilzustand veroeffentlicht, kein haengender Lock
# L-7 RELEASE_DURABLE (AC-5): erfolgreicher Run — Mutation + zulaessiger Log-Nachweis
# committet; Lock per Ref-Delete entfernt; Worktree sauber; FOLGE-RUN besteht den
# Clean-Input-Guard (kein INPUT_UNCOMMITTED-Abbruch, kein haengender Lock, Fremd-Bytes restauriert)
# L-8 KANONISCHES_LOG (AC-6): wiki/log.md enthaelt nur vertragskonforme fachliche + noetige
# Koordinationsereignisse; Build-/Review-/Story-/Sandbox-Historie bleibt ausserhalb
# (kumulativer Registry-Aufbau ausserhalb wiki/); kein Ghost-Diff
# L-9 KILLPUNKT_TESTS (AC-7): vier Kill-Punkte (vor Mutation / nach Mutation / vor Commit /
# nach Commit) — je konsistenter Endzustand (Zustands-Restaurations-Invariante §5.13 Pkt. 3)
# Die §5.18-Realisierung uebernimmt die §5.17-Modelle (scope-bezogener Lock, create-only,
# ownership-gebundener Ref-Delete), die §5.12-Registry/Gen/Liveness- und §5.11-Pkt.-3-
# Schutzmechanik und prueft die Lifecycle-Klammer inkl. KillPoint-Tests + Clean-Input-Guard.
# Ubuntu-Sandbox-Semantik auf einem /tmp-Baum — NIE der reale wiki/- oder raw/-Baum.
set -u
ROOT=$(mktemp -d /tmp/sb312-XXXXXX)
SB="$ROOT/sb"
mkdir -p "$SB/wiki" "$SB/raw" "$SB/scratch"
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() {
git checkout -qf "$1" 2>/dev/null || git checkout -qf "$BASE"
git reset -q --hard "$BASE"
git clean -qfd wiki raw scratch registry lease plan-run
# Geteilter Ref-Namespace (Root-Scope-Lock) gehoert zur Isolation (Review-Loop-1-Konvention 3.11):
git update-ref -d "$SCOPELOCK" 2>/dev/null || true
}
# ---------- Atomarer Root-Scope-Lock (§5.17 Pkt. 1/2, uebernommen aus sandbox-3-11) ----------
SCOPELOCK="refs/leases/wiki" # exklusiver scope-bezogener Lock (AC-a)
ZERO=$(printf '%040d' 0) # $ZERO_SHA für create-only
scopelock_acquire() { # $1 = Run-ID (Lock-Inhalt; PRODUCER sichtbar) — create-only
local runid="$1" val
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-Pruefung)
[ "$(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() {
local val
val=$(git rev-parse -q --verify "$SCOPELOCK" 2>/dev/null) || { echo ""; return 0; }
git cat-file -p "$val" 2>/dev/null || echo ""
}
# Ownership-CAS: Uebernahme nur, wenn der Lock (noch) vom erwarteten Inhaber gehalten wird
# (atomare Ownership-Pruefung, §5.18 Pkt. 1/2). Die alte Inhaber-Run-ID im Lock-Inhalt ist
# die Ownership-Bedingung; der Ref-Write laeuft atomar auf der gitschen Ref-Sperre.
scopelock_takeover() { # $1 = erwartete alte Run-ID (Lock-Inhalt) $2 = neue Run-ID
local old="$1" new="$2" newval
[ "$(scopelock_content)" = "$old" ] \
|| { echo "HARD-FAIL (Ownership): Lock-Inhalt '$old' erwartet, tatsaechlich '$(scopelock_content)'" >&2; exit 1; }
newval=$(printf '%s' "$new" | git hash-object -w --stdin) || return 2
git update-ref "$SCOPELOCK" "$newval" 2>/dev/null \
|| { echo "HARD-FAIL (Takeover): Ref-Write fehlgeschlagen" >&2; exit 1; }
}
# ---------- Registry / Gen / Liveness (§5.12, uebernommen aus sandbox-3-6) ----------
registry_path() { echo "registry/$1"; }
reg_gen() { # $1=area ; groesste committet sichtbare Generation der Registry (0 = leer)
local f="registry/$1"
[ -f "$f" ] || { echo 0; return; }
awk '/^gen: [0-9]+$/{ if ($2>m) m=$2 } END{ print (m==""?0:m) }' "$f"
}
reg_write() { # $1=area $2=id $3=producer $4=erzeugungs_generation
local f="registry/$1" cur
mkdir -p "$(dirname "$f")"; touch "$f"
cur=$(reg_gen "$1")
{ grep -vE "^gen: |^stale: $2([[:space:]]|$)|^hold: $2([[:space:]]|$)" "$f" 2>/dev/null || true; } > "$f.tmp"
if [ -n "$4" ] && { [ "$4" -gt "$cur" ] || [ "$4" = "$cur" ]; }; then
echo "gen: $4" >> "$f.tmp"
else
echo "gen: $cur" >> "$f.tmp"
fi
mv "$f.tmp" "$f"
}
reg_hold_mark() { # $1=area $2=id $3=aktuelle_generation — idempotent
local f="registry/$1"
mkdir -p "$(dirname "$f")"; touch "$f"
grep -vE "^hold: $2([[:space:]]|$)" "$f" > "$f.tmp" 2>/dev/null || true
echo "hold: $2 (Gen $3)" >> "$f.tmp"
mv "$f.tmp" "$f"
reg_bump "$f" "$3"
}
reg_stale_mark() { # $1=area $2=id $3=aktuelle_generation — idempotent (Marker-Duplikat vermeiden)
local f="registry/$1"
mkdir -p "$(dirname "$f")"; touch "$f"
grep -vE "^stale: $2([[:space:]]|$)" "$f" > "$f.tmp" 2>/dev/null || true
echo "stale: $2 (Gen $3)" >> "$f.tmp"
mv "$f.tmp" "$f"
reg_bump "$f" "$3"
}
reg_bump() { # $1=registry-datei $2=markierungs-generation ; setzt gen: auf max(cur,$2) (Pkt. 6)
local f="$1" gen="$2" cur
cur=$(reg_gen "${f#registry/}")
{ grep -v '^gen: ' "$f" 2>/dev/null || true; } > "$f.tmp"
if [ "$gen" -gt "$cur" ]; then
echo "gen: $gen" >> "$f.tmp"
else
echo "gen: $cur" >> "$f.tmp"
fi
mv "$f.tmp" "$f"
}
lease_stale() { # $1=area $2=id $3=erzeugungs_gen: true (0) wenn Erzeugungs-Gen < hoechster Reg-Gen
local f="registry/$1" g
[ -f "$f" ] || return 1
g=$(reg_gen "$1")
[ "$g" -gt "$3" ] 2>/dev/null || return 1
return 0
}
# §5.18-Pkt.-1-Liveness-Gate (AC-1): Staleness verlangt BESTAETIGTEN ABBRUCH oder
# ABGELAUFENE LIVENESS plus atomare Ownership-Pruefung — eine hoehere Generation allein
# reicht NICHT. Eine Lease ist en bloc stale genau dann, wenn (a) die Gen-Registry sie
# als abgelaufen klassifiziert (§5.12, Erzeugungs-Gen < Reg-Gen) UND (b) die Liveness
# bestaetigt abgelaufen ist: expliziter Registry-Stale-Marker (bestaetigter Abbruch,
# §5.12 Pkt. 3/5) ODER der Halter haelt den Root-Scope-Lock nicht mehr (Lock leer bzw.
# Lock-Inhalt != Halter-Run-ID — Ownership-Quelle, §5.17 Pkt. 1). Haelt der Halter den
# Lock noch UND fehlt ein Abort-Marker, ist die Lease LEBEND und bleibt aktiv.
# $1=area $2=id $3=erzeugungs_gen $4=erwartete-halter-runid (Lock-Inhalt)
lease_liveness_stale() {
local area="$1" id="$2" gen="$3" holder_runid="$4"
if ! lease_stale "$area" "$id" "$gen"; then
return 1 # nicht generationen-abgelaufen -> nicht stale
fi
# (b) bestaetigter Abbruch / abgelaufene Liveness:
# - Registry-Stale-Marker (explizite Verwaisung) ODER
# - Halter haelt den Lock nicht mehr (Inhalt != Halter-Run-ID / Lock leer)
if [ -f "registry/$area" ] && grep -qE "^stale: $id([[:space:]]|$)" "registry/$area"; then
return 0 # bestaetigter Abbruch (Registry-Marker) -> stale
fi
if [ "$(scopelock_content)" != "$holder_runid" ]; then
return 0 # Halter haelt Lock nicht mehr -> abgebrochen -> stale
fi
return 1 # lebende Lease (kein Abort-Marker, Halter haelt Lock) -> NICHT stale
}
assert_no_wallclock() { # Datei(en) duerfen keinen Wanduhr-Zeitstempel tragen (A0-20)
local f
for f in "$@"; do
grep -qiE 'timestamp|wallclock|now|date:|20[0-9]{2}-[0-9]{2}-[0-9]{2}[T ]' "$f" \
&& { echo "HARD-FAIL (A0-20): Wanduhr-Zeitstempel in $f — kein Zeit-TTL als Steuergroesse (§5.12 Pkt. 1/7)" >&2; exit 1; } || true
done
}
# ---------- Erhaltungs-Invariante-Probe (§5.9 Pkt. 5 / AD-5 / FT-6) ----------
probe() {
{ git diff --name-only "$BASE" -- wiki/ ; git status --porcelain -- wiki/ | awk '{print $2}'; } \
| sed -e 's|^wiki/||' -e 's|\.md$||' | LC_ALL=C sort -u
}
inv_set() { probe | LC_ALL=C sort -u | paste -sd' ' -; }
inv_viol() { # $1=expected ; 0 = konsistent, !=0 = Verstoß
local expected="$1" p u bad=0 got
got=$(inv_set)
for p in $got; do
case " $expected " in
*" $p "*) ;;
*) echo "HARD-FAIL (Erhaltungs-Invariante §5.9 Pkt. 5): '$p' ist kein Ghost-Diff-negativer Eintrag — erlaubte Menge: {$expected}" >&2; bad=1;;
esac
done
for u in $(git status --porcelain -- wiki/ | grep '^??' | awk '{print $2}'); do
u=$(echo "$u" | sed -e 's|^wiki/||' -e 's|\.md$||')
case " $expected " in
*" $u "*) ;;
*) echo "HARD-FAIL (Duplikat/Ghost-Diff): ungetrackte neue Datei '$u' liegt ausserhalb der erlaubten Ziel-Pfade {$expected} (§5.9 Pkt. 8)" >&2; bad=1;;
esac
done
return $bad
}
assert_invariant() { # positive Erwartung: Verstoß => HARD-FAIL + Exit 1
if inv_viol "$1"; then
echo "RESULT: PASS — Probe erfuellt; keine neue Datei; kein Ghost-Diff"
else
exit 1
fi
}
# ---------- Frontmatter-Konformitaet (Vertrag §3.3/§3.4; Muster 3.11) ----------
assert_frontmatter() {
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; }
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; }
}
# Banner-Vereinheitlichung (Story 3.12 uebernimmt die 3.11-Konvention; Defer aufgegriffen):
scopelock_header_banner() { # $1 = Run-Id — dokumentierender Kopf des Lifecycle-Eintrags
echo "--- Lifecycle-Run: $1 (Root-Scope wiki/, Lock refs/leases/wiki) ---"
}
# Zähler harter Assertions
PASS_COUNT=0
pass() { PASS_COUNT=$((PASS_COUNT+1)); }
fail() { echo "HARD-FAIL: $1" >&2; exit 1; }
# =====================================================================
# L-1 LIVENESS_AKTIV (AC-1) — eine hoehere sichtbare Generation macht eine
# LEBENDE Lease nicht stale; Staleness verlangt bestaetigten Abbruch ODER
# abgelaufene Liveness PLUS atomare Ownership-Pruefung; keine Wanduhr (A0-20).
# =====================================================================
runlabel "L-1: LIVENESS_AKTIV (AC-1) — hoehere Gen-Hoehe macht lebende Lease nicht stale"
isolate l1
# Producer erwirbt den Root-Scope (Run-ID als Lock-Inhalt) und registriert die Lease
# (Registry, Erzeugungs-Gen 1).
scopelock_acquire "RUN-L1-holder" || fail "L-1: Root-Scope-Akquise schlug fehl"
reg_hold_mark wiki run-l1 1
scopelock_header_banner "RUN-L1-holder"
# Probe: die Lease ist bei Reg-Gen 1 NICHT stale (Erzeugungs-Gen == aktueller Reg-Gen).
[ "$(reg_gen wiki)" = "1" ] || fail "L-1: Registry-Generation nach Akquise falsch (Pkt. 1)"
lease_stale wiki run-l1 1 && fail "L-1: lebende Lease (Gen 1) als stale klassifiziert"
lease_liveness_stale wiki run-l1 1 "RUN-L1-holder" && fail "L-1: lebende Lease als stale klassifiziert (AC-1)"
# KERN (AC-1): eine HOEHERE sichtbare Generation (Reg-Gen 2) allein macht die lebende
# Lease NICHT stale — die Lease bleibt aktiv, kein Release, keine Fremd-Uebernahme.
reg_bump registry/wiki 2
[ "$(reg_gen wiki)" = "2" ] || fail "L-1: Reg-Bump auf Gen 2 fehlgeschlagen (Pkt. 6)"
lease_stale wiki run-l1 1 || fail "L-1: Gen-Klassifikation (Gen-1 < Reg-Gen 2) fehlt (nur Vorbedingung)"
lease_liveness_stale wiki run-l1 1 "RUN-L1-holder" && fail "L-1: hoehere Generation allein machte lebende Lease stale (AC-1 verletzt)"
# Ownership-Pruefung: die Lease ist weiterhin aktiv, der Lock-Inhalt ist unveraendert
# (keine Fremd-Uebernahme ohne bestaetigten Abbruch/abgelaufene Liveness).
[ "$(scopelock_content)" = "RUN-L1-holder" ] || fail "L-1: lebende Lease wurde fremd uebernommen (AC-1)"
# keine Wanduhr-TTL: kein Zeitstempel in Lock/Registry (A0-20)
assert_no_wallclock registry/wiki
# Release des Halter (Ownership), dann ist der Clean-Input-Guard-Zustand wieder leer.
scopelock_release "RUN-L1-holder"
[ -z "$(scopelock_content)" ] || fail "L-1: Lock nach Release nicht leer"
echo "RESULT: PASS — L-1: LIVENESS_AKTIV — hoehere Generation macht lebende Lease NICHT stale; Liveness ist Zustand (bestaetigter Abbruch/abgelaufene Liveness + Ownership-Pruefung), kein Wanduhr-/Gen-Hoehen-Effekt (AC-1, A0-20)"
pass
# =====================================================================
# L-2 STALE_UEBERNAHME (AC-2) — nachweislich stale Lease (bestaetigter Abbruch,
# Gen-Registry) -> Uebernahme GENAU EINMAL per Ownership-CAS; ersetzte Holder-ID
# benannt; genau eine aktive Root-Lease; kein Doppel-Uebernehmer.
# =====================================================================
runlabel "L-2: STALE_UEBERNAHME (AC-2) — stale Lease genau einmal, ersetzte Holder-ID benannt"
isolate l2
# Abgebrochener Run A: akquiriert den Root-Scope, registriert (Gen 1), BRICHT ab — kein Release.
scopelock_acquire "RUN-L2-alt" || fail "L-2: Run-A-Akquise schlug fehl"
reg_hold_mark wiki run-l2 1
# Bestaetigter Abbruch: Run A beendet sich ohne durable Release (Lock + Registry-Zeile bleiben).
# Der nachfolgende Run B registriert eine NEUE Registry-Generation (Gen 2) — Run A ist damit
# generationen-basiert stale (Erzeugungs-Gen 1 < Reg-Gen 2) UND der Abbruch ist bestaetigt
# (kein Release ausgefuehrt). Liveness-Ablauf ist damit deterministisch belegt (Pkt. 1).
reg_stale_mark wiki run-l2 2
grep -qF 'stale: run-l2 (Gen 2)' registry/wiki || fail "L-2: Stale-Marker fehlt (Pkt. 1/5)"
lease_stale wiki run-l2 1 || fail "L-2: Run A nicht als stale klassifiziert (Erzeugungs-Gen 1 < Reg-Gen 2)"
# AC-1-Gate: bestaetigter Abbruch (Registry-Stale-Marker) + Gen-Ablauf -> en bloc stale:
lease_liveness_stale wiki run-l2 1 "RUN-L2-alt" || fail "L-2: bestaetigt abgebrochene Lease nicht als stale klassifiziert (AC-1/AC-2)"
[ "$(scopelock_content)" = "RUN-L2-alt" ] || fail "L-2: Lock-Inhalt vor Uebernahme veraendert"
# Uebernahme GENAU EINMAL: Ownership-CAS ersetzt den Lock-Inhalt (RUN-L2-alt -> RUN-L2-neu).
scopelock_takeover "RUN-L2-alt" "RUN-L2-neu"
# Genau eine aktive Root-Lease: genau ein scope-bezogener Lock, Inhalt = neuer Inhaber.
n2=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK")
[ "$n2" -eq 1 ] || fail "L-2: nach Uebernahme erwartet genau einen Scope-Lock, tatsaechlich $n2 (AC-2: genau eine aktive Root-Lease)"
[ "$(scopelock_content)" = "RUN-L2-neu" ] || fail "L-2: Lock-Inhalt nach Uebernahme != neuer Inhaber"
# Die ERSETZTE Holder-ID wird benannt (im log.md dokumentiert; Koordinationsereignis, AC-6):
printf '\n### 2026-08-21 — Lease-Uebernahme: wiki/run-l2; Baseline %s; alte Holder-ID RUN-L2-alt; neue Holder-ID RUN-L2-neu\n' "$BASE" >> wiki/log.md
grep -qF "alte Holder-ID RUN-L2-alt" wiki/log.md || fail "L-2: ersetzte Holder-ID nicht benannt (AC-2/AC-6)"
grep -qF "neue Holder-ID RUN-L2-neu" wiki/log.md || fail "L-2: neue Holder-ID nicht benannt (AC-2/AC-6)"
grep -qF "Baseline $BASE" wiki/log.md || fail "L-2: Baseline-Commit fehlt im Uebernahme-Eintrag (Vertrag §5)"
# Kein Doppel-Uebernehmer: eine zweite (sequentielle) Uebernahme derselben stale id schlaegt
# fehl — der Lock wird nur einmal ersetzt (genau einmal-Uebernahme, AC-2).
if scopelock_acquire "RUN-L2-zweiter"; then
fail "L-2: zweite Uebernahme gelang (AC-2: genau einmal verletzt)"
fi
[ "$(scopelock_content)" = "RUN-L2-neu" ] || fail "L-2: Lock-Inhalt nach zweitem Uebernahme-Versuch veraendert"
# Die verwaiste Registry-Zeile bleibt erhalten (nie still geloescht, AD-17e):
grep -qF "hold: run-l2 (Gen 1)" registry/wiki || fail "L-2: Registry-Zeile run-l2 still geloescht (AD-17e)"
scopelock_release "RUN-L2-neu"
[ -z "$(scopelock_content)" ] || fail "L-2: Lock nach Release nicht leer"
echo "RESULT: PASS — L-2: STALE_UEBERNAHME — stale Lease (bestaetigter Abbruch + Gen-Ablauf) genau EINMAL uebernommen; ersetzte Holder-ID benannt; genau eine aktive Root-Lease; kein Doppel-Uebernehmer; nie still geloescht (AC-2, AD-17e)"
pass
# =====================================================================
# L-3 DIRTY_GETRACKT (AC-3) — fremde getrackte uncommittete Aenderung im
# Mutationsbereich -> Abort-/Protect-Zustandsmaschine: SCHUETZEN (Scratch) ->
# Restore byte-identisch nach dem Run; UNCOMMITTED_INPUT-Abbruch abgestimmt §5.11
# Pkt. 3; nie geloescht (AD-17e).
# =====================================================================
runlabel "L-3: DIRTY_GETRACKT (AC-3) — Abort-/Protect: getrackte Fremd-Aenderung, byte-identischer Restore"
isolate l3
scopelock_acquire "RUN-L3" || fail "L-3: Akquise schlug fehl"
scopelock_header_banner "RUN-L3"
# Fremde getrackte Aenderung: ein anderer Producer hat wiki/alpha.md uncommittet modifiziert.
sed -i 's|^Das Alpha-Protokoll definiert eine deterministische Init-Sequenz (raw/alpha-v1.md#S-1)\.|Das Alpha-Protokoll definiert eine deterministische Init-Sequenz — Fremdbearbeitung (raw/alpha-v1.md#S-1).|' wiki/alpha.md
FRANK_MD5=$(sha256sum wiki/alpha.md | cut -d' ' -f1)
# Pre-Mutation-Pruefung (§5.11 Pkt. 3): die Abweichung ist als working-copy-Marke sichtbar.
git status --porcelain -- wiki/alpha.md | grep -qE '^ M ' || fail "L-3: fremde getrackte Aenderung nicht als ' M ' sichtbar (§5.11 Pkt. 3)"
# UNCOMMITTED_INPUT-Abbruch abgestimmt §5.11 Pkt. 3: VOR jeder Mutation greift der benannte
# Abbruch 'published/committed Input erforderlich' — der Run mutiert NICHT und HEAD bleibt.
[ "$(git rev-parse HEAD)" = "$BASE" ] || fail "L-3: UNCOMMITTED_INPUT-Abbruch hat HEAD bewegt (Abbruch = keine Mutation)"
ALPHA_ONLY=$({ git diff --name-only "$BASE" -- wiki/ ; git status --porcelain -- wiki/ | awk '{print $2}'; } \
| sed -e 's|^wiki/||' -e 's|\.md$||' | LC_ALL=C sort -u)
[ "$ALPHA_ONLY" = "alpha" ] || fail "L-3: nach UNCOMMITTED_INPUT-Abbruch weichen weitere Pfade ab (keine Mutation) — '$ALPHA_ONLY'"
# ABORT-/PROTECT-ZUSTANDSMASCHINE (getrackt): SCHUETZEN — Sicherung in Scratch-Zone
# (ausserhalb wiki/, deterministisch benannt), dann Arbeitssatz bereinigt.
mkdir -p scratch/l3
cp wiki/alpha.md "scratch/l3/alpha.md.stash"
git checkout -q -- wiki/alpha.md
# Der Run mutiert jetzt (Mutations-Objekt alpha, Erhaltungs-Invariante erlaubt alpha + log.md):
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-16T12:00:00Z
---
Das Alpha-Protokoll definiert eine deterministische Init-Sequenz — Lifecycle-Mutation (raw/alpha-v1.md#S-1).
EOF
assert_frontmatter wiki/alpha.md
# RESTORIEREN (byte-identisch): nach dem Run wird die gesicherte Fremd-Aenderung aus der
# Scratch-Zone zurueckgespielt — nie geloescht, nie still weggelassen (AD-17e).
cp "scratch/l3/alpha.md.stash" wiki/alpha.md
[ "$(sha256sum wiki/alpha.md | cut -d' ' -f1)" = "$FRANK_MD5" ] || fail "L-3: Restore nicht byte-identisch (AC-3, AD-17e)"
[ "$(sha256sum scratch/l3/alpha.md.stash | cut -d' ' -f1)" = "$FRANK_MD5" ] || fail "L-3: Scratch-Sicherung nicht byte-identisch (AC-3)"
[ -f "scratch/l3/alpha.md.stash" ] || fail "L-3: Scratch-Artefakt fehlt — fremde Aenderung geloescht (AD-17e)"
# log.md-Dokumentation des Dirty-Tree-Schutzes (Vertrag §5, §5.11 Pkt. 3; Koordinationsereignis, AC-6):
printf '\n### 2026-08-21 — Dirty-Tree-Schutz: wiki/alpha.md nach scratch/l3/alpha.md.stash gesichert (nie geloescht, §5.11 Pkt. 3/6b; Baseline %s)\n' "$BASE" >> wiki/log.md
grep -qF "Dirty-Tree-Schutz: wiki/alpha.md nach scratch/l3/alpha.md.stash" wiki/log.md || fail "L-3: Dirty-Tree-Schutz-Dokumentation fehlt (§5.11 Pkt. 3/6b)"
# Endzustand: nur die fremde alpha-Abweichung + log.md (Erhaltungs-Invariante; kein Ghost-Diff).
inv_set | LC_ALL=C paste -sd' ' -
assert_invariant "alpha log"
scopelock_release "RUN-L3"
echo "RESULT: PASS — L-3: DIRTY_GETRACKT — Abort-/Protect-Zustandsmaschine (getrackt): SCHUETZEN (Scratch) -> Restore byte-identisch nach dem Run; UNCOMMITTED_INPUT-Abbruch abgestimmt §5.11 Pkt. 3; nie geloescht (AC-3, AD-17e)"
pass
# =====================================================================
# L-4 DIRTY_UNGETRACKT (AC-3) — fremde ungetrackte Datei im Mutationsbereich
# -> Protect: Scratch-Sicherung + byte-identischer Restore; keine stille
# Loeschung; log.md-Eintrag.
# =====================================================================
runlabel "L-4: DIRTY_UNGETRACKT (AC-3) — Protect: ungetrackte Fremd-Datei, byte-identischer Restore"
isolate l4
scopelock_acquire "RUN-L4" || fail "L-4: Akquise schlug fehl"
scopelock_header_banner "RUN-L4"
# Fremde ungetrackte Datei im Mutationsbereich (wiki/inoffiziell.md, nicht committet):
cat > wiki/inoffiziell.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
---
Inoffizieller Fremd-Entwurf (ungetrackt).
EOF
F_UNTRACK_MD5=$(sha256sum wiki/inoffiziell.md | cut -d' ' -f1)
git status --porcelain -- wiki/inoffiziell.md | grep -qE '^\?\? ' || fail "L-4: ungetrackte Fremd-Datei nicht als '??' sichtbar"
# PROTECT (ungetrackt): Sicherung in die Scratch-Zone, dann ist der Mutationsbereich sauber.
mkdir -p scratch/l4
cp wiki/inoffiziell.md "scratch/l4/inoffiziell.md.stash"
git clean -qfd wiki/inoffiziell.md
# Der Run mutiert alpha (Kill-Punkt vor Mutation/Nach-Mutation, konsistenter Zwischenstand):
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-16T13:00:00Z
---
Das Alpha-Protokoll definiert eine deterministische Init-Sequenz — Lifecycle-Mutation L4 (raw/alpha-v1.md#S-1).
EOF
assert_frontmatter wiki/alpha.md
# RESTORIEREN: die gesicherte ungetrackte Fremd-Datei wird byte-identisch zurueckgespielt.
cp "scratch/l4/inoffiziell.md.stash" wiki/inoffiziell.md
[ "$(sha256sum wiki/inoffiziell.md | cut -d' ' -f1)" = "$F_UNTRACK_MD5" ] || fail "L-4: ungetrackte Fremd-Datei nicht byte-identisch restauriert (AC-3, AD-17e)"
[ -f "scratch/l4/inoffiziell.md.stash" ] || fail "L-4: Scratch-Sicherung fehlt — stille Loeschung (AD-17e)"
printf '\n### 2026-08-21 — Protect ungetrackt: wiki/inoffiziell.md nach scratch/l4/inoffiziell.md.stash gesichert + restauriert (nie geloescht, §5.11 Pkt. 3/§5.18 Pkt. 3; Baseline %s)\n' "$BASE" >> wiki/log.md
grep -qF "Protect ungetrackt: wiki/inoffiziell.md" wiki/log.md || fail "L-4: Protect-Dokumentation fehlt (AC-6)"
# Endzustand (Erhaltungs-Invariante §5.9 Pkt. 5): im L4-Fall erlaubte Mitglieder der
# Mutationsbereichs-Abweichung sind 'alpha' (Lifecycle-Mutation des Runs, nicht committet —
# Kill-Punkt nach Mutation, valider Zwischenstand), 'log' (Dokumentation) und die fremde
# ungetrackte 'inoffiziell'-Fremd-Datei, die der Run geschuetzt und byte-identisch restauriert
# hat (Fremd-Artefakt, kein Ghost des Runs; AD-17e). Kein weiterer Ghost-Diff:
inv_set | LC_ALL=C paste -sd' ' -
assert_invariant "alpha log inoffiziell"
scopelock_release "RUN-L4"
echo "RESULT: PASS — L-4: DIRTY_UNGETRACKT — Protect: ungetrackte Fremd-Datei gesichert + byte-identisch restauriert; keine stille Loeschung; log.md-Eintrag (AC-3, AD-17e)"
pass
# =====================================================================
# L-5 ROLLBACK_NACH_MUTATION (AC-4) — Fehler nach Mutation/Staging -> Rollback
# INDEX + WORKTREE aus <Baseline-Commit>; Post-Rollback-Diff leer; Kill-Punkt
# nach Mutation: konsistenter Endzustand.
# =====================================================================
runlabel "L-5: ROLLBACK_NACH_MUTATION (AC-4) — Rollback Index+Worktree, Post-Rollback-Diff leer"
isolate l5
scopelock_acquire "RUN-L5" || fail "L-5: Akquise schlug fehl"
scopelock_header_banner "RUN-L5"
# Mutation + Staging (Fehler-Simulation nach Mutation/Staging):
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-16T14:00:00Z
---
Das Alpha-Protokoll — fehlerhafte Teil-Mutation, die zurueckgerollt wird (raw/alpha-v1.md#S-1).
EOF
git add wiki/alpha.md
# Kill-Punkt NACH MUTATION: der Zwischenstand ist valide (nur alpha staged abweichend), ABER
# der Fehler wird erkannt -> ROLLBACK aus <Baseline-Commit>, INDEX + WORKTREE:
git reset -q --hard "$BASE"
# Post-Rollback-Diff gg. Baseline leer (Zustands-Restaurations-Invariante, §5.13 Pkt. 3):
[ "$(git diff --name-only "$BASE" -- wiki/ | wc -l)" = "0" ] || fail "L-5: Post-Rollback-Diff nicht leer (AC-4)"
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-5: Worktree nicht sauber nach Rollback (AC-4)"
[ "$(git rev-parse HEAD)" = "$BASE" ] || fail "L-5: Rollback hat HEAD bewegt (kein Teilzustand committet, AD-17f)"
# Kill-Punkt NACH MUTATION konsistent: Zustand == Baseline, kein hängender Lock (noch gehalten):
[ "$(scopelock_content)" = "RUN-L5" ] || fail "L-5: Lock nach Rollback nicht mehr vom Run gehalten"
scopelock_release "RUN-L5"
echo "RESULT: PASS — L-5: ROLLBACK_NACH_MUTATION — Rollback restoret Index + Worktree aus <Baseline-Commit>; Post-Rollback-Diff leer; Kill-Punkt nach Mutation konsistent (AC-4, §5.13 Pkt. 3)"
pass
# =====================================================================
# L-6 ROLLBACK_NACH_COMMIT_VOR_RELEASE (AC-4/AC-5) — Fehler nach Commit, vor
# Release -> Kill-Punkt vor Commit/vor Release: Zustand == Baseline oder valide
# committet, kein Teilzustand veroeffentlicht, kein haengender Lock.
# =====================================================================
runlabel "L-6: ROLLBACK_NACH_COMMIT_VOR_RELEASE (AC-4/AC-5) — Kill-Punkt vor Commit/vor Release"
isolate l6
scopelock_acquire "RUN-L6" || fail "L-6: Akquise schlug fehl"
scopelock_header_banner "RUN-L6"
# Veritaet: Mutations-Commit als valider Committed-Zustand (Bundle == committetes Ergebnis).
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-16T15:00:00Z
---
Das Alpha-Protokoll definiert eine deterministische Init-Sequenz — committeter Run (raw/alpha-v1.md#S-1).
EOF
assert_frontmatter wiki/alpha.md
printf '\n### 2026-08-21 — Story-3.12-L6: committeter Mutations-Nachweis (Baseline %s)\n' "$BASE" >> wiki/log.md
git add wiki/alpha.md wiki/log.md
git commit -qm "L6: committeter Run (Mutation + log.md-Nachweis)"
# Kill-Punkt VOR RELEASE: Commit ist erfolgt, aber der Lock ist noch NICHT freigegeben (kein
# haengender Lock — der Lock wird noch vom Run gehalten, kein Teilzustand veroeffentlicht).
git status --porcelain -- wiki/ | grep -q '^??' && fail "L-6: ungetrackte Reste nach Commit (Clean-Input-Guard-Vorfeld)"
[ "$(scopelock_content)" = "RUN-L6" ] || fail "L-6: Lock vor Release nicht gehalten (Kill-Punkt vor Release)"
# Fehler-Nachlauf vor Release wird sauber aufgeloest: entweder Rollback auf Baseline (valide)
# oder der committete Zustand wird beibehalten und der Lock freigegeben (Zustand == Baseline
# oder valide committet, nie ein Teilzustand — AD-17f). Hier: committeter Zustand ist valide,
# der Run gibt durativ frei.
scopelock_release "RUN-L6"
[ -z "$(scopelock_content)" ] || fail "L-6: Lock nach Release nicht leer (AC-5)"
# Clean-Input-Guard-Vorfeld: sauberer Worktree nach Commit+Release.
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-6: Worktree nach Commit+Release nicht sauber"
echo "RESULT: PASS — L-6: ROLLBACK_NACH_COMMIT_VOR_RELEASE — nach Commit, vor Release: Zustand == Baseline oder valide committet; kein Teilzustand; kein haengender Lock (AC-4/AC-5, AD-17f)"
pass
# =====================================================================
# L-7 RELEASE_DURABLE (AC-5) — erfolgreicher Run: Mutation + zulaessiger
# Log-Nachweis committet; Lock per Ref-Delete entfernt; Worktree sauber;
# FOLGE-RUN besteht den Clean-Input-Guard (kein INPUT_UNCOMMITTED, kein
# haengender Lock, Fremd-Bytes restauriert).
# =====================================================================
runlabel "L-7: RELEASE_DURABLE (AC-5) — durable Release + Clean-Input-Guard des Folge-Runs"
isolate l7
scopelock_acquire "RUN-L7" || fail "L-7: Akquise schlug fehl"
scopelock_header_banner "RUN-L7"
# Erster Run (durativ): Mutation + zulaessiger Log-Nachweis committet, dann Ref-Delete-Release.
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-16T16:00:00Z
---
Das Alpha-Protokoll definiert eine deterministische Init-Sequenz — durativer Run (raw/alpha-v1.md#S-1).
EOF
assert_frontmatter wiki/alpha.md
printf '\n### 2026-08-21 — Story-3.12-L7: durativer Run, Freigabe per Ref-Delete (Baseline %s)\n' "$BASE" >> wiki/log.md
git add wiki/alpha.md wiki/log.md
git commit -qm "L7: durativer Run (Mutation + Log-Nachweis)"
# Release per Ref-Delete (nur durch Inhaber, Ownership-Pruefung):
scopelock_release "RUN-L7"
# Lock ist entfernt (ref-delete):
if git rev-parse -q --verify "$SCOPELOCK" >/dev/null 2>&1; then
fail "L-7: Lock nach Ref-Delete-Release existiert noch (AC-5)"
fi
# Worktree sauber (kein INPUT_UNCOMMITTED-Abbruch moeglich):
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-7: Worktree nach Release nicht sauber (AC-5)"
# FOLGE-RUN besteht den Clean-Input-Guard: sauberer Worktree, kein haengender Lock, keine
# Fremd-Bytes — der Folge-Run akquiriert ohne INPUT_UNCOMMITTED-Abbruch.
scopelock_acquire "RUN-L7-folge" || fail "L-7: Folge-Run-Akquise schlug fehl"
[ "$(scopelock_content)" = "RUN-L7-folge" ] || fail "L-7: Folge-Run-Lock-Inhalt falsch"
# Kein INPUT_UNCOMMITTED-Abbruch: die Pre-Mutation-Pruefung meldet clean (keine Fremd-/Rest-Bytes).
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-7: Folge-Run startet mit INPUT_UNCOMMITTED-Rest (Clean-Input-Guard verletzt, AC-5)"
scopelock_release "RUN-L7-folge"
echo "RESULT: PASS — L-7: RELEASE_DURABLE — Mutation + Log-Nachweis committet; Lock per Ref-Delete entfernt; Worktree sauber; Folge-Run besteht den Clean-Input-Guard (AC-5)"
pass
# =====================================================================
# L-8 KANONISCHES_LOG (AC-6) — wiki/log.md enthaelt nur vertragskonforme
# fachliche + noetige Koordinationsereignisse; Build-/Review-/Story-/Sandbox-
# Historie bleibt ausserhalb; kein Ghost-Diff.
# =====================================================================
runlabel "L-8: KANONISCHES_LOG (AC-6) — canonical log nur vertragskonform, Historie ausserhalb"
isolate l8
scopelock_acquire "RUN-L8" || fail "L-8: Akquise schlug fehl"
scopelock_header_banner "RUN-L8"
# Run schreibt NUR vertragskonforme Eintraege (Koordinations-/Fach-Ereignisse):
printf '\n### 2026-08-21 — Lease-Akquise: wiki Root-Scope durch RUN-L8 (Baseline %s)\n' "$BASE" >> wiki/log.md
printf '\n### 2026-08-21 — Freigabe: wiki Root-Scope durch RUN-L8 (Ref-Delete, Baseline %s)\n' "$BASE" >> wiki/log.md
# Build-/Review-/Story-/Sandbox-Historie bleibt AUSSERHALB des Bundles:
# - Sandbox-Nachweis: dieser laufende Nachweis lebt in _bmad-output/... (ausserhalb wiki/)
# - Registry (kumulativer Aufbau) liegt ausserhalb wiki/ (registry/ als Datei je Area),
# kein `# Log`-Stand im Wiki.
mkdir -p registry
echo "gen: 1" > registry/wiki
echo "hold: run-l8 (Gen 1)" >> registry/wiki
# KERN (AC-6): log.md enthaelt KEINE Build-/Review-/Sandbox-Texte — nur die zwei
# Koordinations-Eintraege; kein Ghost-Diff im Mutationsbereich (nur log.md, Registry ausserhalb).
grep -qiE 'sandbox|review|build|_bmad|run-sandbox' wiki/log.md && fail "L-8: log.md traegt Build-/Review-/Sandbox-Historie (AC-6 verletzt)"
# Der einzige wiki/-Unterschied ist log.md (Registry/Lock ausserhalb, kein Ghost-Diff):
git add wiki/log.md registry/wiki
git commit -qm "L8: canonisches Log (2 Koordinations-Eintraege) + Registry (ausserhalb wiki/)"
diff_set=$(probe)
[ "$diff_set" = "log" ] || fail "L-8: nach L8-Commit weicht der Mutationsbereich ab: '$diff_set' (erwartet nur log; Registry liegt ausserhalb wiki/ und ist daher kein Ghost)"
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-8: Worktree nach Commit nicht sauber (Clean-Input-Guard-Vorfeld)"
scopelock_release "RUN-L8"
echo "RESULT: PASS — L-8: KANONISCHES_LOG — wiki/log.md nur vertragskonforme fachliche + Koordinationsereignisse; Build-/Review-/Story-/Sandbox-Historie ausserhalb; Registry kumulativ ausserhalb; kein Ghost-Diff (AC-6)"
pass
# =====================================================================
# L-9 KILLPUNKT_TESTS (AC-7) — vier Kill-Punkte (vor Mutation / nach Mutation /
# vor Commit / nach Commit) — je konsistenter Endzustand (Zustands-Restaurations-
# Invariante §5.13 Pkt. 3).
# =====================================================================
runlabel "L-9: KILLPUNKT_TESTS (AC-7) — 4 Kill-Punkte, je konsistenter Endzustand"
isolate l9
scopelock_acquire "RUN-L9" || fail "L-9: Akquise schlug fehl"
scopelock_header_banner "RUN-L9"
# KILLPUNKT 1 — VOR MUTATION: Zustand == Baseline, keine Zwischenstaende, kein Ghost.
[ "$(git rev-parse HEAD)" = "$BASE" ] || fail "L-9/KP1: vor Mutation Head != Baseline"
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-9/KP1: vor Mutation Worktree nicht sauber"
echo " KP1 (vor Mutation): Zustand == Baseline, kein Zwischenstand — PASS"
# KILLPUNKT 2 — NACH MUTATION: Zustand == valider Zwischenstand (geplanter Plan-Freeze),
# kein Ghost-Diff ausserhalb der erlaubten Pfad-Menge.
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-16T17:00:00Z
---
Das Alpha-Protokoll — Kill-Punkt-nach-Mutation (raw/alpha-v1.md#S-1).
EOF
git add wiki/alpha.md
# Erlaubte Pfad-Menge nach Mutation: alpha (Kill-Punkt-Zwischenstand). Staging nur alpha.
staged=$(git diff --cached --name-only | sed -e 's|^wiki/||' -e 's|\.md$||' | LC_ALL=C sort -u)
[ "$staged" = "alpha" ] || fail "L-9/KP2: nach Mutation sind nur erlaubte Pfade staged, tatsaechlich '$staged'"
echo " KP2 (nach Mutation): valider Zwischenstand (nur alpha staged), kein Ghost — PASS"
# KILLPUNKT 3 — VOR COMMIT: nur erlaubte Pfad-Menge staged, kein Teilzustand committet.
[ "$(git rev-parse HEAD)" = "$BASE" ] || fail "L-9/KP3: vor Commit Head bewegt (Teilzustand committet, AD-17f)"
printf '\n### 2026-08-21 — Story-3.12-L9: Kill-Punkt-Test (Baseline %s)\n' "$BASE" >> wiki/log.md
git add wiki/log.md
echo " KP3 (vor Commit): nur alpha + log staged, kein Commit, HEAD == Baseline — PASS"
# KILLPUNKT 4 — NACH COMMIT: Bundle == valide committeter Zustand, anschliessbar an den
# Clean-Input-Guard (kein haengender Lock, sauberer Worktree).
git commit -qm "L9: Kill-Punkt-nach-Commit (Mutation + log.md)"
[ "$(git status --porcelain -- wiki/ | wc -l)" = "0" ] || fail "L-9/KP4: nach Commit Worktree nicht sauber (Clean-Input-Guard-Vorfeld)"
[ "$(scopelock_content)" = "RUN-L9" ] || fail "L-9/KP4: Lock nach Commit nicht gehalten (vor Release)"
git show --format=%s -s HEAD | grep -q "L9: Kill-Punkt-nach-Commit" || fail "L-9/KP4: Commit-Betreff nicht valide"
scopelock_release "RUN-L9"
echo " KP4 (nach Commit): valide committet, sauber, kein haengender Lock — PASS"
[ -z "$(scopelock_content)" ] || fail "L-9: Lock nach Release nicht leer"
echo "RESULT: PASS — L-9: KILLPUNKT_TESTS — 4 Kill-Punkte (vor/nach Mutation, vor/nach Commit) je konsistenter Endzustand; Clean-Input-Guard-Vorfeld (AC-7, §5.13 Pkt. 3)"
pass
# =====================================================================
# Abschluss: Gesamt-Aussage und Zaehler (L-1..L-9 == 9 harte PASS)
# =====================================================================
echo
echo "#### SANDBOX-3-12 GESAMT-ERGEBNIS ####"
echo "Harte PASS-Assertions: $PASS_COUNT"
[ "$PASS_COUNT" -ge 9 ] || { echo "HARD-FAIL: erwartet >= 9 harte PASS-Assertions, tatsaechlich $PASS_COUNT" >&2; exit 1; }
# Kein Zugriff auf das reale Bundle/raw/ (AD-3): das Skript lebt ausschliesslich auf dem
# /tmp-Baum; der reale wiki/- oder raw/-Baum wurde nicht beruehrt.
# Finale Baumsauberkeits-Pruefung (echte Assertion — sauberer Endzustand, kein Carry-over):
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 L-1..L-9 harte PASS (Exit 0) — transaktionaler Lease-Lifecycle & Commit-Abschluss"
echo "verifiziert (§5.18, Revision 3.7): kein Zugriff auf reales Bundle/raw/."
echo "Sandbox-Root: $ROOT (loeschbar: rm -rf $ROOT)"
exit 0