+++ title = "Wire --filter flag into list, ready, and migrate commands" priority = 8 status = "done" ticket_type = "feature" dependencies = ["c2a024"] +++ ## Summary Add `--filter KEY=VALUE` (repeatable) to the `list`, `ready`, and `migrate` CLI commands. Parse filter arguments into a `TicketFilter` and apply it in each command handler. Depends on: TicketFilter module (ticket c2a024). ## CLI changes (src/main.rs) Add to `Commands::List`, `Commands::Ready`, and `Commands::Migrate` variants: ```rust /// Filter tickets: key=value pairs (repeatable). /// Keys: priority, type, status, title. /// Different keys are ANDed; same key with multiple values is ORed. /// Values support glob wildcards: status=* matches all statuses. #[arg(long = "filter", value_name = "KEY=VALUE")] filter: Vec, ``` Update the `dispatch` function to pass filter args through to each handler. ## Handler changes ### cmd_list(filter_args, json) ```rust let filter = filter::parse_filters(&filter_args)?; let tickets: Vec = list_tickets(&root).await? .into_iter() .filter(|t| filter.matches(t)) .collect(); ``` Note: the default done-exclusion behaviour (ticket for that is separate) will also live here, layered on top of this filter application. ### cmd_ready(filter_args, json) Apply the user filter AFTER the ready check. The ready check (not done + all deps done) is always applied first; the user's filter narrows further within ready tickets. ```rust let filter = filter::parse_filters(&filter_args)?; // ... build done_ids as before ... let ready: Vec = all .into_iter() .filter(|t| { t.status \!= Status::Done && t.dependencies.iter().all(|dep| done_ids.contains(dep.as_str())) && filter.matches(t) }) .collect(); ``` ### cmd_migrate(filter_args, dry_run, json) For migrate, the filter selects which tickets are candidates for migration. Tickets not matching the filter are skipped (counted separately, not treated as errors). Add a `skipped` field to `MigrateReport` in `store.rs`: ```rust pub struct MigrateReport { pub updated: usize, pub already_current: usize, pub skipped: usize, // NEW: tickets excluded by filter pub errors: Vec<(String, String)>, } ``` Update `migrate_tickets` signature in `store.rs`: ```rust pub async fn migrate_tickets( root: &Path, dry_run: bool, filter: &TicketFilter, ) -> Result ``` Inside the per-file loop, after deserialising the ticket, check `filter.matches(&ticket)`. If false: increment `report.skipped` and continue to next file. Update `cmd_migrate` to parse the filter and pass it to `migrate_tickets`. ## display.rs changes Update `format_migrate_report` and `format_migrate_report_json` to include the `skipped` count: Human format: ``` Migrated 3 tickets. Current 5 tickets (already up to date). Skipped 2 tickets (did not match filter). Errors 1 ticket could not be migrated: bad_ticket.json: trailing comma at line 4 ``` Only print the "Skipped" line when `skipped > 0`. JSON format: add `"skipped": N` key to the existing object. ## files touched - `src/main.rs` — `filter` fields on List/Ready/Migrate variants, updated dispatch, updated cmd_list/cmd_ready/cmd_migrate handlers - `src/store.rs` — `MigrateReport::skipped`, `migrate_tickets` gains `filter` param - `src/display.rs` — updated `format_migrate_report` and `format_migrate_report_json` - `src/tests.rs` — unit tests for updated migrate report formatting - `tests/integration.rs` — integration tests ## Integration tests to add (tests/integration.rs) **list filtering:** - Create tickets: 1 bug/todo, 1 task/in_progress, 1 bug/done. `nbd list --filter type=bug` shows only the bug tickets (done-exclusion is separate, but this test can use non-done bugs). - `nbd list --filter status=in_progress` shows only in_progress tickets. - `nbd list --filter status=todo --filter status=in_progress` shows both todo and in_progress (OR within same key). - `nbd list --filter type=bug --filter status=todo` shows only bug+todo tickets (AND across keys). - `nbd list --filter title=*login*` shows only tickets whose title contains "login". - `nbd list --filter status=*` matches all statuses (wildcard). - `nbd list --filter type=unknown` exits non-zero with an error (unknown key passes through as a value, but "unknown" does not match any type → empty results, or error? Error on unknown key is preferable). - `nbd list --filter badformat` (no `=`) exits non-zero with an error. **ready filtering:** - `nbd ready --filter type=bug` returns only ready bug tickets. - `nbd ready --filter priority=8` returns only ready tickets with priority 8. **migrate filtering:** - Create two tickets. Run `nbd migrate --filter status=todo --dry-run`. Verify `skipped` count in JSON output matches tickets not matching the filter. - `nbd migrate --filter status=todo --json` includes `skipped` key. **error cases:** - `--filter` with unknown key exits non-zero. - `--filter` with no `=` exits non-zero.