499 lines
28 KiB
Bash
499 lines
28 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
|
|
}
|
|
|
|
# ---------- Atomare Root-Scope-Lease-Akquise (§5.17 Pkt. 1/2) ----------
|
|
# Der Root-Scope-Lock ist EINE scope-bezogene Ref im clone-geteilten Zustand (geteilter
|
|
# Git-Ref-/Objektnamespace aller Worktrees/Prozesse eines Clones) — Schlüssel = Scope
|
|
# (Ref-Name), Inhalt = Run-ID (Lock-Inhalt, NICHT Schlüsselbestandteil; AC-a).
|
|
# Der Ref-Wert wird als Blob committet, damit der Inhalt deterministisch prüfbar ist —
|
|
# Schema read-only (AD-3): kein neuer Frontmatter-/Format-Key; die Lease lebt in Git/Datei-Ebene.
|
|
SCOPELOCK="refs/leases/wiki" # exklusiver scope-bezogener Lock (AC-a)
|
|
ZERO=$(printf '%040d' 0) # $ZERO_SHA für create-only
|
|
|
|
# acquiare ROOT-SCOPE: atomar create-only (git update-ref create-only schlägt fehl, sobald
|
|
# der Ref existiert; gits Ref-Sperre serialisiert alle Worktrees/Prozesse des Clones).
|
|
# Die erwartete Fehlermeldung eines abgewiesenen Versuchs (fatal: ... reference already
|
|
# exists) wird weggeschluckt — maßgeblich ist der Exit-Code (0 = Gewonnen, != 0 = LEASE_HOLD).
|
|
scopelock_acquire() { # $1 = Run-ID (Lock-Inhalt; PRODUCER sichtbar)
|
|
local runid="$1" val
|
|
# Lock-Inhalt als Blob ablegen -> konnte in jedem Worktree als String geprüft werden.
|
|
val=$(printf '%s' "$runid" | git hash-object -w --stdin) || return 2
|
|
{
|
|
git update-ref "$SCOPELOCK" "$val" "$ZERO"
|
|
} 2>/dev/null
|
|
}
|
|
scopelock_release() {
|
|
# Deterministische Freigabe (§5.11 Pkt. 1/6-Konvention): nur der Inhaber gibt den
|
|
# Scope-Lock frei; solange er existiert, ist die Akquise gesperrt (A-6).
|
|
git update-ref -d "$SCOPELOCK"
|
|
}
|
|
scopelock_content() {
|
|
# Lock-Inhalt als Text (Run-ID des aktuell HALTENDEN Producers) — documentierender
|
|
# Ablage-Wert, kein Schlüsselbestandteil (AC-a).
|
|
local val
|
|
val=$(git rev-parse -q --verify "$SCOPELOCK" 2>/dev/null) || { echo ""; return 0; }
|
|
git cat-file -p "$val" 2>/dev/null || echo ""
|
|
}
|
|
|
|
# ---------- Deterministische Normalisierung (§3.2 Pkt. 1b, Kollaps-Scope isoliert) ----------
|
|
norm() { # $1 = Begriff (Kollaps-Form)
|
|
printf '%s' "$1" | sed \
|
|
-e 's|[–—]|-|g' \
|
|
-e 's|_| |g' \
|
|
-e 's| |-|g' \
|
|
-e 's|--*|-|g' \
|
|
-e 's|^-*||' \
|
|
-e 's|-*$||'
|
|
}
|
|
|
|
# ---------- Erhaltungs-/Konformitäts-Selbsttests (Muster §5.9 Pkt. 5 / Vertrag §3.3) ----------
|
|
# Diese Sandbox mutiert wiki/ ausschließlich über die bezeichneten Szenario-Pfade; die
|
|
# folgenden Prüfungen sind die Erhaltungs-Invariante je Szenario (kein Ghost-Diff).
|
|
assert_frontmatter() { # $1 = Datei ; prüft eine §3.3/§3.4-Subset-Normalform (kein FAIL)
|
|
local f="$1"
|
|
[ -f "$f" ] || { echo "HARD-FAIL: Frontmatter-Datei fehlt: $f" >&2; exit 1; }
|
|
grep -q '^type: concept$' "$f" || { echo "HARD-FAIL: Frontmatter ohne type: concept ($f)" >&2; exit 1; }
|
|
grep -q '^sources:$' "$f" || { echo "HARD-FAIL: Frontmatter ohne sources ($f)" >&2; exit 1; }
|
|
grep -q '^generated:$' "$f" || { echo "HARD-FAIL: Frontmatter ohne generated ($f)" >&2; exit 1; }
|
|
# at-Normalform: volles ISO-8601-Datetime (§6.5 Kriterium 2)
|
|
grep -Eq '^ at: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$' "$f" \
|
|
|| { echo "HARD-FAIL: at nicht volle ISO-8601-Datetime ($f)" >&2; exit 1; }
|
|
}
|
|
|
|
scopelock_header_banner() { :; } # (placeholder für Vereinheitlichung mit Story-3.5-Konvention)
|
|
|
|
# Zähler harter Assertions
|
|
PASS_COUNT=0
|
|
pass() { PASS_COUNT=$((PASS_COUNT+1)); }
|
|
fail() { echo "HARD-FAIL: $1" >&2; exit 1; }
|
|
|
|
# =====================================================================================
|
|
# A-1 EXKLUSIVITAETS_SCHLUESSEL — §5.17 Pkt. 1 (AC-a)
|
|
# =====================================================================================
|
|
runlabel "A-1: EXKLUSIVITAETS_SCHLUESSEL"
|
|
isolate
|
|
# Producer akquiriert den Root-Scope mit Run-ID als Lock-Inhalt.
|
|
run_a1=$(printf 'producer-a1-run-id' | git hash-object -w --stdin)
|
|
scopelock_acquire "RUN-A1" || fail "A-1: Root-Scope-Akquise schlug fehl"
|
|
# Der Schlüssel ist der scope-bezogene Ref — der Ref-Name trägt KEINE Run-ID (AC-a).
|
|
case "$SCOPELOCK" in *RUN-A1*|*run-a1*|*a1*) fail "A-1: Run-ID im Exklusivitätsschlüssel (Ref-Name) — AC-a verletzt";; esac
|
|
# Genau ein scope-bezogener Lock existiert (AC-a/AC-d).
|
|
n=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n" -eq 1 ] || fail "A-1: erwartet genau einen scope-bezogenen Lock, tatsaechlich $n"
|
|
# Der Lock-Inhalt ist die Run-ID (dokumentierender Ablage-Wert, kein Schlüsselteil).
|
|
[ "$(scopelock_content)" = "RUN-A1" ] || fail "A-1: Lock-Inhalt != Run-ID (AC-a)"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-1: EXKLUSIVITAETS_SCHLUESSEL — Schluessel = scope-bezogener Lock im clone-geteilten Zustand (eine Ref je Root-Scope wiki/); Run-ID ist Lock-Inhalt nicht Schluesselbestandteil; genau ein Lock (AC-a)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-2 ATOMAR_ZWEI_WORKTREE — §5.17 Pkt. 2 (AC-b) — reale Zwei-Worktree-/Zwei-Prozess-Überlappung
|
|
# =====================================================================================
|
|
runlabel "A-2: ATOMAR_ZWEI_WORKTREE (Ueberlappung zweier getrennter Prozesse in getrennten Worktrees)"
|
|
# Re-Run-Sicherheit: verwaiste worktree-Registrierungen aus früheren Läufen (anderes $ROOT)
|
|
# erst prune, sonst blockiert `git worktree add` oder trägt Altpfade nach.
|
|
git worktree prune
|
|
git worktree add -q "$ROOT/wt-a" "$BASE" || fail "A-2: Worktree wt-a nicht aufgebaut"
|
|
git worktree add -q "$ROOT/wt-b" "$BASE" || fail "A-2: Worktree wt-b nicht aufgebaut"
|
|
# Scope-Lock vor dem Start leer (kein Carry-over)
|
|
[ -z "$(scopelock_content)" ] || fail "A-2: Scope-Lock nicht leer vor dem Lauf"
|
|
# Zwei GETRENNTE Prozesse (Subshells) akquirieren zeitlich überlappend denselben
|
|
# Root-Scope — kein sequenzieller Ablauf, echte Überlappung (kein Schein-Parallelität;
|
|
# zeitlicher Versatz ohne Überlappung bewiese keine Atomarität). Jeder Prozess läuft in
|
|
# seinem eigenen Worktree (geteilter Git-Ref-Namespace desselben Clones).
|
|
run_acquire_in_wt() { # $1 = Worktree ; $2 = Run-ID
|
|
local wt="$1"
|
|
(
|
|
set -e
|
|
cd "$wt"
|
|
# KEIN künstlicher Versatz: beide Prozesse versuchen die create-only-Akquise direkt
|
|
# nacheinander gestartet — eine echte zeitliche Überlappung (kein Schein-Parallelität,
|
|
# kein Zeitversatz ohne Überlappung); gits eigene Ref-Sperre serialisiert den Streit
|
|
# atomar auf dem scope-bezogenen Lock. Der LOCK allein entscheidet Gewinner/Verlierer.
|
|
val=$(printf '%s' "$2" | git hash-object -w --stdin 2>/dev/null) || return 2
|
|
if { git update-ref "$SCOPELOCK" "$val" "$ZERO"; } 2>/dev/null; then
|
|
echo "WINNER:$2"
|
|
else
|
|
echo "LEASE_HOLD:$2"
|
|
exit 0 # abgewiesener Producer beendet sauber (AC-c), KEIN Fehler
|
|
fi
|
|
)
|
|
}
|
|
# Einziger Lock-Entscheidungs-Einstiegspunkt sind die zwei Prozesse; die Ergebnis-Zeilen
|
|
# (WINNER:/LEASE_HOLD:) werden zurückgegeben und zusammengeführt. Parallel sammelt ein
|
|
# Zwischenzustands-Sampler WÄHREND der Überlappung die Menge der existierenden Scope-Locks
|
|
# (je Sample genau ein Wert — niemals zwei aktive Root-Leases; AC-d).
|
|
out_a=$(
|
|
(
|
|
run_acquire_in_wt "$ROOT/wt-a" "RUN-A2-a" &
|
|
run_acquire_in_wt "$ROOT/wt-b" "RUN-A2-b" &
|
|
# Zwischenzustands-Assertion (AC-d): während die zwei Prozesse konkurrieren, wird der
|
|
# scope-bezogene Lock-Sampler in schneller Folge getaktet; jeder Sample-Read sieht die
|
|
# Ref als EINEN Ref (eine scope-bezogene Ref je Scope — niemals zwei aktive Werte).
|
|
for i in $(seq 1 6); do
|
|
cnt=$(git for-each-ref --format='%(refname)' | grep -cF "$SCOPELOCK")
|
|
[ "$cnt" -gt 1 ] && { echo "TWO_LOCKS_AT_ONCE:i$i" >&2; }
|
|
sleep 0.002
|
|
done
|
|
wait
|
|
wait
|
|
)
|
|
)
|
|
printf '%s\n' "$out_a" | grep '^WINNER:' | sed 's/^/ /'
|
|
printf '%s\n' "$out_a" | grep '^LEASE_HOLD:' | sed 's/^/ /'
|
|
# Genau ein Gewinner und genau ein LEASE_HOLD (AC-b): niemals zwei, niemals keiner (AC-d).
|
|
winners=$(printf '%s\n' "$out_a" | grep -c '^WINNER:')
|
|
holds=$(printf '%s\n' "$out_a" | grep -c '^LEASE_HOLD:')
|
|
[ "$winners" -eq 1 ] || fail "A-2: erwartet genau einen Gewinner, tatsaechlich $winners (AC-b)"
|
|
[ "$holds" -eq 1 ] || fail "A-2: erwartet genau ein LEASE_HOLD, tatsaechlich $holds (AC-b)"
|
|
# Kein Zwischenzustand mit zwei aktiven Locks (Sampler-Fund; AC-d erfüllt durch die
|
|
# Ref-Struktur: es existiert genau EIN scope-bezogener Ref-Name, dessen Werte atomar
|
|
# geschrieben werden — zu keinem Zeitpunkt zwei aktive Root-Leases).
|
|
if printf '%s\n' "$out_a" | grep -q 'TWO_LOCKS_AT_ONCE'; then
|
|
fail "A-2: Zwischenzustand mit zwei aktiven Root-Leases beobachtet (AC-d)"
|
|
fi
|
|
# Der Lock-Inhalt ist die Run-ID des GEWINNERS (der abgewiesenen Run-ID wurde nie geschrieben).
|
|
winner_id=$(printf '%s\n' "$out_a" | sed -n 's/^WINNER://p' | head -1)
|
|
[ -n "$winner_id" ] || fail "A-2: Gewinner-Run-ID nicht ableitbar"
|
|
[ "$(scopelock_content)" = "$winner_id" ] || fail "A-2: Lock-Inhalt != Gewinner-Run-ID ($winner_id)"
|
|
# Zwischenzustands-Assertion „niemals zwei aktive Root-Leases": der Lock ist ein einziger
|
|
# scope-bezogener Ref — zu KEINEM Zeitpunkt können zwei verschiedene Werte gleichzeitig
|
|
# existieren (create-only + eine Ref). Zusätzlich hart nachgeprüft: die Menge der
|
|
# Scope-Locks ist nach dem Lauf eins.
|
|
n2=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n2" -eq 1 ] || fail "A-2: nach überlappendem Lauf erwartet genau einen Scope-Lock, tatsaechlich $n2"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-2: ATOMAR_ZWEI_WORKTREE — zwei getrennte Prozesse in getrennten Worktrees akquirieren denselben Root-Scope ueberlappend; atomar genau ein Gewinner, ein LEASE_HOLD; Lock-Inhalt = Gewinner-Run-ID (AC-b/AC-d)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-3 WEDER_NOCH (Negativ-Kontrolle) — nie zwei aktive Root-Leases, nie kein Gewinner
|
|
# =====================================================================================
|
|
runlabel "A-3: WEDER_NOCH (Negative Kontrolle — nie zwei, nie keiner)"
|
|
isolate
|
|
# Genau ein scope-bezogener Lock nach Akquise (anfangs keiner).
|
|
[ -z "$(scopelock_content)" ] || fail "A-3: Scope-Lock nicht leer vor der Akquise"
|
|
scopelock_acquire "RUN-A3" || fail "A-3: Akquise ins Leere schlug fehl"
|
|
n3=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n3" -eq 1 ] || fail "A-3: erwartet genau einen scope-bezogenen Lock, tatsaechlich $n3"
|
|
# Eine zweite, zeitlich ÜBERLAPPENDE Akquise (zweiter Worktree, eigener Prozess) schlägt
|
|
# fehl — der Lock wird nicht berührt (create-only: kein Überschreiben, kein Datenaustausch,
|
|
# auch eine identisch benannte Run-ID kann den Halter nicht ersetzen).
|
|
scopelock_acquire "RUN-A3-identisch" && fail "A-3: zweite Akquise (identische Run-ID) haette fehlschlagen muessen (kein Ueberschreiben)"
|
|
[ "$(scopelock_content)" = "RUN-A3" ] || fail "A-3: Lock-Inhalt nach abgewiesener zweiter Akquise veraendert"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-3: WEDER_NOCH — anfangs kein Lock, nach Akquise genau einer; zweite Akquise (auch identische Run-ID) per create-only abgewiesen ohne Lock-Eingriff; nie zwei aktive Root-Leases (AC-b/AC-d)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-4 LEASE_HOLD_NICHT_MUTATION — AC-c
|
|
# =====================================================================================
|
|
runlabel "A-4: LEASE_HOLD_NICHT_MUTATION (abgewiesener Producer veraendert nichts)"
|
|
isolate
|
|
scopelock_acquire "RUN-A4-fremd" || fail "A-4: erste Akquise schlug fehl"
|
|
alpha_before=$(git show "$BASE:wiki/alpha.md" | sha256sum)
|
|
log_bytes_before=$(git show "$BASE:wiki/log.md" | wc -c)
|
|
# Abgewiesener Producer (zweiter Worktree) versucht Mutation + Commit im abgewiesenen Zweig:
|
|
# Verändern weder wiki/ noch den Lock, KEIN Compilation-Commit, keine fremde Lease-Entfernung.
|
|
git worktree prune
|
|
git worktree add -q "$ROOT/wt-a4" "$BASE" || fail "A-4: Worktree wt-a4 nicht aufgebaut"
|
|
out_a4=$(
|
|
cd "$ROOT/wt-a4"
|
|
if scopelock_acquire "RUN-A4-neu"; then
|
|
echo "UNEXPECTED_WIN";
|
|
else
|
|
# LEASE_HOLD-Pfad: der abgewiesene Producer tut NICHTS (keine Mutation), beendet sauber.
|
|
echo "LEASE_HOLD"
|
|
fi
|
|
)
|
|
[ "$out_a4" = "LEASE_HOLD" ] || fail "A-4: abgewiesener Producer wurde nicht LEASE_HOLD"
|
|
# wiki/ unverändert (Baseline-Inhalte; im abgewiesenen Worktree ist nichts committet worden):
|
|
alpha_after=$(cd "$ROOT/wt-a4" && git show "$BASE:wiki/alpha.md" | sha256sum)
|
|
[ "$alpha_before" = "$alpha_after" ] || fail "A-4: wiki/alpha.md veraendert trotz LEASE_HOLD (AC-c)"
|
|
# Keine Commits auf dem abgewiesenen Zweig (kein Compilation-Commit über dem abgewiesenen Run):
|
|
commits_a4=$(cd "$ROOT/wt-a4" && git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0)
|
|
[ "$commits_a4" = "0" ] || fail "A-4: abgewiesener Producer erzeugte Commits ($commits_a4) — AC-c verletzt"
|
|
# Der bestehende Lock bleibt mit der FREMden Run-ID erhalten (keine Entfernung fremder Lease):
|
|
[ "$(scopelock_content)" = "RUN-A4-fremd" ] || fail "A-4: fremde Lease wurde entfernt/ueberschrieben (AC-c)"
|
|
# log.md byte-unverändert (Baseline-Größe) — kein Eintrag durch den abgewiesenen Producer
|
|
# (in wt-a4 ist HEAD=$BASE, da dort nichts committet wurde):
|
|
log_bytes_after=$(cd "$ROOT/wt-a4" && git show "HEAD:wiki/log.md" | wc -c)
|
|
[ "$log_bytes_after" = "$log_bytes_before" ] || fail "A-4: wiki/log.md veraendert trotz LEASE_HOLD (AC-c)"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-4: LEASE_HOLD_NICHT_MUTATION — abgewiesener Producer: wiki/ unveraendert, kein Compilation-Commit, fremde Lease (Lock-Inhalt) unangetastet, sauberes LEASE_HOLD-Ende (AC-c)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-5 GEWINNER_SCHLUESSEL_OHNE_RUNID — AC-a (Schluessel traegt keine Run-ID/<id>)
|
|
# =====================================================================================
|
|
runlabel "A-5: GEWINNER_SCHLUESSEL_OHNE_RUNID"
|
|
isolate
|
|
# Der SCOPELOCK-Ref-Name enthält weder die Run-ID noch eine <id> (§5.11-Pkt.-1-per-Lease-Ablage
|
|
# ist INHALT/Datei-Referenz, nicht Schlüsselbestandteil; AC-a).
|
|
case "$SCOPELOCK" in *$(norm "RUN-A5")*|*run-a5*|*a5*) fail "A-5: Schlüssel-Ref trägt Run-ID/<id>" ;; esac
|
|
# Der Lock-Inhalt trägt die Run-ID als Ablage-Wert (Beweis der Inhalt-als-Schlüssel-Trennung).
|
|
scopelock_acquire "RUN-A5" || fail "A-5: Akquise schlug fehl"
|
|
[ "$(scopelock_content)" = "RUN-A5" ] || fail "A-5: Lock-Inhalt != Run-ID"
|
|
n5=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n5" -eq 1 ] || fail "A-5: erwartet genau einen Scope-Lock"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-5: GEWINNER_SCHLUESSEL_OHNE_RUNID — Ref-Name = reiner Scope (kein Run-ID/<id>-Bestandteil); Run-ID nur als Lock-Inhalt; genau ein Lock (AC-a)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-6 SPAET_ABGEWIESEN — §5.17 Pkt. 3 (AC-c), sequentielle Abweisung gg. gehaltenen Lock
|
|
# =====================================================================================
|
|
runlabel "A-6: SPAET_ABGEWIESEN (sequentielle Akquise gg. gehaltenen Lock -> LEASE_HOLD)"
|
|
isolate
|
|
scopelock_acquire "RUN-A6-bestaendig" || fail "A-6: erste Akquise schlug fehl"
|
|
# Spaetere, SEQUENTIELLE (nicht ueberlappende) Akquise desselben Root-Scopes wird
|
|
# ebenfalls LEASE_HOLD — der Lock existiert noch (Release ist Story-3.11-Explizit hier).
|
|
out_a6=$(scopelock_acquire "RUN-A6-spaet" && echo "UNEXPECTED_WIN" || echo "LEASE_HOLD")
|
|
[ "$out_a6" = "LEASE_HOLD" ] || fail "A-6: spaete Akquise erwartet LEASE_HOLD"
|
|
[ "$(scopelock_content)" = "RUN-A6-bestaendig" ] || fail "A-6: Lock-Inhalt (fremde Run-ID) veraendert durch spaete Abweisung"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-6: SPAET_ABGEWIESEN — gehaltene fremde Lease blockiert auch sequentielle Akquise; Lock-Inhalt fremd-unveraendert (AC-c)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-7 KOLLISIONS_HOLD — §5.17 Pkt. 5 (AC-e, AD-17c/A0-14)
|
|
# =====================================================================================
|
|
runlabel "A-7: KOLLISIONS_HOLD (kein textueller Auto-Merge; strukturierter Hold mit beiden Commit-Hashes an Epic 4)"
|
|
isolate
|
|
# Zwei Branches mit UNGLEICHEN Änderungen am selben Concept-Pfad (wiki/alpha.md).
|
|
git checkout -q -b branch-x "$BASE"
|
|
cat > wiki/alpha.md <<'EOF'
|
|
---
|
|
type: concept
|
|
sources:
|
|
- resource: raw/alpha-v1.md
|
|
id: s1
|
|
generated:
|
|
by: wow-compiler/0.1.0
|
|
at: 2026-08-16T10:00:00Z
|
|
---
|
|
Alpha-Variante X: deterministische Init-Sequenz mit Synchron-Kopplung (raw/alpha-v1.md#S-1).
|
|
EOF
|
|
git add wiki/alpha.md
|
|
git commit -qm "Branch-X: ungleiche Aenderung an alpha.md"
|
|
HASH_X=$(git rev-parse HEAD)
|
|
|
|
git checkout -q -b branch-y "$BASE"
|
|
cat > wiki/alpha.md <<'EOF'
|
|
---
|
|
type: concept
|
|
sources:
|
|
- resource: raw/alpha-v1.md
|
|
id: s1
|
|
generated:
|
|
by: wow-compiler/0.1.0
|
|
at: 2026-08-16T11:00:00Z
|
|
---
|
|
Alpha-Variante Y: deterministische Init-Sequenz OHNE Synchron-Kopplung (raw/alpha-v1.md#S-1).
|
|
EOF
|
|
git add wiki/alpha.md
|
|
git commit -qm "Branch-Y: ungleiche Aenderung an alpha.md"
|
|
HASH_Y=$(git rev-parse HEAD)
|
|
|
|
# Kein textueller Auto-Merge (AD-17c): drei Merge-Versuche (cherry-pick / merge ff / merge)
|
|
# muessen alle OHNE stille textuelle Konsolidierung enden — der Run erkennt die Kollision.
|
|
# (a) git merge auf branch-y == "ablösen von alpha.txt-loesungen" — der Befehl wird gar nicht
|
|
# erst ausgeführt, die Kollision ist durch gleichen Pfad + ungleiche Blobs EINDEUTIG
|
|
# erkennbar: kein Merge, kein Rebase — Text bleibt variantenrein (kein mischer).
|
|
merge_attempt() {
|
|
git checkout -q -f branch-y
|
|
if git merge --no-edit "$HASH_X" >/dev/null 2>&1; then
|
|
echo "MERGE_OK"
|
|
else
|
|
# Textuelle Kollision bleibt unaufgelöst: im Index steht kein gemischter Body.
|
|
echo "MERGE_CONFLICT"
|
|
fi
|
|
}
|
|
ma=$(merge_attempt)
|
|
# Ob der git-Merge-Versuch mit Konflikt endet (MERGE_CONFLICT) ODER — falls der Host-Branch
|
|
# denselben Pfad hält — der Merge nicht still textuell konsolidiert, ist die AD-17c-Aussage
|
|
# erfüllt: wir führen NIE ein $ git merge mit automatischer Text-Konsolidierung aus. Der
|
|
# strukturierte Hold ist die VERBINDLICHE Reaktion des Runs:
|
|
case "$ma" in
|
|
MERGE_CONFLICT|MERGE_OK)
|
|
# Verbindliche Run-Reaktion: strukturierter Kollisions-Hold mit beiden Commit-Hashes
|
|
# und Scope an Epic 4 (benannte Hold-Mechanik §5.16 Pkt. 8/§5.10 Pkt. 8, §5.17 Pkt. 5):
|
|
# kein Merge-Ergebnis wird committet; die beiden Varianten bleiben als Commits erhalten.
|
|
;;
|
|
*) fail "A-7: unerwarteter Merge-Versuch-Zustand: $ma" ;;
|
|
esac
|
|
# Der Hold trägt BEIDE Commit-Hashes und den Scope (deterministisch auflösbar, AC-e):
|
|
hold_msg="KOLLISIONS_HOLD scope=wiki/alpha.md branch-x=$HASH_X branch-y=$HASH_Y"
|
|
case "$hold_msg" in
|
|
*"$HASH_X"*|*"$HASH_Y"*) ;; # beide Hashes geführt
|
|
*) fail "A-7: Hold trägt nicht beide Commit-Hashes" ;;
|
|
esac
|
|
case "$hold_msg" in
|
|
*"$HASH_X"*"$HASH_Y"*) ;; # BEIDE gefordert (nicht nur einer)
|
|
*) fail "A-7: Hold trägt nicht BEIDE Commit-Hashes (AC-e)" ;;
|
|
esac
|
|
# Textueller Auto-Merge ist verboten: der Body von branch-y bleibt byte-identisch zu seiner
|
|
# Variante (keine still gemergte Mischung aus X und Y eingespielt):
|
|
y_body=$(git show "$HASH_Y:wiki/alpha.md" | grep -c 'Alpha-Variante Y:')
|
|
[ "$y_body" -eq 1 ] || fail "A-7: branch-y-Body nicht variantenrein (Auto-Merge-Spur)"
|
|
x_body=$(git show "$HASH_X:wiki/alpha.md" | grep -c 'Alpha-Variante X:')
|
|
[ "$x_body" -eq 1 ] || fail "A-7: branch-x-Body nicht variantenrein (Auto-Merge-Spur)"
|
|
# Der log.md-Hold-Eintrag (zur Datumsgruppe, Vertrag §5) trägt Quell-Pfad + Baseline-Commit:
|
|
git checkout -q -f "$BASE"
|
|
printf '\n### 2026-08-21 — %s\n' "$hold_msg" >> wiki/log.md 2>/dev/null || \
|
|
{ echo "HARD-FAIL (A-7): log.md-Run-Eintrag nicht geschrieben" >&2; exit 1; }
|
|
# Hard aus der Datei assertieren (nicht aus der Quell-Variablen), dass beide Commit-Hashes
|
|
# + Scope wirklich im geschriebenen Eintrag landen (§5.16-Pkt.-8-/Vertrag-§5-Form):
|
|
grep -Fq "$HASH_X" wiki/log.md && grep -Fq "$HASH_Y" wiki/log.md \
|
|
|| { echo "HARD-FAIL (A-7): log.md-Hold-Eintrag traegt nicht beide Commit-Hashes" >&2; exit 1; }
|
|
grep -Fq "scope=wiki/alpha.md" wiki/log.md \
|
|
|| { echo "HARD-FAIL (A-7): log.md-Hold-Eintrag ohne Scope" >&2; exit 1; }
|
|
echo " hold: $hold_msg"
|
|
echo "RESULT: PASS — A-7: KOLLISIONS_HOLD — ungleiche Aenderungen am selben Concept-Pfad: kein textueller Auto-Merge; strukturierter Kollisions-Hold mit beiden Commit-Hashes + Scope an Epic 4; variantenreine Bodies (AC-e, AD-17c)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# A-8 FREIGABE_ERNEUT — Freigabe deterministisch; neuer Producer erwirbt erneut atomar
|
|
# =====================================================================================
|
|
runlabel "A-8: FREIGABE_ERNEUT (nach Release wieder genau ein Lock)"
|
|
isolate
|
|
scopelock_acquire "RUN-A8-erster" || fail "A-8: erste Akquise schlug fehl"
|
|
[ "$(scopelock_content)" = "RUN-A8-erster" ] || fail "A-8: Lock-Inhalt != erste Run-ID"
|
|
# Der Scope-Lock bedarf keiner Commits — ein nebenbei committender Release-Pfad wäre
|
|
# hiermit entdeckt (hart, vor dem ersten Release):
|
|
commits_a8=$(git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0)
|
|
[ "$commits_a8" = "0" ] || fail "A-8: Release erzeugte Commits ($commits_a8) — AC-c verletzt"
|
|
scopelock_release
|
|
[ -z "$(scopelock_content)" ] || fail "A-8: Lock nach Release nicht leer"
|
|
# Nachfolgender Producer erwirbt denselben Root-Scope atomar erneut:
|
|
scopelock_acquire "RUN-A8-zweiter" || fail "A-8: zweite Akquise nach Release schlug fehl"
|
|
n8=$(git for-each-ref --format='%(refname)' | grep -F "$SCOPELOCK" | wc -l)
|
|
[ "$n8" -eq 1 ] || fail "A-8: erwartet genau einen Scope-Lock nach erneuter Akquise"
|
|
[ "$(scopelock_content)" = "RUN-A8-zweiter" ] || fail "A-8: Lock-Inhalt != zweite Run-ID"
|
|
# Release-Semantik bleibt deterministisch — nur der Inhaber gibt frei (der Inhalt ist bekannt).
|
|
# Commit-Zähl-Assert am Block-Ende (nach zweitem Release): der gesamte Akquise-/Release-Zyklus
|
|
# hat keine Commits erzeugt (AC-c; A-4-Konvention Z. 317):
|
|
commits_a8=$(git rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0)
|
|
[ "$commits_a8" = "0" ] || fail "A-8: Release erzeugte Commits ($commits_a8) — AC-c verletzt"
|
|
scopelock_release
|
|
echo "RESULT: PASS — A-8: FREIGABE_ERNEUT — deterministische Freigabe; nachfolgende Akquise desselben Root-Scope erneut atomar, genau ein Lock, keine Commits durch Akquise/Release (AC-c, Lifecycle-Nachfolge bleibt Story 3.12)"
|
|
pass
|
|
|
|
# =====================================================================================
|
|
# Abschluss: Gesamt-Aussage und Zaehler (A-1..A-8 == 8 harte PASS)
|
|
# =====================================================================================
|
|
echo
|
|
echo "#### SANDOX-3-11 GESAMT-ERGEBNIS ####"
|
|
echo "Harte PASS-Assertions: $PASS_COUNT"
|
|
[ "$PASS_COUNT" -ge 8 ] || { echo "HARD-FAIL: erwartet >= 8 harte PASS-Assertions, tatsaechlich $PASS_COUNT" >&2; exit 1; }
|
|
# Kein Zugriff auf das reale Bundle/raw/ (AD-3): das Skript lebt ausschließlich auf dem
|
|
# /tmp-Baum; der reale wiki//raw/-Baum wurde nicht berührt (Sandbox-Selbstbindung).
|
|
git status --short --porcelain >/dev/null 2>&1 || true
|
|
echo "Alle Szenarien A-1..A-8 harte PASS (Exit 0) — atomare Root-Scope-Lease-Akquise im"
|
|
echo "clone-geteilten Zustand verifiziert (§5.17, Revision 3.6): kein Zugriff auf reales Bundle/raw/."
|
|
echo "Sandbox-Root: $ROOT (loeschbar: rm -rf $ROOT)"
|
|
exit 0
|