#!/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 (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 [ -n "${1:-}" ] || return 2 # PATCH 4a: keine Akquise mit leerer Run-ID / leerem Lock-Inhalt 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; } # PATCH 4c: Ref existiert, aber cat-file scheitert = korrupter/fehlender Blob -> harter # LOCK_READ_ERROR statt stiller leerer Rueckgabe (leer ist nur "Ref nicht vorhanden"). git cat-file -p "$val" 2>/dev/null || { echo "HARD-FAIL (LOCK_READ_ERROR): Lock-Ref '$SCOPELOCK' zeigt auf '$val', aber Blob nicht lesbar (korrupt/fehlend)" >&2; exit 1; } } # 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 oldblob # Schnelle Fehldiagnose-Hilfe (bleibt, PATCH 1): Ownership-Mismatch klar beschrifteter # HARD-FAIL. Der entscheidende Guard ist danach der ATOMARE Old-Value-Write (CAS) — die # atomare Abweisung selbst wird im negativen L-2-Test durch direkten atomaren Ref-Write # geprueft (PATCH 2), der schnelle Diagnose-Zweig bleibt fuer echte Aufrufer fatal. [ "$(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 # Atomarer Ownership-CAS (PATCH 1, §5.18 Pkt. 2): der Ref-Write traegt den Old-Value # (Blob der erwarteten alten Inhaber-Run-ID). Schlaegt atomar fehl (Exit != 0) und laesst den # Lock UNVERAENDERT, wenn der Lock nicht mehr exakt diesen Inhalt traegt. oldblob=$(printf '%s' "$old" | git hash-object -w --stdin) || return 2 git update-ref "$SCOPELOCK" "$newval" "$oldblob" 2>/dev/null \ || { echo "HARD-FAIL (Takeover): atomarer Ownership-CAS fehlgeschlagen (Lock traegt nicht mehr exakt den erwarteten Inhalt — kein Clobber)" >&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" [ -n "${4:-}" ] || return 1 # PATCH 4b: leere holder_runid -> keine vacuous-stale-Klassifikation 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). # PATCH 2 — atomarer Ownership-CAS: POSITIVE genau-einmal-Kontrolle und NEGATIVE Kontrolle. # Der korrekte Old-Value flippt (CAS) genau einmal und hinterlaesst den neuen Inhaber: scopelock_takeover "RUN-L2-alt" "RUN-L2-neu" [ "$(scopelock_content)" = "RUN-L2-neu" ] || fail "L-2: CAS mit korrektem Old-Value flippte den Lock-Inhalt nicht (PATCH 2, AC-2)" # NEGATIV-Kontrolle (PATCH 2): Takeover-Aufruf mit FALSCHEM Old-Value (die alte Holder-ID ist # nach der Uebernahme nicht mehr Inhaber) muss atomar FEHLSCHLAGEN und den Lock UNVERAENDERT # lassen — kein Clobber. Um die ATOMARE Abweisung zu beweisen (nicht nur die schnelle Diagnose), # wird der atomare Old-Value-Ref-Write direkt ausgefuehrt: der Lock traegt Blob('RUN-L2-neu'), # der falsche Old-Value Blob('RUN-L2-alt') ist NICHT Old-Value -> git update-ref schlaegt atomar # fehl (Exit != 0, kein Clobber, Lock unveraendert) — identisch zu dem, was ein um die # Diagnose herumlaufender Aufrufer treffen wuerde. alt_blob=$(printf '%s' 'RUN-L2-alt' | git hash-object -w --stdin) || fail "L-2: alt_blob nicht erzeugt (PATCH 2)" falsch_blob=$(printf '%s' 'RUN-L2-falsch' | git hash-object -w --stdin) || fail "L-2: falsch_blob nicht erzeugt (PATCH 2)" if git update-ref "$SCOPELOCK" "$falsch_blob" "$alt_blob" 2>/dev/null; then fail "L-2: atomarer Old-Value-Write mit falschem Old-Value gelang (PATCH 2, AC-2: kein Clobber)" fi # Lock-Ref existiert unveraendert (eine aktive Root-Lease, kein Clobber): n2b=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK") [ "$n2b" -eq 1 ] || fail "L-2: Lock-Ref nach negativem atomaren CAS veraendert (PATCH 2)" # Lock-Inhalt unveraendert (Inhalt danach == aktuelle Inhaber-Run-ID, kein Clobber): [ "$(scopelock_content)" = "RUN-L2-neu" ] || fail "L-2: negativer atomarer CAS hat den Lock-Inhalt veraendert (Clobber — PATCH 2, AC-2)" # 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 ; 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 , 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 ; 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 traegt KEINE Build-/Review-/Story-/Sandbox-Historie — weder als # Kategorie-benennende Eintraege (z. B. 'Build-Historie', 'Sandbox-Protokoll', 'Story-Log') # noch als Sandbox-Artefakt-/Pfad-Referenz (_bmad-output, run-sandbox, sandbox-3-12); # legitime Fach-/Koordinationswoerter im Fliesstext (z. B. einzelnes 'Build'/'Review' ohne # Kategorie-Hyphenat; Story-Nummern wie 'Story-3.12-L6' ohne '-Historie') werden NICHT # getroffen (PATCH 4d — Edge-Case-Hunter). grep -qiE '(Build|Review|Story|Sandbox)-(Histor|Log|Protokoll|Bericht|Nachweis)|_bmad-output|run-sandbox|sandbox-3-12' wiki/log.md \ && fail "L-8: log.md traegt Build-/Review-/Story-/Sandbox-Historie (AC-6 verletzt; PATCH 4d)" # Positiv-Kontrolle (PATCH 4d): die beiden vertragskonformen Koordinations-Eintraege sind # vorhanden und loesen den Kategorie-Check NICHT aus — kein false-HARD-FAIL auf legitime # Koordinations-Eintraege im Fliesstext. grep -qF 'Lease-Akquise: wiki Root-Scope durch RUN-L8' wiki/log.md || fail "L-8: Koordinations-Eintrag Akquise fehlt (PATCH 4d Positiv-Kontrolle)" grep -qF 'Freigabe: wiki Root-Scope durch RUN-L8' wiki/log.md || fail "L-8: Koordinations-Eintrag Freigabe fehlt (PATCH 4d Positiv-Kontrolle)" # 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