feat: Story 3.2 Code-Review-Loop-3-Patches (bmad-code-review, 4 Layer; Nutzer-Entscheidungen D-1/D-2/D-3/D-4/D-5 = 1/1/1/1/1 + P-1/P-2) — compiler.md Revision 2.7, §3.2-Pkt.-3b-lexikografisch, canonical-terms-Lookup-Semantik + Invariante + Konflikt-Verfahren, Sandbox T5-T7 + T1/T4-Asssertionen (7/7 PASS), Stale-Beleg (b) korrigiert, Statuskette in-progress → done, P-2-Typos, Story done

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Tamse
2026-08-19 13:21:48 +02:00
co-authored by Claude
parent 3aa484b2f4
commit 52f88fdc5f
7 changed files with 294 additions and 22 deletions
@@ -1,9 +1,11 @@
#!/usr/bin/env bash
# Story 3.2 — Sandbox-Tests der textuell-deterministischen Relevanzbestimmung (§3.2)
# Re-executierbar: bash run-sandbox.sh (ab Workspace-Root; Sandbox unter /tmp/sb32)
# Zweck: Membership-Pin + Zwei-Run-Identität + NO_MATCH (leere Candidate-Liste)
# + log.md-Kontaminations-Pin (der Term liegt AUCH in log.md — die Candidate-Liste
# bleibt trotzdem auf Concept-Pfade definiert, §3.2-Pkt.-2a).
# Zweck: Membership-Pin (T1) + Zwei-Run-Identität (T2) + NO_MATCH (T3) +
# log.md-Kontaminations-Pin (T1: der Term liegt AUCH in log.md — die
# Candidate-Liste bleibt trotzdem auf Concept-Pfade definiert, §3.2-Pkt.-2a)
# + LINK_FOLLOWING_ZYKLUS (T5) + TRAVERSAL_REACH_ONLY (T6) +
# TERM_ABLEITUNG_SYNONYM (T7); T1/T4 mit harten Assertions (Review-Patch P-1).
set -u
ROOT=$(mktemp -d /tmp/sb32-XXXXXX)
SB="$ROOT/sb"
@@ -74,6 +76,74 @@ terms() { # $1=Term; nutzt rg wenn vorhanden (native -g-Exklusion), sonst grep -
# Normalisierung der Ausgabe: wiki/-Präfix + .md-Suffix strippen (§3.2-Pkt.-3a)
normalize() { sed -e 's|^wiki/||' -e 's|\.md$||'; }
# ---------- Helfer: Stufe b/c (Erhebung) + Terminormalisierung (Review-Loop-2, D-2) ----------
# Stufe b — index.md-Traversal (TRAVERSAL_REACH_ONLY, §3.2-Pkt.-2b):
# $1 = normalisierter Treffer-Name einer index.md (z. B. 'trafo/index' oder 'index');
# gibt die darunter gewurzelten Concept-Pfade (normalisiert) aus — die Concept-Links
# der getroffenen index.md, file-relativ aufgelöst (§5.7-Pkt.-4).
traverse() {
local idx="wiki/$1.md" d tgt
d=$(dirname "$idx")
grep -oE '\]\([^)]+\.md\)' "$SB/$idx" 2>/dev/null | sed -e 's|^\](||' -e 's|)$||' | while read -r tgt; do
case "$tgt" in ./*) tgt=${tgt#./} ;; esac
printf '%s/%s\n' "$d" "$tgt" | sed -e 's|^wiki/||' -e 's|\.md$||'
done
}
# Stufe c — Link-Following mit besuchter Menge (LINK_FOLLOWING_ZYKLUS, §3.2-Pkt.-2c):
# $@ = in Stufe a getroffene Concept-Pfade (normalisiert, z. B. 'sigma');
# Concept-Links werden file-relativ verfolgt (§5.7-Pkt.-4); ein bereits besuchter
# Concept-Pfad wird nicht erneut besucht — Zyklen (A -> B -> A) enden, die
# Candidate-Liste bleibt endlich. Ausgabe: getroffene + erreichte Pfade,
# in Besuchsreihenfolge (Stufe a zuerst, dann Stufe c; §3.2-Pkt.-3b).
follow() {
local visited="$ROOT/visited.$$" seed f base tgt res
: > "$visited"
local queue=()
for seed in "$@"; do
printf '%s\n' "$seed" >> "$visited"
queue+=("$seed")
done
while [ "${#queue[@]}" -gt 0 ]; do
f="wiki/${queue[0]}.md"
queue=("${queue[@]:1}")
base=$(dirname "$f")
for tgt in $(grep -oE '\]\([^)]+\.md\)' "$SB/$f" 2>/dev/null | sed -e 's|^\](||' -e 's|)$||'); do
case "$tgt" in ./*) tgt=${tgt#./} ;; esac
res=$(printf '%s/%s\n' "$base" "$tgt" | sed -e 's|^wiki/||' -e 's|\.md$||')
grep -qx "$res" "$visited" && continue
printf '%s\n' "$res" >> "$visited"
queue+=("$res")
done
done
cat "$visited"
}
# Terminormalisierung (§3.2-Pkt.-1b): (i) lowercasing; (ii) Binde-Varianten-Kollaps
# [-_ ] -> - (jedes Vorkommen wird in einen einzelnen Bindestrich kollabiert).
# Normalisierte Form = die Form, die in der Registry gespeichert/verglichen wird.
norm_term() { printf '%s' "$1" | LC_ALL=C tr 'A-Z' 'a-z' | sed -e 's/[-_ ]/-/g'; }
# Registry-Lookup (§3.2-Pkt.-1b, canonical-terms.md-Format):
# $1 = normalisierter Term, $2 = Registry-Datei; gibt die canonische Form aus,
# oder — nicht auflösbar — den Term selbst (§3.2-Pkt.-1b: kein stiller Ausschluss).
lookup() {
awk -F'|' -v t="$1" '
NR > 2 {
canon = $2; gsub(/^[ \t]+|[ \t]+$/, "", canon)
if (canon ~ /^—/) next
if (canon == t) { print canon; found = 1; exit }
vars = $3; gsub(/`/, " ", vars)
n = split(vars, a, ",")
for (i = 1; i <= n; i++) {
gsub(/^[ \t]+|[ \t]+$/, "", a[i])
if (a[i] != "" && a[i] == t) { print canon; found = 1; exit }
}
}
END { if (!found) print t }
' "$2"
}
# =====================================================================
runlabel() { echo; echo "########## $1 ##########"; }
@@ -81,6 +151,13 @@ runlabel "T1: Membership-Pin — Term trifft Concept-Body UND log.md; Candidate-
echo "--- Kandidaten-Erhebung: terms 'deterministische-relevanz-bestimmung' (Exklusions-Form) ---"
terms 'deterministische-relevanz-bestimmung' | normalize
echo "--- Erwartet: AUSSCHLIESSLICH 'alpha' (Concept-Pfad, ohne wiki/ + ohne .md) ---"
T1_OUT=$(terms 'deterministische-relevanz-bestimmung' | normalize)
if [ "$T1_OUT" = "alpha" ]; then
echo "RESULT: PASS (Candidate-Liste = [alpha]; log.md strukturell exkludiert, §3.2-Pkt.-2a)"
else
echo "FAIL: T1 — erwartet 'alpha', erhalten: '${T1_OUT}'" >&2
exit 1
fi
echo "--- Kontaminations-Kontrolle: was OHNE Exklusion getroffen würde (rg-OHNE -g / grep-OHNE --exclude) ---"
if command -v rg >/dev/null 2>&1; then
rg -l 'deterministische-relevanz-bestimmung' wiki/ | normalize
@@ -97,7 +174,7 @@ FIRST2=$(terms 'alpha' | normalize)
SECOND2=$(terms 'alpha' | normalize)
echo "Lauf 1 (Term deterministische-relevanz-bestimmung): $FIRST1"
echo "Lauf 2 (Term deterministische-relevanz-bestimmung): $SECOND1"
echo "Lauf 1 (Term alpha): $FIRST2 # 'index' ist der legitime Stufe-b-Traversal-Treffer (Bundleroot verlinkt alpha.md), kein Kontaminations-Fehler"
echo "Lauf 1 (Term alpha): $FIRST2 # 'index' ist ein Stufe-a-Treffer auf wiki/index.md — Stufe b (TRAVERSAL_REACH_ONLY, §3.2-Pkt.-2b) löst die darunter gewurzelten Concept-Pfade als Candidate aus (index.md selbst ist KEIN Concept-Kandidat); kein Kontaminations-Fehler"
echo "Lauf 2 (Term alpha): $SECOND2 # identische Ausgabe (Zwei-Run-Identität, AD-17h)"
if [ "$FIRST1" = "$SECOND1" ] && [ "$FIRST2" = "$SECOND2" ]; then
echo "RESULT: IDENTISCH (gleicher Git-State + gleiches Eingabeset -> identische Candidate-Liste, AD-17h)"
@@ -133,10 +210,144 @@ generated:
Gamma teilt den Term deterministische-relevanz-bestimmung (Stelle S-1).
EOF
git add -A; git commit -qm "Zuwachs gamma (teilt den Term)"
echo "--- Kandidaten-Erhebung (Term in alpha + gamma): ---"
terms 'deterministische-relevanz-bestimmung' | normalize | LC_ALL=C sort
echo "--- Erwartet: alpha, gamma (beide Concept-Pfade, lexikografisch; log ABGESCHNITTEN) ---"
echo "--- Kandidaten-Erhebung (Term in alpha + gamma; §3.2-Pkt.-3b: innerhalb jeder Stufe rein lexikografisch, LC_ALL=C): ---"
terms 'deterministische-relevanz-bestimmung' | normalize | LC_ALL=C sort | paste -sd, -
echo "--- Erwartet: alpha,gamma (beide Concept-Pfade, lexikografisch; log ABGESCHNITTEN) ---"
T4_OUT=$(terms 'deterministische-relevanz-bestimmung' | normalize | LC_ALL=C sort | paste -sd, -)
if [ "$T4_OUT" = "alpha,gamma" ]; then
echo "RESULT: PASS (Vereinigung beider Concept-Pfade, lexikografisch; log.md exkludiert)"
else
echo "FAIL: T4 — erwartet 'alpha,gamma', erhalten: '${T4_OUT}'" >&2
exit 1
fi
# =====================================================================
# T5-T7 (Review-Loop-2, D-2): Zyklen-Endlichkeit, Traversal-Only, Synonym-Auflösung
# =====================================================================
# ---------- T5: LINK_FOLLOWING_ZYKLUS — A -> B -> A endet endlich (besuchte Menge) ----------
runlabel "T5: LINK_FOLLOWING_ZYKLUS — Zyklus alpha <-> omega endet endlich; Candidate-Liste endlich (§3.2-Pkt.-2c/3b)"
cat > wiki/omega.md <<'EOF'
---
type: concept
sources:
- resource: raw/omega-v1.md
id: s1
generated:
by: wow-compiler/0.1.0
at: 2026-08-16T09:00:00Z
---
Omega verweist zirkular auf Alpha: [Alpha](alpha.md).
EOF
# Backlink: alpha -> omega (erzeugt den echten A->B->A-Zyklus, der die besuchte Menge übt):
printf '\nOmega-Verweis: [Omega](omega.md).\n' >> wiki/alpha.md
git add -A; git commit -qm "Zuwachs omega (Link-Zyklus alpha <-> omega)"
echo "--- Stufe a (Term zirkular): ---"
STAGE_A=$(terms 'zirkular' | normalize | LC_ALL=C sort | paste -sd, -)
echo "$STAGE_A"
echo "--- Stufe c (Follow aus Stufe a; ZYKLUS omega -> alpha -> omega ... MUSS enden): ---"
STAGE_C=$(follow omega)
echo "$STAGE_C"
# Deduplication über die besuchte Menge (sort -u: endliche, doppelungsfreie Menge —
# die Stufe-Ordnung a→c ist bereits in STAGE_A/STAGE_C sichtbar; hier wird die Endlichkeit geprüft):
T5_EXPECT="alpha,omega"
T5_LIST=$(printf '%s\n%s\n' "$STAGE_A" "$STAGE_C" | grep -v '^$' | LC_ALL=C sort -u | paste -sd, -)
if [ "$T5_LIST" = "$T5_EXPECT" ]; then
echo "RESULT: PASS (endlich, dedupliziert: omega, alpha — Zyklus endete; jeder Pfad genau einmal)"
else
echo "FAIL: T5 — erwartet '$T5_EXPECT', erhalten: '$T5_LIST'" >&2
exit 1
fi
# ---------- T6: TRAVERSAL_REACH_ONLY — Term nur in index.md -> gewurzelte Concept-Pfade ----------
runlabel "T6: TRAVERSAL_REACH_ONLY — Term 'trafo-gebiet' trifft NUR wiki/trafo/index.md -> gewurzelte Concept-Pfade sind die Treffer (index.md selbst kein Concept-Kandidat, §3.2-Pkt.-2b)"
mkdir -p wiki/trafo
cat > wiki/trafo/index.md <<'EOF'
# Trafo-Gebiet-Index
Dieses trafo-gebiet wird hier indexiert.
- [Delta](delta.md)
- [Epsilon](epsilon.md)
EOF
cat > wiki/trafo/delta.md <<'EOF'
---
type: concept
sources:
- resource: raw/delta-v1.md
id: s1
generated:
by: wow-compiler/0.1.0
at: 2026-08-16T09:00:00Z
---
Delta-Body (trägt den Index-Term NICHT — nur der Index trägt ihn).
EOF
cat > wiki/trafo/epsilon.md <<'EOF'
---
type: concept
sources:
- resource: raw/epsilon-v1.md
id: s1
generated:
by: wow-compiler/0.1.0
at: 2026-08-16T09:00:00Z
---
Epsilon-Body (trägt den Index-Term NICHT — nur der Index trägt ihn).
EOF
git add -A; git commit -qm "Zuwachs trafo/Area (Index trägt den Term, Bodies nicht)"
echo "--- Stufe a (Term trafo-gebiet; nur index.md trifft — der Traversal-Trigger): ---"
STAGE_A=$(terms 'trafo-gebiet' | normalize)
echo "$STAGE_A"
echo "--- Stufe b (Traversal: gewurzelte Concept-Pfade der getroffenen index.md): ---"
STAGE_B=$(traverse 'trafo/index')
echo "$STAGE_B"
# Kandidaten = gewurzelte Concept-Pfade (lexikografisch; index.md selbst ist KEIN Concept-Kandidat):
T6_OUT=$(printf '%s\n' "$STAGE_B" | grep -v '^$' | LC_ALL=C sort | paste -sd, -)
if [ "$T6_OUT" = "trafo/delta,trafo/epsilon" ]; then
echo "RESULT: PASS (Candidate-Liste = gewurzelte Concept-Pfade; Index-Body-Trigger, kein Kontaminations-Fehler)"
else
echo "FAIL: T6 — erwartet 'trafo/delta,trafo/epsilon', erhalten: '$T6_OUT'" >&2
exit 1
fi
# ---------- T7: TERM_ABLEITUNG_SYNONYM — Schreibvariante -> canonische Form -> Kandidaten ----------
runlabel "T7: TERM_ABLEITUNG_SYNONYM — gezogene Schreibvariante wird über canonical-terms.md auf die canonische Form aufgelöst (Lookup auf normalisierter Form, §3.2-Pkt.-1b)"
mkdir -p "$ROOT/registry"
cat > "$ROOT/registry/canonical-terms.md" <<'EOF'
# Sandbox-Registry (Test-Doppel des canonical-terms-Formats; append-only, ein Eintrag = Canon + Varianten;
# D-5-Invariante: Canon- UND Varianten-Spalte tragen ausschliesslich NORMALISIERTE Formen)
| canonische Form | erlaubte Schreibvarianten | Semantik |
|---|---|---|
| quanten-protocol | `quantenprotocol` | Sandbox-Semantik: Test-Synonymgruppe |
EOF
# Invariante-Check (D-5): keine normalisierte Form in beiden Spalten oder in zwei Einträgen:
# hier: 'quanten-protocol' nur Canon-Spalte, 'quantenprotocol' nur Varianten-Spalte — eindeutig.
# (Registry liegt ausserhalb des Sandbox-Git-Roots — sie modelliert den Git-State-Anteil des Resolvers;
# der Sandbox-Repo-Commit trägt sie deshalb nicht, hier nur Notiz statt Commit-Noise:)
echo "Registry (ausserhalb des Sandbox-Roots, modelliert den Resolver-Git-State): $ROOT/registry/canonical-terms.md"
echo "--- Gezogene Schreibvarianten (Roh-Notat): 'Quanten Protocol' + 'QuantenProtocol' ---"
RAW_A='Quanten Protocol'
RAW_B='QuantenProtocol'
NORM_A=$(norm_term "$RAW_A")
NORM_B=$(norm_term "$RAW_B")
echo "Normalisierung (§3.2-Pkt.-1b): '$RAW_A' -> '$NORM_A'; '$RAW_B' -> '$NORM_B'"
CANON_A=$(lookup "$NORM_A" "$ROOT/registry/canonical-terms.md")
CANON_B=$(lookup "$NORM_B" "$ROOT/registry/canonical-terms.md")
echo "Lookup: '$NORM_A' -> '$CANON_A'; '$NORM_B' -> '$CANON_B'"
if [ "$CANON_A" = "quanten-protocol" ] && [ "$CANON_B" = "quanten-protocol" ]; then
echo "RESULT: PASS (beide Varianten -> canonische Form 'quanten-protocol'; Auflösung auf normalisierter Form)"
else
echo "FAIL: T7 — erwartet 'quanten-protocol' für beide, erhalten: '$CANON_A' / '$CANON_B'" >&2
exit 1
fi
# Nicht aufgelöster Term wird wie notiert (Kollaps-normalisiert) verwendet — kein stiller Ausschluss:
CANON_MISS=$(lookup 'nicht-in-registry' "$ROOT/registry/canonical-terms.md")
if [ "$CANON_MISS" = 'nicht-in-registry' ]; then
echo "PASS (Negativ-Fall: 'nicht-in-registry' -> wie notiert, kein stiller Ausschluss)"
else
echo "FAIL: T7-Negativ — erwartet 'nicht-in-registry', erhalten: '$CANON_MISS'" >&2
exit 1
fi
echo
echo "===== Sandbox abgeschlossen (alle 4 Tests) ====="
echo "===== Sandbox abgeschlossen (alle 7 Tests) ====="
echo "Sandbox-Root: $ROOT"