Skip to content

refactor(dispatch): SlashCmd enum + parser, replace 34-branch chain (#827 stage 1)#832

Open
NikolayS wants to merge 5 commits into
mainfrom
refactor/dispatch-stage1
Open

refactor(dispatch): SlashCmd enum + parser, replace 34-branch chain (#827 stage 1)#832
NikolayS wants to merge 5 commits into
mainfrom
refactor/dispatch-stage1

Conversation

@NikolayS
Copy link
Copy Markdown
Owner

Summary

Stage 1 of the three-stage command-dispatch refactor in #827.

Slash commands (/-prefix) were dispatched through a 34-branch if let Some(...) = input.strip_prefix(...) chain in dispatch_ai_command, with a parallel hand-curated 25-entry "budget exempt" list that had to be kept in sync. This PR introduces a typed SlashCmd enum and parse() mirroring MetaCmd / metacmd::parse, eliminating both forms of duplication while preserving every existing observable behaviour.

What changed

  • New module src/slashcmd.rs (296 LOC of impl + ~600 LOC of parser tests):

    • pub enum SlashCmd — one variant per /-command currently dispatched (35 variants including Unknown). Mirrors the shape of MetaCmd.
    • pub struct ParsedSlash { cmd, raw } — analogous to ParsedMeta, carries only the fields handlers actually need.
    • pub fn parse(input) -> ParsedSlash — recognises every /-command, parses arguments into typed fields (e.g. SlashCmd::Dba { subcommand, plus }, SlashCmd::Session*, SlashCmd::LogFile { path }, SlashCmd::ExplainShare { service }).
    • SlashCmd::requires_ai_budget() — returns true for /ask, /fix, /explain, /optimize, /describe; false for the 25 non-AI commands plus the four AI-management commands (/clear, /compact, /budget, /init). Replaces the hand-curated is_budget_exempt chain.
  • src/repl/ai_commands.rs::dispatch_ai_command:

    • Calls slashcmd::parse(input) once at the top.
    • Replaces the budget-exemption OR-chain (lines 399–428) with parsed.cmd.requires_ai_budget() && check_token_budget(settings).
    • Replaces the 34-branch if let Some / else if chain with a single match parsed.cmd block. Each arm calls the existing handle_* function with typed args.
    • No new helpers; no signature change; no handler change.
  • Module wiring: mod slashcmd; added to src/main.rs and (cfg-gated) src/lib.rs.

LOC delta

  • dispatch_ai_command body: 404 → 362 lines (-42, -10%). The budget chain (30 lines) is gone; argument parsing moved into the parser.
  • src/repl/ai_commands.rs overall: 3155 → 3113 (-42).
  • src/slashcmd.rs: 0 → 976 (296 impl + 680 tests).
  • Files touched: 4 (src/slashcmd.rs new, src/repl/ai_commands.rs, src/main.rs, src/lib.rs).

Test plan

Red/green/refactor TDD as mandated by CLAUDE.md:

  1. Red committest(slashcmd): add failing parser tests for SlashCmd enum. Tests fail to compile because SlashCmd doesn't exist yet.
  2. Green commitfeat(slashcmd): implement SlashCmd enum and parser. All 62 parser tests pass.
  3. Refactor commitrefactor(dispatch): route slash commands through SlashCmd. All 2125 lib + bin tests pass; integration tests untouched and still green.

Parser test coverage (62 tests in src/slashcmd.rs::tests):

  • Each command's basic recognition (e.g., parse("/ask hello")SlashCmd::Ask { prompt: "hello" })
  • Argument-parsing edge cases — /dba+ plus flag, /session save [name], /log-file [path], /explain-share <service> disambiguated from /explain
  • Bare-vs-arg variants — /ask vs /ask hello, /n+ vs /n vs /ns
  • requires_ai_budget() returns the correct value for AI commands (true), AI-management commands (false), all 25 non-AI commands (false), and Unknown (false)
  • Unknown slash commands → SlashCmd::Unknown { input }

Verification commands:

  • cargo test --bin rpg — 2125 passed, 0 failed
  • cargo test (full suite incl. integration) — all green
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo fmt -- --check — clean
  • cargo check --target wasm32-unknown-unknown --lib — clean

Out of scope (per #827)

Nik Samokhvalov and others added 3 commits April 25, 2026 15:16
Red phase of #827 stage 1 — TDD discipline mandated by CLAUDE.md.
Introduces src/slashcmd.rs with parser tests for every / command currently
recognised by dispatch_ai_command. The enum SlashCmd, struct ParsedSlash,
parse(), and requires_ai_budget() do not yet exist; the tests fail to
compile. The green phase commit will add the implementation.

Tests cover:
- Each /-command's basic recognition (mirrors metacmd::parse tests).
- Argument parsing edge cases (e.g., /dba+, /session save [name],
  /log-file [path], /explain-share <service>, /n+ vs /n).
- requires_ai_budget() returns true for AI commands (/ask, /fix, /explain,
  /optimize, /describe) and false for the 25 non-AI commands plus the
  AI-management ones (/clear, /compact, /budget, /init).
- Unknown / commands fall through to SlashCmd::Unknown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Green phase of #827 stage 1 — make the parser tests from the previous
commit pass. Adds:

- SlashCmd enum with one variant per /-command currently dispatched in
  ai_commands.rs::dispatch_ai_command (mirrors the shape of MetaCmd in
  src/metacmd.rs).
- ParsedSlash carrying the recognised command plus the trimmed raw input.
- parse(input) recognising every /-command, parsing arguments into typed
  variant fields (e.g. /dba <subcommand>, /session <sub> [arg],
  /log-file [path], /explain-share <service>).
- SlashCmd::requires_ai_budget() — true only for /ask, /fix, /explain,
  /optimize, /describe; false for the 25 non-AI commands and the four
  AI-management commands (/clear, /compact, /budget, /init). This will
  replace the hand-curated is_budget_exempt list at ai_commands.rs:399
  in the next commit.

Order-sensitive prefix checks: /explain-share is matched before /explain;
/ns, /nd, /np match before /n so /n+ (handled as a bare match earlier)
and /n don't swallow them.

All 62 parser tests in src/slashcmd.rs::tests now pass. The new types are
not yet wired into dispatch_ai_command; that follows in the refactor
commit. Dead-code lint is suppressed there by the integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor phase of #827 stage 1 — wire SlashCmd into
dispatch_ai_command. Replaces the 34-branch
\`if let Some(...) = input.strip_prefix(...)\` chain with a single
\`match parsed.cmd { ... }\` block, and replaces the hand-curated
\`is_budget_exempt\` list with \`parsed.cmd.requires_ai_budget()\`.

Mechanics:
- Call slashcmd::parse(input) once at the top of the function.
- Gate AI commands on parsed.cmd.requires_ai_budget() &&
  check_token_budget(settings) instead of the 25-line OR chain.
- Each former \`else if\` branch becomes one match arm. Argument
  parsing moves out of the dispatcher into slashcmd::parse, so
  every arm receives typed fields (e.g. SlashCmd::Dba { subcommand,
  plus }) instead of re-parsing input strings.

No observable behaviour change: the same handlers run on the same
inputs and produce the same output. All 2125 lib + bin tests pass,
including the 62 slashcmd parser tests added in the previous
commit. cargo fmt --check and cargo clippy --all-targets
--all-features -- -D warnings are clean.

Stages 2 (Command trait + CommandRegistry) and 3 (per-command
modules under src/commands/) are out of scope and remain tracked
in #827 for follow-up PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS
Copy link
Copy Markdown
Owner Author

REV review — PR #832

Stage 1 of the dispatch refactor (#827) — typed SlashCmd enum + parser replaces a 34-branch if-let-Some-strip_prefix chain in dispatch_ai_command, and the hand-curated 25-entry budget-exemption list becomes a method on the variant. Reviewed across two axes (bugs + correctness; tests + docs + guidelines). Both PASS.

Bugs + correctness

  • INFO bug-equiv: All ~30 OLD branches map 1:1 to SlashCmd variants (verified manually). /dba +-modifier preserved (parser sets plus); /explain vs /explain-share longer-prefix-wins preserved; /n vs /n+ vs /ns vs /nd vs /np ordering correct; /session subcommand parsing including aliases preserved.
  • INFO budget-gate equivalence: requires_ai_budget() returns true only for Ask | Fix | Explain | Optimize | Describe. Cross-checked against the deleted is_budget_exempt chain (lines 399–428 of the old file) — every command in the old list returns false; the five AI requests (which were NOT in the old exempt list) return true. Equivalent.
  • POTENTIAL bug-equiv: No-space command forms now parse as Unknown (src/slashcmd.rs:992-1033). The old chain used input.strip_prefix("/foo").map(str::trim), which accepted /askhello, /dbafoo, /explainSELECT 1, etc. The new parser requires either bare equality or a trailing space, so all of those route to the help hint instead of executing. Realistic user impact is small (no test depends on these forms, no docs document them, every doc example uses a space) but it IS observable. Recommend documenting in the PR description as intentional.
  • INFO behavior shift for typos under exhausted budget: pre-PR, an unrecognized /asg was silently swallowed by the budget gate via starts_with matches. Post-PR, Unknown falls through to the help hint — arguably a small UX improvement. Already flagged by author.
  • NON-BLOCKING style: let cmd = match trimmed { ... _ => None::<SlashCmd> }; let _ = cmd; (src/slashcmd.rs:966-988) is awkward — the match either returns or drops to a useless None binding. Cleaner: drop the binding and use match trimmed { ... _ => () } or an if-let chain.
  • INFO build: mod slashcmd correctly cfg-gated under #[cfg(target_arch = "wasm32")] in src/lib.rs:142-143; unconditional in src/main.rs:60 (consistent with metacmd). cargo check --target wasm32-unknown-unknown --lib succeeds.

Tests + docs + guidelines

  • INFO tests: 62 parser tests, all real assert_eq! on SlashCmd variants (not smoke). Disambiguations covered: /explain vs /explain-share, /n+ vs /ns/nd/np/n, /dba vs /dba+. requires_ai_budget() exhaustively asserted across all 30+ variants.
  • NON-BLOCKING tests/edge: no test for leading/trailing whitespace inside the input. The parser does input.trim() but no test exercises it. Cheap to add.
  • NON-BLOCKING tests/integration: no test invokes dispatch_ai_command end-to-end with the new match. tests/integration_repl.rs exercises /ask and /fix semantically, providing decent indirect coverage.
  • INFO docs: module-level rustdoc cites issue refactor: unify slash and backslash command dispatch behind a registry #827 indirectly via docs/COMMANDS.md and docs/GLOSSARY.md. Per-variant rustdoc on SlashCmd, ParsedSlash, parse, requires_ai_budget — all useful.
  • INFO docs/glossary: SlashCmd and ParsedSlash are already in docs/GLOSSARY.md from PR docs: add GLOSSARY.md — shared vocabulary for architecture refactor #828; no glossary update needed.
  • INFO guidelines: 3 commits cleanly mirror red/green/refactor (570a28b, c53e168, 562ed5a); commit messages use glossary-aligned vocabulary (no banned synonyms). Copyright 2026 correct in the new file.
  • INFO scope: exactly four files changed (src/slashcmd.rs new, src/lib.rs, src/main.rs, src/repl/ai_commands.rs). No drive-by changes.

Verdict

PASS (both axes). Recommend a one-line note in the PR description that no-space command forms (/askhello) now route to the help hint — intentional simplification.


End-to-end test evidence

Systematic — full test suite

$ cargo test
test result: ok. 2125 passed; 0 failed; 4 ignored; ...

(Was 2063 + 62 new parser tests in src/slashcmd.rs::tests.)

$ cargo test --bin rpg slashcmd::
running 62 tests
...
test result: ok. 62 passed; 0 failed; 0 ignored; 0 measured; 2067 filtered out; finished in 0.00s

Live smoke test

Built a fresh rpg from 562ed5a and drove it through a Python pty against local PostgreSQL 18:

$ rpg -h localhost -d postgres -U nik
rpg 0.11.0+6-562ed5a7 (562ed5a7, built 2026-04-26)
Server: PostgreSQL 18.3 (Homebrew)
You are connected to database "postgres" as user "nik" on host "localhost"
AI: openai/default (key not set — edit ~/.config/rpg/config.toml)
Type \? for help. \-commands are psql-compatible; /-commands are rpg extensions (AI and non-AI).

postgres=# /version
[ rpg version + server version printed ]

postgres=# /commands
[ list of /-commands printed ]

postgres=# /n+
[ named-queries listing ]

postgres=# \q

All /-commands route through the new SlashCmd::parse + match and produce the same output as before the refactor. No crashes, no behavior regressions on the sampled commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant