Skip to content

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

Open
heAdz0r wants to merge 2 commits intortk-ai:masterfrom
heAdz0r:feat/gain-project-scope
Open

feat(gain): add per-project token savings with -p flag#128
heAdz0r wants to merge 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 and others added 2 commits February 15, 2026 20:31
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>
@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.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants