diff --git a/plugin-shared/drift-check.sh b/plugin-shared/drift-check.sh new file mode 100755 index 0000000..d17b6d0 --- /dev/null +++ b/plugin-shared/drift-check.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +# drift-check.sh — Reference-drift detector for idea2self-skills +# ============================================================================= +# ZWECK +# plugin-shared/ haelt die Master-Versionen der geteilten Referenzen +# (kommunikationsregeln.md, export-formate.md, methodik.md, recherche-regeln.md, +# artefakt-pipeline.md + templates/). Jeder Skill unter +# skills//references/ traegt KURATIERTE AUSZUEGE daraus — KEINE +# 1:1-Kopien. Die Auszuege sind bewusst umformuliert (Umlaut-Transliteration +# ue/oe/ae, gekuerzte Bloecke, skill-spezifische Zusatz-Bloecke). Dieses Script +# erkennt Drift, ohne diese gewollten Abweichungen als Fehler zu melden. +# +# GEWAEHLTER ANSATZ — Contract-Verifikation (NICHT Content-Diff) +# Ein naiver Hash- oder Section-Content-Vergleich wuerde wegen der +# Umformulierung (Umlaute, gekuerzte/umbenannte Bloecke) bei JEDER Datei +# Drift melden — unbrauchbar. Auch der Substring-pro-Zeile-Fallback scheitert, +# weil schon die Umlaut-Transliteration (z.B. "Huerden" im Auszug vs "Hürden" +# im Master) jede Zeile zum Fehlschlag bringt. +# +# Der einzige verlaessliche, maschinen-pruefbare Vertrag ist der Footer +# **Master version:** `plugin-shared/.md` [ (Block ``) ] +# den die kuratierten Auszuege tragen. Das Script prueft diesen Vertrag: +# 1. Master-Pfad aus dem Footer existiert -> sonst DRIFT (broken pointer) +# 2. Optionaler "(Block ``)"-Verweis existiert im Master -> sonst DRIFT +# 3. Skill-Ref OHNE Footer -> MISSING-FOOTER (informativ, +# zaehlt NICHT als Drift; die klarheit-mit-idea2self-Refs sind bewusste +# Voll-Mirror ohne Footer-Vertrag). +# +# Damit faengt das Script die echte, handlungsrelevante Drift ab (Master +# umbenannt/geloescht, referenzierter Block aus Master entfernt) ohne +# Tausende False-Positives durch die gewollte Umformulierung. +# +# NUTZUNG +# bash plugin-shared/drift-check.sh +# Exit 0 = kein Drift, Exit 1 = Drift gefunden. +# ============================================================================= + +shopt -s nullglob + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(dirname "$SCRIPT_DIR")" # plugin-shared liegt direkt unter Repo-Root + +command -v rg >/dev/null 2>&1 || true # rg optional; wir nutzen Bash-native Tools + +SKILLS_DIR="$ROOT/skills" +if [[ ! -d "$SKILLS_DIR" ]]; then + echo "ERROR: skills directory not found at $SKILLS_DIR" >&2 + exit 1 +fi + +checked=0 +drift=0 + +# Sammle alle Skill-Reference-Dateien defensiv. +ref_files=() +if [[ -d "$SKILLS_DIR" ]]; then + while IFS= read -r f; do + ref_files+=("$f") + done < <(find "$SKILLS_DIR" -path '*/references/*.md' -type f 2>/dev/null | sort) +fi + +for ref in "${ref_files[@]}"; do + [[ -e "$ref" ]] || continue + rel="${ref#"$ROOT"/}" + checked=$((checked + 1)) + + # Footer-Zeile extrahieren (erste Zeile mit "Master version:"). + footer="$(grep -i "Master version" "$ref" 2>/dev/null | head -1 || true)" + + if [[ -z "$footer" ]]; then + echo "MISSING-FOOTER $rel: kein 'Master version:'-Footer (Voll-Mirror? informativ, kein Drift)" + continue + fi + + # Master-Pfad aus Backticks ziehen: erstes `plugin-shared/...md`. + master_rel="$(printf '%s\n' "$footer" \ + | grep -oE 'plugin-shared/[A-Za-z0-9._/-]+\.md' | head -1 || true)" + + if [[ -z "$master_rel" ]]; then + echo "DRIFT $rel: Footer vorhanden, aber kein plugin-shared/...md-Pfad parsbar" + drift=$((drift + 1)) + continue + fi + + master_abs="$ROOT/$master_rel" + if [[ ! -f "$master_abs" ]]; then + echo "DRIFT $rel: Master-Pfad existiert nicht: $master_rel" + drift=$((drift + 1)) + continue + fi + + # Optionalen Block-Verweis pruefen: (Block ``) + block="$(printf '%s\n' "$footer" \ + | grep -oE 'Block `<[A-Za-z0-9_]+>`' | head -1 || true)" + if [[ -n "$block" ]]; then + tag="$(printf '%s\n' "$block" | grep -oE '<[A-Za-z0-9_]+>' | head -1 || true)" + if [[ -n "$tag" ]]; then + if ! grep -qF "$tag" "$master_abs" 2>/dev/null; then + echo "DRIFT $rel: referenzierter Block $tag fehlt im Master $master_rel" + drift=$((drift + 1)) + continue + fi + fi + fi + + echo "OK $rel (master: $master_rel)" +done + +echo "Checked $checked files, $drift drift(s)" + +if [[ "$drift" -gt 0 ]]; then + exit 1 +fi +exit 0