#!/usr/bin/env python3
"""Do the tree's in-house `version` requirements still resolve?

DO NOT EDIT IN PLACE. The master is _private/infra/bootstrap/githooks/internal-deps.py.

Usage:
    internal-deps.py <tree-root> [repo-root] [pushed-sha]

With a repo root, only pairs that repo is on either side of can fail the run;
everything else is reported as a note. Without one, every pair is graded, which
is the whole-tree report:

    python3 internal-deps.py ~/Code

The run grades FOUR views and fails on any of them:

  working copy   what this machine builds today. The historical check.
  as pushed      the same question asked of the pushing repo's manifests AS THEY
                 EXIST AT THAT COMMIT, against the rest of the tree on disk.
                 Needs a pushed sha, so it is skipped in the by-hand report.
  as published   every requirement against the version the provider has actually
                 PUSHED, read from its last-fetched remote-tracking ref. This is
                 the only view that predicts a build on a machine that is not
                 this one.
  as published to crates.io
                 the same question for the siblings that are consumed from the
                 REGISTRY rather than by git URL: the makeover suite, alloy_tui,
                 pter. A requirement only the local checkout satisfies is a
                 finding, and the remedy is to publish rather than to push.

WHY THE SECOND VIEW EXISTS. The first one reads manifests off the filesystem, so
an uncommitted forward-fix makes it grade text that git is not publishing. That
is not hypothetical: on 2026-08-24 mnw-cli's `synckit-client` requirement had
been advanced to "0.9" in the working copy and never committed, this gate printed
`internal deps coherent (42 requirements)`, the push went out, and Sando failed
to resolve `^0.8` against 0.9.0 minutes later. The gate was checking a tree that
was not the tree being published, and nothing distinguished that from real
coherence.

WHAT IT GRADES. Every dependency in the tree that carries both a `git` URL on one
of our forges and a `version` requirement, against the version in the working
copy of the crate that URL names. That is the pairing cargo enforces and the one
that broke: a requirement of "0.11" against a sibling that has moved to 0.14 is
not a warning, it is a graph that will not resolve on any machine.

WHY DISK AND REMOTES BOTH. `~/Code/.cargo/config.toml` patches every one of these
dependencies to the working copy in the tree, so what is on disk here is what
every local build reads: a bump that has not been pushed yet breaks its consumers
on this machine just as thoroughly, and finding that out at push time is the
point. But cargo resolves a git dependency against the branch head at the URL, so
the `[patch]` block also HIDES an unpushed sibling from every local check. That
gap cost a production build on 2026-08-26 (infra `5c4928c1`): MNW required quasi
"^0.63", quasi's working copy was 0.63.0 and its `mnw/main` was 0.56.0, both disk
views were clean, and Sando could not resolve. The three views answer three
different questions and all of them matter.

WHY THE FOURTH VIEW EXISTS. The three above all pair a requirement against the
sibling repo its git URL names, and a makeover crate has no git URL in a consumer
manifest: it is consumed from crates.io. So the failure class this gate exists to
stop -- bump a library, forget its consumers -- had a second half that was not
merely ungraded but confidently reported coherent. On 2026-08-28 quasi-basics
required makeover-layout "^0.38.0" while the index's newest was 0.37.0 and 0.38.0
existed in one working directory; `cargo check` could not select a version and
this gate printed `internal deps coherent [as published] (42 requirements)`. The
same afternoon, stripping the [patch.crates-io] blocks mid-publish put MNW/server
on makeover "^3.1" against an index whose newest was 3.0.0, with the same verdict.
That window -- manifest bumped, crate not yet on the index -- is a normal stage of
any correct publish order, short when the publish succeeds and unbounded when it
fails partway across a seven-crate suite. makeover-layout carries `links`, so one
unpublished crate in the suite takes down every consumer in the graph at once.

Blame for this view is CONSUMER-side only. Pushing the provider is how a crate
reaches the index in the first place (Bento publishes from a pushed tag), so
refusing that push because its own new version is unpublished would deadlock the
release. What is worth refusing is a push that carries a requirement nothing on
the index satisfies.

NETWORK. None on the happy path. The published view reads the last-fetched
remote-tracking ref, and only when a requirement FAILS against it does it fetch
that one repo's one branch and re-check, so a ref nobody has fetched since the
sibling was pushed cannot refuse a good push. A repo with no fetched remote at
all is reported as ungraded, never as passing. The crates.io view works the same
way one layer over: it reads cargo's own sparse-index cache under
~/.cargo/registry/index, and only a requirement that fails against the cache
costs one HTTPS fetch of that crate's index file, so a cache older than the
sibling's release cannot refuse a good push either. A crate whose index file can
be read from neither place is reported as ungraded.

WHAT IT DOES NOT GRADE, on purpose:

  third-party deps    a version requirement on somebody else's crate is not
                      something this tree can forward-fix.
  path deps           no version requirement to be wrong about.
  ranges and wildcards  `>=`, `<`, `*` and comma lists are deliberate statements
                      about a span, not a pin that drifts. Counted as unchecked.
"""

import glob
import json
import os
import re
import subprocess
import sys
import tomllib
import urllib.error
import urllib.request

# The forges that make a git URL ours. A dependency on somebody else's git repo
# is not something this tree can forward-fix.
OURS = re.compile(r"(makenot\.work|git\.sr\.ht/~maxmj)", re.I)

# Directories that hold code we do not grade: retired, staged for deletion, or
# not ours. Mirrors the sweep's exclusions rather than inventing a second list.
SKIP_DIRS = {
    "target", ".git", "node_modules", "dist", "vendor",
    "_archive", "_scratch", "trash", "_meta", "vtebench",
}
MAX_DEPTH = 4

DEP_SECTIONS = ("dependencies", "dev-dependencies", "build-dependencies")

# The crates.io view's label. Long on purpose: "as published" already means the
# git-URL siblings, and the remedy for the two is different.
REGISTRY_LABEL = "as published to crates.io"


def manifests(root):
    """Every Cargo.toml in the tree, shallow-walked."""
    out = []
    stack = [(root, 0)]
    while stack:
        d, depth = stack.pop()
        try:
            entries = list(os.scandir(d))
        except OSError:
            continue
        for e in entries:
            if e.is_file() and e.name == "Cargo.toml":
                out.append(e.path)
            elif e.is_dir() and e.name not in SKIP_DIRS and depth < MAX_DEPTH:
                stack.append((e.path, depth + 1))
    return out


def load(path):
    try:
        with open(path, "rb") as fh:
            return tomllib.load(fh)
    except (OSError, tomllib.TOMLDecodeError):
        return None


def dep_tables(doc):
    """Every dependency table in a manifest, including per-target and workspace."""
    for section in DEP_SECTIONS:
        table = doc.get(section)
        if isinstance(table, dict):
            yield table
    for cfg in (doc.get("target") or {}).values():
        if not isinstance(cfg, dict):
            continue
        for section in DEP_SECTIONS:
            table = cfg.get(section)
            if isinstance(table, dict):
                yield table
    ws = doc.get("workspace") or {}
    table = ws.get("dependencies")
    if isinstance(table, dict):
        yield table


def parse_version(v):
    """A version as a 3-tuple, prerelease dropped. Junk sorts as (0, 0, 0)."""
    core = str(v).split("+")[0].split("-")[0]
    parts = []
    for piece in core.split(".")[:3]:
        try:
            parts.append(int(piece))
        except ValueError:
            parts.append(0)
    while len(parts) < 3:
        parts.append(0)
    return tuple(parts)


def satisfies(req, version):
    """Cargo's default (caret) requirement semantics. None means 'not graded'.

    The rule that matters here is the 0.x one: under 0.1.0 and above, the MINOR
    is the compatibility boundary, which is why a 0.11 requirement rejects 0.14
    outright rather than treating it as a newer patch.
    """
    req = req.strip()
    if not req or any(c in req for c in "<>*,~"):
        return None
    # A prerelease satisfies nothing that does not ask for a prerelease of the
    # same version, so a plain requirement rejects it. This is the shape the
    # maturity ladder produces at beta entry: a sibling at 1.0.0-beta.1 does not
    # resolve for a consumer requiring "1.0", and cargo says so.
    if "-" in str(version).split("+")[0] and "-" not in req:
        return False
    exact = req.startswith("=")
    req = req.lstrip("^=").strip()
    if not req:
        return None
    given = req.split(".")
    try:
        r = [int(p) for p in given[:3]]
    except ValueError:
        return None
    v = parse_version(version)
    if exact:
        return tuple(v[: len(r)]) == tuple(r)
    if r[0] > 0:
        return v[0] == r[0] and v[1:] >= tuple(r[1:] + [0] * (2 - len(r[1:])))
    if len(r) == 1:
        return v[0] == 0
    if r[1] > 0:
        return v[0] == 0 and v[1] == r[1] and v[2] >= (r[2] if len(r) > 2 else 0)
    # 0.0.x: every patch is its own compatibility island.
    if len(r) > 2:
        return v[:3] == (0, 0, r[2])
    return v[0] == 0 and v[1] == 0


def git_lines(repo, *args):
    """Run git in `repo` and return stdout lines, or None if it failed."""
    try:
        out = subprocess.run(
            ["git", "-C", repo, *args],
            capture_output=True, text=True, check=True,
        )
    except (OSError, subprocess.CalledProcessError):
        return None
    return out.stdout.splitlines()


def git_manifests(repo, sha):
    """Repo-relative paths of every Cargo.toml at `sha`, or None if unreadable."""
    lines = git_lines(repo, "ls-tree", "-r", "--name-only", sha)
    if lines is None:
        return None
    out = []
    for rel in lines:
        if os.path.basename(rel) != "Cargo.toml":
            continue
        if any(part in SKIP_DIRS for part in rel.split("/")):
            continue
        out.append(rel)
    return out


def load_at(repo, sha, rel):
    """One manifest as it exists at `sha`. None if missing or unparseable."""
    lines = git_lines(repo, "show", f"{sha}:{rel}")
    if lines is None:
        return None
    try:
        return tomllib.loads("\n".join(lines))
    except tomllib.TOMLDecodeError:
        return None


def pushed_view(docs, repo, sha):
    """`docs` with everything under `repo` replaced by its content at `sha`.

    The rest of the tree stays as it is on disk, which is what a local build
    resolves against either way. Returns None if the commit cannot be read, so
    the caller can skip the view rather than invent a verdict about it.
    """
    rels = git_manifests(repo, sha)
    if rels is None:
        return None
    out = {k: v for k, v in docs.items() if not k.startswith(repo + os.sep)}
    for rel in rels:
        out[os.path.join(repo, rel)] = load_at(repo, sha, rel)
    return out


def repo_of(path, tree):
    """The git repo `path` belongs to, or None if it is not in one under `tree`."""
    d = os.path.dirname(path)
    while d.startswith(tree):
        if os.path.exists(os.path.join(d, ".git")):
            return d
        if d == tree:
            break
        parent = os.path.dirname(d)
        if parent == d:
            break
        d = parent
    return None


def publishing_ref(repo, cache):
    """The remote-tracking ref a git dependency on `repo` would resolve against.

    Cargo reads the branch head at the URL, and every in-house dependency URL is
    on one of our forges (CLAUDE.md, "what each remote is for": `mnw` is the
    public face, `srht` a backup, `astra` the private mirror). So prefer `mnw`,
    then any other remote whose URL is ours, and fall back to `origin`.

    Returns `<remote>/<branch>` or None when the repo has no such remote or the
    ref has never been fetched. None is not a verdict: the caller reports the
    repo as ungraded rather than inventing one.
    """
    if repo in cache:
        return cache[repo]
    ref = None
    lines = git_lines(repo, "remote", "-v") or []
    urls = {}
    for line in lines:
        parts = line.split()
        if len(parts) >= 2:
            urls.setdefault(parts[0], parts[1])
    order = [r for r in ("mnw",) if r in urls]
    order += [r for r, u in urls.items() if r not in order and OURS.search(u)]
    order += [r for r in ("origin",) if r in urls and r not in order]
    for remote in order:
        head = git_lines(repo, "symbolic-ref", "--quiet", f"refs/remotes/{remote}/HEAD")
        candidates = []
        if head:
            candidates.append(head[0].rsplit("/", 1)[-1])
        candidates += ["main", "master"]
        for branch in candidates:
            if git_lines(repo, "rev-parse", "--verify", "--quiet",
                         f"refs/remotes/{remote}/{branch}"):
                ref = f"{remote}/{branch}"
                break
        if ref:
            break
    cache[repo] = ref
    return ref


def version_at(repo, ref, rel, cache):
    """A crate's version in `repo` at `ref`, following a workspace inheritance.

    `rel` is the manifest's path relative to the repo. Returns None when the
    manifest is not at that ref at all, which is what a crate added since the
    last push looks like.
    """
    key = (repo, ref, rel)
    if key in cache:
        return cache[key]
    version = None
    doc = load_at(repo, ref, rel)
    if doc:
        pkg = doc.get("package")
        if isinstance(pkg, dict):
            v = pkg.get("version")
            if isinstance(v, str):
                version = v
            elif isinstance(v, dict) and v.get("workspace") is True:
                # Walk up to the workspace root as it exists at the same ref.
                d = os.path.dirname(rel)
                while True:
                    root_rel = os.path.join(d, "Cargo.toml") if d else "Cargo.toml"
                    root = load_at(repo, ref, root_rel) if root_rel != rel else None
                    inherited = (
                        ((root or {}).get("workspace") or {}).get("package") or {}
                    ).get("version")
                    if isinstance(inherited, str):
                        version = inherited
                        break
                    if not d:
                        break
                    d = os.path.dirname(d)
    cache[key] = version
    return version


def analyze_published(docs, disk_docs, tree, repo, sha):
    """Grade every requirement against what its provider has actually PUSHED.

    This is the view that predicts a build somewhere other than this machine.
    The other two read the provider's version off the filesystem, and the
    `[patch]` block in ~/Code/.cargo/config.toml means that is what a local
    build resolves -- but a git dependency resolves against the branch head at
    the URL, so an unpushed sibling passes both of them and fails everywhere
    else. That is exactly what happened on 2026-08-26: MNW required quasi
    "^0.63", quasi's working copy was 0.63.0 and `mnw/main` was 0.56.0, both
    existing views were clean, and Sando build 72 could not resolve.

    The repo being pushed is read at `sha` rather than at its remote, since what
    it is about to publish is the thing to grade. Every other repo is read at
    its last-fetched remote ref: no network on the happy path. A break is
    re-checked after fetching that one repo, so a stale ref cannot refuse a push
    on its own.

    Returns (broken, graded, ungraded), where ungraded maps a repo to why.
    """
    ref_cache, version_cache, fetched = {}, {}, set()
    versions_on_disk = crate_index(disk_docs, tree)
    broken, graded, ungraded = [], 0, {}

    for consumer_manifest, name, req in requirements(docs):
        known = versions_on_disk.get(name)
        if known is None:
            continue  # Not in this tree; the disk views already say so.
        provider_manifest = known[1]
        provider_repo = repo_of(provider_manifest, tree)
        if provider_repo is None:
            ungraded.setdefault(os.path.dirname(provider_manifest), "not a git repo")
            continue
        if repo is not None and provider_repo == repo and sha:
            # The repo under the hook: what it is about to publish is `sha`,
            # which the "as pushed" view already read off disk into `docs`.
            continue
        ref = publishing_ref(provider_repo, ref_cache)
        if ref is None:
            ungraded.setdefault(provider_repo, "no fetched remote to read")
            continue
        rel = os.path.relpath(provider_manifest, provider_repo)
        have = version_at(provider_repo, ref, rel, version_cache)
        if have is None:
            ungraded.setdefault(provider_repo, f"{name} is not at {ref} yet")
            continue
        verdict = satisfies(req, have)
        if verdict is None:
            continue
        if not verdict and provider_repo not in fetched:
            # Only now, and only for this one repo: a ref nobody has fetched
            # since the sibling was pushed would otherwise refuse a good push.
            fetched.add(provider_repo)
            remote, branch = ref.split("/", 1)
            git_lines(provider_repo, "fetch", "--quiet", remote, branch)
            version_cache.pop((provider_repo, ref, rel), None)
            have = version_at(provider_repo, ref, rel, version_cache) or have
            verdict = satisfies(req, have)
        graded += 1
        if not verdict:
            broken.append(
                (consumer_manifest, name, req, have, provider_manifest, ref)
            )
    return broken, graded, ungraded


def crate_index(docs, tree):
    """Crate name -> (version, manifest path), workspace inheritance resolved.

    A member saying `version.workspace = true` gets its number from the root,
    and reporting it as 0.0.0 would be a false break.
    """
    ws_version = {}
    for p, doc in docs.items():
        if not doc:
            continue
        v = ((doc.get("workspace") or {}).get("package") or {}).get("version")
        if isinstance(v, str):
            ws_version[os.path.dirname(p)] = v

    def resolve_version(manifest_path, pkg):
        v = pkg.get("version")
        if isinstance(v, str):
            return v
        d = os.path.dirname(manifest_path)
        while d.startswith(tree):
            if d in ws_version:
                return ws_version[d]
            parent = os.path.dirname(d)
            if parent == d:
                break
            d = parent
        return None

    versions = {}
    for p, doc in docs.items():
        if not doc:
            continue
        pkg = doc.get("package")
        if not isinstance(pkg, dict) or not isinstance(pkg.get("name"), str):
            continue
        v = resolve_version(p, pkg)
        if v:
            versions[pkg["name"]] = (v, p)
    return versions


def requirements(docs):
    """Every in-house git+version pair: (consumer manifest, crate, requirement).

    A dependency qualifies when it carries both a `git` URL on one of our forges
    and a `version`. That is the pairing cargo enforces and the one that breaks:
    a requirement of "0.11" against a sibling that has moved to 0.14 is not a
    warning, it is a graph that will not resolve on any machine.
    """
    for p, doc in docs.items():
        if not doc:
            continue
        for table in dep_tables(doc):
            for key, spec in table.items():
                if not isinstance(spec, dict):
                    continue
                git = spec.get("git")
                req = spec.get("version")
                if not isinstance(git, str) or not isinstance(req, str):
                    continue
                if not OURS.search(git):
                    continue
                name = spec.get("package") if isinstance(spec.get("package"), str) else key
                yield p, name, req


# --- the crates.io view -------------------------------------------------------

# Cargo's own sparse-index cache. One file per crate, holding the same
# newline-delimited JSON the registry serves, prefixed by a format byte and an
# etag and separated by NULs. Reading it is why the happy path costs no network.
INDEX_CACHE_GLOB = os.path.expanduser(
    "~/.cargo/registry/index/index.crates.io-*/.cache"
)
INDEX_URL = "https://index.crates.io"
INDEX_TIMEOUT = 5


def index_prefix(name):
    """The registry's directory prefix for a crate name (cargo's own scheme)."""
    n = name.lower()
    if len(n) == 1:
        return "1"
    if len(n) == 2:
        return "2"
    if len(n) == 3:
        return os.path.join("3", n[0])
    return os.path.join(n[:2], n[2:4])


def parse_index_blob(blob):
    """Every non-yanked version in an index file, newest first.

    Takes the cache format and the wire format both: the cache is the wire
    format with a header and NUL separators, so splitting on NUL and newline
    and keeping whatever parses as a version record covers each of them.
    """
    if isinstance(blob, bytes):
        blob = blob.decode("utf-8", "replace")
    out = []
    for chunk in blob.replace("\x00", "\n").splitlines():
        chunk = chunk.strip()
        if not chunk.startswith("{"):
            continue
        try:
            rec = json.loads(chunk)
        except ValueError:
            continue
        vers = rec.get("vers")
        if isinstance(vers, str) and not rec.get("yanked"):
            out.append(vers)
    out.sort(key=parse_version, reverse=True)
    return out


def index_from_cache(name):
    """Published versions from cargo's sparse-index cache, or None if absent."""
    for cache in glob.glob(INDEX_CACHE_GLOB):
        path = os.path.join(cache, index_prefix(name), name.lower())
        try:
            with open(path, "rb") as fh:
                return parse_index_blob(fh.read())
        except OSError:
            continue
    return None


def index_from_network(name):
    """Published versions from the sparse index itself, or None if unreachable."""
    url = f"{INDEX_URL}/{index_prefix(name)}/{name.lower()}"
    try:
        with urllib.request.urlopen(url, timeout=INDEX_TIMEOUT) as resp:
            return parse_index_blob(resp.read())
    except (urllib.error.URLError, OSError, ValueError):
        return None


def published_versions(name, cache, refresh=False):
    """Every non-yanked published version of `name`, newest first.

    Reads cargo's cache first and goes to the network only when the caller says
    the cached answer was not good enough, which is the same escalation the
    remote-ref view does: a cache older than the sibling's release must not be
    able to refuse a good push.
    """
    if not refresh and name in cache:
        return cache[name]
    versions = None if refresh else index_from_cache(name)
    if versions is None:
        versions = index_from_network(name)
    cache[name] = versions
    return versions


def publishable_index(docs, tree):
    """In-house crates that go to crates.io: name -> (version, manifest path).

    `publish = false` is the marker for everything that does not (quasi, shop,
    everycycle, wam), and it inherits from the workspace root the same way
    `version` does, so resolve it the same way rather than reading the member
    alone.
    """
    ws_publish = {}
    for p, doc in docs.items():
        if not doc:
            continue
        v = ((doc.get("workspace") or {}).get("package") or {}).get("publish")
        if isinstance(v, bool):
            ws_publish[os.path.dirname(p)] = v

    def publishes(manifest_path, pkg):
        v = pkg.get("publish")
        if isinstance(v, bool):
            return v
        if isinstance(v, list):
            return bool(v)
        if isinstance(v, dict) and v.get("workspace") is True:
            d = os.path.dirname(manifest_path)
            while d.startswith(tree):
                if d in ws_publish:
                    return ws_publish[d]
                parent = os.path.dirname(d)
                if parent == d:
                    break
                d = parent
        return True

    out = {}
    for name, (version, manifest) in crate_index(docs, tree).items():
        doc = docs.get(manifest) or {}
        pkg = doc.get("package")
        if isinstance(pkg, dict) and publishes(manifest, pkg):
            out[name] = (version, manifest)
    return out


def registry_requirements(docs, publishable):
    """Every requirement on an in-house crate taken from the registry.

    The pairing is the mirror image of `requirements()`: a `version` and NO git
    URL, naming a crate this tree both holds and publishes. A path dependency is
    excluded for the same reason it is everywhere else here -- what it resolves
    against is the file next to it, not a release.
    """
    for p, doc in docs.items():
        if not doc:
            continue
        for table in dep_tables(doc):
            for key, spec in table.items():
                req = None
                if isinstance(spec, str):
                    req = spec
                elif isinstance(spec, dict):
                    if spec.get("git") or spec.get("path"):
                        continue
                    if isinstance(spec.get("version"), str):
                        req = spec["version"]
                if req is None:
                    continue
                name = key
                if isinstance(spec, dict) and isinstance(spec.get("package"), str):
                    name = spec["package"]
                if name in publishable:
                    yield p, name, req


def analyze_registry(docs, tree):
    """Grade every registry requirement on an in-house crate against the index.

    The escalation is deliberate and is what keeps the happy path free: the
    local version is tried first, and the index is consulted only for a
    requirement the checkout is the one thing satisfying (or that nothing
    satisfies). Working ahead of a release is fine; requiring a version that
    exists in no published release is not, because no other machine can resolve
    it and `links` on makeover-layout makes that the whole graph at once.

    Returns (broken, graded, unchecked, ungraded).
    """
    publishable = publishable_index(docs, tree)
    index_cache = {}
    broken, graded, unchecked, ungraded = [], 0, 0, {}

    for consumer_manifest, name, req in registry_requirements(docs, publishable):
        local_version, provider_manifest = publishable[name]
        if satisfies(req, local_version) is None:
            unchecked += 1
            continue
        versions = published_versions(name, index_cache)
        if versions is None:
            ungraded.setdefault(name, "no index entry, cached or fetched")
            continue
        if not any(satisfies(req, v) for v in versions):
            # A cache older than the sibling's release would otherwise refuse a
            # good push, so pay for one fetch before calling it broken.
            versions = published_versions(name, index_cache, refresh=True) or versions
        graded += 1
        if not any(satisfies(req, v) for v in versions):
            newest = versions[0] if versions else "nothing published"
            broken.append(
                (consumer_manifest, name, req, newest, provider_manifest,
                 "the index")
            )
    return broken, graded, unchecked, ungraded


def analyze(docs, tree):
    """Grade every in-house git+version pair in `docs` against the tree on disk.

    Returns (broken, unchecked, absent, graded), where a broken entry is
    (consumer manifest, crate, requirement, version found, provider manifest,
    source label). The source label is None here: this view reads the version
    off a manifest, and naming the manifest already says where it came from.
    """
    versions = crate_index(docs, tree)
    broken, unchecked, absent, graded = [], 0, set(), 0
    for p, name, req in requirements(docs):
        known = versions.get(name)
        if known is None:
            # A repo that is not on this machine (ripgrow lives on mbp only).
            # Not a finding: nothing here can be wrong about it.
            absent.add(name)
            continue
        verdict = satisfies(req, known[0])
        if verdict is None:
            unchecked += 1
            continue
        graded += 1
        if not verdict:
            broken.append((p, name, req, known[0], known[1], None))
    return broken, unchecked, absent, graded


def split_blame(broken, repo, blame_provider=True):
    """Breaks this push owns, and breaks that were already there.

    `blame_provider` is off for the crates.io view: there, the provider side of
    a break is a crate that has been bumped and not released yet, and pushing it
    is how it reaches the index at all (Bento publishes from a pushed tag). Only
    the consumer side of that view is a push worth refusing.
    """
    ours, theirs = [], []
    for item in broken:
        consumer_manifest, _name, _req, _have, provider_manifest, _src = item
        mine = repo is not None and (
            consumer_manifest.startswith(repo + os.sep)
            or (blame_provider and provider_manifest.startswith(repo + os.sep))
        )
        (ours if mine else theirs).append(item)
    return ours, theirs


def report(broken, repo, tree, label, blame_provider=True):
    """Print one view's breaks. Returns True if this push has to be refused."""
    ours, theirs = split_blame(broken, repo, blame_provider)

    def rel(path):
        return os.path.relpath(path, tree)

    for consumer_manifest, name, req, have, provider_manifest, src in ours + theirs:
        where = (
            f"{src} has {have} ({rel(provider_manifest)})"
            if src
            else f"the tree has {have} ({rel(provider_manifest)})"
        )
        print(
            f"  [{label}] {rel(consumer_manifest)}: requires {name} \"{req}\", {where}",
            file=sys.stderr,
        )
    if repo is None:
        return bool(broken)
    if not ours:
        # Somebody else's skew. Worth seeing, never worth blocking this push on:
        # a gate that fails for a reason the pusher cannot fix is a gate that
        # gets bypassed by reflex, and then it is not a gate.
        if theirs:
            print(
                f"pre-push: [{label}] {len(theirs)} unresolvable requirements "
                "elsewhere in the tree (listed above, not this push's).",
            )
        return False
    return True


def main():
    if len(sys.argv) < 2:
        print(__doc__.strip(), file=sys.stderr)
        return 2
    tree = os.path.realpath(sys.argv[1])
    repo = os.path.realpath(sys.argv[2]) if len(sys.argv) > 2 else None
    sha = sys.argv[3] if len(sys.argv) > 3 else None

    docs = {p: load(p) for p in manifests(tree)}

    views = [("working copy", docs)]
    skipped_push_view = False
    if repo and sha:
        pushed = pushed_view(docs, repo, sha)
        if pushed is None:
            skipped_push_view = True
        else:
            views.append(("as pushed", pushed))

    refuse = False
    summaries = []
    for label, view in views:
        broken, unchecked, absent, graded = analyze(view, tree)
        summaries.append((label, graded, unchecked, absent, bool(broken)))
        if broken and report(broken, repo, tree, label):
            refuse = True

    # The third view: what the rest of the tree has actually PUSHED. Graded from
    # the most authoritative consumer view available, so the requirements read
    # are the ones about to be published.
    consumer_view = views[-1][1]
    pub_broken, pub_graded, ungraded = analyze_published(
        consumer_view, docs, tree, repo, sha
    )
    summaries.append(("as published", pub_graded, 0, set(), bool(pub_broken)))
    if pub_broken and report(pub_broken, repo, tree, "as published"):
        refuse = True

    # The fourth view: the siblings that are consumed from crates.io rather than
    # by git URL. Nothing above can see them, because the pairing every other
    # view makes is against the repo a git URL names.
    reg_broken, reg_graded, reg_unchecked, reg_ungraded = analyze_registry(
        consumer_view, tree
    )
    summaries.append(
        (REGISTRY_LABEL, reg_graded, reg_unchecked, set(), bool(reg_broken))
    )
    if reg_broken and report(
        reg_broken, repo, tree, REGISTRY_LABEL, blame_provider=False
    ):
        refuse = True

    if refuse:
        bad = {lbl for lbl, _g, _u, _a, broke in summaries if broke}
        clean = [lbl for lbl, _g, _u, _a, broke in summaries if not broke]
        print("", file=sys.stderr)
        print(
            "pre-push: this push leaves a dependency that cannot resolve.\n"
            "  A version requirement states which major a consumer was written against,\n"
            "  so bumping a library and fixing its consumers is one pass (CLAUDE.md,\n"
            "  \"a breaking bump of an in-house crate is forward-fixed, in the same pass\").\n"
            "  Fix: bump the requirement in the manifests above, make the consumers\n"
            "  compile, and push them with this one.",
            file=sys.stderr,
        )
        # Which view broke is the diagnosis, so say what the views disagree
        # about rather than only that they disagree.
        if REGISTRY_LABEL in bad:
            print(
                "  A requirement above resolves against no version on crates.io, so the\n"
                "  fix is to PUBLISH the sibling, not to push it. Until it is on the\n"
                "  index no machine can resolve the graph, this one included once the\n"
                "  [patch.crates-io] block is out of the way.",
                file=sys.stderr,
            )
        if "as published" in bad and "working copy" not in bad:
            print(
                "  The working copy is fine and the published tree is not, so the\n"
                "  difference is what has been PUSHED: a git dependency resolves against\n"
                "  the branch head at the URL, and ~/Code/.cargo/config.toml's [patch]\n"
                "  block hides that locally. Push the sibling named above first.",
                file=sys.stderr,
            )
        elif "as pushed" in bad and "working copy" not in bad:
            print(
                "  The working copy is fine and the commit is not, so the difference is\n"
                "  what is COMMITTED. An uncommitted manifest edit is the usual cause.",
                file=sys.stderr,
            )
        elif clean:
            print(
                f"  Note: the {clean[0]} view is clean, so the views disagree; the one\n"
                "  that failed is named on each line above.",
                file=sys.stderr,
            )
        return 1

    for label, graded, unchecked, absent, bad in summaries:
        # A view that found a break somewhere else in the tree is not a view
        # that passed. Reaching here means the break is not this push's to fix,
        # which is a reason not to refuse and never a reason to print coherent.
        verdict = "internal deps coherent" if not bad else "internal deps BROKEN elsewhere"
        print(
            f"pre-push: {verdict} [{label}] ({graded} requirements"
            + (f", {unchecked} unchecked" if unchecked else "")
            + (f", {len(absent)} crates not in this tree" if absent else "")
            + ")."
        )
    for where, why in sorted(ungraded.items()):
        # Never silently: a repo nobody could read is not a repo that passed.
        print(
            f"pre-push: [as published] {os.path.relpath(where, tree)} not graded ({why})."
        )
    for name, why in sorted(reg_ungraded.items()):
        print(f"pre-push: [{REGISTRY_LABEL}] {name} not graded ({why}).")
    if skipped_push_view:
        # Never silently: a view that did not run must not read as one that passed.
        print("pre-push: could not read the pushed commit; graded the working copy only.")
    return 0


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