You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
5.6 KiB
Markdown

+++
title = "Add nbd next subcommand"
priority = 7
status = "done"
ticket_type = "feature"
dependencies = ["887344"]
+++
## Summary
Add `nbd next` subcommand that selects the single highest-priority ticket that is
ready to work on. Supports `--filter` for additional narrowing within the ready set.
Depends on: `--filter` wired into CLI (ticket 887344). Implicitly depends on the
TicketFilter module (ticket c2a024) through that.
## Definition of "ready"
Same semantics as `nbd ready`:
- `status \!= done`
- Every ID in `ticket.dependencies` belongs to a ticket with `status == done`.
- Missing dependency IDs are treated conservatively: the ticket is NOT ready.
## Behaviour
1. Load all tickets with `list_tickets(&root)` (already sorted by priority desc).
2. Build `done_ids` set (same as `cmd_ready`).
3. Find the first ticket (highest priority) where:
- status \!= done
- all dependencies are in done_ids
- `filter.matches(t)` (if a filter was provided)
4. If found: display the single ticket.
5. If not found: print a "no ready tickets" message. Exit 0 (not an error).
## Output
Without `--json`:
- Found: print the ticket in the same tabular format as `nbd read` (via `display::print_ticket`).
- Not found: `No ready tickets.`
With `--json`:
- Found: `{"next": { ...ticket fields including id... }}`
- Not found: `{"next": null}`
Wrapping in `{"next": ...}` (rather than bare ticket or bare null) makes the JSON
unambiguously parseable by consumers — they always get an object with a `next` key.
## Implementation
Add to `Commands` enum:
```rust
/// Choose the highest-priority ticket that is ready to work on.
///
/// A ticket is ready when its status is not `done` and every ticket it
/// depends on has status `done`. Returns the single highest-priority
/// ready ticket, optionally narrowed by `--filter KEY=VALUE`.
///
/// Exits 0 even when no ready ticket exists.
Next {
/// Filter ready tickets: key=value pairs (repeatable).
/// AND between different keys, OR within same key.
#[arg(long = "filter", value_name = "KEY=VALUE")]
filter: Vec<String>,
},
```
Add dispatch arm:
```rust
Commands::Next { filter } => cmd_next(filter, cli.json).await,
```
Implement `cmd_next`:
```rust
async fn cmd_next(filter_args: Vec<String>, json: bool) -> store::Result<()> {
let root = find_nbd_root()?;
let all = list_tickets(&root).await?; // sorted by priority desc
let filter = filter::parse_filters(&filter_args)?;
let done_ids: std::collections::HashSet<&str> = all
.iter()
.filter(|t| t.status == Status::Done)
.map(|t| t.id.as_str())
.collect();
let next = all.iter().find(|t| {
t.status \!= Status::Done
&& t.dependencies.iter().all(|dep| done_ids.contains(dep.as_str()))
&& filter.matches(t)
});
if json {
match next {
Some(ticket) => {
let value = serde_json::json\!({
"next": display::ticket_to_json_value(ticket)
});
println\!("{}", serde_json::to_string_pretty(&value)?);
}
None => println\!("{}", serde_json::json\!({"next": null})),
}
} else {
match next {
Some(ticket) => display::print_ticket(ticket),
None => println\!("No ready tickets."),
}
}
Ok(())
}
```
## display.rs: make ticket_to_json_value pub(crate)
`ticket_to_json_value` is currently private in `display.rs`. It needs to be accessible
from `cmd_next` in `main.rs`. Change its visibility:
```rust
pub(crate) fn ticket_to_json_value(ticket: &Ticket) -> serde_json::Value { ... }
```
This is the cleanest approach — it reuses the existing id-injection logic rather than
duplicating it.
## README update
Add a `### Find the next ticket to work on` section:
```markdown
### Find the next ticket to work on
Returns the single highest-priority ticket that is ready to work on — not done
and with all dependencies completed.
```sh
nbd next
nbd next --json
nbd next --filter type=bug # highest-priority ready bug
nbd next --filter priority=9 # highest-priority ready ticket with priority 9
```
Exits 0 even when no ready ticket exists.
```
## CLAUDE.md update
Update the "Workflow" section to mention `nbd next` as an alternative to `nbd ready`
when the caller just wants to begin the single most important task:
```
**To get the single best ticket to work on next:**
```sh
cargo run -- next --json
```
```
## Files touched
- `src/main.rs``Next` command variant, `cmd_next` handler, dispatch arm
- `src/display.rs``ticket_to_json_value` changed to `pub(crate)`
- `tests/integration.rs` — integration tests
- `README.md` — new section for `nbd next`
- `CLAUDE.md` — update workflow section
## Integration tests to add (tests/integration.rs)
- Three tickets: A (priority 5, no deps), B (priority 8, dep A), C (priority 7, no deps).
`nbd next --json` returns C (highest priority ready ticket — B is blocked by A).
- After marking A done: `nbd next --json` returns B (priority 8, now unblocked).
- With only done tickets: `nbd next --json` returns `{"next": null}`.
- `nbd next` (no `--json`) with no ready tickets prints "No ready tickets." and exits 0.
- `nbd next --filter type=bug --json`: create a bug and a task, both ready.
Returns the bug if it's the highest-priority bug, otherwise the highest-priority bug.
(Create bug priority 8, task priority 9: with filter, should return the bug.)
- `nbd next --json` returns a JSON object with a `"next"` key containing all ticket fields
including `"id"`.
- `nbd next --filter priority=99 --json` returns `{"next": null}` (no ticket has priority 99).