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>
544 lines
31 KiB
Bash
544 lines
31 KiB
Bash
#!/usr/bin/env bash
|
|
# Story 3.11 — Sandbox-Tests der atomaren Root-Scope-Lease-Akquise (§5.17, Revision 3.6)
|
|
# im clone-geteilten Zustand (AD-17b/A0-13, AC-a..AC-e)
|
|
# Re-executierbar: bash run-sandbox.sh (ab Workspace-Root; Sandbox unter /tmp/sb311)
|
|
# Zweck: die §5.17-Atomaritäts-Präzisierung (Revision 3.6) als re-executierbarer
|
|
# Run-Demonstrator durchspielen —
|
|
# A-1 EXKLUSIVITAETS_SCHLUESSEL: ein Mini-Bundle + Root-Scope-Lock im clone-geteilten
|
|
# Zustand; der Exklusivitätsschlüssel ist der scope-bezogene Lock (eine committete
|
|
# Ref je Root-Scope wiki/), die Run-ID ist Lock-Inhalt und NICHT Teil des Schlüssels
|
|
# (Ref-Name trägt keine Run-ID) — AC-a
|
|
# A-2 ATOMAR_ZWEI_WORKTREE (nicht-sequenziell): zwei getrennte Worktrees desselben Clones
|
|
# akquirieren zeitlich überlappend in Getrennten Prozessen denselben Root-Scope;
|
|
# genau ein Gewinner, der andere LEASE_HOLD — AC-b (Ask-First-Primitiv: git update-ref
|
|
# <ref> <wert> $ZERO_SHA, create-only; atomar im geteilten Ref-Namespace)
|
|
# A-3 WEDER_NOCH (Negativ-Kontrolle): nie zwei aktive Root-Leases gleichzeitig,
|
|
# nie kein Gewinner (genau ein scope-bezogener Lock existiert nach der Akquise) — AC-b/AC-d
|
|
# A-4 LEASE_HOLD_NICHT_MUTATION: der abgewiesene Producer verändert weder wiki/ noch den
|
|
# bestehenden Lock, erzeugt keinen Compilation-Commit, entfernt keine fremde Lease,
|
|
# beendet sauber mit LEASE_HOLD — AC-c
|
|
# A-5 GEWINNER_SCHLUESSEL_OHNE_RUNID: der Lock-Name ist ausschließlich ein scope-bezogener
|
|
# Ref (kein Run-ID-/<id>-Bestandteil im Schlüssel); die per-Lease-<id>-Ablage (§5.11
|
|
# Pkt. 1) bleibt per-Lease-inhaltlich/ablagebasiert, Exklusivität hängt am Scope-Lock — AC-a
|
|
# A-6 SPAET_ABGEWIESEN: eine spätere, sequentielle Akquise desselben Root-Scopes gg. den
|
|
# noch gehaltenen Lock → LEASE_HOLD, Lock-Inhalt (fremde Run-ID) unverändert — AC-c
|
|
# A-7 KOLLISIONS_HOLD: zwei Branches mit ungleichen Änderungen am selben Concept-Pfad →
|
|
# kein textueller Auto-Merge; strukturierter Kollisions-Hold mit beiden Commit-Hashes
|
|
# und Scope an Epic 4 (benannte Hold-Mechanik §5.16 Pkt. 8/§5.10 Pkt. 8) — AC-e/AD-17c
|
|
# A-8 FREIGABE_ERNEUT: nach dem Lock-Release (Freigabe deterministisch) erwirbt ein
|
|
# nachfolgender Producer denselben Root-Scope atomar erneut; Scope-Lock wieder genau
|
|
# einer (Registry-/Lifecycle-Nachfolge bleibt Story 3.12; hier nur Akquise-Atomarität belegt)
|
|
# Atomare Akquise (§5.17 Pkt. 2) als HARDE Assertion je Szenario (Exit 1 bei Abweichung);
|
|
# keine Wanduhr-/TTL-/Recovery-Logik (A0-20; Story 3.6/3.12); Frontmatter-/log.md-Konformitaet
|
|
# (Vertrag §3.3/§3.4, §5). Ubuntu-Sandbox-Semantik auf einem /tmp-Baum — NIE der reale
|
|
# wiki/- oder raw/-Baum.
|
|
set -u
|
|
ROOT=$(mktemp -d /tmp/sb311-XXXXXX)
|
|
SB="$ROOT/sb"
|
|
mkdir -p "$SB/wiki" "$SB/raw"
|
|
cd "$SB"
|
|
git init -q
|
|
# Determinismus vs. Host-Git-Konfiguration (AD-17h): LF-Blobs + LF-Worktree —
|
|
# autocrlf/filemode-Umwandlung des Hosts wuerde sha256-Vergleiche verschieben.
|
|
git config core.autocrlf false
|
|
git config core.filemode false
|
|
git config user.email "sandbox@test"
|
|
git config user.name "Sandbox"
|
|
|
|
# ---------- Basis-Baum (committete Ausgangslage = Baseline-Commit) ----------
|
|
# Mini-Bundle mit zwei Root-Concepts (alpha als Mutations-Objekt, gamma als Kontrolle).
|
|
cat > wiki/index.md <<'EOF'
|
|
# Index
|
|
- [Alpha](alpha.md)
|
|
- [Gamma](gamma.md)
|
|
EOF
|
|
cat > wiki/alpha.md <<'EOF'
|
|
---
|
|
type: concept
|
|
sources:
|
|
- resource: raw/alpha-v1.md
|
|
id: s1
|
|
generated:
|
|
by: wow-compiler/0.1.0
|
|
at: 2026-08-16T09:00:00Z
|
|
---
|
|
Das Alpha-Protokoll definiert eine deterministische Init-Sequenz (raw/alpha-v1.md#S-1).
|
|
EOF
|
|
cat > wiki/gamma.md <<'EOF'
|
|
---
|
|
type: concept
|
|
sources:
|
|
- resource: raw/gamma-v1.md
|
|
id: s1
|
|
generated:
|
|
by: wow-compiler/0.1.0
|
|
at: 2026-08-16T09:00:00Z
|
|
---
|
|
Gamma beschreibt ein anderes, hier nicht betroffenes Thema.
|
|
EOF
|
|
cat > wiki/log.md <<'EOF'
|
|
# Log
|
|
EOF
|
|
cat > raw/alpha-v1.md <<'EOF'
|
|
### S-1
|
|
Evidenz v1: deterministische Init-Sequenz.
|
|
EOF
|
|
cat > raw/gamma-v1.md <<'EOF'
|
|
### S-1
|
|
Evidenz v1: Gamma-Thema.
|
|
EOF
|
|
git add -A
|
|
git commit -qm "Baseline"
|
|
BASE=$(git rev-parse HEAD)
|
|
echo "BASELINE-COMMIT (Merge-Base, eindeutiger Commit-Object-Wert): $BASE"
|
|
echo "Sandbox-Root: $ROOT (loeschbar: rm -rf $ROOT)"
|
|
echo
|
|
|
|
# ---------- Helfer: Run-Label / Isolation (kein Carry-over zwischen Szenarien) ----------
|
|
runlabel() { echo; echo "########## $1 ##########"; }
|
|
isolate() {
|
|
# Section-Refs (B1..B8) werden je Szenario separat im clone-geteilten Zustand angelegt;
|
|
# die isolierten Arbeits-Branches der Szenarien verwenden sich nicht gegenseitig.
|
|
git checkout -qf "$BASE"
|
|
git reset -q --hard "$BASE"
|
|
git clean -qfd wiki raw lease registry scratch plan-run
|
|
# 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) ----------
|
|
# Der Root-Scope-Lock ist EINE scope-bezogene Ref im clone-geteilten Zustand (geteilter
|
|
# Git-Ref-/Objektnamespace aller Worktrees/Prozesse eines Clones) — Schlüssel = Scope
|
|
# (Ref-Name), Inhalt = Run-ID (Lock-Inhalt, NICHT Schlüsselbestandteil; AC-a).
|
|
# Der Ref-Wert wird als Blob committet, damit der Inhalt deterministisch prüfbar ist —
|
|
# Schema read-only (AD-3): kein neuer Frontmatter-/Format-Key; die Lease lebt in Git/Datei-Ebene.
|
|
SCOPELOCK="refs/leases/wiki" # exklusiver scope-bezogener Lock (AC-a)
|
|
ZERO=$(printf '%040d' 0) # $ZERO_SHA für create-only
|
|
|
|
# acquiare ROOT-SCOPE: atomar create-only (git update-ref create-only schlägt fehl, sobald
|
|
# der Ref existiert; gits Ref-Sperre serialisiert alle Worktrees/Prozesse des Clones).
|
|
# Die erwartete Fehlermeldung eines abgewiesenen Versuchs (fatal: ... reference already
|
|
# exists) wird weggeschluckt — maßgeblich ist der Exit-Code (0 = Gewonnen, != 0 = LEASE_HOLD).
|
|
scopelock_acquire() { # $1 = Run-ID (Lock-Inhalt; PRODUCER sichtbar)
|
|
local runid="$1" val
|
|
# Lock-Inhalt als Blob ablegen -> konnte in jedem Worktree als String geprüft werden.
|
|
val=$(printf '%s' "$runid" | git hash-object -w --stdin) || return 2
|
|
{
|
|
git update-ref "$SCOPELOCK" "$val" "$ZERO"
|
|
} 2>/dev/null
|
|
}
|
|
scopelock_release() { # $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
|
|
# Ablage-Wert, kein Schlüsselbestandteil (AC-a).
|
|
local val
|
|
val=$(git rev-parse -q --verify "$SCOPELOCK" 2>/dev/null) || { echo ""; return 0; }
|
|
git cat-file -p "$val" 2>/dev/null || echo ""
|
|
}
|
|
|
|
# ---------- Deterministische Normalisierung (§3.2 Pkt. 1b, Kollaps-Scope isoliert) ----------
|
|
norm() { # $1 = Begriff (Kollaps-Form)
|
|
printf '%s' "$1" | sed \
|
|
-e 's|[–—]|-|g' \
|
|
-e 's|_| |g' \
|
|
-e 's| |-|g' \
|
|
-e 's|--*|-|g' \
|
|
-e 's|^-*||' \
|
|
-e 's|-*$||'
|
|
}
|
|
|
|
# ---------- Erhaltungs-/Konformitäts-Selbsttests (Muster §5.9 Pkt. 5 / Vertrag §3.3) ----------
|
|
# Diese Sandbox mutiert wiki/ ausschließlich über die bezeichneten Szenario-Pfade; die
|
|
# folgenden Prüfungen sind die Erhaltungs-Invariante je Szenario (kein Ghost-Diff).
|
|
assert_frontmatter() { # $1 = Datei ; prüft eine §3.3/§3.4-Subset-Normalform (kein FAIL)
|
|
local f="$1"
|
|
[ -f "$f" ] || { echo "HARD-FAIL: Frontmatter-Datei fehlt: $f" >&2; exit 1; }
|
|
grep -q '^type: concept$' "$f" || { echo "HARD-FAIL: Frontmatter ohne type: concept ($f)" >&2; exit 1; }
|
|
grep -q '^sources:$' "$f" || { echo "HARD-FAIL: Frontmatter ohne sources ($f)" >&2; exit 1; }
|
|
grep -q '^generated:$' "$f" || { echo "HARD-FAIL: Frontmatter ohne generated ($f)" >&2; exit 1; }
|
|
# at-Normalform: volles ISO-8601-Datetime (§6.5 Kriterium 2)
|
|
grep -Eq '^ at: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$' "$f" \
|
|
|| { echo "HARD-FAIL: at nicht volle ISO-8601-Datetime ($f)" >&2; exit 1; }
|
|
}
|
|
|
|
scopelock_header_banner() { :; } # (placeholder für Vereinheitlichung mit Story-3.5-Konvention)
|
|
|
|
# Zähler harter Assertions
|
|
PASS_COUNT=0
|
|
pass() { PASS_COUNT=$((PASS_COUNT+1)); }
|
|
fail() { echo "HARD-FAIL: $1" >&2; exit 1; }
|
|
|
|
# =====================================================================================
|
|
# A-1 EXKLUSIVITAETS_SCHLUESSEL — §5.17 Pkt. 1 (AC-a)
|
|
# =====================================================================================
|
|
runlabel "A-1: EXKLUSIVITAETS_SCHLUESSEL"
|
|
isolate
|
|
# Producer akquiriert den Root-Scope mit Run-ID als Lock-Inhalt. (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
|
|
# Genau ein scope-bezogener Lock existiert (AC-a/AC-d).
|
|
n=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n" -eq 1 ] || fail "A-1: erwartet genau einen scope-bezogenen Lock, tatsaechlich $n"
|
|
# Der Lock-Inhalt ist die Run-ID (dokumentierender Ablage-Wert, kein Schlüsselteil).
|
|
[ "$(scopelock_content)" = "RUN-A1" ] || fail "A-1: Lock-Inhalt != Run-ID (AC-a)"
|
|
scopelock_release "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
|
|
|
|
# =====================================================================================
|
|
# A-2 ATOMAR_ZWEI_WORKTREE — §5.17 Pkt. 2 (AC-b) — reale Zwei-Worktree-/Zwei-Prozess-Überlappung
|
|
# =====================================================================================
|
|
runlabel "A-2: ATOMAR_ZWEI_WORKTREE (Ueberlappung zweier getrennter Prozesse in getrennten Worktrees)"
|
|
# Re-Run-Sicherheit: verwaiste worktree-Registrierungen aus früheren Läufen (anderes $ROOT)
|
|
# erst prune, sonst blockiert `git worktree add` oder trägt Altpfade nach.
|
|
git worktree prune
|
|
git worktree add -q "$ROOT/wt-a" "$BASE" || fail "A-2: Worktree wt-a nicht aufgebaut"
|
|
git worktree add -q "$ROOT/wt-b" "$BASE" || fail "A-2: Worktree wt-b nicht aufgebaut"
|
|
# Scope-Lock vor dem Start leer (kein Carry-over)
|
|
[ -z "$(scopelock_content)" ] || fail "A-2: Scope-Lock nicht leer vor dem Lauf"
|
|
# Zwei GETRENNTE Prozesse (Subshells) akquirieren zeitlich überlappend denselben
|
|
# Root-Scope — kein sequenzieller Ablauf, echte Überlappung (kein Schein-Parallelität;
|
|
# zeitlicher Versatz ohne Überlappung bewiese keine Atomarität). Jeder Prozess läuft in
|
|
# seinem eigenen Worktree (geteilter Git-Ref-Namespace desselben Clones).
|
|
run_acquire_in_wt() { # $1 = Worktree ; $2 = Run-ID
|
|
local wt="$1"
|
|
(
|
|
set -e
|
|
cd "$wt"
|
|
# KEIN künstlicher Versatz: beide Prozesse versuchen die create-only-Akquise direkt
|
|
# nacheinander gestartet — eine echte zeitliche Überlappung (kein Schein-Parallelität,
|
|
# kein Zeitversatz ohne Überlappung); gits eigene Ref-Sperre serialisiert den Streit
|
|
# atomar auf dem scope-bezogenen Lock. Der LOCK allein entscheidet Gewinner/Verlierer.
|
|
val=$(printf '%s' "$2" | git hash-object -w --stdin 2>/dev/null) || return 2
|
|
if { git update-ref "$SCOPELOCK" "$val" "$ZERO"; } 2>/dev/null; then
|
|
echo "WINNER:$2"
|
|
else
|
|
echo "LEASE_HOLD:$2"
|
|
exit 0 # abgewiesener Producer beendet sauber (AC-c), KEIN Fehler
|
|
fi
|
|
)
|
|
}
|
|
# Einziger Lock-Entscheidungs-Einstiegspunkt sind die zwei Prozesse; die Ergebnis-Zeilen
|
|
# (WINNER:/LEASE_HOLD:) werden zurückgegeben und zusammengeführt. Parallel 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" &
|
|
seen_vals=""
|
|
for i in $(seq 1 25); do
|
|
cnt=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK")
|
|
[ "${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
|
|
)
|
|
)
|
|
printf '%s\n' "$out_a" | grep '^WINNER:' | sed 's/^/ /'
|
|
printf '%s\n' "$out_a" | grep '^LEASE_HOLD:' | sed 's/^/ /'
|
|
# Genau ein Gewinner und genau ein LEASE_HOLD (AC-b): niemals zwei, niemals keiner (AC-d).
|
|
winners=$(printf '%s\n' "$out_a" | grep -c '^WINNER:')
|
|
holds=$(printf '%s\n' "$out_a" | grep -c '^LEASE_HOLD:')
|
|
[ "$winners" -eq 1 ] || fail "A-2: erwartet genau einen Gewinner, tatsaechlich $winners (AC-b)"
|
|
[ "$holds" -eq 1 ] || fail "A-2: erwartet genau ein LEASE_HOLD, tatsaechlich $holds (AC-b)"
|
|
# Kein Zwischenzustand mit zwei aktiven Locks (Sampler-Fund, 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 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)
|
|
[ -n "$winner_id" ] || fail "A-2: Gewinner-Run-ID nicht ableitbar"
|
|
[ "$(scopelock_content)" = "$winner_id" ] || fail "A-2: Lock-Inhalt != Gewinner-Run-ID ($winner_id)"
|
|
# Zwischenzustands-Assertion „niemals zwei aktive Root-Leases": der Lock ist ein einziger
|
|
# scope-bezogener Ref — zu KEINEM Zeitpunkt können zwei verschiedene Werte gleichzeitig
|
|
# existieren (create-only + eine Ref). Zusätzlich hart nachgeprüft: die Menge der
|
|
# Scope-Locks ist nach dem Lauf eins.
|
|
n2=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n2" -eq 1 ] || fail "A-2: nach überlappendem Lauf erwartet genau einen Scope-Lock, tatsaechlich $n2"
|
|
scopelock_release "$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
|
|
|
|
# =====================================================================================
|
|
# A-3 WEDER_NOCH (Negativ-Kontrolle) — nie zwei aktive Root-Leases, nie kein Gewinner
|
|
# =====================================================================================
|
|
runlabel "A-3: WEDER_NOCH (Negative Kontrolle — nie zwei, nie keiner)"
|
|
isolate
|
|
# Genau ein scope-bezogener Lock nach Akquise (anfangs keiner).
|
|
[ -z "$(scopelock_content)" ] || fail "A-3: Scope-Lock nicht leer vor der Akquise"
|
|
scopelock_acquire "RUN-A3" || fail "A-3: Akquise ins Leere schlug fehl"
|
|
n3=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n3" -eq 1 ] || fail "A-3: erwartet genau einen scope-bezogenen Lock, tatsaechlich $n3"
|
|
# Eine zweite, 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 "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: Mutation + Commit ABGEWEHRT)"
|
|
isolate
|
|
scopelock_acquire "RUN-A4-fremd" || fail "A-4: erste Akquise schlug fehl"
|
|
# 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"
|
|
else
|
|
# 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"
|
|
# 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)"
|
|
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
|
|
|
|
# =====================================================================================
|
|
# A-5 GEWINNER_SCHLUESSEL_OHNE_RUNID — AC-a (Schluessel traegt keine Run-ID/<id>)
|
|
# =====================================================================================
|
|
runlabel "A-5: GEWINNER_SCHLUESSEL_OHNE_RUNID"
|
|
isolate
|
|
# Der SCOPELOCK-Ref-Name enthält weder die Run-ID noch eine <id> (§5.11-Pkt.-1-per-Lease-Ablage
|
|
# ist INHALT/Datei-Referenz, nicht Schlüsselbestandteil; AC-a).
|
|
case "$SCOPELOCK" in *$(norm "RUN-A5")*|*run-a5*|*a5*) fail "A-5: Schlüssel-Ref trägt Run-ID/<id>" ;; esac
|
|
# Der Lock-Inhalt trägt die Run-ID als Ablage-Wert (Beweis der Inhalt-als-Schlüssel-Trennung).
|
|
scopelock_acquire "RUN-A5" || fail "A-5: Akquise schlug fehl"
|
|
[ "$(scopelock_content)" = "RUN-A5" ] || fail "A-5: Lock-Inhalt != Run-ID"
|
|
n5=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n5" -eq 1 ] || fail "A-5: erwartet genau einen Scope-Lock"
|
|
scopelock_release "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
|
|
|
|
# =====================================================================================
|
|
# A-6 SPAET_ABGEWIESEN — §5.17 Pkt. 3 (AC-c), sequentielle Abweisung gg. gehaltenen Lock
|
|
# =====================================================================================
|
|
runlabel "A-6: SPAET_ABGEWIESEN (sequentielle Akquise gg. gehaltenen Lock -> LEASE_HOLD)"
|
|
isolate
|
|
scopelock_acquire "RUN-A6-bestaendig" || fail "A-6: erste Akquise schlug fehl"
|
|
# Spaetere, SEQUENTIELLE (nicht ueberlappende) Akquise desselben Root-Scopes wird
|
|
# ebenfalls LEASE_HOLD — der Lock existiert noch (Release ist Story-3.11-Explizit hier).
|
|
out_a6=$(scopelock_acquire "RUN-A6-spaet" && echo "UNEXPECTED_WIN" || echo "LEASE_HOLD")
|
|
[ "$out_a6" = "LEASE_HOLD" ] || fail "A-6: spaete Akquise erwartet LEASE_HOLD"
|
|
[ "$(scopelock_content)" = "RUN-A6-bestaendig" ] || fail "A-6: Lock-Inhalt (fremde Run-ID) veraendert durch spaete Abweisung"
|
|
scopelock_release "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
|
|
|
|
# =====================================================================================
|
|
# A-7 KOLLISIONS_HOLD — §5.17 Pkt. 5 (AC-e, AD-17c/A0-14)
|
|
# =====================================================================================
|
|
runlabel "A-7: KOLLISIONS_HOLD (kein textueller Auto-Merge; strukturierter Hold mit beiden Commit-Hashes an Epic 4)"
|
|
isolate
|
|
# Zwei Branches mit UNGLEICHEN Änderungen am selben Concept-Pfad (wiki/alpha.md).
|
|
git checkout -q -b branch-x "$BASE"
|
|
cat > wiki/alpha.md <<'EOF'
|
|
---
|
|
type: concept
|
|
sources:
|
|
- resource: raw/alpha-v1.md
|
|
id: s1
|
|
generated:
|
|
by: wow-compiler/0.1.0
|
|
at: 2026-08-16T10:00:00Z
|
|
---
|
|
Alpha-Variante X: deterministische Init-Sequenz mit Synchron-Kopplung (raw/alpha-v1.md#S-1).
|
|
EOF
|
|
# 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)
|
|
|
|
git checkout -q -b branch-y "$BASE"
|
|
cat > wiki/alpha.md <<'EOF'
|
|
---
|
|
type: concept
|
|
sources:
|
|
- resource: raw/alpha-v1.md
|
|
id: s1
|
|
generated:
|
|
by: wow-compiler/0.1.0
|
|
at: 2026-08-16T11:00:00Z
|
|
---
|
|
Alpha-Variante Y: deterministische Init-Sequenz OHNE Synchron-Kopplung (raw/alpha-v1.md#S-1).
|
|
EOF
|
|
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): 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 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
|
|
# + 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
|
|
|
|
# =====================================================================================
|
|
# A-8 FREIGABE_ERNEUT — Freigabe deterministisch; neuer Producer erwirbt erneut atomar
|
|
# =====================================================================================
|
|
runlabel "A-8: FREIGABE_ERNEUT (nach Release wieder genau ein Lock)"
|
|
isolate
|
|
scopelock_acquire "RUN-A8-erster" || fail "A-8: erste Akquise schlug fehl"
|
|
[ "$(scopelock_content)" = "RUN-A8-erster" ] || fail "A-8: Lock-Inhalt != erste Run-ID"
|
|
# 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: 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"
|
|
# 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: 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
|
|
|
|
# =====================================================================================
|
|
# Abschluss: Gesamt-Aussage und Zaehler (A-1..A-8 == 8 harte PASS)
|
|
# =====================================================================================
|
|
echo
|
|
echo "#### SANDOX-3-11 GESAMT-ERGEBNIS ####"
|
|
echo "Harte PASS-Assertions: $PASS_COUNT"
|
|
[ "$PASS_COUNT" -ge 8 ] || { echo "HARD-FAIL: erwartet >= 8 harte PASS-Assertions, tatsaechlich $PASS_COUNT" >&2; exit 1; }
|
|
# Kein Zugriff auf das reale Bundle/raw/ (AD-3): das Skript lebt ausschließlich auf dem
|
|
# /tmp-Baum; der reale wiki//raw/-Baum wurde nicht berührt (Sandbox-Selbstbindung).
|
|
# 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)"
|
|
exit 0
|