scripts.tools_import_closure
Reference › scripts › tools_import_closure
scripts/tools_import_closure.py computes the static import closure of the tools-deploy entry modules over src/: the set of modules the deployed tools tree needs, and the third-party packages those modules pull in. The deploy tooling (see vendor_members_hook for the packaging side) and a human use it to decide what to vendor. It has no internal imports, because it reads the source with ast rather than importing it.
build_closure returns a walker; compute(entries) follows imports from the entry modules, treating imports under a TYPE_CHECKING guard as non-runtime (_is_type_checking) and resolving relative names (_names, _top_package). With --eager only module-level imports count, so lazy function-level imports are excluded. The entries come from --entries, defaulting to DEPLOYED_ENTRIES. Output modes, one per run: --json, --manifest (vendor manifest lines), --third-party (third-party names), --pip (the distributions the virtual environment needs to import the deployed tree, which uses the eager closure plus the entry scripts) and --check, which prints "closure OK" or lists every unresolved import on standard error and exits 1, naming a vendoring gap. --src overrides the source root.
At a glance
| Source | scripts/tools_import_closure.py |
| Lines of code | 349 |
| Classes | 6 |
| Functions | 5 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
Dependency diagram
No internal imports and no importers.
API
Constants
| Name | Type | Value |
|---|---|---|
OURS |
tuple[str, ...] |
('orchestrata_ultimate', 'orchestrata_ast', 'orchestrata_tools', 'scripts') |
DEPLOYED_ENTRIES |
tuple[str, ...] |
('orchestrata_ultimate.refactor.codemod._daemon_entry', 'orchestrata_ultimate.r… |
SCRIPT_ENTRIES |
tuple[str, ...] |
('scripts.swarm_supervisor', 'scripts.observe_pytest', 'scripts.ast_live_sync',… |
LOCAL_NATIVES |
frozenset[str] |
frozenset({'codemod_fast', 'ctok'}) |
PROJECT_PROVIDED |
frozenset[str] |
frozenset({'pytest', '_pytest'}) |
DIST_NAMES |
Mapping[str, str] |
{'yaml': 'pyyaml', 'pydantic_settings': 'pydantic-settings', 'prometheus_client… |
Classes
ImportCollector (protocol)
Bases: Protocol · Line 85
Strategy: which import statements of a parsed module count.
| Method | Signature | Summary |
|---|---|---|
imports_of |
imports_of(tree: ast.Module) -> Iterator[str] |
Yield absolute dotted names; from m import n yields m and m.n. |
_Candidate
Bases: str · Line 93
from m import n yields m.n: a module only if one exists, else an attribute of m.
AllScopesImportCollector
Line 116
Every import anywhere in the module -- function bodies and TYPE_CHECKING too.
| Method | Signature | Summary |
|---|---|---|
imports_of |
imports_of(tree: ast.Module) -> Iterator[str] |
EagerImportCollector
Line 128
Only what import executes at runtime: module/class level, outside if TYPE_CHECKING: and outside function bodies.
| Method | Signature | Summary |
|---|---|---|
imports_of |
imports_of(tree: ast.Module) -> Iterator[str] |
|
_block |
_block(body: Sequence[ast.stmt]) -> Iterator[str] |
ClosureReport (dataclass)
Decorators: dataclass(frozen=True) · Line 153
The closure: modules, per-package counts, third-party names, unresolved names.
| Attribute | Type | Default |
|---|---|---|
modules |
frozenset[str] |
|
top_packages |
Mapping[str, int] |
|
third_party |
frozenset[str] |
|
unresolved |
frozenset[str] |
|
vendor_roots |
frozenset[str] |
frozenset() |
| Method | Signature | Summary |
|---|---|---|
vendor_manifest |
vendor_manifest() -> list[str] |
Sorted src/... paths (subtree dirs or single files) to archive. |
pip_requirements |
pip_requirements() -> list[str] |
Distributions to uv pip install: third-party names minus the local natives (built separately) and the project-provided pytest. |
as_json |
as_json() -> dict[str, object] |
ImportClosure
Line 191
Walks entries over src_root with an injected :class:ImportCollector.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(src_root: Path, collector: ImportCollector, extra_roots: Sequence[Path] = ()) -> None |
|
_locate |
_locate(name: str) -> Path | None |
Module file for name; a directory with no __init__.py is NOT a module. |
_is_dir |
_is_dir(name: str) -> bool |
|
compute |
compute(entries: Sequence[str]) -> ClosureReport |
|
_unresolved |
_unresolved(seen: Mapping[str, Path | None], hard: set[str]) -> Iterator[str] |
Names with no module file that had to be one. |
_vendor_roots |
_vendor_roots(seen: Mapping[str, Path | None]) -> Iterator[str] |
src/ subtrees to archive; modules outside src/ (scripts) have none. |
Functions
| Function | Signature | Summary |
|---|---|---|
_names |
_names(node: ast.Import | ast.ImportFrom) -> Iterator[str] |
Absolute names an import statement can bind (relative imports are banned). |
_is_type_checking |
_is_type_checking(test: ast.expr) -> bool |
True for TYPE_CHECKING / typing.TYPE_CHECKING. |
_top_package |
_top_package(module: str) -> str |
|
build_closure |
build_closure(src_root: Path, *, eager: bool) -> ImportClosure |
The closure walker with the collector strategy --eager selects. |
main |
main(argv: Sequence[str] | None = None) -> int |
Source
#!/usr/bin/env python3
"""Static import closure of the tools-deploy entry modules over ``src/``.
``deploy/install-tools.sh`` used to vendor a hand-written list of four subtrees.
Measured 2026-10-08: the codemod daemon imports in ONE eager module, but its first
default review (``InMemoryReviewEngine(registry=None)`` -> ``review_checks``) pulls
103 eager modules across six packages (core 40, refactor 34, verification 14,
openspec 7, knowledge_graph 2, orchestrata_ast 5) -- ``verification`` and
``openspec`` were never vendored, so the first ship of 3bb5319df failed with
``ModuleNotFoundError: orchestrata_ultimate.verification``. Deferring that import
hid the gap instead of closing it. This script COMPUTES what must be vendored and
which third-party distributions the deployed venv needs, so neither list is guessed.
STDLIB ONLY: it runs inside the installer, where the repo venv is not guaranteed.
Task 580 / plans/recorded-run-2026-10-08-eight-failures-design.md section 3.2.
Usage::
tools_import_closure.py [--entries MOD ...] [--eager] [--json] [--src DIR]
tools_import_closure.py --manifest # vendor manifest lines (one per line)
tools_import_closure.py --third-party # third-party import names (one per line)
"""
from __future__ import annotations
import argparse
import ast
import json
import sys
from collections.abc import Iterator, Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
OURS: tuple[str, ...] = (
"orchestrata_ultimate",
"orchestrata_ast",
"orchestrata_tools",
"scripts",
)
ROOT_PACKAGE = "orchestrata_ultimate"
# Every module the deployed tree executes: the codemod daemon, the review checks its
# first default review loads lazily (review_engine.py), and the pytest observer pair
# plus the KG syncer that ast_live_sync imports. Measured 2026-10-08 on this tree.
DEPLOYED_ENTRIES: tuple[str, ...] = (
"orchestrata_ultimate.refactor.codemod._daemon_entry",
"orchestrata_ultimate.refactor.codemod.review_checks",
"orchestrata_ultimate.core.threading.pytest_live_reader",
"orchestrata_ultimate.core.threading.pytest_live_observer",
"orchestrata_ultimate.knowledge_graph.sync",
)
# The deployed entry scripts (install-tools.sh archive list). They live outside
# src/, so they feed only the third-party computation (``--pip``), never the vendor
# manifest.
SCRIPT_ENTRIES: tuple[str, ...] = (
"scripts.swarm_supervisor",
"scripts.observe_pytest",
"scripts.ast_live_sync",
"scripts.ast_daemon",
"scripts.ast_api",
"scripts.serialize_codebase_ast",
"scripts.build_runtime_callgraph",
)
# Third-party imports built from the staged sources by their own installer step
# (not installed from an index), and import-name -> distribution-name spellings.
LOCAL_NATIVES: frozenset[str] = frozenset({"codemod_fast", "ctok"})
# pytest is the TARGET project's own: `pytest-live` execs it, and the deployed venv
# ships no pytest BY DESIGN (install-tools.sh proves the reader runs without it).
PROJECT_PROVIDED: frozenset[str] = frozenset({"pytest", "_pytest"})
DIST_NAMES: Mapping[str, str] = {
"yaml": "pyyaml",
"pydantic_settings": "pydantic-settings",
"prometheus_client": "prometheus-client",
"janus_swi": "janus-swi",
}
REPO_ROOT = Path(__file__).resolve().parent.parent
REPO_SRC = REPO_ROOT / "src"
class ImportCollector(Protocol):
"""Strategy: which import statements of a parsed module count."""
def imports_of(self, tree: ast.Module) -> Iterator[str]:
"""Yield absolute dotted names; ``from m import n`` yields ``m`` and ``m.n``."""
...
class _Candidate(str):
"""``from m import n`` yields ``m.n``: a module only if one exists, else an
attribute of ``m``. Marked so a missing one is not reported as unresolved."""
def _names(node: ast.Import | ast.ImportFrom) -> Iterator[str]:
"""Absolute names an import statement can bind (relative imports are banned)."""
if isinstance(node, ast.Import):
for alias in node.names:
yield alias.name
elif node.module and node.level == 0:
yield node.module
for alias in node.names:
yield _Candidate(f"{node.module}.{alias.name}")
def _is_type_checking(test: ast.expr) -> bool:
"""True for ``TYPE_CHECKING`` / ``typing.TYPE_CHECKING``."""
if isinstance(test, ast.Name):
return test.id == "TYPE_CHECKING"
return isinstance(test, ast.Attribute) and test.attr == "TYPE_CHECKING"
class AllScopesImportCollector:
"""Every import anywhere in the module -- function bodies and TYPE_CHECKING too.
What a long-lived daemon can execute eventually, so it decides what is vendored.
"""
def imports_of(self, tree: ast.Module) -> Iterator[str]:
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
yield from _names(node)
class EagerImportCollector:
"""Only what ``import`` executes at runtime: module/class level, outside
``if TYPE_CHECKING:`` and outside function bodies."""
def imports_of(self, tree: ast.Module) -> Iterator[str]:
yield from self._block(tree.body)
def _block(self, body: Sequence[ast.stmt]) -> Iterator[str]:
for node in body:
if isinstance(node, (ast.Import, ast.ImportFrom)):
yield from _names(node)
elif isinstance(node, ast.If):
if not _is_type_checking(node.test):
yield from self._block(node.body)
yield from self._block(node.orelse)
elif isinstance(node, ast.Try):
for part in (node.body, node.orelse, node.finalbody):
yield from self._block(part)
for handler in node.handlers:
yield from self._block(handler.body)
elif isinstance(node, (ast.With, ast.ClassDef)):
yield from self._block(node.body)
@dataclass(frozen=True)
class ClosureReport:
"""The closure: modules, per-package counts, third-party names, unresolved names."""
modules: frozenset[str]
top_packages: Mapping[str, int]
third_party: frozenset[str]
unresolved: frozenset[str]
vendor_roots: frozenset[str] = frozenset()
def vendor_manifest(self) -> list[str]:
"""Sorted ``src/...`` paths (subtree dirs or single files) to archive."""
return sorted(self.vendor_roots)
def pip_requirements(self) -> list[str]:
"""Distributions to ``uv pip install``: third-party names minus the local
natives (built separately) and the project-provided pytest."""
names = self.third_party - LOCAL_NATIVES - PROJECT_PROVIDED
return sorted(DIST_NAMES.get(n, n) for n in names)
def as_json(self) -> dict[str, object]:
return {
"module_count": len(self.modules),
"modules": sorted(self.modules),
"top_packages": dict(sorted(self.top_packages.items())),
"third_party": sorted(self.third_party),
"pip_requirements": self.pip_requirements(),
"unresolved": sorted(self.unresolved),
"vendor_manifest": self.vendor_manifest(),
}
def _top_package(module: str) -> str:
parts = module.split(".")
return (
".".join(parts[:2]) if parts[0] == ROOT_PACKAGE and len(parts) > 1 else parts[0]
)
class ImportClosure:
"""Walks ``entries`` over ``src_root`` with an injected :class:`ImportCollector`."""
def __init__(
self,
src_root: Path,
collector: ImportCollector,
extra_roots: Sequence[Path] = (),
) -> None:
self._src_root = Path(src_root)
self._collector = collector
# ``scripts.*`` entries resolve under the repo root, not under src/.
self._roots = (self._src_root, *map(Path, extra_roots))
def _locate(self, name: str) -> Path | None:
"""Module file for ``name``; a directory with no ``__init__.py`` is NOT a module."""
rel = Path(*name.split("."))
for root in self._roots:
for cand in (root / rel.with_suffix(".py"), root / rel / "__init__.py"):
if cand.exists():
return cand
return None
def _is_dir(self, name: str) -> bool:
rel = Path(*name.split("."))
return any((root / rel).is_dir() for root in self._roots)
def compute(self, entries: Sequence[str]) -> ClosureReport:
seen: dict[str, Path | None] = {}
third: set[str] = set()
hard: set[str] = set(entries) # names that MUST be modules, not attributes
stack = list(entries)
while stack:
name = stack.pop()
if name in seen:
continue
path = self._locate(name)
seen[name] = path
if path is None:
continue
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for dep in self._collector.imports_of(tree):
top = dep.split(".")[0]
if top in OURS:
parts = dep.split(".")
# importing a.b.c executes a and a.b first
chain = [".".join(parts[:i]) for i in range(1, len(parts) + 1)]
if isinstance(dep, _Candidate):
chain = [str(dep)] # its prefixes came with ``from m import``
else:
hard.update(chain)
stack.extend(chain)
elif top not in sys.stdlib_module_names:
third.add(top)
modules = frozenset(m for m, p in seen.items() if p is not None)
counts: dict[str, int] = {}
for module in modules:
top = _top_package(module)
counts[top] = counts.get(top, 0) + 1
return ClosureReport(
modules=modules,
top_packages=counts,
third_party=frozenset(third),
unresolved=frozenset(self._unresolved(seen, hard)),
vendor_roots=frozenset(self._vendor_roots(seen)),
)
def _unresolved(
self, seen: Mapping[str, Path | None], hard: set[str]
) -> Iterator[str]:
"""Names with no module file that had to be one.
``from pkg import Name`` yields the candidate ``pkg.Name``, legitimately not a
module when ``Name`` is an attribute, so a candidate is skipped -- EXCEPT a
directory with no ``__init__.py`` (an accidental namespace package), which is
always reported.
"""
for name, path in seen.items():
if path is None and (name in hard or self._is_dir(name)):
yield name
def _vendor_roots(self, seen: Mapping[str, Path | None]) -> Iterator[str]:
"""``src/`` subtrees to archive; modules outside ``src/`` (scripts) have none."""
for module, path in seen.items():
if path is None or not path.is_relative_to(self._src_root):
continue
parts = module.split(".")
if parts[0] == ROOT_PACKAGE and len(parts) == 1:
yield f"src/{ROOT_PACKAGE}/__init__.py"
elif parts[0] == ROOT_PACKAGE:
two = self._src_root / ROOT_PACKAGE / parts[1]
suffix = "" if two.is_dir() else ".py"
yield f"src/{ROOT_PACKAGE}/{parts[1]}{suffix}"
else:
yield f"src/{parts[0]}"
def build_closure(src_root: Path, *, eager: bool) -> ImportClosure:
"""The closure walker with the collector strategy ``--eager`` selects.
``scripts.*`` entries resolve against the directory above ``src_root``.
"""
collector: ImportCollector = (
EagerImportCollector() if eager else AllScopesImportCollector()
)
return ImportClosure(src_root, collector, extra_roots=(Path(src_root).parent,))
def main(argv: Sequence[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=(__doc__ or "").split("\n\n")[0])
ap.add_argument("--entries", nargs="+", default=None)
ap.add_argument("--eager", action="store_true", help="module-level imports only")
ap.add_argument("--json", action="store_true")
ap.add_argument(
"--manifest", action="store_true", help="print vendor manifest lines"
)
ap.add_argument(
"--third-party", action="store_true", help="print third-party names"
)
ap.add_argument(
"--pip", action="store_true", help="print pip distributions to install"
)
ap.add_argument(
"--check",
action="store_true",
help="exit 1 naming every unresolved import (a vendoring gap)",
)
ap.add_argument("--src", type=Path, default=REPO_SRC)
args = ap.parse_args(argv)
# --pip answers "what must the venv hold to IMPORT the deployed tree": the eager
# closure of the entries plus the entry scripts. Everything else is lazy.
entries = args.entries or list(DEPLOYED_ENTRIES)
if args.pip and not args.entries:
entries = [*DEPLOYED_ENTRIES, *SCRIPT_ENTRIES]
report = build_closure(args.src, eager=args.eager or args.pip).compute(entries)
if args.check:
if report.unresolved:
print("unresolved:", *sorted(report.unresolved), file=sys.stderr)
return 1
print(f"closure OK: {len(report.modules)} modules, nothing unresolved")
elif args.manifest:
print("\n".join(report.vendor_manifest()))
elif args.pip:
print("\n".join(report.pip_requirements()))
elif args.third_party:
print("\n".join(sorted(report.third_party)))
elif args.json:
print(json.dumps(report.as_json(), indent=2))
else:
print(f"modules in closure: {len(report.modules)}")
for top, n in sorted(report.top_packages.items()):
print(f" {top:55s} {n}")
print(f"third-party: {sorted(report.third_party)}")
print(f"unresolved: {sorted(report.unresolved)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())