claude/lib/tests/run-reconcile.sh
Bastien Chanot 82e6322a9f feat(reconcile): deterministic declared-vs-real engine + thin gated skill
/reconcile confronts declarative sources (TODO checkboxes, registry
statuses, ## Index) against real git/fs state and surfaces the gaps,
in 4 categories + contradiction candidates.

- lib/reconcile.sh: engine — body-only enumeration (never the Index),
  git/fs oracles, BLK last-block-wins status, lexical deferral sweep,
  contradiction candidates, pure reconcile_verdict kernel.
- lib/tests/run-reconcile.sh + fixtures (neutral-named): 20/20;
  recursive-coherence T1 reds if the engine reads the Index (teeth).
- skills/reconcile/SKILL.md: thin orchestration + A/B/C write-back gate,
  honest limits (lexical deferrals, contradictions surfaced not asserted).
- CLAUDE.md: Skill routing line.

Founding principle: never trust a declarative source as an oracle — the
skill practices what it preaches (tested). Built via writing-skills TDD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj
2026-06-30 13:42:24 +02:00

73 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# run-reconcile.sh — TDD harness for lib/reconcile.sh.
#
# Iron Law: these tests were RED before the engine existed (scratchpad RED-B). They prove
# the engine VERIFIES (git/fs/body) rather than BELIEVES (checkbox/Index/name). Fixtures
# carry NEUTRAL names on purpose — the engine must reach the truth by querying git, never by
# reading a path hint (the a0f68 baseline failure that read "pre-reconcile" from the dir name).
set -uo pipefail
GREP=/usr/bin/grep # LRN-074: pin grep
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/.." && pwd)"
FIX="$HERE/fixtures"
MEM="$REPO/../.claude/memory"; [ -d "$MEM" ] || MEM="$REPO/.claude/memory"
# shellcheck source=/dev/null
source "$REPO/reconcile.sh"
pass=0; fail=0
ok() { echo "GREEN ✓ $*"; pass=$((pass+1)); }
no() { echo "RED ✗ $*"; fail=$((fail+1)); }
has() { printf '%s\n' "$1" | $GREP -qF -- "$2"; } # substring present in multiline string
echo "=== T1 recursive coherence — enumerate from BODY, never the ## Index ==="
DRIFT="$FIX/registry-index-drift.md"
mapfile -t IDS < <(reconcile_enumerate_ids "$DRIFT" LRN)
if [ "${#IDS[@]}" -eq 72 ]; then ok "T1a enumerated 72 body ids"; else no "T1a got ${#IDS[@]}, expected 72 (an Index-reader gives 51)"; fi
if printf '%s\n' "${IDS[@]}" | $GREP -qx "LRN-020"; then ok "T1b includes body-only canary LRN-020"; else no "T1b dropped LRN-020 — read the Index, not the body"; fi
# teeth: an Index-based enumerator would RED here (the fixture discriminates)
idx_only=$($GREP -oE '^\| LRN-[0-9]+' "$DRIFT" | $GREP -oE 'LRN-[0-9]+' | sort -u)
if printf '%s\n' "$idx_only" | $GREP -qx "LRN-020"; then no "T1c teeth LOST — Index path also yields LRN-020"; else ok "T1c teeth intact — Index path OMITS LRN-020 (engine reading the Index would fail T1b)"; fi
echo; echo "=== T2 BLK status — LAST block wins (the BLK-008 trap) ==="
b="$MEM/blockers.md"
case "$(reconcile_blk_current_status "$b" BLK-008)" in
*RESOLVED*|*resolved*) ok "T2a BLK-008 current = resolved (read FINAL, not the middle REVERTED)";;
*) no "T2a BLK-008 misread as non-resolved — fell into the compound-status trap";;
esac
case "$(reconcile_blk_current_status "$b" BLK-009)" in
*open*|*upstream*) ok "T2b BLK-009 current = upstream/open";;
*) no "T2b BLK-009 misread";;
esac
open_ids=$(reconcile_blk_open "$b" | cut -f1 | sort | tr '\n' ' ')
if [ "$open_ids" = "BLK-001 BLK-003 BLK-009 " ]; then ok "T2c open blockers = {001,003,009}"; else no "T2c open = [$open_ids], expected {001,003,009}"; fi
echo; echo "=== T3 deferral lexical sweep (HONEST LIMIT: marked-only) ==="
defer=$(reconcile_deferrals "$FIX/todo-snapshot.md" "$MEM/decisions.md")
for mark in "OUT-OF-SCOPE" "DEFERRED" "follow-up" "one-line ticket"; do
if has "$defer" "$mark"; then ok "T3 found marked deferral: $mark"; else no "T3 missed marker: $mark"; fi
done
if $GREP -qE '^\s*- \[~\] Cleanup machine' "$FIX/todo-snapshot.md"; then ok "T3e [~] cleanup present for checkbox-state detection"; else no "T3e [~] cleanup not found"; fi
echo; echo "=== T4 reconciliation kernel (pure) + snapshot composition ==="
if [ "$(reconcile_verdict ' ' true)" = "STALE:open-but-done" ]; then ok "T4a ' '+done → STALE"; else no "T4a wrong"; fi
if [ "$(reconcile_verdict 'x' false)" = "STALE:done-but-open" ]; then ok "T4b 'x'+!done → STALE"; else no "T4b wrong"; fi
if [ "$(reconcile_verdict '~' true)" = "STALE:partial-but-done" ]; then ok "T4c '~'+done → STALE"; else no "T4c wrong"; fi
if [ "$(reconcile_verdict 'x' true)" = "CONSISTENT" ]; then ok "T4d 'x'+done → CONSISTENT"; else no "T4d wrong"; fi
truths=$($GREP -cE '=(true|resolved|present)$' "$FIX/real-state.snapshot")
if [ "$truths" -ge 6 ]; then ok "T4e snapshot supplies $truths real-true facts → kernel yields STALE for the 6 git-verifiable items"; else no "T4e snapshot facts=$truths (<6)"; fi
echo " (7th cat-4 item — twin doc-sync [~] cross-ref — is SURFACED for review, not auto-verified: honest limit)"
echo; echo "=== T5 contradiction candidates (surface, never assert) ==="
cand=$(reconcile_contradiction_candidates "$MEM/decisions.md" "$FIX/todo-snapshot.md")
if has "$cand" "--help"; then ok "T5 surfaced --help candidate (BDR-001 ⇄ --help chantier)"; else no "T5 missed --help candidate"; fi
echo; echo "=== T6 live oracle smoke — oracles QUERY real git/fs (not a name) ==="
if reconcile_oracle_merge_done "$REPO" "prune-memory"; then ok "T6a merge_done(prune-memory) via git log"; else no "T6a merge not found in git"; fi
if reconcile_oracle_sha_exists "$REPO" "be1dcef"; then ok "T6b sha_exists(be1dcef) via cat-file"; else no "T6b sha missing"; fi
dk="$MEM/../skills/darwin-skill"
if reconcile_oracle_path_present "$dk"; then ok "T6c path_present(darwin-skill) via fs"; else no "T6c path absent"; fi
echo; echo "================ $pass GREEN / $fail RED ================"
[ "$fail" -eq 0 ] && exit 0 || exit 1