You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Not a bug in cw's grammar — a gap in what cw gives a migrator to check their work with. Filing it because it just bit the fleet's flagship migration.
What happened
thorwhalen/priv#120 migrated a 101-command CLI to cw and shipped a committed characterization golden proving zero --help diff across 105 vectors. By every gate cw offers, that migration was complete. It was not: priv/git_branch_tool.py still had
— twice, verbatim — and priv/git_ops.py still ended in a live argh.dispatch_commands under a __main__ guard that python -m priv.git_ops reaches today.
The CommandError one was not cosmetic. It is the exact trap cw/compat.py warns about in prose ("One trap the shim structurally cannot fix… Grep for it"). Because argh.CommandError is cw.CommandError → False, all five raise sites escaped cw.run as unhandled tracebacks where argh had printed CommandError: <msg> / exit 1. A user-visible regression, live on main for the whole interval between #120 and thorwhalen/priv#122.
Why nothing caught it
Each existing gate is blind to it by construction, and that is the point:
cw.testing goldens characterize --help and parser-reject vectors. A CommandError fires after dispatch, past everything a --help corpus can reach. priv's corpus additionally must never actually run a command (they mutate the local ecosystem), so it structurally cannot cover this.
python -m cw specs / help work on one function's signature. They never see a module-level import.
cw.compat aliases CommandError correctly, but only helps a module that switched to from cw import compat as argh. A module that imported the name from real argh is untouched.
Declared-dependency checks don't fire either: priv's pyproject.toml correctly listed only ["cw…", "python-dotenv"], so the imports were undeclared and worked purely because argh happened to be installed on the dev machine. On a clean install the module would have silently fallen back to ValueError.
So the failure mode is: the grammar is provably identical, the metadata is provably clean, and the migration is still incomplete. The prose warning in cw/compat.py is the only defence, and prose does not run in CI.
Suggested shape
A python -m cw audit <path> (name TBD) that greps a migrated tree and reports, with line numbers:
import argh / from argh import … anywhere — the headline check. Special-case from argh import CommandError with the class-identity explanation, since it is the one that silently changes runtime behaviour rather than merely failing to import.
argh.dispatch* / ArghParser call sites still reachable — including under if __name__ == "__main__", which is easy to skim past precisely because it looks dead. (priv's was reachable: python -m priv.git_ops --help prints a 22-command parser.)
Optionally: a cw.dispatch(...) call whose result is discarded. cw returns the exit code where argh exited itself, so cw.dispatch(funcs) without raise SystemExit(...) silently turns every non-zero exit into 0 — another difference no --help golden can see.
Checks 1-3 are pure grep and could ship as a doctest-able function plus a __main__ subcommand; check 4 wants ast. Even just 1 and 2, runnable in a repo's CI after migration, would have turned both of today's residuals into a red build instead of a six-week silence.
Not a bug in cw's grammar — a gap in what cw gives a migrator to check their work with. Filing it because it just bit the fleet's flagship migration.
What happened
thorwhalen/priv#120 migrated a 101-command CLI to cw and shipped a committed characterization golden proving zero
--helpdiff across 105 vectors. By every gate cw offers, that migration was complete. It was not:priv/git_branch_tool.pystill had— twice, verbatim — and
priv/git_ops.pystill ended in a liveargh.dispatch_commandsunder a__main__guard thatpython -m priv.git_opsreaches today.The
CommandErrorone was not cosmetic. It is the exact trapcw/compat.pywarns about in prose ("One trap the shim structurally cannot fix… Grep for it"). Becauseargh.CommandError is cw.CommandError→False, all five raise sites escapedcw.runas unhandled tracebacks where argh had printedCommandError: <msg>/ exit 1. A user-visible regression, live onmainfor the whole interval between #120 and thorwhalen/priv#122.Why nothing caught it
Each existing gate is blind to it by construction, and that is the point:
cw.testinggoldens characterize--helpand parser-reject vectors. ACommandErrorfires after dispatch, past everything a--helpcorpus can reach. priv's corpus additionally must never actually run a command (they mutate the local ecosystem), so it structurally cannot cover this.python -m cw specs/helpwork on one function's signature. They never see a module-level import.cw.compataliasesCommandErrorcorrectly, but only helps a module that switched tofrom cw import compat as argh. A module that imported the name from real argh is untouched.pyproject.tomlcorrectly listed only["cw…", "python-dotenv"], so the imports were undeclared and worked purely because argh happened to be installed on the dev machine. On a clean install the module would have silently fallen back toValueError.So the failure mode is: the grammar is provably identical, the metadata is provably clean, and the migration is still incomplete. The prose warning in
cw/compat.pyis the only defence, and prose does not run in CI.Suggested shape
A
python -m cw audit <path>(name TBD) that greps a migrated tree and reports, with line numbers:import argh/from argh import …anywhere — the headline check. Special-casefrom argh import CommandErrorwith the class-identity explanation, since it is the one that silently changes runtime behaviour rather than merely failing to import.argh.dispatch*/ArghParsercall sites still reachable — including underif __name__ == "__main__", which is easy to skim past precisely because it looks dead. (priv's was reachable:python -m priv.git_ops --helpprints a 22-command parser.)arghininstall_requires/dependencieswith no corresponding import — the LGPL-exposure-only case. i2mint/oui_notebook was exactly this and no wave caught it until Remove the argh dependency declaration, which nothing imports oui_notebook#2.cw.dispatch(...)call whose result is discarded. cw returns the exit code where argh exited itself, socw.dispatch(funcs)withoutraise SystemExit(...)silently turns every non-zero exit into 0 — another difference no--helpgolden can see.Checks 1-3 are pure grep and could ship as a doctest-able function plus a
__main__subcommand; check 4 wantsast. Even just 1 and 2, runnable in a repo's CI after migration, would have turned both of today's residuals into a red build instead of a six-week silence.Related: #29 (deletion sweep), #30 (ledger).