claude/lib/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

109 lines
5.5 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# reconcile.sh — deterministic engine for /reconcile.
#
# Confronts DECLARATIVE sources (TODO checkboxes, registry statuses) against REAL
# state (git, fs, registry BODY). It is the engine behind the /reconcile skill;
# the skill orchestrates + gates, this file holds the mechanical truth-probes.
#
# FOUNDING PRINCIPLE — recursive coherence (LRN-055): a reconciler must NEVER trust
# a declarative source as an oracle. So this engine NEVER reads the `## Index` table
# nor believes a `[x]`/`[ ]` checkbox; it enumerates registry entries from the BODY
# `## ID —` headings and decides "done/stale" from git/fs only. It practices what it
# preaches — the recursive-coherence test (run-reconcile.sh T1) reds if this is broken.
#
# HONEST LIMITS (graven, do not over-read):
# - Deferral detection is LEXICAL (reconcile_deferrals): it catches deferrals MARKED
# by a keyword, misses ones phrased without one ("à reprendre quand X"). Deterministic
# on the detectable; the skill SURFACES the ambiguous for human review, never asserts.
# - Contradiction detection surfaces CANDIDATES (token overlap), never asserts a verdict.
#
# Layers:
# reconcile_enumerate_ids — body-only ID enumeration (recursive-coherence core)
# reconcile_oracle_* — live git/fs truth probes
# reconcile_blk_current_status — registry status, LAST block wins (compound/UPDATE/FINAL)
# reconcile_blk_open — blockers whose CURRENT status is not resolved
# reconcile_verdict — pure kernel: declared checkbox × real fact → verdict
# reconcile_deferrals — lexical deferral sweep (honest limit)
# reconcile_contradiction_candidates — accepted-BDR ⇄ open-chantier token overlap (surface)
set -uo pipefail
RECONCILE_GREP="${RECONCILE_GREP:-/usr/bin/grep}" # LRN-074: pin grep, never assume GNU flags
# markers that LEXICALLY signal a deferral/follow-up (honest limit: marked-only)
RECONCILE_DEFER_RE='[Dd]efer|[Ff]ollow-?up|[Oo]ut.of.scope|OUT-OF-SCOPE|[Rr]econsider|[Rr]evisit|2e passage|won.t.do|optional\)|one-line ticket'
# --- recursive coherence: enumerate IDs from the BODY, never the ## Index ---
# $1 registry file, $2 prefix (BDR|LRN|BLK|EVAL). Emits one id per line, unique.
reconcile_enumerate_ids() {
"$RECONCILE_GREP" -oE "^## ${2}-[0-9]+" "$1" | "$RECONCILE_GREP" -oE "${2}-[0-9]+" | sort -u
}
# --- live truth probes (query the REAL repo/fs; this is the "verify, don't believe" core) ---
reconcile_oracle_tree_clean() { # rc 0 = working tree clean
[ -z "$(git -C "${1:-.}" status --porcelain 2>/dev/null)" ]
}
reconcile_oracle_merge_done() { # $1 repo, $2 branch fragment → rc 0 if a merge commit exists
[ -n "$(git -C "${1:-.}" log --oneline --grep "Merge .*$2" -1 2>/dev/null)" ]
}
reconcile_oracle_pushed() { # $1 repo, $2 branch → rc 0 if nothing unpushed vs origin
[ -z "$(git -C "${1:-.}" rev-list "origin/$2..$2" 2>/dev/null)" ]
}
reconcile_oracle_sha_exists() { # $1 repo, $2 sha → rc 0 if the commit object exists
git -C "${1:-.}" cat-file -e "${2}^{commit}" 2>/dev/null
}
reconcile_oracle_msg_committed() { # $1 repo, $2 grep → rc 0 if a commit message matches
[ -n "$(git -C "${1:-.}" log --oneline --grep "$2" -1 2>/dev/null)" ]
}
reconcile_oracle_path_present() { [ -e "${1:?}" ]; } # $1 path → rc 0 if it still exists on disk
# --- registry status: LAST status-bearing line wins (the BLK-008 trap A fell into) ---
# $1 blockers file, $2 id. Echoes the current status line (compound/UPDATE/FINAL aware).
reconcile_blk_current_status() {
# drop ALL `## BLK-` header lines first: the range is inclusive of the NEXT entry's
# header, and a sibling header may carry a status word (e.g. BLK-005 "...upstream rename")
# → cross-entry bleed. The entry's own header carries no status, so dropping it is safe.
sed -n "/^## ${2} /,/^## BLK-/p" "$1" \
| "$RECONCILE_GREP" -v '^## BLK-' \
| "$RECONCILE_GREP" -iE 'status|RESOLVED|REVERTED|upstream|resolved|[^a-z]open' \
| tail -1
}
# blockers whose CURRENT status is not resolved → emits "id<TAB>status"
reconcile_blk_open() {
local id st
for id in $(reconcile_enumerate_ids "$1" BLK); do
st=$(reconcile_blk_current_status "$1" "$id")
case "$st" in
*RESOLVED*|*resolved*) : ;;
*) printf '%s\t%s\n' "$id" "$st" ;;
esac
done
}
# --- pure reconciliation kernel: declared checkbox × real fact → verdict (no git, fully testable) ---
# $1 checkbox char (x| |~), $2 real_done (true|false).
reconcile_verdict() {
case "$1:$2" in
" :true") echo "STALE:open-but-done" ;;
"x:false") echo "STALE:done-but-open" ;;
"~:true") echo "STALE:partial-but-done" ;;
*) echo "CONSISTENT" ;;
esac
}
# --- lexical deferral sweep (HONEST LIMIT: marked-only) → "src<TAB>line<TAB>text" ---
reconcile_deferrals() {
[ -f "$1" ] && "$RECONCILE_GREP" -nE "$RECONCILE_DEFER_RE" "$1" 2>/dev/null | sed 's/^/TODO\t/'
[ -f "$2" ] && "$RECONCILE_GREP" -nE "$RECONCILE_DEFER_RE" "$2" 2>/dev/null | sed 's/^/BDR\t/'
return 0
}
# --- contradiction CANDIDATES (surface, never assert): CLI-flag token shared by a BDR + the TODO ---
# $1 decisions file, $2 todo file. CLI-flag-like tokens are distinctive enough to flag for review.
reconcile_contradiction_candidates() {
local tok
for tok in $("$RECONCILE_GREP" -oE '\-\-[a-z][a-z-]+' "$1" 2>/dev/null | sort -u); do
"$RECONCILE_GREP" -qF -- "$tok" "$2" 2>/dev/null \
&& printf 'CANDIDATE\t%s\tflag "%s" in a BDR title and an open TODO chantier — review for contradiction\n' "$tok" "$tok"
done
return 0
}