Skip to main content

max / docengine

chore: refresh the internal-deps gate from the bootstrap master Adds the --release-tags mode and drops Bento's .bento worktrees from the walk. The master is _private/infra/bootstrap/githooks/internal-deps.py; this copy is versioned per repo and refreshed by install-githooks.sh.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-08-31 15:06 UTC
Signed with PGP, not checked
Commit: e853ac48e9bcc757e236f6b0a90aa838f4260cc7
Parent: 871b176
1 file changed, +305 insertions, -4 deletions
@@ -12,6 +12,16 @@
12 12
13 13 python3 internal-deps.py ~/Code
14 14
15 + A fifth mode, run on a schedule rather than at push:
16 +
17 + python3 internal-deps.py ~/Code --release-tags [--json]
18 +
19 + grades the RELEASE TAGS instead of the branch, one per repo whose own
20 + `bento.toml` declares a release: the tag matching the version it is on today.
21 + See "the release-tag view" below for why the four views cannot see this. It is
22 + never run by the hook -- the answer changes while the repo sits still, and a
23 + push cannot fix a tag cut last week.
24 +
15 25 The run grades FOUR views and fails on any of them:
16 26
17 27 working copy what this machine builds today. The historical check.
@@ -114,6 +124,14 @@
114 124 SKIP_DIRS = {
115 125 "target", ".git", "node_modules", "dist", "vendor",
116 126 "_archive", "_scratch", "trash", "_meta", "vtebench",
127 + # Bento's release worktrees. Detached copies of tree code AT TAGS, re-pinned
128 + # with --force and never edited by a person, so grading them says nothing
129 + # about the tree -- and says it loudly, because a tag legitimately carries
130 + # the requirement that was current when it was cut. Measured 2026-08-31:
131 + # 11 of 131 manifests in ~/Code were Bento worktrees, and a copy of a
132 + # library under here can shadow the real one in the crate index, which turns
133 + # noise into a false verdict.
134 + ".bento",
117 135 }
118 136 MAX_DEPTH = 4
119 137
@@ -502,6 +520,198 @@
502 520 yield p, name, req
503 521
504 522
523 + # --- the release-tag view -----------------------------------------------------
524 + #
525 + # THE HOLE THIS CLOSES, and it is about WHEN the gate runs rather than what it
526 + # checks. Every view above grades a commit once, on the way out, and nothing
527 + # re-grades it when a sibling moves underneath it. A tag is the worst case,
528 + # because a tag is what a release builds from and a tag never moves.
529 + #
530 + # Measured 2026-08-31: Bento's release of mnw-cli 0.1.3 failed on astra with
531 + #
532 + # error: failed to select a version for the requirement `synckit-client = "^0.9"`
533 + # candidate versions found which didn't match: 0.10.0
534 + #
535 + # `mnw-cli-v0.1.3` was cut before the commit that took synckit-client 0.10, so
536 + # the tagged tree still required ^0.9 while the pushed synckit was 0.10.0. Every
537 + # view was green when MNW was pushed, because ^0.9 was current then. The branch
538 + # had the forward fix; the tag did not, and Bento builds from the tag.
539 + #
540 + # It is invisible locally as well: `~/Code/.cargo/config.toml`'s `[patch]` block
541 + # redirects synckit-client to the working copy, so the stale requirement resolves
542 + # on fw13 and fails only where the patch is absent.
543 + #
544 + # WHICH TAGS. One per repo: the tag matching the version its own `bento.toml`
545 + # declares today. Grading every tag ever cut is unbounded and mostly pointless --
546 + # a stale old tag nobody will rebuild is not a defect. A stale tag at the CURRENT
547 + # version is a broken release waiting to happen, and it makes the check answer a
548 + # question worth asking: would a release of this, right now, resolve?
549 + #
550 + # READ FROM THE REPO-SIDE `bento.toml`, not from `~/.config/bento/bento.toml`.
551 + # The daemon's config is machine state and is not in the tree, so a tree-level
552 + # tool cannot see it; the repo-side file is the durable declaration, is versioned
553 + # with the code it describes, and carries both fields this needs.
554 + #
555 + # A version with no tag yet is NOT a finding. It means the release has not been
556 + # cut, and what a release would cut is the branch head, which the views above
557 + # already grade.
558 +
559 +
560 + def bento_configs(tree):
561 + """Every repo-side `bento.toml` under `tree`, shallow-walked.
562 +
563 + One per PRODUCT, not one per repo: MNW is a single .git over pom, wam,
564 + magicmirror and mnw-cli, and each declares its own release with its own
565 + `tag_format`. Walking for the file rather than probing repo roots is what
566 + makes the monorepo's four visible.
567 + """
568 + out = []
569 + stack = [(tree, 0)]
570 + while stack:
571 + d, depth = stack.pop()
572 + try:
573 + entries = list(os.scandir(d))
574 + except OSError:
575 + continue
576 + for e in entries:
577 + if e.is_file() and e.name == "bento.toml":
578 + out.append(e.path)
579 + elif e.is_dir() and e.name not in SKIP_DIRS and depth < MAX_DEPTH:
580 + stack.append((e.path, depth + 1))
581 + return out
582 +
583 +
584 + def bento_version(product, version_path):
585 + """A product's release version, following Bento's own resolution rules.
586 +
587 + Mirrors `AppConfig::version_path` in `bento/daemon/src/topology.rs`: unset
588 + means `src-tauri/tauri.conf.json` and then the root `Cargo.toml`, a `.json`
589 + file is read as a tauri config, and anything else as a `Cargo.toml`. Most
590 + products declare nothing here -- the field lives in the daemon's config, not
591 + the repo's -- so getting the default right is what makes the check cover the
592 + tree rather than the four products that spell it out.
593 + """
594 + candidates = (
595 + [version_path]
596 + if isinstance(version_path, str)
597 + else ["src-tauri/tauri.conf.json", "Cargo.toml"]
598 + )
599 + for rel in candidates:
600 + full = os.path.join(product, rel)
601 + if not os.path.exists(full):
602 + continue
603 + if rel.endswith(".json"):
604 + try:
605 + with open(full, encoding="utf-8") as fh:
606 + v = json.load(fh).get("version")
607 + except (OSError, ValueError):
608 + continue
609 + if isinstance(v, str):
610 + return v
611 + continue
612 + doc = load(full)
613 + pkg = (doc or {}).get("package")
614 + if isinstance(pkg, dict) and isinstance(pkg.get("version"), str):
615 + return pkg["version"]
616 + ws = ((doc or {}).get("workspace") or {}).get("package") or {}
617 + if isinstance(ws.get("version"), str):
618 + return ws["version"]
619 + return None
620 +
621 +
622 + def bento_release_tag(cfg_path):
623 + """The tag a release declared by `cfg_path` would build from today, or None.
624 +
625 + Returns the tag string whether or not it exists; existence is the caller's
626 + question, and a version with no tag is not a finding -- it means the release
627 + has not been cut, and what a release would cut is the branch head, which the
628 + views above already grade. That is also what makes the defaults above safe to
629 + apply: a wrong guess names a tag that does not exist, so it can only ever
630 + lose coverage, never invent a failure.
631 + """
632 + try:
633 + with open(cfg_path, "rb") as fh:
634 + cfg = tomllib.load(fh)
635 + except (OSError, tomllib.TOMLDecodeError):
636 + return None
637 + version = bento_version(os.path.dirname(cfg_path), cfg.get("version_path"))
638 + if not version:
639 + return None
640 + # `v{version}` is Bento's default; a repo holding several products spells it
641 + # per product (`mnw-cli-v{version}`), which is why this is read rather than
642 + # assumed.
643 + fmt = cfg.get("tag_format")
644 + if not isinstance(fmt, str):
645 + fmt = "v{version}"
646 + return fmt.replace("{version}", version)
647 +
648 +
649 + def release_tags(tree, only=None):
650 + """(repo, product, tag) for every declared release whose tag exists, plus notes.
651 +
652 + `only` narrows to the products under one path, which is how a per-repo caller
653 + (the sweep) gets a cell about its own repo instead of the whole tree's.
654 +
655 + Notes carry the products that declare a release and have no tag for the
656 + version they are on. Not findings -- an uncut release is the normal state
657 + between them -- but counted, because "nothing to grade" and "all clear" are
658 + different answers and a check that conflates them is not evidence.
659 + """
660 + pairs, uncut = [], {}
661 + for cfg in sorted(bento_configs(tree)):
662 + product = os.path.dirname(cfg)
663 + if only and not (product == only or product.startswith(only + os.sep)):
664 + continue
665 + repo = repo_of(cfg, tree)
666 + if repo is None:
667 + continue
668 + tag = bento_release_tag(cfg)
669 + if tag is None:
670 + continue
671 + if git_lines(repo, "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"):
672 + pairs.append((repo, product, tag))
673 + else:
674 + uncut[product] = tag
675 + return pairs, uncut
676 +
677 +
678 + def analyze_release_tags(disk_docs, tree, only=None):
679 + """Grade each release tag's own manifests against what siblings have pushed.
680 +
681 + The consumer view is the tagged tree and nothing else: this asks whether THAT
682 + tag resolves, not whether the working copy does, and the working copy is
683 + already three views above. Providers are read at their remote refs by
684 + `analyze_published`, which is the same machinery and the same no-network
685 + happy path.
686 +
687 + Scoped to the PRODUCT's directory inside the tag, not to the whole repo. At
688 + `mnw-cli-v0.1.4` the entire MNW tree exists, and grading all of it would
689 + report pom's requirements as mnw-cli's release problem.
690 + """
691 + pairs, uncut = release_tags(tree, only)
692 + broken, graded, ungraded = [], 0, {}
693 + for repo, product, tag in pairs:
694 + rels = git_manifests(repo, tag)
695 + if rels is None:
696 + ungraded.setdefault(product, f"{tag} is not readable")
697 + continue
698 + prefix = os.path.relpath(product, repo)
699 + prefix = "" if prefix == "." else prefix + os.sep
700 + tagged = {}
701 + for rel in rels:
702 + if prefix and not rel.startswith(prefix):
703 + continue
704 + tagged[os.path.join(repo, rel)] = load_at(repo, tag, rel)
705 + if not tagged:
706 + ungraded.setdefault(product, f"{tag} carries no manifest under {prefix or '.'}")
707 + continue
708 + b, g, u = analyze_published(tagged, disk_docs, tree, None, None)
709 + broken += [(*row, tag) for row in b]
710 + graded += g
711 + ungraded.update(u)
712 + return broken, graded, ungraded, pairs, uncut
713 +
714 +
505 715 # --- the crates.io view -------------------------------------------------------
506 716
507 717 # Cargo's own sparse-index cache. One file per crate, holding the same
@@ -779,13 +989,104 @@
779 989 return True
780 990
781 991
992 + def release_tag_report(tree, as_json, only=None):
993 + """The release-tag view on its own, for the scheduled caller.
994 +
995 + Separate from `main`'s four views and never run at push time. The question is
996 + "would a release of this resolve right now", and the answer changes while the
997 + repo sits still -- which makes it a nightly's question, not a hook's. A hook
998 + that refused a push over a tag cut last week would also be refusing it for
999 + something the push cannot fix.
1000 + """
1001 + docs = {p: load(p) for p in manifests(tree)}
1002 + broken, graded, ungraded, pairs, uncut = analyze_release_tags(docs, tree, only)
1003 +
1004 + if as_json:
1005 + # witchbroom's `json` capture: one finding per requirement the tag cannot
1006 + # resolve, under a key the grid's parser knows.
1007 + print(json.dumps({
1008 + "release_tags": [
1009 + {
1010 + "ok": False,
1011 + "kind": "tag-unresolvable",
1012 + "message": (
1013 + f"{tag}: {os.path.relpath(consumer, tree)} requires "
1014 + f"{name} {req}, and {ref} has {have}"
1015 + ),
1016 + "path": os.path.relpath(consumer, tree),
1017 + }
1018 + for consumer, name, req, have, _provider, ref, tag in broken
1019 + ],
1020 + "tags_graded": [
1021 + {"product": os.path.relpath(prod, tree), "tag": t}
1022 + for _repo, prod, t in pairs
1023 + ],
1024 + # Counted rather than silent, the `coherence` reading exactly: a repo
1025 + # between releases has no tag at its current version, and that is a
1026 + # different answer from a tag that resolves.
1027 + "uncut": [
1028 + {"product": os.path.relpath(prod, tree), "tag": t}
1029 + for prod, t in sorted(uncut.items())
1030 + ],
1031 + "requirements_graded": graded,
1032 + "ungraded": {os.path.relpath(k, tree): v for k, v in ungraded.items()},
1033 + }, indent=2))
1034 + return 1 if broken else 0
1035 +
1036 + for consumer, name, req, have, _provider, ref, tag in broken:
1037 + print(
1038 + f"{tag}: {os.path.relpath(consumer, tree)} requires {name} {req}; "
1039 + f"{ref} has {have}",
1040 + file=sys.stderr,
1041 + )
1042 + if broken:
1043 + print(
1044 + "\nA release cut from one of the tags above cannot resolve. The tag was\n"
1045 + "coherent when it was cut and a sibling has moved since; a tag never\n"
1046 + "moves, so the fix is a new tag at a version whose manifests carry the\n"
1047 + "current requirement, not an edit to this one.",
1048 + file=sys.stderr,
1049 + )
1050 + return 1
1051 + print(
1052 + f"release tags coherent ({len(pairs)} tags, {graded} requirements"
1053 + + (f", {len(uncut)} versions not yet tagged" if uncut else "")
1054 + + ")."
1055 + )
1056 + for where, why in sorted(ungraded.items()):
1057 + print(f"[release tags] {os.path.relpath(where, tree)} not graded ({why}).")
1058 + return 0
1059 +
1060 +
782 1061 def main():
783 - if len(sys.argv) < 2:
1062 + raw = sys.argv[1:]
1063 + args, flags, skip = [], set(), False
1064 + for i, a in enumerate(raw):
1065 + if skip:
1066 + skip = False
1067 + continue
1068 + if a.startswith("--"):
1069 + flags.add(a)
1070 + # The one flag that takes a value. Consuming it here keeps its path
1071 + # out of the positional list, where it would be read as the tree.
1072 + if a == "--only":
1073 + skip = True
1074 + else:
1075 + args.append(a)
1076 + if not args:
784 1077 print(__doc__.strip(), file=sys.stderr)
785 1078 return 2
786 - tree = os.path.realpath(sys.argv[1])
787 - repo = os.path.realpath(sys.argv[2]) if len(sys.argv) > 2 else None
788 - sha = sys.argv[3] if len(sys.argv) > 3 else None
1079 + tree = os.path.realpath(args[0])
1080 + if "--release-tags" in flags:
1081 + # `--only <path>` narrows to one repo or product, for a per-repo caller.
1082 + only = None
1083 + if "--only" in sys.argv:
1084 + i = sys.argv.index("--only")
1085 + if i + 1 < len(sys.argv):
1086 + only = os.path.realpath(sys.argv[i + 1])
1087 + return release_tag_report(tree, "--json" in flags, only)
1088 + repo = os.path.realpath(args[1]) if len(args) > 1 else None
1089 + sha = args[2] if len(args) > 2 else None
789 1090
790 1091 docs = {p: load(p) for p in manifests(tree)}
791 1092