#!/usr/bin/env python3
"""Evidence trail for the coalescing layer's session-start check.

The run log's source_sha cues are the layer's retrieval contract, and
until this gate they were unchecked prose — the 2026-07-28 field audit
found cues that resolve locally but not in published history. This tool:

  1. verifies every `<sha>` cited in docs/coalescing.md resolves
     (`git cat-file -e`), here or in an identified sibling repository;
  2. verifies every `git show <sha>:<path>` cue's path exists at that
     SHA;
  3. checks each local SHA against the published remote (origin),
     reporting `local-only pin` where it does not resolve there — a cue
     that survives only on one machine is a claim the world cannot
     verify;
  4. derives the lessons-tier count with this repository's documented
     derivation so a session-start check can quote real numbers.

Adopted from agent-guidance (now agent-theory) @ e42762c via
`docs/plans/2026-07-28-agent-guidance-delta-wave-propagation-plan.md`;
shallow-clone skip and known-limitation note adopted from agent-theory
@ ec716e8 via
`docs/plans/2026-08-08-agent-theory-delta-wave-propagation-plan.md`.
The foreign-attribution and sibling-opt-in design predates the hub's
copy here — it was back-ported hub-side from this repository's landing.

Local adaptation — LESSONS DERIVATION. This repository's ledger uses
dated H2 sections in TWO shapes, and both are eligible material:

    ## 2026-07-01: Four-Way Implementation Bake-Off      (leading date)
    ## Specs: state contracts as rules, not examples (2026-07-03)

This tool implements the derivation command declared in
`docs/coalescing.md` (repaired 2026-07-28 to count both dated-H2
shapes; the prior leading-date-only form counted 1 of 22 sections —
the 2026-07-17 wave review's pre-existing owner item, closed by
repair-in-sweep). `docs/coalescing.md` remains the spec: when this
script and the declared commands disagree, the file wins and the
script is the defect.

Read-only by construction. Exit 1 only on an unresolvable local cue
(a broken retrieval contract); local-only pins and count reports are
informational — publication discipline is the owner's call.

On a shallow clone the SHA-resolution and retrieval-cue legs cannot be
truthful (history is absent), so they are SKIPPED loudly with exit 0 —
a false BROKEN is as invalid as a silent pass. Enforcement CI must run
this gate on full history (checkout with fetch-depth: 0).
"""

from __future__ import annotations

import os
import re
import subprocess
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
STATE = REPO_ROOT / "docs" / "coalescing.md"

SHA_RE = re.compile(r"`([0-9a-f]{7,40})`")
CUE_RE = re.compile(r"git show ([0-9a-f]{7,40}):([A-Za-z0-9_./-]+)")

# Lessons ledger: dated H2 sections, either shape (see module docstring).
LESSON_LEADING_DATE = re.compile(r"^## 20[0-9]{2}-[0-9]{2}-[0-9]{2}", re.M)
LESSON_TRAILING_DATE = re.compile(r"^## .*\(20[0-9]{2}-[0-9]{2}-[0-9]{2}\)\s*$", re.M)

# Foreign attribution is a REPORTING aid, never a resolution mechanism: it
# names repositories this repo's own citations refer to, makes no filesystem
# assumption, and resolves nothing.
FOREIGN_MARKERS = (
    "agent-theory",
    "agent-guidance",
    "mm",
    "weft",
    "taut",
    "backstitch",
    "engram",
    "simplebroker",
)

# Opt-in local convenience, unset by default. A sibling working copy is an
# environment fact, not a portability claim: anything found this way is
# reported separately and never counted as verified.
SIBLING_ROOT = os.environ.get("COALESCE_SIBLING_ROOT")


def git(*args: str, repo: Path | None = None) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", "-C", str(repo or REPO_ROOT), *args], capture_output=True, text=True
    )


def attributed_repo(text: str, pos: int) -> str | None:
    """Does the citation name the repository it came from? Reporting only."""
    ctx = text[max(0, pos - 80) : pos].lower()
    hits = [m for m in FOREIGN_MARKERS if m in ctx]
    return max(hits, key=len) if hits else None


def resolves_locally(sha: str) -> bool:
    return git("cat-file", "-e", f"{sha}^{{commit}}").returncode == 0


def in_local_checkout(sha: str) -> str | None:
    """Opt-in only (COALESCE_SIBLING_ROOT). Environment-dependent by nature,
    so the caller reports it as such and never as verification."""
    if not SIBLING_ROOT:
        return None
    root = Path(SIBLING_ROOT)
    if not root.is_dir():
        return None
    for cand in sorted(root.iterdir()):
        if (cand / ".git").exists() and git(
            "cat-file", "-e", f"{sha}^{{commit}}", repo=cand
        ).returncode == 0:
            return cand.name
    return None


def _claim_dispositions(
    text: str,
    sha_hits: list[tuple[str, int]],
    *,
    remote_ok: bool,
) -> tuple[set[str], list[str], list[str], list[str]]:
    seen: set[str] = set()
    broken: list[str] = []
    local_only: list[str] = []
    foreign: list[str] = []
    for sha, pos in sha_hits:
        if sha in seen:
            continue
        seen.add(sha)
        if resolves_locally(sha):
            if (
                remote_ok
                and git("merge-base", "--is-ancestor", sha, "origin/main").returncode
                != 0
            ):
                local_only.append(sha)
            continue
        who = attributed_repo(text, pos)
        if who is None:
            broken.append(
                f"`{sha}` resolves nowhere here and names no repository — "
                "a cue must say where it is retrievable"
            )
            continue
        extra = in_local_checkout(sha)
        suffix = f" (present in a local checkout of {extra})" if extra else ""
        foreign.append(f"{sha} @ {who}{suffix}")
    return seen, broken, local_only, foreign


def _broken_retrieval_cues(cues: list[tuple[str, str]]) -> list[str]:
    return [
        f"cue `git show {sha}:{path}` does not resolve here"
        for sha, path in cues
        if not (
            resolves_locally(sha)
            and git("cat-file", "-e", f"{sha}:{path}").returncode == 0
        )
    ]


def _lesson_counts() -> tuple[int, int]:
    lessons = REPO_ROOT / "docs" / "lessons.md"
    if not lessons.is_file():
        return 0, 0
    text = lessons.read_text()
    return (
        len(LESSON_LEADING_DATE.findall(text)),
        len(LESSON_TRAILING_DATE.findall(text)),
    )


def _render_result(
    *,
    claims: int,
    cues: int,
    leading: int,
    trailing: int,
    foreign: list[str],
    local_only: list[str],
    broken: list[str],
    remote_ok: bool,
) -> int:
    print(
        f"coalesce-check: {claims} SHA claim(s) ({len(foreign)} foreign claim(s)), "
        f"{cues} retrieval cue(s); lessons dated H2 sections: "
        f"{leading + trailing} ({leading} leading-date, {trailing} trailing-date)"
    )
    if foreign:
        print(
            f"  foreign claim ({len(foreign)}) — not verifiable in this "
            "repository;\n    check the citing repository: " + ", ".join(foreign)
        )
    if local_only:
        print(
            f"  local-only pin ({len(local_only)}): "
            + ", ".join(local_only)
            + "  [not in origin/main — published history cannot verify these]"
        )
    if broken:
        print(f"  BROKEN ({len(broken)}):")
        for message in broken:
            print(f"    {message}")
        return 1
    suffix = "" if remote_ok else "  [no origin remote — publication check skipped]"
    print("  all cues resolve" + suffix)
    return 0


def main() -> int:
    if not STATE.is_file():
        print("coalesce-check: no docs/coalescing.md — nothing to check")
        return 0
    text = STATE.read_text()

    if git("rev-parse", "--is-shallow-repository").stdout.strip() == "true":
        # History-dependent legs cannot be truthful here; skip loudly.
        leading, trailing = _lesson_counts()
        cues = sorted(set(CUE_RE.findall(text)))
        print(
            "coalesce-check: shallow clone detected — SHA-resolution and "
            "retrieval-cue checks SKIPPED (history absent; cues cannot be "
            "verified here). Enforcement requires full history "
            "(fetch-depth: 0)."
        )
        print(f"  retrieval cues found (syntax only, unverified here): {len(cues)}")
        print(
            "  lessons dated H2 sections: "
            f"{leading + trailing} ({leading} leading-date, {trailing} trailing-date)"
        )
        return 0

    sha_hits = [(m.group(1), m.start()) for m in SHA_RE.finditer(text)]
    cues = sorted(set(CUE_RE.findall(text)))

    # Known limitation (hub docs/lessons.md 2026-08-07): the ancestry
    # check in _claim_dispositions hardcodes origin/main; a repository
    # with a different default branch gets false local-only-pin reports.
    # Fix candidate: resolve the default via origin/HEAD, falling back
    # to origin/main.
    remote_ok = (
        git("rev-parse", "--verify", "origin/HEAD").returncode == 0
        or git("rev-parse", "--verify", "origin/main").returncode == 0
    )

    seen, broken, local_only, foreign = _claim_dispositions(
        text, sha_hits, remote_ok=remote_ok
    )
    broken.extend(_broken_retrieval_cues(cues))
    leading, trailing = _lesson_counts()
    return _render_result(
        claims=len(seen),
        cues=len(cues),
        leading=leading,
        trailing=trailing,
        foreign=foreign,
        local_only=local_only,
        broken=broken,
        remote_ok=remote_ok,
    )


if __name__ == "__main__":
    sys.exit(main())
