#!/bin/bash
#
# pre-commit: block commits that would leave specs/implement_issues.md in a
# structurally invalid state.
#
# Why this exists (2026-09-12): the ledger is position-structured, and the
# failure mode is SILENT — a misfiled entry looks registered, the statistics
# table looks plausible, nothing errors. An instruction telling writers to
# self-check cannot reach a session that is already running (it does not
# re-read its command file), so the check has to be enforced at a point every
# writer must pass through. A commit is that point.
#
# Installed by: scripts/install-git-hooks.sh   (git hooks are not versioned —
# the hook SOURCE lives here so a fresh clone installs the same check)
# Bypass (deliberate, e.g. committing a known-broken WIP state): git commit --no-verify
#
# Scope: exits immediately unless the LEDGER itself is staged, so it costs
# nothing on unrelated commits.
#
# ⚠️ **Two-repository layout** (adapted 2026-09-17 — the original assumed the
# ledger and `.specpro/` share a repository, which is false here): `specs/` is
# its own repository and is gitignored by the root one, so the staged path is
# `implement_issues.md` when the hook runs inside `specs/` and
# `specs/implement_issues.md` when it runs in a single-repository layout. Both
# are recognised, and the verifier is located relative to the repository root
# rather than assumed to be a sibling.

set -e

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root" || exit 0

staged=$(git diff --cached --name-only)

# ---------------------------------------------------------------------------
# Second check: the shape register and the shape-detection table must agree.
#
# `spec.md` -> FR-047 carries the register (judgement criteria — the single
# source); `plan.md` -> Constraint 7 carries the detection table (the same
# shape numbers, plus HOW each is detected — a plan-layer decision, FR-048).
# Two views of one fact, and nothing compared them: the register grew to 11 in
# v0.17 while the detection table sat at 10 until a human noticed.
#
# The assertion below is the one written into Constraint 7 itself; this is its
# enforcement point. Keep the two in step — if the assertion changes, change
# this with it.
# ---------------------------------------------------------------------------
shape_sync_needed=0
for cand in specs/spec.md spec.md; do
    echo "$staged" | grep -qx "$cand" && shape_sync_needed=1
done
for cand in specs/plan.md plan.md; do
    echo "$staged" | grep -qx "$cand" && shape_sync_needed=1
done

if [ "$shape_sync_needed" = 1 ]; then
    spec_f=""; plan_f=""
    for cand in "$root/specs/spec.md" "$root/spec.md"; do
        [ -f "$cand" ] && { spec_f="$cand"; break; }
    done
    for cand in "$root/specs/plan.md" "$root/plan.md"; do
        [ -f "$cand" ] && { plan_f="$cand"; break; }
    done
    if [ -n "$spec_f" ] && [ -n "$plan_f" ]; then
        drift=$(diff \
            <(grep -oE '^    \| \*\*[0-9]+\*\*' "$spec_f" | grep -oE '[0-9]+' | sort -n) \
            <(grep -oE '^\| \*\*[0-9]+\*\*' "$plan_f" | grep -oE '[0-9]+' | sort -n) || true)
        if [ -n "$drift" ]; then
            echo "✗ Commit blocked: the shape register and the shape-detection table disagree." >&2
            echo "    register  : $spec_f  (FR-047)" >&2
            echo "    detection : $plan_f  (Constraint 7)" >&2
            echo "$drift" | sed 's/^/    /' >&2
            echo "" >&2
            echo "  Two views of one fact. Add the missing row to the detection table" >&2
            echo "  (with its 落点), or remove the one whose shape is gone. Bypass with" >&2
            echo "  git commit --no-verify if the drift is deliberate and in progress." >&2
            exit 1
        fi
    fi
fi

# ---------------------------------------------------------------------------
# Third check: the deployment mirror must match its source.
#
# 宪法 1.1 requires that "命令与模板的改动与部署动作成对提交, 不留'已改源未部署'的
# 中间态"; 宪法 Q1 requires that "一份内容在多个位置存在时，它们不得在无人察觉的情况
# 下分叉". Both name the same act — deploy before committing — and this is where that
# act is enforced. Until this section existed, the only thing enforcing either was an
# instruction to run three `diff` commands by hand, which is a discipline, not a gate.
#
# ⚠️ Conditional, like the two checks around it: it runs only when a mirrored path is
# staged, so an unrelated commit pays nothing.
#
# ⚠️ Placed BEFORE the ledger check's early exit. That exit returns for every commit
# that does not touch the ledger — which is most of them — so a section below it would
# never run for exactly the commits this check is for.
# ---------------------------------------------------------------------------
deploy_check_needed=0
for cand in commands/ templates/ scripts/; do
    echo "$staged" | grep -q "^${cand}" && deploy_check_needed=1
done

if [ "$deploy_check_needed" = 1 ]; then
    deploy_verifier=""
    for cand in scripts/bash/verify-deployment.sh .specpro/scripts/bash/verify-deployment.sh; do
        [ -f "$cand" ] && { deploy_verifier="$cand"; break; }
    done
    if [ -z "$deploy_verifier" ]; then
        echo "✗ Commit blocked: a mirrored path is staged, but verify-deployment.sh was not found." >&2
        echo "    Looked for: scripts/bash/verify-deployment.sh" >&2
        echo "    → a check that cannot run MUST NOT report as passing." >&2
        exit 1
    fi
    if ! bash "$deploy_verifier"; then
        cat >&2 <<'EOF'

────────────────────────────────────────────────────────────────────────
✗ Commit blocked: the deployment mirror is out of sync with its source.

宪法 1.1: 命令与模板的改动与部署动作成对提交，不留"已改源未部署"的中间态。
宪法 Q1 : 多副本不得在无人察觉的情况下分叉。

    Fix: deploy the changed source first —
           commands/    → .claude/skills/   (CLI install: tools/install/.venv/bin/python
                                              -c "from specpro_cli.cli.entry import app; app()"
                                              init . --agent claude --source .)
           templates/   → .specpro/templates/
           scripts/bash/→ .specpro/scripts/bash/
         then re-stage and commit.
    Bypass deliberately with: git commit --no-verify
────────────────────────────────────────────────────────────────────────
EOF
        exit 1
    fi
fi

# ---------------------------------------------------------------------------
# Fourth check: a cross-round record must not sit on a wholesale-rebuild path.
#
# plan.md → Constraint 11 (`FR-025`) requires it, and its `**强制点**:` line names
# `scripts/bash/verify-cumulative-records.sh`. ⚠️ Until this section existed that
# line named a script **nothing ran** — which is the very shape the section below
# exists to stop: a constraint whose enforcement point is a *statement* rather
# than a *gate*. The failure it guards is silent by construction — the rebuild is
# a successful `cp`, so every exit code in the chain is 0 while the record's
# contents are gone (measured 2026-09-19: a baseline section went 10 rows → 0).
#
# ⚠️ Conditional, like the checks around it: it runs only when a path that can
# carry EITHER side is staged — `commands/` (where the rebuild sites live) or
# `specs/` (where cross-round records live). An unrelated commit pays nothing.
#
# ⚠️ Placed BEFORE the ledger check's early exit below, for the reason stated
# there: a section placed after it would never run for most commits.
#
# ⚠️ The verifier declares both sides itself (a record says `**Cumulative**:` in
# its own header; a rebuild is a `cp` to a shell variable in `commands/`), so this
# section adds no registry — it only supplies the thing that was missing.
# ---------------------------------------------------------------------------
records_check_needed=0
for cand in commands/ specs/; do
    echo "$staged" | grep -q "^${cand}" && records_check_needed=1
done

if [ "$records_check_needed" = 1 ]; then
    records_verifier=""
    for cand in scripts/bash/verify-cumulative-records.sh .specpro/scripts/bash/verify-cumulative-records.sh; do
        [ -f "$cand" ] && { records_verifier="$cand"; break; }
    done
    if [ -z "$records_verifier" ]; then
        echo "✗ Commit blocked: commands/ or specs/ is staged, but verify-cumulative-records.sh was not found." >&2
        echo "    Looked for: scripts/bash/verify-cumulative-records.sh" >&2
        echo "    → a check that cannot run MUST NOT report as passing." >&2
        exit 1
    fi
    if ! bash "$records_verifier"; then
        cat >&2 <<'EOF'

────────────────────────────────────────────────────────────────────────
✗ Commit blocked: a cross-round record sits on a wholesale-rebuild path.

plan.md → Constraint 11: 一份跨轮累积的记录 MUST 住在没有任何命令每轮重建的
产物里 —— 容器每轮重建 ⇒ 累积的内容静默消失，而没有任何一步会报错。

    Fix: move the record out of the rebuilt artifact (the verifier's hit line
         names both the record and the command that rebuilds it), then
         re-stage and commit.

    Why this is a gate and not a note: a record's value IS its accumulation,
    and a rebuild destroys it with exit code 0 — nothing else would say so.
    Bypass deliberately with: git commit --no-verify
────────────────────────────────────────────────────────────────────────
EOF
        exit 1
    fi
fi

# ---------------------------------------------------------------------------
# First check: the ledger's own structural invariants.
# ---------------------------------------------------------------------------
ledger_rel=""
case "$staged" in
    *implement_issues.md*)
        for cand in specs/implement_issues.md implement_issues.md; do
            if echo "$staged" | grep -qx "$cand"; then ledger_rel="$cand"; break; fi
        done
        ;;
esac
[ -z "$ledger_rel" ] && exit 0

# Locate the verifier. It lives beside `.specpro/` — in the specs repository
# that is the PARENT directory, not a sibling.
verifier=""
for cand in .specpro/scripts/bash/verify-ledger.sh ../.specpro/scripts/bash/verify-ledger.sh; do
    if [ -x "$cand" ]; then verifier="$cand"; break; fi
done
if [ -z "$verifier" ]; then
    echo "pre-commit: verify-ledger.sh not found — skipping ledger check" >&2
    exit 0
fi

if ! LEDGER="$root/$ledger_rel" "$verifier"; then
    cat >&2 <<'EOF'

────────────────────────────────────────────────────────────────────────
✗ Commit blocked: specs/implement_issues.md failed its structural checks.

The violations are listed above. In short, the ledger's invariants are:
  · every issue entry sits INSIDE one of its '## [...] Phase Issues'
    sections — never appended to the end of the file
  · every entry's block carries exactly ONE `**Related**:` line, and every
    field line lies inside some entry's block
  · the statistics table at the top matches the actual per-section counts
  · the file ends with exactly one newline

The section entries are authoritative; the statistics table is derived.
Fix the file and re-stage it, or bypass deliberately with:
    git commit --no-verify
────────────────────────────────────────────────────────────────────────
EOF
    exit 1
fi

exit 0
