mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-10 01:51:32 -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]>
410 lines
14 KiB
Python
410 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Production/test line counter for the crate split (read-only).
|
|
|
|
python3 scripts/split/count_lines.py [crates_dir] [--json out.json] [--crate tui]
|
|
|
|
Two counters are printed for every crate and gated side by side (FD-07):
|
|
|
|
* [M] strict production: physical lines of production code, test files and
|
|
`cfg(test)`-only items excluded;
|
|
* [F] file level: physical lines of every non-test file, inline `cfg(test)`
|
|
modules included (the design document's method; [F] = [M] + inline test).
|
|
|
|
A file is TEST when any of:
|
|
A. a path component is `tests`
|
|
B. the basename contains `tests` (tests.rs, foo_tests.rs, tests_foo.rs)
|
|
C. it is declared by a `#[cfg(test)]`-gated `mod x;` (resolved to x.rs or
|
|
x/mod.rs next to the declaring module, recursively everything below)
|
|
D. it carries an inner `#![cfg(test)]` attribute
|
|
E. a string-literal `include!` from test code pulls it in
|
|
Inside non-test files, an item annotated with a test-only cfg
|
|
(`#[cfg(test)]`, `#[cfg(all(test, ..))]`, `#[cfg(any(test, ..))]`,
|
|
`#[cfg(feature = "test-support")]`) is TEST up to the end of its braces/semicolon.
|
|
`benches/`, `examples/` and `build.rs` are counted as OTHER (neither).
|
|
Lines are physical lines (newline count). Accuracy on tui is about +/-2%.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
TOKEN = re.compile(
|
|
r"""//[^\n]*"""
|
|
r"""|/\*.*?\*/"""
|
|
r"""|b?r(?P<h>\#*)".*?"(?P=h)"""
|
|
r"""|b?"(?:\\.|[^"\\])*\""""
|
|
r"""|'(?:\\.[^']*|[^\\'])'""",
|
|
re.S,
|
|
)
|
|
|
|
|
|
def blank(s: str) -> str:
|
|
return re.sub(r"[^\n]", " ", s)
|
|
|
|
|
|
def mask(src: str):
|
|
def comments_only(m):
|
|
t = m.group(0)
|
|
if t.startswith("//") or t.startswith("/*"):
|
|
return blank(t)
|
|
return t
|
|
|
|
def everything(m):
|
|
t = m.group(0)
|
|
if t.startswith("//") or t.startswith("/*"):
|
|
return blank(t)
|
|
return t[0] + blank(t[1:-1]) + t[-1] if len(t) >= 2 else t
|
|
|
|
return TOKEN.sub(comments_only, src), TOKEN.sub(everything, src)
|
|
|
|
|
|
CFG_OPEN = re.compile(r"#\s*\[\s*cfg\s*\(")
|
|
CFG_TOK = re.compile(r'\s*(?:(?P<id>[A-Za-z_][A-Za-z0-9_]*)|(?P<str>"[^"]*")|(?P<p>[(),=]))')
|
|
|
|
|
|
def cfg_possible(expr: str):
|
|
toks = [m for m in CFG_TOK.finditer(expr)]
|
|
pos = [0]
|
|
|
|
def peek():
|
|
return toks[pos[0]] if pos[0] < len(toks) else None
|
|
|
|
def parse():
|
|
t = toks[pos[0]]
|
|
pos[0] += 1
|
|
name = t.group("id")
|
|
n = peek()
|
|
if n is not None and n.group("p") == "(":
|
|
pos[0] += 1
|
|
kids = []
|
|
while True:
|
|
n = peek()
|
|
if n is None:
|
|
break
|
|
if n.group("p") == ")":
|
|
pos[0] += 1
|
|
break
|
|
if n.group("p") == ",":
|
|
pos[0] += 1
|
|
continue
|
|
kids.append(parse())
|
|
if name == "not":
|
|
return {not v for v in kids[0]} if kids else {True, False}
|
|
if name == "all":
|
|
r = set()
|
|
if all(True in k for k in kids):
|
|
r.add(True)
|
|
if any(False in k for k in kids):
|
|
r.add(False)
|
|
return r or {False}
|
|
if name == "any":
|
|
r = set()
|
|
if any(True in k for k in kids):
|
|
r.add(True)
|
|
if all(False in k for k in kids):
|
|
r.add(False)
|
|
return r or {False}
|
|
return {True, False}
|
|
if n is not None and n.group("p") == "=":
|
|
pos[0] += 1
|
|
v = peek()
|
|
pos[0] += 1
|
|
if name == "feature" and v is not None and v.group("str") == '"test-support"':
|
|
return {False}
|
|
return {True, False}
|
|
if name == "test":
|
|
return {False}
|
|
return {True, False}
|
|
|
|
try:
|
|
return parse()
|
|
except Exception:
|
|
return {True, False}
|
|
|
|
|
|
INCLUDE = re.compile(r'\binclude!\s*\(\s*"([^"]+)"\s*\)')
|
|
PATH_ATTR = re.compile(r'#\s*\[\s*path\s*=\s*"([^"]+)"\s*\]')
|
|
INNER_CFG = re.compile(r"#\s*!\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]")
|
|
ATTR_CLOSE = re.compile(r"\s*\]")
|
|
ATTR = re.compile(r"\s*#\s*\[[^\]]*\]")
|
|
MODDECL = re.compile(r"\s*(?:pub(?:\([^)]*\))?\s+)?mod\s+(\w+)\s*;")
|
|
BR = re.compile(r"[{}]")
|
|
|
|
|
|
ITEM_KW = re.compile(
|
|
r"(?:pub(?:\s*\([^)]*\))?\s+)?(?:async\s+|unsafe\s+|const\s+|extern\s+(?:\"[^\"]*\"\s+)?)*"
|
|
r"(?:mod|fn|impl|use|struct|enum|trait|type|static|const|macro_rules|extern|let|union)\b"
|
|
)
|
|
FALLBACK = []
|
|
|
|
|
|
def item_end(m2: str, j: int) -> int:
|
|
depth = 0
|
|
n = len(m2)
|
|
k = j
|
|
comma_ends = not ITEM_KW.match(m2, j) and not m2.startswith("{", j)
|
|
if comma_ends:
|
|
FALLBACK.append(j)
|
|
while k < n:
|
|
c = m2[k]
|
|
if c in "([":
|
|
depth += 1
|
|
elif c in ")]":
|
|
depth -= 1
|
|
elif depth <= 0 and c == ";":
|
|
return k
|
|
elif depth <= 0 and comma_ends and c == ",":
|
|
return k
|
|
elif depth <= 0 and c == "{":
|
|
d = 0
|
|
for b in BR.finditer(m2, k):
|
|
if b.group(0) == "{":
|
|
d += 1
|
|
else:
|
|
d -= 1
|
|
if d == 0:
|
|
return b.start()
|
|
return n - 1
|
|
k += 1
|
|
return n - 1
|
|
|
|
|
|
def analyse_file(path: str, src: str):
|
|
"""returns (test_ranges[(a,b) char offsets], cfg_test_mod_names, whole_file_test)"""
|
|
m1, m2 = mask(src)
|
|
if INNER_CFG.search(m2) or any(
|
|
INNER_CFG.match(m1, mm.start()) for mm in re.finditer(r"#\s*!", m2)
|
|
):
|
|
return [], [], True, [(m.start(), m.group(1)) for m in INCLUDE.finditer(m1) if m2[m.start()] == "i"]
|
|
ranges = []
|
|
mods = []
|
|
covered = -1
|
|
spans = []
|
|
for m in CFG_OPEN.finditer(m1):
|
|
if m2[m.start()] != "#":
|
|
continue
|
|
depth = 1
|
|
k = m.end()
|
|
while k < len(m1) and depth:
|
|
if m1[k] == "(":
|
|
depth += 1
|
|
elif m1[k] == ")":
|
|
depth -= 1
|
|
k += 1
|
|
close = ATTR_CLOSE.match(m1, k)
|
|
if not close:
|
|
continue
|
|
expr = m1[m.end():k - 1]
|
|
if not re.search(r"\btest\b|test-support", expr):
|
|
continue
|
|
if True in cfg_possible(expr):
|
|
continue
|
|
spans.append((m.start(), close.end()))
|
|
for ms, me in spans:
|
|
if ms <= covered:
|
|
continue
|
|
j = me
|
|
path_attr = None
|
|
while True:
|
|
a = ATTR.match(m2, j)
|
|
if not a:
|
|
break
|
|
pm = PATH_ATTR.match(m1, j + (len(m1[j:]) - len(m1[j:].lstrip())))
|
|
if pm:
|
|
path_attr = pm.group(1)
|
|
j = a.end()
|
|
md = MODDECL.match(m2, j)
|
|
if md:
|
|
mods.append(("path:" + path_attr) if path_attr else md.group(1))
|
|
end = md.end()
|
|
else:
|
|
while j < len(m2) and m2[j].isspace():
|
|
j += 1
|
|
end = item_end(m2, j)
|
|
ranges.append((ms, end))
|
|
covered = end
|
|
incs = [(m.start(), m.group(1)) for m in INCLUDE.finditer(m1) if m2[m.start()] == "i"]
|
|
return ranges, mods, False, incs
|
|
|
|
|
|
def read(path):
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
return f.read()
|
|
|
|
|
|
def collect(crate_dir):
|
|
files = []
|
|
for dp, dn, fn in os.walk(crate_dir):
|
|
dn[:] = [d for d in dn if d not in ("target", "node_modules", ".git")]
|
|
for f in fn:
|
|
if f.endswith(".rs"):
|
|
files.append(os.path.join(dp, f))
|
|
return sorted(files)
|
|
|
|
|
|
def classify_crate(crate_dir, tui_style=False):
|
|
files = collect(crate_dir)
|
|
info = {}
|
|
for p in files:
|
|
src = read(p)
|
|
rel = os.path.relpath(p, crate_dir)
|
|
info[p] = dict(rel=rel, src=src, nl=src.count("\n") + (0 if src.endswith("\n") or not src else 1))
|
|
test_files = set()
|
|
other_files = set()
|
|
cfg_mods = {}
|
|
for p, d in info.items():
|
|
rel = d["rel"]
|
|
parts = rel.split("/")
|
|
base = parts[-1]
|
|
if parts[0] in ("benches", "examples") or rel == "build.rs":
|
|
other_files.add(p)
|
|
continue
|
|
if "tests" in parts[:-1] or "tests" in base:
|
|
test_files.add(p)
|
|
for p, d in info.items():
|
|
if p in other_files:
|
|
continue
|
|
ranges, mods, whole, incs = analyse_file(p, d["src"])
|
|
d["ranges"] = ranges
|
|
d["incs"] = incs
|
|
if whole:
|
|
test_files.add(p)
|
|
base = os.path.basename(p)
|
|
stem = base[:-3]
|
|
mdir = os.path.dirname(p) if base in ("mod.rs", "lib.rs", "main.rs") else os.path.join(os.path.dirname(p), stem)
|
|
for name in mods:
|
|
if name.startswith("path:"):
|
|
cands = (os.path.normpath(os.path.join(os.path.dirname(p), name[5:])),)
|
|
else:
|
|
cands = (os.path.join(mdir, name + ".rs"), os.path.join(mdir, name, "mod.rs"))
|
|
for cand in cands:
|
|
if cand in info:
|
|
test_files.add(cand)
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for p, d in info.items():
|
|
for off, rel in d.get("incs", []):
|
|
tgt = os.path.normpath(os.path.join(os.path.dirname(p), rel))
|
|
if tgt not in info or tgt in test_files:
|
|
continue
|
|
if p in test_files or any(a <= off <= b for a, b in d.get("ranges", [])):
|
|
test_files.add(tgt)
|
|
changed = True
|
|
for p in list(test_files):
|
|
base = os.path.basename(p)
|
|
stem = base[:-3]
|
|
if base == "mod.rs":
|
|
sub = os.path.dirname(p) + os.sep
|
|
else:
|
|
sub = os.path.join(os.path.dirname(p), stem) + os.sep
|
|
for q in info:
|
|
if q.startswith(sub) and q not in test_files and q not in other_files:
|
|
test_files.add(q)
|
|
changed = True
|
|
res = {}
|
|
for p, d in info.items():
|
|
nl = d["nl"]
|
|
if p in other_files:
|
|
res[p] = (0, 0, nl)
|
|
elif p in test_files:
|
|
res[p] = (0, nl, 0)
|
|
else:
|
|
src = d["src"]
|
|
tl = 0
|
|
for a, b in d.get("ranges", []):
|
|
tl += src.count("\n", a, b) + 1
|
|
tl = min(tl, nl)
|
|
res[p] = (nl - tl, tl, 0)
|
|
return info, res
|
|
|
|
|
|
def summarise(crates_dir):
|
|
out = {}
|
|
tui_detail = None
|
|
for c in sorted(os.listdir(crates_dir)):
|
|
cd = os.path.join(crates_dir, c)
|
|
if not os.path.isfile(os.path.join(cd, "Cargo.toml")):
|
|
continue
|
|
info, res = classify_crate(cd)
|
|
prod = sum(v[0] for v in res.values())
|
|
test = sum(v[1] for v in res.values())
|
|
other = sum(v[2] for v in res.values())
|
|
inline_test = sum(tl for (pl, tl, ol) in res.values() if pl or ol)
|
|
big = []
|
|
for p, d in info.items():
|
|
if res[p][0] or (res[p][1] == 0 and res[p][2] == 0):
|
|
for a, b in d.get("ranges", []):
|
|
big.append((d["src"].count("\n", a, b) + 1, d["rel"], " ".join(d["src"][a:b + 1].split())[:90]))
|
|
big.sort(reverse=True)
|
|
out[c] = dict(production_lines=prod, file_level_lines=prod + inline_test, test_lines=test,
|
|
other_lines=other, inline_cfg_test_lines=inline_test, files=len(res),
|
|
inline_ranges=len(big), inline_top=big[:6])
|
|
if c == "tui":
|
|
tui_detail = (cd, info, res)
|
|
tui_mods = {}
|
|
if tui_detail:
|
|
cd, info, res = tui_detail
|
|
src_root = os.path.join(cd, "src") + os.sep
|
|
mods = defaultdict(lambda: dict(production_lines=0, file_level_lines=0, test_lines=0, files=0, biggest=[]))
|
|
for p, (pl, tl, ol) in res.items():
|
|
if not p.startswith(src_root):
|
|
continue
|
|
rel = p[len(src_root):]
|
|
parts = rel.split("/")
|
|
if len(parts) == 1:
|
|
stem = parts[0][:-3]
|
|
mod = {"lib": "<lib.rs>", "main": "<main.rs>"}.get(stem, stem)
|
|
else:
|
|
mod = parts[0]
|
|
m = mods[mod]
|
|
m["production_lines"] += pl
|
|
m["file_level_lines"] += pl + (tl if (pl or ol) else 0)
|
|
m["test_lines"] += tl
|
|
m["files"] += 1
|
|
if pl:
|
|
m["biggest"].append((pl, rel))
|
|
for m in mods.values():
|
|
m["biggest"] = sorted(m["biggest"], reverse=True)[:3]
|
|
tui_mods = dict(mods)
|
|
out["tui"]["production_outside_src"] = sum(v[0] for p, v in res.items() if not p.startswith(src_root))
|
|
return dict(crates=out, tui_modules=tui_mods, fallback_uses=len(FALLBACK))
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
repo = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
ap.add_argument("crates_dir", nargs="?", default=os.path.join(repo, "crates"))
|
|
ap.add_argument("--json", metavar="FILE", help="write the full result as JSON ('-' for stdout)")
|
|
ap.add_argument("--crate", action="append", help="print only these crates")
|
|
ap.add_argument("--top", type=int, default=0, help="also list the N largest tui modules")
|
|
args = ap.parse_args()
|
|
result = summarise(args.crates_dir)
|
|
if args.json:
|
|
text = json.dumps(result, indent=1)
|
|
if args.json == "-":
|
|
print(text)
|
|
else:
|
|
with open(args.json, "w", encoding="utf-8") as fh:
|
|
fh.write(text + "\n")
|
|
rows = result["crates"]
|
|
names = args.crate or sorted(rows, key=lambda n: -rows[n]["production_lines"])
|
|
print(f"{'crate':22}{'[M] prod':>11}{'[F] file':>11}{'test':>11}{'inline test':>13}{'files':>7}")
|
|
for n in names:
|
|
r = rows[n]
|
|
print(f"{n:22}{r['production_lines']:>11,}{r['file_level_lines']:>11,}{r['test_lines']:>11,}{r['inline_cfg_test_lines']:>13,}{r['files']:>7}")
|
|
tot_m = sum(r["production_lines"] for r in rows.values())
|
|
tot_f = sum(r["file_level_lines"] for r in rows.values())
|
|
print(f"{'all crates':22}{tot_m:>11,}{tot_f:>11,}")
|
|
if args.top and result["tui_modules"]:
|
|
print("\nlargest tui modules ([M] / [F]):")
|
|
for name, m in sorted(result["tui_modules"].items(), key=lambda kv: -kv[1]["production_lines"])[: args.top]:
|
|
print(f" {name:28}{m['production_lines']:>9,}{m['file_level_lines']:>10,}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|