feat(doc-sync): MINOR-shape oracle + fail-loud commit (strengthen MINOR gate)

(1) lib/doc-shape.sh — deterministic shape oracle re-checks each LLM-classified
MINOR patch before the silent auto-commit; a shape-suspect patch (added heading
/ oversize / new file / non-doc) escalates to the EXISTING SIGNIFICANT gate.
Genuine MINOR still auto-commits (zero friction, BDR-036 preserved). A structural
floor under the LLM call, not a semantic SIGNIFICANT-detector (BDR-040).

(2) lib/doc-commit.sh — guard the commit itself: a rejected git commit (pre-commit
hook / protected branch / signing) now fails loud with exit 5 + empty stdout,
instead of a false "committed" + the previous HEAD's hash + exit 0 that left docs
silently uncommitted (LRN-071, 3rd occurrence of the swallowed-commit pattern
after LRN-066 and LRN-068/BLK-012).

Wired doc-syncer STEP A4 (whole-set escalation: no=revert all, select=keep subset)
+ doc-commit.md (rc-5 consumer row + ACKNOWLEDGMENTS coherence).
TDD RED->GREEN: run-doc-commit.sh 32/32, run-doc-shape.sh 19/19, behavioral C/D.
shellcheck clean. Branch-guard (3) deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj
This commit is contained in:
Bastien Chanot
2026-06-29 17:41:24 +02:00
co-authored by Claude Opus 4.8
parent 4da2b905de
commit 09f5b28d99
8 changed files with 398 additions and 11 deletions
+16
View File
@@ -13,6 +13,8 @@
# T5 idempotent — empty list / clean tree → no-op exit 0
# T6 unsafe git state (detached HEAD) → exit 3, no commit
# T7 path WITH A SPACE passed as one arg → committed (argv is space-safe, no separator)
# T8 pre-commit hook REJECTS the commit → fail LOUD (exit 5), no stale hash on stdout,
# HEAD unmoved — the script must NOT report "committed" when git commit failed
#
# No -e: run every test and report, even after a failure.
set -uo pipefail
@@ -173,6 +175,20 @@ if git -C "$R" cat-file -e "HEAD:docs/My Guide.md" 2>/dev/null; then ok "spaced
if [ -z "$(git -C "$R" status --porcelain -- "docs/My Guide.md")" ]; then ok "spaced doc clean (embarked as ONE file, not split)"; else ko "spaced doc still dirty"; fi
rm -rf "$R"
echo "T8 — pre-commit hook REJECTS commit → exit 5 LOUD, no stale hash, HEAD unmoved"
R="$(new_repo)"
printf '#!/bin/sh\nexit 1\n' >"$R/.git/hooks/pre-commit"; chmod +x "$R/.git/hooks/pre-commit"
BEFORE="$(git -C "$R" rev-parse HEAD)"
printf 'feature added\n' >>"$R/README.md"
run "$R" commit "docs: T8 rejected" "README.md"
printf ' rc=%s out=[%s]\n' "$RC" "$OUT"
printf ' err: %s\n' "$(printf '%s' "$ERR" | head -1)"
if [ "$RC" -eq 5 ]; then ok "rejected commit → exit 5"; else ko "expected 5, got $RC (commit failure swallowed = masking)"; fi
if [ -z "$OUT" ]; then ok "stdout empty (no stale hash)"; else ko "stale hash leaked on failure: [$OUT]"; fi
if printf '%s' "$ERR" | grep -qi 'REJECTED'; then ok "stderr is loud (REJECTED)"; else ko "stderr not loud (no REJECTED — likely a false 'committed')"; fi
if [ "$(git -C "$R" rev-parse HEAD)" = "$BEFORE" ]; then ok "HEAD unmoved (nothing committed)"; else ko "HEAD moved despite hook reject"; fi
rm -rf "$R"
rm -f "$ERRFILE"
echo ""
printf 'RESULT: %d passed, %d failed\n' "$PASS" "$FAIL"