feat(nbd): remove id from ticket JSON body; inject from filename [d1634a]
The ticket id is now stored only in the filename stem (.nbd/tickets/{id}.json).
`id` is annotated with `#[serde(skip)]` so it is never written to disk,
eliminating the consistency hazard of id-in-body vs. filename disagreement.
- ticket.rs: add `#[serde(skip)]` to `Ticket::id`
- store.rs: `read_ticket` and `list_tickets` inject id from filename stem
- display.rs: `ticket_to_json_value` re-inserts id for CLI `--json` output
- tests.rs: new unit tests for omission, injection, and old-format compat
- integration.rs: assert written files lack "id"; assert read --json has id
Backwards-compatible: old files with "id" in JSON body still parse correctly
(serde ignores the unknown field), so existing stores work without migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
parent
7ada8ef951
commit
f1715d18eb
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "0f51af",
|
||||||
|
"title": "nbd migrate command",
|
||||||
|
"body": "Add `nbd migrate` to bring all ticket files on disk into conformance with the current schema. This is the standard mechanism for handling any schema change — field removals, field additions, renames, or type changes.\n\n## Motivation\n\nSchema changes (like removing `id` from the JSON body, adding new fields with defaults, or renaming a field) leave existing ticket files in an old format. `nbd migrate` re-serialises every ticket through the current serde schema, which:\n- **Removes** fields that no longer exist in `Ticket` (they are ignored on deserialise, then absent on re-serialise).\n- **Adds** new fields with their `#[serde(default)]` values.\n- **Normalises** any formatting differences (e.g. key order, whitespace).\n\nThe current immediate use case is scrubbing the `\"id\"` key from all existing `.json` files after the id-from-filename schema change.\n\n## Design principles\n\n- **Idempotent.** Running `nbd migrate` on an already-current store is a no-op (files are re-written identically).\n- **Non-destructive.** A failure on one ticket does not abort the rest; errors are collected and reported at the end.\n- **Source of truth unchanged.** If a ticket cannot be parsed, it is left on disk as-is and reported as an error.\n- **Dry-run available.** `--dry-run` prints what would change without writing.\n\n## Approach\n\n### main.rs\n\nAdd `Migrate` variant to `Commands`:\n```\nMigrate {\n /// Print changes without writing them.\n #[arg(long)]\n dry_run: bool,\n}\n```\n\nImplement `cmd_migrate(dry_run: bool) -> store::Result<()>`:\n1. `find_nbd_root()`\n2. Call `store::migrate_tickets(&root, dry_run).await`\n3. Print a summary: `Migrated N tickets (M errors)`.\n\n### store.rs\n\nAdd `migrate_tickets(root: &Path, dry_run: bool) -> Result<MigrateReport>`:\n1. `fs::read_dir(tickets_dir(root))`\n2. For each `*.json`, `*.md`, `*.toml`, `*.jsonb` file (all supported formats — the ones that exist now and future ones added by the multi-format feature):\n a. Read the raw bytes.\n b. Attempt to deserialise into current `Ticket`, injecting `id` from filename.\n c. Re-serialise to the current schema (same format as the original file's extension).\n d. Compare raw bytes. If unchanged, skip (count as already-current).\n e. If changed and `dry_run`: print `would update {filename}`, do not write.\n f. If changed and not `dry_run`: write the new bytes to the same path.\n g. If deserialise fails: record the error, leave the file untouched.\n3. Return `MigrateReport { updated: usize, already_current: usize, errors: Vec<(String, String)> }`.\n\n```rust\npub struct MigrateReport {\n pub updated: usize,\n pub already_current: usize,\n pub errors: Vec<(String, String)>, // (filename, error message)\n}\n```\n\n### display.rs\n\nAdd `print_migrate_report(report: &MigrateReport)`:\n```\nMigrated 3 tickets.\nCurrent 5 tickets (already up to date).\nErrors 1 ticket could not be migrated:\n bad_ticket.json: trailing comma at line 4\n```\n\nWhen `--json`, serialise `MigrateReport` directly (derive `Serialize`).\n\n## How schema changes use this\n\nFor **field removal** (e.g. removing `id` from JSON):\n- Old files have `\"id\": \"...\"` → on deserialise, serde ignores it (unknown field).\n- Re-serialise → `id` is absent (since `#[serde(skip)]`).\n- File bytes differ → `migrate` rewrites.\n\nFor **field addition** (e.g. adding `tags: Vec<String>` later):\n- New field in `Ticket` gets `#[serde(default)]`.\n- Old files lack `tags` → deserialise gives `vec![]`.\n- Re-serialise → `\"tags\": []` is written.\n- File bytes differ → `migrate` rewrites.\n\n## Tests\n\nUnit tests (`src/tests.rs`):\n- `migrate_tickets` on a store with old-format files (containing `\"id\"`) rewrites them without `id`.\n- `migrate_tickets` on an already-current store returns `updated: 0`, `already_current: N`.\n- `migrate_tickets --dry-run` does not modify files on disk.\n- A file with invalid JSON is counted in `errors` and left unchanged.\n\nIntegration tests (`tests/integration.rs`):\n- Create tickets with old code (inject `id` manually into JSON), run `nbd migrate`, verify `id` is gone from files.\n- `nbd migrate --dry-run` reports changes but does not modify files.\n- `nbd migrate` exits zero even when some tickets error (but prints error summary).\n- `nbd migrate --json` outputs a valid JSON object with `updated`, `already_current`, `errors` fields.\n\n## Files touched\n- `src/main.rs` — `Migrate` command, `cmd_migrate`\n- `src/store.rs` — `migrate_tickets`, `MigrateReport`\n- `src/display.rs` — `print_migrate_report`\n- `src/tests.rs` — unit tests\n- `tests/integration.rs` — integration tests\n- `README.md` — document `nbd migrate`",
|
||||||
|
"priority": 9,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [
|
||||||
|
"d1634a"
|
||||||
|
],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "1939a7",
|
||||||
|
"title": "nbd archive command and Closed status",
|
||||||
|
"body": "Add `Status::Closed` (serialised as `\"closed\"`) and a convenience `nbd archive <id>` command that sets it.\n\n## Motivation\n\n`done` tickets clutter `nbd list`. `closed` provides a soft-delete: the ticket is preserved on disk but excluded from normal listings by default.\n\n## Approach\n\n### ticket.rs\n- Add `Closed` variant to `Status` enum (after `Done`).\n- `#[serde(rename_all = \"snake_case\")]` already handles serialisation → `\"closed\"`.\n\n### main.rs\n- Update `parse_status` to accept `\"closed\"`.\n- Update `status_str` in `display.rs` to map `Status::Closed` → `\"closed\"`.\n- Add `Archive` variant to `Commands`:\n ```\n Archive { id: String }\n ```\n- Implement `cmd_archive(id, json)`: read ticket → set status to `Closed` → write → print.\n This is syntactic sugar for `nbd update <id> --status closed`.\n\n### display.rs\n- Add `\"closed\"` to `status_str` match arm.\n\n### list filtering\n- `nbd list` currently shows all tickets. After this change, it should by default **hide** `Closed` tickets.\n- Add a `--all` flag to `nbd list` to show all tickets including closed ones.\n- Update `list_tickets` or filter at the command handler level. Prefer filtering in `cmd_list` to keep `list_tickets` generic.\n\n## Tests\n- Unit test: `Status::Closed` serialises to `\"closed\"` and back.\n- Integration test: `nbd archive <id>` sets status to `closed`.\n- Integration test: `nbd list` does not show closed tickets.\n- Integration test: `nbd list --all` shows closed tickets.\n\n## Files touched\n- `src/ticket.rs` — add `Closed` variant\n- `src/main.rs` — `Archive` command, `parse_status` update, `--all` flag on `list`\n- `src/display.rs` — `status_str` update\n- `src/tests.rs` — unit tests\n- `tests/integration.rs` — integration tests\n- `README.md` — document archive and --all",
|
||||||
|
"priority": 6,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "460caf",
|
||||||
|
"title": "Multiple file format support (md, toml, jsonb)",
|
||||||
|
"body": "Add `--ftype` flag to `create` and `update` to write tickets in markdown, TOML, or binary JSON (CBOR) in addition to the existing JSON format. Format is detected from file extension on read.\n\n## Motivation\n\nMarkdown format lets agents write long-form ticket bodies with full markdown syntax, and makes tickets human-readable in a file browser. TOML is a natural config format. CBOR offers compact binary storage.\n\n## Approach\n\n### New crate dependencies (Cargo.toml)\nEvaluate and add:\n- `toml` — TOML serialisation (likely `toml = \"0.8\"`)\n- `serde_yml` or `serde_yaml` — YAML frontmatter (for `.md` files)\n- `ciborium` — CBOR binary JSON (`.jsonb`)\n\n### ticket.rs\nNo changes needed — `Ticket` already derives `Serialize`/`Deserialize`.\n\n### store.rs\nNew enum `FileFormat { Json, Markdown, Toml, Jsonb }`.\n\nNew function `detect_format(path: &Path) -> FileFormat`:\n- `.json` → `Json`\n- `.md` → `Markdown`\n- `.toml` → `Toml`\n- `.jsonb` → `Jsonb`\n- Unknown → `Json` (fallback)\n\nUpdate `ticket_path(root, id, format)` to use the format-appropriate extension. This is a breaking change to the function signature — update all callers.\n\nUpdate `read_ticket(root, id)`:\n1. Try each known extension in order until a file is found.\n2. Read the file and dispatch to the format-appropriate deserialiser.\n\nSerialisation helpers (private):\n- `serialize_json(ticket) -> String`\n- `serialize_toml(ticket) -> String`\n- `serialize_markdown(ticket) -> String` — TOML frontmatter (`+++` delimiters) with body as file content\n- `serialize_jsonb(ticket) -> Vec<u8>`\n\nDeserialization helpers (private):\n- `deserialize_markdown(bytes) -> Result<Ticket>` — parse frontmatter + body\n\nUpdate `list_tickets` to scan for `*.json`, `*.md`, `*.toml`, `*.jsonb` files.\n\nUpdate `write_ticket` to accept `format: FileFormat` and write in the appropriate format.\n\n### main.rs\nAdd `--ftype [json|md|toml|jsonb]` option (default `json`) to `create` and `update`.\nConversion on `update --ftype`: read old file, write new format, delete old file (if extension changed).\n\n## Markdown format (TOML frontmatter)\n```\n+++\nid = \"a3f9c2\"\ntitle = \"Fix login bug\"\npriority = 8\nstatus = \"in_progress\"\nticket_type = \"bug\"\ndependencies = [\"b7d41e\"]\n+++\n\nLong-form body text goes here. Supports full markdown.\n```\n\n## Tests\n- Unit tests: roundtrip each format (JSON already tested).\n- Integration tests: `nbd create --ftype md` creates a `.md` file; `nbd read` finds and parses it.\n- Integration test: `nbd update <id> --ftype toml` converts format and removes old file.\n\n## Files touched\n- `Cargo.toml` — new dependencies\n- `src/store.rs` — format detection, multi-format read/write, updated `list_tickets`\n- `src/main.rs` — `--ftype` flags\n- `src/tests.rs` — format roundtrip tests\n- `tests/integration.rs` — format integration tests\n- `README.md` — document `--ftype`\n- `docs/ARCHITECTURE.md` — update storage layout section",
|
||||||
|
"priority": 5,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "4d2359",
|
||||||
|
"title": "nbd init command",
|
||||||
|
"body": "Add an explicit `nbd init` subcommand that creates `.nbd/tickets/` in the current working directory, analogous to `git init`.\n\n## Motivation\n\nCurrently users must run `mkdir -p .nbd/tickets` manually before first use. This is a friction point — especially for first-time users and agent workflows bootstrapping a new project.\n\n## Approach\n\n1. Add `Init` variant to the `Commands` enum in `main.rs`.\n2. Implement `cmd_init(json: bool) -> store::Result<()>`:\n - Get cwd via `std::env::current_dir()`.\n - Call `store::ensure_tickets_dir(&cwd)` (already exists, idempotent).\n - Print confirmation: `initialised .nbd/tickets/ in <path>` (or JSON: `{\"root\": \"<path>\"}`).\n3. No changes to `store.rs` needed — `ensure_tickets_dir` already uses `create_dir_all` (idempotent).\n4. The command should NOT require `.nbd/` to already exist (i.e. do NOT call `find_nbd_root` here — use cwd directly).\n\n## Tests\n\n- Integration test: run `nbd init` in a fresh tempdir, verify `.nbd/tickets/` is created.\n- Integration test: run `nbd init` twice in the same dir — succeeds both times (idempotent).\n- Integration test: `nbd init --json` outputs valid JSON with a `root` field.\n\n## Files touched\n- `src/main.rs` — new `Init` variant and `cmd_init` handler\n- `tests/integration.rs` — new integration tests\n- `README.md` — update Initialise section",
|
||||||
|
"priority": 7,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "5f1495",
|
||||||
|
"title": "nbd update diff output",
|
||||||
|
"body": "Show a git-diff-style +/- summary of what changed when `nbd update` is run without `--json`.\n\n## Motivation\n\nCurrently `nbd update` prints the full ticket after the change, making it hard to see at a glance what actually changed. A diff view — showing only changed fields — is more informative.\n\n## Approach\n\n### display.rs\nAdd `format_diff(old: &Ticket, new: &Ticket) -> String`:\n- Compare each field between `old` and `new`.\n- For each field that changed, emit two lines:\n ```\n - status: todo\n + status: in_progress\n ```\n- If no fields changed, emit `(no changes)`.\n- Fields compared: `title`, `body`, `priority`, `status`, `ticket_type`, `dependencies`.\n- `id` is never shown (it cannot change).\n- Label width matches `format_ticket` (LABEL_WIDTH).\n\nAdd `print_diff(old: &Ticket, new: &Ticket)` that calls `println!(\"{}\" format_diff(...))`.\n\n### main.rs\nIn `cmd_update`:\n- Before applying changes: `let old = ticket.clone();`\n- After `write_ticket`:\n - If `json`: current behaviour (print new ticket as JSON).\n - Else: `display::print_diff(&old, &ticket)`.\n\n## Tests\n\nUnit tests in `src/tests.rs`:\n- `format_diff` shows changed fields only.\n- `format_diff` with identical tickets outputs `(no changes)`.\n- Changed dependencies are shown as comma-separated lists on each line.\n\nIntegration test:\n- `nbd update <id> --status in_progress` (no `--json`) prints `- status:` and `+ status:` lines.\n- `nbd update <id> --json` still prints full JSON (no diff).\n\n## Files touched\n- `src/display.rs` — `format_diff`, `print_diff`\n- `src/main.rs` — `cmd_update` uses `print_diff`\n- `src/tests.rs` — unit tests for `format_diff`\n- `tests/integration.rs` — integration tests",
|
||||||
|
"priority": 5,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "6e4239",
|
||||||
|
"title": "Nix flake for nbd",
|
||||||
|
"body": "Add `nbd/flake.nix` so that `nbd` can be consumed as a Nix package and used as a CLI tool in other projects in the mono-repo.\n\n## Motivation\n\nOther `vibed` services and external projects should be able to include `nbd` as a dev dependency via Nix, getting a pinned, reproducible binary without needing Cargo installed.\n\n## Approach\n\n### nbd/flake.nix\n\nFollow the pattern from the repo root `flake.nix`. The service flake should:\n1. Inherit the base flake's inputs (nixpkgs, rust-overlay, etc.) or declare its own.\n2. Define a `packages.default` attribute that builds the `nbd` crate with `rustPlatform.buildRustPackage`.\n3. Define a `devShells.default` that includes the `nbd` binary and standard Rust tooling (rustfmt, clippy, cargo).\n4. Optionally expose an `apps.default` for `nix run .#nbd`.\n\n### Cargo.lock\nEnsure `Cargo.lock` is committed (it already is) — required for reproducible Nix builds.\n\n### cargoHash / cargoSha256\nThe `buildRustPackage` derivation requires a `cargoHash` (or `cargoSha256`). Use the correct fetcher approach (vendored deps or `fetchCargoTarball`).\n\n## Steps\n1. Read the root `flake.nix` to understand the base structure.\n2. Write `nbd/flake.nix` following the same conventions.\n3. Run `nix build .#nbd` from the `nbd/` directory to verify it builds.\n4. Run `nix run .#nbd -- --help` to verify the binary works.\n\n## Files touched\n- `nbd/flake.nix` — new file\n- `README.md` — add `nix run` usage section",
|
||||||
|
"priority": 4,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "task"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "833807",
|
||||||
|
"title": "SQLite cache for list performance",
|
||||||
|
"body": "Add an optional SQLite cache in `.nbd/cache.db` to accelerate `nbd list` and `nbd ready` for large ticket stores.\n\n## Motivation\n\n`list_tickets` currently does O(n) file reads on every call. For stores with hundreds of tickets this is measurably slow. A SQLite cache avoids re-reading unchanged files by comparing file modification times (mtimes).\n\n## Approach\n\n### Crate dependency\nAdd `sqlx` with the `sqlite` and `runtime-async-std` features to `Cargo.toml`.\n\n### store.rs additions\nNew async function `open_cache(root: &Path) -> Result<sqlx::SqlitePool>`:\n- Opens (or creates) `.nbd/cache.db`.\n- Runs a migration: `CREATE TABLE IF NOT EXISTS tickets (id TEXT PRIMARY KEY, json TEXT NOT NULL, mtime INTEGER NOT NULL)`.\n\nNew function `list_tickets_cached(root: &Path) -> Result<Vec<Ticket>>`:\n1. Open cache.\n2. Read directory listing to get file names and mtimes.\n3. For each file: if the DB has a row with matching mtime, use cached JSON; otherwise read file, parse, insert/update row.\n4. Delete DB rows for IDs no longer on disk.\n5. Return deserialized tickets sorted by priority desc.\n\nKeep existing `list_tickets` as the non-cached fallback. Consider making `cmd_list` and `cmd_ready` use `list_tickets_cached` when available.\n\n### Migration strategy\n- The cache is always optional. If `cache.db` can't be opened, fall back to `list_tickets` (log a warning to stderr).\n- The cache is never the source of truth — the JSON files are. The cache is always reconstructable by deleting `.nbd/cache.db`.\n\n## Decision point\nDecide whether to enable the cache unconditionally or gate it behind a flag (`--cache` / `NBD_CACHE=1`). Recommendation: enable by default once the feature is stable.\n\n## Tests\n- Unit test: cache hit returns same data as direct file read.\n- Unit test: cache miss (mtime changed) re-reads the file.\n- Unit test: deleted ticket is evicted from cache.\n- Performance test (optional): benchmark 1000-ticket list with and without cache.\n\n## Files touched\n- `Cargo.toml` — add `sqlx`\n- `src/store.rs` — `open_cache`, `list_tickets_cached`\n- `src/tests.rs` — cache unit tests\n- `docs/ARCHITECTURE.md` — document the cache layer",
|
||||||
|
"priority": 3,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "c9d551",
|
||||||
|
"title": "Partial ID matching",
|
||||||
|
"body": "Allow `nbd read`, `nbd update`, and dependency resolution to accept a prefix of a ticket ID (e.g. `nbd read a3f` resolves to `a3f9c2`).\n\n## Motivation\n\n6-character hex IDs are tedious to type in full. Prefix matching — like git's short-SHA resolution — significantly improves interactive ergonomics and makes agent-generated commands shorter.\n\n## Approach\n\nAdd `resolve_id(root: &Path, id_or_prefix: &str) -> Result<String>` in `store.rs`:\n1. If `id_or_prefix` is exactly 6 characters, try `read_ticket` as-is (fast path, existing behaviour).\n2. Otherwise (or if not found), scan `.nbd/tickets/` for files whose stem starts with `id_or_prefix`.\n3. Collect all matches.\n - 0 matches → error: `\"no ticket found matching '{prefix}'\"`\n - 1 match → return the full ID\n - 2+ matches → error: `\"ambiguous prefix '{prefix}' matches: {id1}, {id2}, ...\"`\n\nUse `resolve_id` inside `cmd_read` and `cmd_update` (replacing the bare `id` string passed to `read_ticket`). Also use it inside `validate_deps` so dependency flags can use short IDs too.\n\n## Tests\n\nUnit tests in `src/tests.rs`:\n- Exact 6-char match still works.\n- 3-char prefix resolves correctly.\n- Ambiguous prefix returns an error listing all matching IDs.\n- Unknown prefix returns a not-found error.\n\nIntegration tests in `tests/integration.rs`:\n- `nbd read <3-char-prefix>` resolves and prints the ticket.\n- `nbd update <3-char-prefix> --status done` succeeds.\n- Ambiguous prefix exits non-zero with an informative message.\n\n## Files touched\n- `src/store.rs` — new `resolve_id` function\n- `src/main.rs` — `cmd_read`, `cmd_update`, `validate_deps` use `resolve_id`\n- `src/tests.rs` — unit tests\n- `tests/integration.rs` — integration tests",
|
||||||
|
"priority": 8,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"title": "Remove id field from ticket JSON body",
|
||||||
|
"body": "The ticket ID is already encoded in the filename (`a3f9c2.json`). Storing it redundantly inside the JSON body creates a potential consistency hazard (the two could disagree) and wastes space. The filename should be the sole source of truth for the ID.\n\n## Motivation\n\n- Eliminates a redundancy: filename stem already IS the id.\n- Removes the risk of id mismatch (e.g. if a file is renamed manually).\n- Simplifies the JSON schema — consumers only need the body fields, not a duplicated key.\n\n## Serde approach\n\nIn `ticket.rs`, annotate the `id` field with `#[serde(skip)]`:\n\n```rust\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct Ticket {\n #[serde(skip)]\n pub id: String,\n // ...\n}\n```\n\n`#[serde(skip)]` means:\n- **Serialise:** the `id` field is omitted from JSON output entirely.\n- **Deserialise:** the field is not read from JSON; it is initialised with `String::default()` (empty string) and must be set manually after deserialization.\n\nBecause serde ignores unknown fields by default, **existing files with `\"id\": \"...\"` in the JSON body continue to deserialise without error** — the field is simply discarded. This means the change is backwards-compatible for reads; existing stores work immediately. A separate `nbd migrate` command (see companion ticket) cleans up the stale `id` field from disk.\n\n## store.rs changes\n\n`read_ticket(root, id)` — inject id from the `id` parameter after deserialising:\n```rust\nlet mut ticket: Ticket = serde_json::from_slice(&bytes)?;\nticket.id = id.to_string(); // authoritative source: the filename\nOk(ticket)\n```\n\n`list_tickets(root)` — inject id from each file's stem:\n```rust\nlet stem = path.file_stem().and_then(|s| s.to_str()).ok_or(\"invalid filename\")?;\nlet mut ticket: Ticket = serde_json::from_slice(&bytes)?;\nticket.id = stem.to_string();\ntickets.push(ticket);\n```\n\n`write_ticket` — no change needed; `#[serde(skip)]` already prevents `id` from being written.\n\n`ticket_path` — no change; it already takes `id: &str` as a separate parameter.\n\n## Impact on other tickets\n\n- The `nbd migrate` companion ticket (see deps) provides the command to scrub the old `id` field from existing files.\n- Partial ID matching (`resolve_id`) is unaffected — it works on filenames, not JSON content.\n- All display, list, and read commands continue to work; `ticket.id` is populated from the filename in every read path.\n\n## Tests\n\nUnit tests (`src/tests.rs`):\n- `write_ticket` output does NOT contain the `\"id\"` key.\n- `read_ticket` with a file that has NO `id` field correctly sets `ticket.id` from the parameter.\n- `read_ticket` with an old-format file (has `\"id\"` in JSON) still sets `ticket.id` from the parameter (ignores JSON value).\n- `list_tickets` injects correct ids from filenames for all tickets.\n- Serialisation roundtrip: write then read, id is preserved via filename not JSON.\n\nIntegration tests (`tests/integration.rs`):\n- `nbd create` output (tabular and `--json`) contains the correct ID.\n- The created `.json` file on disk does NOT contain the `\"id\"` key.\n- `nbd read <id>` displays the correct ID.\n\n## Files touched\n- `src/ticket.rs` — add `#[serde(skip)]` to `id`\n- `src/store.rs` — `read_ticket` and `list_tickets` inject id from filename\n- `src/tests.rs` — update and add unit tests\n- `tests/integration.rs` — add assertion that written files lack `\"id\"` key",
|
||||||
|
"priority": 9,
|
||||||
|
"status": "done",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "task"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "e1968f",
|
||||||
|
"title": "nbd ready command",
|
||||||
|
"body": "Add `nbd ready` subcommand that lists tickets which are actionable right now: not yet done and with all dependencies completed.\n\n## Motivation\n\nAgent workflows need to know which tickets are unblocked. `nbd list` shows everything; `nbd ready` narrows to what can actually be started immediately.\n\n## Approach\n\n1. Add `Ready` variant to `Commands` enum in `main.rs`.\n2. Implement `cmd_ready(json: bool)`:\n a. `list_tickets(root)` to fetch all tickets.\n b. Build a set of IDs for tickets with `status == Status::Done`.\n c. Filter to tickets where:\n - `ticket.status != Status::Done` (not already finished)\n - All IDs in `ticket.dependencies` are in the done-set (or the dep doesn't exist — treat missing deps as unresolved, not ready).\n d. Print the filtered slice using existing `display::print_list` / `print_list_json`.\n3. No new store or display functions needed — reuse existing.\n\n## Edge cases\n- A ticket with no dependencies and status `todo` → ready.\n- A ticket whose dep is `in_progress` → NOT ready.\n- Missing dep ID → NOT ready (treat conservatively).\n- Empty store → returns empty list (not an error).\n\n## Tests\n\nUnit-style integration tests:\n- Three tickets: A (no deps, todo), B (dep A, todo), C (no deps, done). `nbd ready` should return only A.\n- After marking A done, `nbd ready` should return B.\n- `nbd ready --json` returns a JSON array of the ready tickets.\n\n## Files touched\n- `src/main.rs` — new `Ready` variant and `cmd_ready` handler\n- `tests/integration.rs` — new integration tests",
|
||||||
|
"priority": 7,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "fc444f",
|
||||||
|
"title": "nbd claude-md command",
|
||||||
|
"body": "Add `nbd claude-md` subcommand that prints a ready-to-paste CLAUDE.md snippet for adopting `nbd` in any project. The snippet content is maintained as a source file and baked into the binary at compile time via `include_str!`.\n\n## Motivation\n\nEvery project that wants to use `nbd` needs the same boilerplate in its CLAUDE.md: what `nbd` is, how to invoke it, the create/update/done workflow, and the key guidelines. Without this command, that block has to be written by hand and tends to drift out of date as `nbd` evolves. By owning the canonical snippet in the `nbd` source tree and embedding it into the binary, the snippet stays in sync with the tool automatically — projects just run `nbd claude-md >> CLAUDE.md` when they adopt or upgrade `nbd`.\n\n## Snippet file\n\nCreate `src/claude_md_snippet.md`. This is a plain markdown file, checked into the repository, that contains the CLAUDE.md section any adopting project should paste in. It should cover:\n\n- One-sentence description of `nbd`.\n- Initialisation (`nbd init`).\n- The four core commands with examples (`create`, `list`, `read`, `update`).\n- The `nbd ready` command for finding unblocked tickets.\n- The workflow (create before starting → set in_progress → set done).\n- Guidelines: always `--json`, priority scale, type choices, dep usage.\n\nThe file is written for a project where `nbd` is installed in PATH (i.e. not via `cargo run`). It should be self-contained — no references to the `nbd` crate internals.\n\n## Binary embedding\n\nIn `main.rs`, embed the snippet at compile time:\n\n```rust\nconst CLAUDE_MD_SNIPPET: &str = include_str!(\"claude_md_snippet.md\");\n```\n\n`include_str!` resolves paths relative to the source file (`src/`), so this looks for `src/claude_md_snippet.md`. Cargo rebuilds the binary automatically when the file changes.\n\n## Command implementation\n\nAdd `ClaudeMd` variant to `Commands` in `main.rs`:\n\n```rust\n/// Print a CLAUDE.md snippet for adopting nbd in a project.\nClaudeMd,\n```\n\nThe `--json` global flag applies:\n- Without `--json`: `print!(\"{CLAUDE_MD_SNIPPET}\")` — raw markdown, suitable for redirect (`nbd claude-md >> CLAUDE.md`).\n- With `--json`: output `{\"snippet\": \"<escaped content>\"}` — for programmatic consumption.\n\nNo store access needed — this command is pure output from the embedded constant. It does not call `find_nbd_root()`.\n\n## No display.rs changes needed\n\nThe output is a single `print!` call in the command handler. No tabular formatting.\n\n## Tests\n\nIntegration tests (`tests/integration.rs`):\n- `nbd claude-md` exits zero and stdout is non-empty.\n- stdout contains key strings (`\"nbd\"`, `\"CLAUDE.md\"` or similar section header, `\"--json\"`).\n- `nbd claude-md --json` exits zero and stdout is valid JSON with a `\"snippet\"` key whose value is a non-empty string.\n- `nbd claude-md` works even when run from a directory with no `.nbd/` (no `find_nbd_root` call).\n\n## Files touched\n- `src/claude_md_snippet.md` — new file; the canonical snippet content\n- `src/main.rs` — `include_str!` constant, `ClaudeMd` command variant and handler\n- `tests/integration.rs` — integration tests\n- `README.md` — mention `nbd claude-md` in the Usage section",
|
||||||
|
"priority": 6,
|
||||||
|
"status": "todo",
|
||||||
|
"dependencies": [],
|
||||||
|
"ticket_type": "feature"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue