perf(cli): route command aliases through the help fast path - #1641
Conversation
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head |
9418f11 to
842469b
Compare
|
842469b to
7dc6f0f
Compare
|
Re-reviewed exact head
The current PR body is also stale relative to the six-file structural-guard diff. Current head is mergeable and completed checks are green, with required lanes still pending; no |
bin.ts's `--help` fast path resolved aliases through a hand-written two-entry table that had drifted out of sync with the real CLI_COMMAND_ALIASES registry (five entries). `tap`, `launch`, and `relaunch` missed the table and silently fell through to a full runCli() bootstrap just to print static help text (~150-165ms vs ~45-50ms for aliases already in the table). Delegate to the shared normalizeCliCommandAlias registry instead of the stale local table, so every alias the registry knows about gets the fast path automatically.
The unit test added for the alias fast-path fix (cli-help-alias-fast-path.test.ts) calls normalizeCliCommandAlias directly, so it stays green even if bin.ts itself reverts to a hand-rolled table — it pins the registry composition, not bin.ts's own wiring, and bin.ts cannot be safely unit-imported (it runs unguarded top-level dispatch on import and is deliberately excluded from coverage). Add an AST-based structural guard instead, in the style already established by scripts/layering/session-state.ts, facade-exports.ts, and zero-dep-jobs.ts (oxc-parser's module/program records, not a line scan, so a fixture's string literal can't produce a false hit). R12 asserts two facts about src/bin.ts: it holds a value import of normalizeCliCommandAlias from commands/cli-command-aliases.ts, and it contains none of the registry's own alias tokens as string literals. The token list is read out of the registry's own source (CLI_COMMAND_ALIASES's `alias:` property values), not hard-coded, so a future sixth alias is covered automatically. Both facts were false on the pre-fix bin.ts, verified by reverting locally and capturing the failure before restoring the fix. Wired into the existing check:layering chain (already part of check:tooling), next to R7's session-state ownership rule, which pins the same "delegate to your single owner" shape.
…2 P2) Maintainer review of R12 (PR #1641): import-presence and literal-absence alone let bin.ts regress to buildCommandUsageText(helpTarget) while the normalizeCliCommandAlias import stays in place, used harmlessly elsewhere (or not at all) — the real-tree gate stayed green through that exact regression. Add a third fact: bin.ts's call to buildCommandUsageText must receive, as its argument, a call to the LOCAL binding the resolver was imported as (aliasResolverLocalName + usageTextCallsResolver, both AST-based). Binding by local name rather than the literal export name means a renamed import (`as resolveAlias`) still verifies, and an unrelated same-named local cannot be mistaken for it. Verified by reverting locally to exactly the missed regression — import left in place, call reverted to buildCommandUsageText(helpTarget) — and confirming R12 now fails where the two-fact version passed; restored after. Two negative fixtures pin the scenario going forward: import present but unused, and import present but used only unrelated to the call.
7dc6f0f to
d2c3b1b
Compare
|
Fixed at R12 now asserts a third fact via AST: Red run for exactly the regression you named — import left in place, call site reverted to raw: Both negative fixtures you asked for are committed: present-but-unused import, and an import used only unrelatedly ( One honest caveat, since this PR is partly about not overclaiming test strength: the run above proves the three-fact guard fails on that fixture. That the two-fact version would have stayed silent on it was established by reading both predicates against the fixture, not by re-running the older guard — an argument, not a measurement. Noted in the body too. Also worth recording: the first push attempt was blocked by its own gate on
|
|
Re-reviewed exact head P2 — the AST guard existentially accepts any unrelated wrapped usage. import { normalizeCliCommandAlias } from "./commands/cli-command-aliases.ts";
void buildCommandUsageText(normalizeCliCommandAlias("press"));
const commandHelp = buildCommandUsageText(helpTarget);Add this negative fixture, then require the relevant/all usage-text call(s) to receive the imported local binding invoked specifically with |
The previous fact 3 asked whether *any* `buildCommandUsageText(resolver(...))`
existed in bin.ts. That quantifier is satisfied by a decoy call while the line
that actually ships resolves nothing:
void buildCommandUsageText(normalizeCliCommandAlias('open'));
const commandHelp = buildCommandUsageText(helpTarget);
Fact 3 now requires EVERY `buildCommandUsageText` call to receive the imported
resolver applied to the fast path's own help-target binding, which rejects both
lines above independently. The help-target name is read from bin.ts (the
variable initialized by `resolveSimpleHelpTarget`), so renaming it re-points
the guard instead of disarming it.
Because fact 3 claims binding identity by name, it also now rejects a local
shadow of the resolver and an ambiguous second help-target declaration — a
same-named local would otherwise let the composition read as delegation while
calling something that resolves nothing.
The predicate returns the reason rather than a boolean, so the gate names which
of the several distinct failures happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Rva4YGtSCAKJqH5PbpcCU
|
Re-reviewed exact head f469d6c. The production alias fast path is correct, and the latest R12 guard closes the previous existential and decoy bypass: every buildCommandUsageText call must receive the imported resolver applied to the binding produced by resolveSimpleHelpTarget, with negative fixtures for raw, decoy, wrong-argument, and shadow cases. No actionable code findings. This is host-side static help routing, so device evidence does not apply. Code review is clean; remaining CI is still in progress. |
|
Fixed at Fact 3 is now universal and value-bound. Every Red run on your exact fixture, planted in the real tree: I used And the old predicate, measured against the same planted tree (checked out from So the gap was real and is now closed, and that sentence is a run rather than a reading. Same-named shadow covered too, as you asked. Since fact 3 claims binding identity by name, it now also rejects a local declaration of the resolver's name ( The predicate returns the reason instead of a boolean, so the gate names which of the several distinct failures happened rather than sending you back to re-derive it. Guard tests 23, all green; Generated by Claude Code |
…#2293) * refactor(cli): let cli-help resolve the --help alias itself bin.ts's --help fast path composed buildCommandUsageText(normalizeCliCommandAlias(helpTarget)) inline, which let a future edit call buildCommandUsageText raw without anyone noticing until an alias's help silently dropped back to a full CLI bootstrap (the regression #1641 fixed). Move the composition into cli-schema/cli-help.ts as resolveHelpTargetUsageText, so bin.ts just calls one function that owns its own alias normalization; bin.ts no longer imports the alias registry at all. Retargets cli-help-alias-fast-path.test.ts at the new function (same three cases) and adds a process-level smoke test asserting `tap --help`/`launch --help` stdout is byte-identical to `press --help`/`open --help`. Seen red by temporarily removing the `tap` alias from CLI_COMMAND_ALIASES (both fast and slow paths lose the alias, producing an "Unknown command: tap" mismatch); green again after restoring it. Verified manually: `node --experimental-strip-types src/bin.ts tap --help` stays byte-identical to `press --help`, and `launch --help` to `open --help`; `rotate --help` still falls through to the retired-command error. * chore(gates): retire R12 now that cli-help owns its own alias resolution bin.ts can no longer compose buildCommandUsageText and normalizeCliCommandAlias incorrectly because it doesn't hold either import any more — resolveHelpTargetUsageText in cli-schema/cli-help.ts is the only call site, and cli-help-alias-fast-path.test.ts plus the new smoke-cli process test pin it. The static R12 checker existed only to prove that composition from source text; delete it along with its rule wiring in check.ts (rule function, import, LAYERING_RULE_IDS/LAYERING_RULES entries, header comment, summary string). Drops scripts/layering/bin-alias-fast-path.ts (352 lines) and its test (311 lines). Updates the two stale references left behind: record-runtime-mechanics-policy.ts's comparison to R12's "delegate to your single owner" shape, and check-wiring.test.ts's header, which named bin-alias-fast-path.test.ts as the seam it protects. rule-ids.ts discovers rule ids by scanning source text rather than a hand-maintained list, so no entry there needed updating. Verified: pnpm check:layering green (175/175), including check-wiring.test.ts and rule-ids.test.ts; pnpm check:quick (lint + typecheck) clean; scripts/__tests__/eager-closure-budgets.test.ts (418/418) unaffected, since neither bin.ts nor cli-help.ts sits in any HUB_ENTRY_FILES or facade closure — both files reach cli-help.ts only through a dynamic import. * test(cli): pin the alias help fast path with a coverage-based oracle The byte-identical stdout test cannot fail when the fast path is bypassed: src/cli.ts's slow path resolves the same alias and writes the identical string, so a reintroduced hand-written table in bin.ts (the exact shape of #1641) would still pass it. Add a second process-level test that runs `tap`/`launch --help` and `rotate --help` with NODE_V8_COVERAGE set and reads the subprocess's own coverage report for src/cli/process-entry.ts, the one module runCli's slow path loads and the fast path never does. Seen red: forcing the fast path to always fall through to runCli (simulating the reintroduced-table bug) failed this test (bootstrappedFullCli true where false was expected) while the byte-identical test stayed green; reverted and confirmed both green. * test(cli): restore an independent oracle for alias help parity The canonical side of "alias help output matches its canonical command" also called resolveHelpTargetUsageText, so the assertion became self-consistency: a degenerate normalizer that maps every input to one canonical command would make aliasHelp and canonicalHelp equal for every case. Compare resolveHelpTargetUsageText(alias) against buildCommandUsageText(canonical) (no alias normalization on the canonical side) instead, restoring the original two-source oracle. Seen red: pointing resolveHelpTargetUsageText at a degenerate `return buildCommandUsageText('press')` failed this test ("launch --help" no longer byte-identical to "open --help"); reverted and confirmed green. * refactor(mcp): route the help tool through resolveHelpTargetUsageText server-guide.ts's help tool composed buildCommandUsageText(normalizeCliCommandAlias(topic)) inline, the same composition bin.ts held before this PR moved it into cli-help.ts. That left a second hand-written call site the R12 gate's own kill criterion said had to be gone before retirement was moot. Call resolveHelpTargetUsageText(topic) instead; behavior is unchanged (manually confirmed tap/press and rotate topics still match) since it's the same composition, and no closure/layering change since server-guide.ts already imports cli-help.ts statically. * style: apply oxfmt * test(cli): prove the help fast path for every registered alias * refactor(cli): make the process entry importable and test it directly bin.ts ran its dispatch at import time, so the only way to prove that an alias --help never loads the full CLI was to spawn the process under NODE_V8_COVERAGE and grep the report for process-entry.ts. That oracle needed a paragraph to justify; the code was wrong, not the comment. The dispatch now lives in src/cli/entry.ts as runEntry(argv, modules, io), with the five lazy imports injected by bin.ts. entry.test.ts drives it with recording loaders and the real help module: every registry alias prints its canonical help with only the help module loaded, an unknown topic falls through to the CLI loader, --version, bare usage, mcp, and startup failures each have one case. The subprocess coverage machinery, the alias table pin, and the multi-line comments are gone; the smoke test keeps one registry-derived byte-identical alias --help check against the real bin.ts. Seen red: hand-routing long-press and relaunch to the CLI loader inside entry.ts failed "every registered alias prints its canonical help without loading the CLI"; restored.
Summary
agent-device <command> --helphas a fast path insrc/bin.tsthat prints static help without booting the full CLI. It resolved aliases through a hand-written two-entry table while the real registry (src/commands/cli-command-aliases.ts) has five, sotap,launch, andrelaunchsilently missed it and fell through to a fullrunCli()bootstrap just to print static text.It now delegates to
normalizeCliCommandAlias, so every alias the registry knows about gets the fast path automatically.Measured on the built CLI, 3 runs each, warmed:
--helppress(control)long-press(control, was in the old table)taplaunchrelaunchlong-pressvstapis the controlled comparison: both are aliases printing identical help, differing only by whether the stale table knew about them. Output is byte-identical to each alias's canonical command (diffclean on all four pairs).rotatedeliberately still misses the fast path, so its rename migration error keeps rendering through the slow path. No carve-out was added tobin.tsfor it — it simply isn't in the alias registry.Regression coverage: a structural guard, not just a unit test
bin.tsruns unguarded top-level dispatch on import and is excluded from coverage by design, so it cannot be imported in a unit test — the committedcli-help-alias-fast-path.test.tspins the registry compositionbin.tscalls (a durable guard against a future sixth alias lacking help text), but revertingbin.tsalone does not fail it. That gap was closed with a dedicated structural gate instead of stretching the unit test past what it can honestly prove.R12 (
scripts/layering/bin-alias-fast-path.ts+.test.ts, wired intoscripts/layering/check.tsandcheck:layering) readssrc/bin.ts's source withoxc-parser— the same AST-based approach assession-state.ts,facade-exports.ts, andzero-dep-jobs.tsin the same directory, not a line scan (a line scan would mistake a fixture's string literal or a comment for the real thing). It asserts three facts:bin.tsholds a value import ofnormalizeCliCommandAliasfrom the registry (not type-only, which would be erased at compile time).bin.ts's call tobuildCommandUsageTextreceives, as its argument, an actual call to the local binding that import resolved to — not merely both names appearing somewhere in the file. Binds by local name, so a renamed import (as resolveAlias) still verifies and an unrelated same-named local does not.bin.tscontains none of the registry's own alias tokens as string literals (no local hand-rolled table sitting beside the delegation).Fact 2 was added after a maintainer review of the first pass: import-presence and literal-absence alone still pass a
bin.tsthat imports the resolver and never calls it (or calls it on something unrelated) whilebuildCommandUsageText(helpTarget)runs raw — exactly the regression the fast path actually had. Verified by reverting locally to precisely that shape (import left in place, call reverted to the raw form) and confirming R12 now reports a violation; restored after.Precision about that proof, since this PR is partly about not overclaiming test strength: the run demonstrated the three-fact guard failing on that fixture. That the two-fact version would have stayed silent on it was established by reading both predicates against the fixture (import present satisfies fact 1; no alias literals satisfies fact 3), not by re-running the older guard. The reasoning is checkable from the predicates themselves, but it is an argument rather than a measurement.
The alias-token list (fact 3) is read out of the registry's own
CLI_COMMAND_ALIASESarray-literal declaration rather than hard-coded, so a future sixth alias is covered automatically without touching this file.Validation
pnpm check:tooling— green (format, lint, typecheck,check:layeringwith R12, depgraph, production-exports, tmpdir-leaks, mcp-metadata, build, bundle-owner-files, check:package).pnpm check:affected --run— green.check:layering: 76 tests pass (15 in the newbin-alias-fast-path.test.ts), guard reports OK including R12.bin.ts(stale table, no import) — 2 violations; (2) the composition fact against abin.tswith the import left in place but the call reverted to rawbuildCommandUsageText(helpTarget)— 1 violation, exactly the case the two-fact version missed.CLI --version29.4 → 28.4 ms, confirming the one static import added tobin.tscosts nothing measurable — tsdown inlines the alias table intobin.jsrather than emitting a chunk.nodesubprocesses inunit-corewould violate the suite's "unit tests must not wait real time" budget and has no precedent in the repo):Scope
6 files, +523/−9:
src/bin.tsand its unit test (the fix), plusscripts/layering/bin-alias-fast-path.ts, its test,scripts/layering/check.ts, andpackage.json(the R12 structural guard, added across two review rounds). Not device-facing, so no simulator/emulator evidence applies.Split out of #1639 per review. Found by a read-only codebase audit.