mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 09:59:45 -10:00
Scripts only under scripts/split/. No Rust, no manifest, no budget touched. Base00be3d2e4f. tui strict production [M] 634,761 -> 634,761, file level [F] 943,686 -> 943,686, largest SCC 79 -> 79 (partition_report --non-test: [79, 8, 1, 1, 1]). Nothing was compiled: this is python; the rewrites it produces were checked with rustfmt parse/format, module_graph.py, the path-keyed budget scripts and counts, on a scratch `git init` export of this base (never the shared tree). A compile of a rewritten tree is the first Lane A window. What changed and why (reasoning lives here, not in comments): move-modules.py (rewritten). R2.5: the old tool exited 0 in a tree without .git after "moved 6 module(s)" (git failures were check=False), and its dry-run headline (354 files / 3,487 lines) counted 12 files / 212 lines inside the moved set that a real run never rewrites. - --dest-crate/--src-crate replace every hard-coded runtime literal; --json; --require-clean (dirty set from `git status --porcelain -z` plus files younger than --quiet-minutes, default 15); --apply-cfg (flips only `#[cfg(test)]` items that code outside the moved set uses; never touches cfg(not(test)) forks, which are an FD-04 decision); --restore <journal>. - No git mv (the shared index is not touched): plain file operations. Every write is compare-and-swap (stat+sha against the bytes the plan was computed from, temp file, os.replace) with a pre-image journal under CW/backups/; a change between plan and write refuses before writing (exit 3); a change or SIGTERM/SIGHUP/SIGINT during the write phase restores every pre-image and removes created files and directories (exit 4). git/git grep failures and a missing or foreign .git are fatal (exit 2). --require-clean refusals exit 5. - Rewritten Rust goes through `rustfmt --edition 2024 --emit stdout` on stdin per file; a file whose base text is not rustfmt-stable (the 17 include!d test_cases_*.rs fragments) is left exactly as rewritten, so no foreign line is reflowed. - Coverage: plain and grouped `use crate::{..}` (3 sites), alias, unqualified `<m>::` in the source lib (5 sites; cfg(test)-only use gets a cfg(test) import), `<dest>::` -> `crate::` inside moved files (code only), `codewhale_tui::<m>` in every other Rust file (cli config_keys 15 lines), sweep of path strings (plain, `tui/src/..`, segmented `"tui" / "src" / "<m>"`: this adds scripts/check_orcarouter_contract.py, which the old sweep missed) with vendor/, crates/config/, scripts/split/, CHANGELOG* and web/data/*.generated.json skipped; --derive-facts regenerates web/data (journalled pre-images). - Reports (and plans, never applies): residue edges from moved files to staying modules, super-root residue, manifest needs (deps, features, consumer crates), cfg sites by class. reexports.py (--resolve-reexports): resolves `pub use` facade names (globs through the upstream sources and the build.rs-generated constants via catalog_corrections.json / provider_descriptors.json) and rewrites `crate::config::NAME` and grouped lists to the upstream path. preflight.py (G11): whole-repo include_str!/include_bytes!/include!/#[path] resolution against the old-to-new map, features.toml owners, path strings the sweep cannot fix, `../src/<m>` references, CI test identities; exit 1 on residue. widen.py: --dest-crate/--src-crate/--packages, signal-safe lints restore (SIGTERM/SIGHUP/SIGINT and atexit; a manifest left capped by a killed run is restored first), atomic writes, --prepass static widening, unused_imports removed with the compiler's machine-applicable suggestions. count_lines.py promoted from the plan's scratch (identical [M]) with the [F] counter beside it. testsplit.py: function-level move/stay classifier with call closure. partition_report/closure_report --non-test; module_graph --tui-src/ --runtime-src and a regex-driven lexer (11.0 s -> 2.5 s over 1,585 files, 0 mismatches against the old lex_mask on every tracked .rs). Acceptance (scratch export of00be3d2e4f, config settings config_persistence config_keys oauth codex_model_cache): - dry run: staying callers 342 files / 3,275 lines + inside the moved set 12 / 212 = the old tool's 354 / 3,487; grouped 3; self paths 105 + 5 in comments = 110; other crates 2 files / 16 lines (15 cli + 1 runtime doc comment); sweep 31 files (+15 skipped); 81 cfg sites (53 test items, 11 test mods, 17 not(test) forks = the brief's 17); 9 s. - real rewrite: 403 paths, 18.6 s, counts identical to the dry run line for line; 334 of 351 changed .rs files rustfmt-stable (the 17 others are the base-unstable fragments); no `crate::<moved>` left in tui, no `codewhale_tui::<moved>` in cli; module_graph.py --check refuses this premature move with 43 residue edges (it is the closedness gate working). - closed module (remote_setup) real move: module_graph --check PASS with the unchanged baseline, check-blocking-calls-budget PASS (543 sites, 165 files; the sweep re-keyed the path-keyed JSON), dead-code budget PASS 209, check-provider-registry PASS, G11 unresolved 0. - guards on the scratch repo: change between plan and write -> exit 3 nothing written; change at write 201 of 403 -> exit 4 and `git status` back to only the concurrent edit; SIGTERM at 201 of 403 -> exit 4, status empty; dirty file and a file touched < 15 min -> exit 5; no .git -> exit 2. - widen.py: SIGTERM and SIGHUP while a compile round runs -> exit 130 and crates/runtime/Cargo.toml back to `[lints] workspace = true`; a capped manifest from an earlier run is restored at start; with a fake cargo JSON stream the E0603 widening, --prepass and the unused-import removal applied. - reexports: 226 config names (213 via models) -> 1,109 direct lines in 142 files and 68 grouped lists in 60 files outside the cluster, 166 files; the ProviderKind figure matches the plan exactly (339 lines / 95 files); the plan's 1,189 / 190 and 89 / 75 are not reproduced and likely include the cluster's own files; module_graph --check unchanged afterwards. - preflight on the cluster: residue 8 = lib.rs:20230 include_str, tests/ integration/main.rs:16 (path + relative), 5 skip-listed mentions in crates/config (catalog_corrections.json:4584 and models_dev.bundled.json:11769 key a source receipt `crates/tui/src/config/models.rs`), vendor picker.rs. - testsplit: config/tests.rs 407 tests (378 move, 29 stay), scope_tests.rs 8 (0 move: needs provider_readiness/fleet), whole cluster 687 tests (649 move, 38 stay); count_lines [M]/[F]: tui 634,761/943,686, cli 20,204/33,761, runtime 9,526/15,121, config 29,415/35,830, app-server 7,572/13,095. Not proven: no cargo build of any rewritten tree; widen.py's compile loop ran only against a fake cargo; --apply-cfg flipping is planned, not applied or compiled; Windows/FreeBSD cfg code; whether any test reads the source-receipt keys in crates/config assets as paths; the re-export resolver does not rewrite relative `config::NAME` uses (none found at this base); SCC/edge counts were not re-run after a rewrite (the cargo-free G2 stays Lane A's). Evidence: /Volumes/VIXinSSD/CW/artifacts/crate-split-w0-20261025/proof-w0.log and guard-demos.log. Dirty work of other writers in the checkout was left untouched (about 20 tui/core/models files, window W1). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
141 lines
4.4 KiB
Python
141 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Per-crate dependency-closure size report for the crate split.
|
|
|
|
For every workspace crate: its own Rust lines, the lines in its workspace
|
|
dependency closure (what an agent sandbox must hold to build it), and the
|
|
lines in its reverse closure (what a change to it can rebuild).
|
|
|
|
python3 scripts/split/closure_report.py
|
|
python3 scripts/split/closure_report.py --json
|
|
python3 scripts/split/closure_report.py --sort closure --top 15
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_spec = importlib.util.spec_from_file_location("count_lines", Path(__file__).with_name("count_lines.py"))
|
|
cl = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(cl)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def metadata() -> dict:
|
|
out = subprocess.run(
|
|
["cargo", "metadata", "--format-version", "1", "--no-deps", "--offline", "--locked"],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if out.returncode != 0:
|
|
sys.exit(out.stderr.strip() or "cargo metadata failed")
|
|
return json.loads(out.stdout)
|
|
|
|
|
|
def rust_lines(root: Path, tests: bool) -> int:
|
|
total = 0
|
|
for path in root.rglob("*.rs"):
|
|
rel = path.relative_to(root).parts
|
|
is_test = (
|
|
"tests" in rel
|
|
or path.name in ("tests.rs", "test_support.rs")
|
|
or path.name.endswith("_tests.rs")
|
|
or path.name.startswith("test_cases_")
|
|
)
|
|
if is_test and not tests:
|
|
continue
|
|
try:
|
|
total += sum(1 for _ in path.open("rb"))
|
|
except OSError:
|
|
pass
|
|
return total
|
|
|
|
|
|
def closure(graph: dict[str, set[str]], start: str) -> set[str]:
|
|
seen: set[str] = set()
|
|
stack = [start]
|
|
while stack:
|
|
node = stack.pop()
|
|
for nxt in graph.get(node, ()):
|
|
if nxt not in seen:
|
|
seen.add(nxt)
|
|
stack.append(nxt)
|
|
return seen
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--json", action="store_true")
|
|
parser.add_argument("--sort", choices=["name", "own", "closure", "reverse"], default="closure")
|
|
parser.add_argument("--top", type=int, default=0)
|
|
parser.add_argument("--tests", action="store_true", help="count test files too")
|
|
parser.add_argument(
|
|
"--non-test",
|
|
action="store_true",
|
|
help="size crates by strict production lines (inline cfg(test) code excluded) instead of file-level lines",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
meta = metadata()
|
|
packages = {p["name"]: p for p in meta["packages"] if p["id"] in set(meta["workspace_members"])}
|
|
forward: dict[str, set[str]] = {}
|
|
reverse: dict[str, set[str]] = {name: set() for name in packages}
|
|
for name, pkg in packages.items():
|
|
deps = {
|
|
d["name"]
|
|
for d in pkg["dependencies"]
|
|
if d["name"] in packages and d.get("kind") != "dev"
|
|
}
|
|
forward[name] = deps
|
|
for dep in deps:
|
|
reverse[dep].add(name)
|
|
|
|
own = {}
|
|
for name, pkg in packages.items():
|
|
crate_dir = Path(pkg["manifest_path"]).parent
|
|
src = crate_dir / "src"
|
|
if args.non_test:
|
|
_, res = cl.classify_crate(str(crate_dir))
|
|
own[name] = sum(prod for (prod, _t, _o) in res.values())
|
|
else:
|
|
own[name] = rust_lines(src, args.tests) if src.is_dir() else 0
|
|
|
|
rows = []
|
|
for name in packages:
|
|
down = closure(forward, name)
|
|
up = closure(reverse, name)
|
|
rows.append(
|
|
{
|
|
"crate": name,
|
|
"own": own[name],
|
|
"closure": own[name] + sum(own[d] for d in down),
|
|
"closure_crates": len(down),
|
|
"reverse": sum(own[u] for u in up),
|
|
"reverse_crates": len(up),
|
|
}
|
|
)
|
|
rows.sort(key=lambda r: r["crate"] if args.sort == "name" else -r[args.sort])
|
|
if args.top:
|
|
rows = rows[: args.top]
|
|
|
|
if args.json:
|
|
json.dump(rows, sys.stdout, indent=2)
|
|
print()
|
|
return
|
|
print(f"{'crate':32} {'own':>9} {'closure':>10} {'deps':>5} {'reverse':>10} {'users':>6}")
|
|
for r in rows:
|
|
print(
|
|
f"{r['crate']:32} {r['own']:>9} {r['closure']:>10} {r['closure_crates']:>5} "
|
|
f"{r['reverse']:>10} {r['reverse_crates']:>6}"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|