Skip to content

feat(gain): add per-project token savings with -p flag#128

Merged
pszymkowiak merged 2 commits intortk-ai:masterfrom
heAdz0r:feat/gain-project-scope
Feb 28, 2026
Merged

feat(gain): add per-project token savings with -p flag#128
pszymkowiak merged 2 commits intortk-ai:masterfrom
heAdz0r:feat/gain-project-scope

Conversation

@heAdz0r
Copy link
Contributor

@heAdz0r heAdz0r commented Feb 15, 2026

Summary

Adds per-project scoping to rtk gain so users can see token savings for the current project instead of global aggregates.

Problem: Users working on multiple projects see combined statistics in rtk gain. There's no way to answer "how much does rtk save in this project?"

Solution: rtk gain -p filters all statistics to the current working directory (and subdirectories).

Usage

# Global view (default, unchanged)
rtk gain

# Project-scoped view (new)
rtk gain -p
rtk gain --project

# Works with all existing flags
rtk gain -p --daily
rtk gain -p --format json
rtk gain -p --history

Implementation Details

Files Changed

File Changes Purpose
src/tracking.rs +120 project_path column, migration, filtered query methods
src/gain.rs +80 project scope resolution, scope-aware display
src/main.rs +15 --project/-p Clap flag

Total: 215 lines added, 50 lines modified

tracking.rs

  • Schema migration: Adds project_path TEXT column via ALTER TABLE (idempotent, backward-compatible)
  • Auto-recording: current_project_path_string() captures canonical cwd on every command execution
  • Index: idx_project_path_timestamp for fast project-scoped queries
  • NULL normalization: Pre-existing rows get empty string (migration runs once)
  • Filtered API: All query methods get _filtered(project_path: Option<&str>) variants:
    • get_summary_filtered()
    • get_all_days_filtered()
    • get_by_week_filtered()
    • get_by_month_filtered()
    • get_recent_filtered()
  • SQL filter: WHERE (?1 IS NULL OR project_path = ?1 OR project_path LIKE ?2) — exact match + subdirectory prefix match
  • Original unfiltered methods delegate to _filtered(None) (zero behavior change)

gain.rs

  • resolve_project_scope(): Resolves --project flag to canonical cwd path
  • shorten_path(): Abbreviates long paths for display (/Users/foo/bar/project/.../bar/project)
  • Scope-aware header: "RTK Token Savings (Project Scope)" with path display
  • All exports (JSON, CSV) pass project scope through

Backward Compatibility

  • Existing databases: Migration adds column with empty default. All existing data remains accessible.
  • Existing CLI: rtk gain without -p behaves identically (filtered with None returns all rows).
  • No new dependencies added.

Security Compliance

Critical files check

  • src/tracking.rs is a critical file (per security-check.yml)
  • No shell execution: All changes are SQLite queries via rusqlite parameterized statements
  • No user input in SQL: Project path comes from std::env::current_dir(), not CLI arguments
  • No Cargo.toml changes: No new dependencies

Verification

  • cargo build — compiles clean
  • cargo clippy — no errors
  • cargo test tracking — 7 passed, 1 pre-existing failure (unrelated test_default_db_path)
  • cargo fmt — formatted

🤖 Generated with Claude Code

///
/// When `project_path` is `Some`, matches the exact working directory
/// or any subdirectory (prefix match with path separator).
pub fn get_summary_filtered(&self, project_path: Option<&str>) -> Result<GainSummary> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIKE ?2 without a trailing % wildcard is an exact match — subdirectory prefix matching doesn't work. project_path LIKE '/home/user/project/' will never match /home/user/project/src.

Fix: Some(format!("{}{}%", p, std::path::MAIN_SEPARATOR))

But then _ and % in path names become LIKE wildcards (e.g. my_project matches myXproject). Consider using GLOB instead of LIKE, or add an ESCAPE clause.

src/tracking.rs Outdated
let _ = conn.execute("ALTER TABLE commands ADD COLUMN project_path TEXT", []);
// Normalize NULL values for pre-project-aware rows // added
let _ = conn.execute(
"UPDATE commands SET project_path = '' WHERE project_path IS NULL",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UPDATE commands SET project_path = '' WHERE project_path IS NULL in Tracker::new() runs on every startup, not just once. After the first migration it's a no-op query on every
command invocation. Consider wrapping it in a schema version check or checking if the column already has a default.

heAdz0r added a commit to heAdz0r/rtk that referenced this pull request Feb 15, 2026
Address reviewer feedback on PR rtk-ai#128:

1. Replace SQL LIKE with GLOB in all project-scoped queries to prevent
   `_` and `%` characters in path names from being interpreted as
   wildcards (e.g., `my_project` matching `myXproject`). GLOB uses `*`
   for wildcard matching which is safer for file system paths.

2. Guard the startup `UPDATE commands SET project_path = ''` migration
   with an `EXISTS` check so it only runs when NULL rows actually exist,
   avoiding a no-op UPDATE on every startup after the first migration.

3. Add `DEFAULT ''` to the ALTER TABLE migration so new installs never
   create NULL project_path values.

4. Add 3 new unit tests for project_filter_params GLOB behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
heAdz0r added a commit to heAdz0r/rtk that referenced this pull request Feb 15, 2026
Address reviewer feedback on PR rtk-ai#128:

1. Replace SQL LIKE with GLOB in all project-scoped queries to prevent
   `_` and `%` characters in path names from being interpreted as
   wildcards (e.g., `my_project` matching `myXproject`). GLOB uses `*`
   for wildcard matching which is safer for file system paths.

2. Guard the startup `UPDATE commands SET project_path = ''` migration
   with an `EXISTS` check so it only runs when NULL rows actually exist,
   avoiding a no-op UPDATE on every startup after the first migration.

3. Add `DEFAULT ''` to the ALTER TABLE migration so new installs never
   create NULL project_path values.

4. Add 3 new unit tests for project_filter_params GLOB behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@heAdz0r heAdz0r force-pushed the feat/gain-project-scope branch from 722e053 to 172099f Compare February 15, 2026 17:32
@heAdz0r
Copy link
Contributor Author

heAdz0r commented Feb 15, 2026

@pszymkowiak Both issues have been addressed and the branch has been rebased on latest master (conflicts resolved):

1. LIKE without trailing wildcard — Fixed in commit 722e053. Replaced LIKE with GLOB throughout. project_filter_params() now returns GLOB patterns with * wildcard instead of LIKE with %. This also fixes the _ character issue (GLOB treats _ as literal, LIKE does not).

  • Tests added: project_filter_glob_pattern, project_filter_glob_underscore_safe

2. UPDATE on every startup — The UPDATE is now guarded by SELECT EXISTS(SELECT 1 FROM commands WHERE project_path IS NULL). It only runs when there are actual NULL values to migrate. After the first successful migration, the EXISTS check returns false immediately (cheap O(1) check) and the UPDATE is skipped entirely.

Additionally, the ALTER TABLE ADD COLUMN now includes DEFAULT '', so new rows never have NULL project_path.

Branch rebased cleanly on master (merged dashboard viz from #129). Ready for re-review.

heAdz0r and others added 2 commits February 20, 2026 22:48
Record project_path (cwd) in tracking database and add filtered query
methods. `rtk gain -p` shows savings scoped to the current project
directory instead of global aggregates.

- tracking.rs: Add project_path column with auto-migration, index,
  and filtered variants for all query methods (summary, daily, weekly,
  monthly, recent)
- gain.rs: Add resolve_project_scope(), shorten_path(), scope-aware
  header, pass project filter to all queries and exports
- main.rs: Add --project/-p flag to Gain command

Backward-compatible: existing rows get empty project_path, unfiltered
queries delegate to filtered(None) which returns all data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address reviewer feedback on PR rtk-ai#128:

1. Replace SQL LIKE with GLOB in all project-scoped queries to prevent
   `_` and `%` characters in path names from being interpreted as
   wildcards (e.g., `my_project` matching `myXproject`). GLOB uses `*`
   for wildcard matching which is safer for file system paths.

2. Guard the startup `UPDATE commands SET project_path = ''` migration
   with an `EXISTS` check so it only runs when NULL rows actually exist,
   avoiding a no-op UPDATE on every startup after the first migration.

3. Add `DEFAULT ''` to the ALTER TABLE migration so new installs never
   create NULL project_path values.

4. Add 3 new unit tests for project_filter_params GLOB behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pszymkowiak
Copy link
Collaborator

Clean implementation. Reviewed locally:

  • rtk gain (global) still works correctly
  • rtk gain -p correctly filters to CWD and subdirectories
  • rtk gain -p --history works as expected

Security: All SQL queries use parameterized statements — no injection risk. GLOB instead of LIKE is the right call (avoids _ and % in paths acting as wildcards). SQLite migration uses let
_ = to silently ignore duplicate column — correct pattern.

The test_custom_db_path_env failure is a pre-existing race condition in master (env var mutation between parallel tests), not introduced by this PR.

417 tests pass. LGTM.

@pszymkowiak pszymkowiak merged commit 2b550ee into rtk-ai:master Feb 28, 2026
2 of 3 checks passed
maxkulish added a commit to maxkulish/rtk that referenced this pull request Mar 2, 2026
Bug fixes (4):
- fix(registry): move "fi"/"done" to IGNORED_EXACT to prevent shadowing
  find commands (rtk-ai#246)
- fix(hook): filter docker compose rewrites to supported subcommands
  only (ps/logs/build), avoid hard failures on up/down/exec (rtk-ai#245)
- fix(git): add is_blob_show_arg() to prevent duplicate output on
  git show rev:path style args (rtk-ai#248)
- fix(go): surface build failures (build-output/build-fail/FailedBuild)
  in go test -json summary (rtk-ai#274)

Features (2):
- feat(mypy): add rtk mypy command with grouped error output, 80% token
  reduction; delegate from lint_cmd.rs; add hook rewrites and registry
  entries (rtk-ai#109)
- feat(gain): add --project/-p flag to scope rtk gain stats to the
  current working directory; add shorten_path() display helper (rtk-ai#128)

Tests: 501 passed (10 new tests added)
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.

2 participants