handover agent doing docgen-graphgen

This commit is contained in:
2026-08-31 03:54:20 -03:00
parent 8fad2223f3
commit 514efc5e52
2 changed files with 172 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
"""
What the station tools share.
One thing so far — `cli.py`, the command-line shape every tool was growing its
own slightly different copy of. It is the source; `histgen` and `modelgen`
vendor it because they leave the tree, `shuntgen` and `tester` import it because
they do not. See `cli.py`'s docstring for why that split is deliberate.
Nothing here imports anything outside the standard library, and nothing here
imports a tool. A shared module that reaches back into its users is not shared,
it is circular.
"""
__all__ = ["cli"]

View File

@@ -0,0 +1,158 @@
"""
The shape a station tool's command line has.
Every tool here grew its own copy of the same three things: argparse subcommands
wired to `cmd_*` functions, a flag vocabulary that is nearly but not quite the
same between tools, and an error convention. shuntgen spells `-s` as --spec in
one subcommand and --source in another; modelgen calls the same idea --source
everywhere; tester has neither. The differences are accidents, not decisions.
This is that shared shape, factored out.
## Where this lives, and why it lives in two places
It was written inside `histgen/`, with exactly one user, on the principle that a
scaffold earns adoption by being there when the next tool is written rather than
by a flag-day. It has earned it; this is the promoted copy, and `station/tools/
common/` is now the source.
It is **both imported and vendored**, which is deliberate:
shuntgen, tester import it. `from ..common.cli import Tool, fail`
Neither is standalone, so there is nothing to protect.
histgen, modelgen vendor a copy at `<tool>/cli.py`.
Both are designed to leave the tree — modelgen is a
published pip package, and histgen/__init__.py says the
directory "can be copied out of soleprint and run on its
own — a repo that needs a history seeded is, by
definition, not one that already has this framework on
its path." An import would make that false.
Vendoring is safe only if drift is visible, so the copies are registered as
`soleprint-cli` in `registry.json` and `ctrl/spr.py diff` reports it — the same
arrangement that already keeps `soleprint-ui` honest across `unt` and `mpr`.
python ctrl/spr.py diff soleprint-cli soleprint/station/tools/histgen
## Using it
from ..common.cli import Tool, fail # or `from .cli import` if vendored
tool = Tool("histgen", __doc__)
tool.command("scan", cmd_scan, "Read the repo and cache what was read.")
tool.argument("scan", "repo", help="The tree to read.")
tool.run()
Conventions it encodes, so they stop being re-decided:
--source/-s what to read --output/-o where to write
--name/-n what to call it --force/-f write anyway
--dry-run print, do not do
Progress goes to stdout as plain print(). Errors go to stderr prefixed
'Error: ' and exit 1 — never a traceback, which tells a user nothing they
can act on. `fail()` is the only exit path.
Heavy imports live inside the cmd_* function, never at module top, so
`--help` stays instant and an optional dependency only costs the one
subcommand that needs it.
"""
import argparse
import sys
# The flags worth having exactly one spelling of. A tool adds its own on top;
# it does not redefine these.
COMMON = {
"source": dict(flags=("--source", "-s"), help="What to read."),
"output": dict(flags=("--output", "-o"), help="Where to write."),
"name": dict(flags=("--name", "-n"), help="What to call it."),
"force": dict(flags=("--force", "-f"), action="store_true",
help="Write even if the destination is occupied."),
"dry_run": dict(flags=("--dry-run",), action="store_true",
help="Print what would happen; change nothing."),
}
def fail(message, hint=None):
"""The only way out on error: a line a user can act on, never a traceback."""
print(f"Error: {message}", file=sys.stderr)
if hint:
print(f" {hint}", file=sys.stderr)
sys.exit(1)
def refuse_to_clobber(path, force, marker, what):
"""
Regenerating is fine; overwriting something we did not write is not.
Lifted from shuntgen, which refuses a non-empty output directory unless it
carries the file its own generator leaves behind. The check is cheap and the
failure it prevents — silently eating a directory someone hand-wrote — is
not recoverable from.
"""
if force or not path.exists():
return
if not any(path.iterdir()):
return
if (path / marker).exists():
return
fail(f"{path} already exists and was not written by {what}.",
"Pick another path, or pass --force to write into it anyway.")
def _prog(package, name):
"""
How this tool was actually invoked, for the usage line.
The folder is meant to be copied out and run on its own, so a usage line
hardcoding `python -m station.tools.histgen` is wrong the moment it is —
it names a path that does not exist on the machine reading it.
"""
return f"python -m {package or name}"
class Tool:
"""A tool's whole command line: subcommands, shared flags, one exit path."""
def __init__(self, name, description, package=None):
self.name = name
self.parser = argparse.ArgumentParser(
prog=_prog(package, name),
description=(description or "").strip().split("\n\n")[0],
formatter_class=argparse.RawDescriptionHelpFormatter,
)
self._subparsers = self.parser.add_subparsers(dest="command", required=True)
self._commands = {}
def command(self, verb, func, help_text):
"""Register a subcommand. `func` takes parsed args and returns None."""
sub = self._subparsers.add_parser(verb, help=help_text, description=help_text)
sub.set_defaults(func=func)
self._commands[verb] = sub
return sub
def argument(self, verb, *args, **kwargs):
"""Add a positional or flag to one subcommand."""
self._commands[verb].add_argument(*args, **kwargs)
def common(self, verb, *names, **overrides):
"""Add shared flags by name, so their spelling is decided in one place."""
for key in names:
spec = dict(COMMON[key])
flags = spec.pop("flags")
spec.update(overrides.get(key, {}))
self._commands[verb].add_argument(*flags, **spec)
def run(self, argv=None):
args = self.parser.parse_args(argv)
try:
args.func(args)
except KeyboardInterrupt:
# Ctrl-C is a decision, not a crash. Say so and leave quietly.
print("\nInterrupted.", file=sys.stderr)
sys.exit(130)
except BrokenPipeError:
# `... | head` closes the pipe early; that is the caller's business.
sys.exit(0)