From 5af4cfabd161cd9a22523a3555e88f6f4807e3ac Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Mon, 18 May 2026 22:23:52 -0500 Subject: [PATCH 1/5] docs(rivets): rivets-wsix audit dir (gilfoyle prove-it + design + plan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit-trail commit for rivets-wsix. Following the gilfoyle workflow, the diagnostic dir holds the probe scripts, oracle, design, and plan that preceded the regression-fence work landing in subsequent slices. Key finding from prove-it-prototype: ZERO new bugs. The schema's ON DELETE CASCADE chain (refs.in_symbol_id, attributes.symbol_id, arch_*.parent FKs) plus per-file `DELETE FROM symbols WHERE file_id` quietly handles re-index correctness for most tables — not the `clear_all_X` pattern that lcb6 established. wsix's mental model ("UPSERT-only ⇒ needs clear_all_X") was a special case, not the general principle. See `.rivets-wsix/what-i-learned.md` for the per-table inventory. Empirical probes (sqlite3 against a real tempdir workspace) contradicted my initial code-reading hypothesis that `refs` would accumulate without an explicit clear. The cascade via in_symbol_id catches it. Documented in `.rivets-wsix/probe_refs_bug.sh` and `.rivets-wsix/probe_null_in_symbol.sh`. Subsequent slices ship three integration tests that lock in the audited cascade-correctness (claims 1-3 in `.rivets-wsix/design.md`) so future schema changes can't silently regress the bug class wsix was looking for. Also includes the `.rivets/issues.jsonl` status update marking wsix in_progress. --- .rivets-wsix/design.md | 95 +++++ .rivets-wsix/oracle.sh | 58 +++ .rivets-wsix/plan.md | 328 ++++++++++++++++ .rivets-wsix/probe.sh | 32 ++ .rivets-wsix/probe_null_in_symbol.sh | 94 +++++ .rivets-wsix/probe_refs_bug.sh | 108 ++++++ .rivets-wsix/related-issues.md | 23 ++ .rivets-wsix/what-i-learned.md | 59 +++ .rivets/issues.jsonl | 540 +++++++++++++-------------- 9 files changed, 1067 insertions(+), 270 deletions(-) create mode 100644 .rivets-wsix/design.md create mode 100644 .rivets-wsix/oracle.sh create mode 100644 .rivets-wsix/plan.md create mode 100644 .rivets-wsix/probe.sh create mode 100644 .rivets-wsix/probe_null_in_symbol.sh create mode 100644 .rivets-wsix/probe_refs_bug.sh create mode 100644 .rivets-wsix/related-issues.md create mode 100644 .rivets-wsix/what-i-learned.md diff --git a/.rivets-wsix/design.md b/.rivets-wsix/design.md new file mode 100644 index 0000000..6580e55 --- /dev/null +++ b/.rivets-wsix/design.md @@ -0,0 +1,95 @@ +# rivets-wsix design: cascade-correctness regression fences + +## Purpose + +The wsix audit found **zero bugs** (see `what-i-learned.md`). The schema's +`ON DELETE CASCADE` chain plus per-file `DELETE FROM symbols WHERE file_id` +quietly handles re-index correctness for refs, attributes, and other tables — +not the `clear_all_X` pattern lcb6 established. The remaining risk is **schema +drift**: a future change to the cascade FKs, or a new INSERT site added without +a paired clear or cascade, would silently re-introduce the bug class wsix +worried about. + +This design defines integration-test fences that lock in the current +correctness so CI catches schema drift before it merges. + +## Approach + +**One new integration test file**: `crates/tethys/tests/reindex_cascade.rs`. + +Each test exercises one cascade or per-file-clear invariant against a tiny +tempdir fixture, using `Tethys::index()` (the production entry point) and +direct SQLite queries (independent of the indexing pipeline's internal state). +Pattern is the same as `pass2_qualified_paths.rs` from PR #74: `tempfile` + +`workspace_with_files` helper + `rusqlite` for the oracle. + +## Falsification + +| # | Claim | Falsifier | Oracle | Cost | Status | Regression fence | +|---|-------|-----------|--------|------|--------|------------------| +| 1 | After a file's source is mutated to remove a function-body call, re-indexing without DB reset removes the corresponding row from `refs` (the `in_symbol_id`-cascade chain works). | Construct a fixture with `fn entry() { helper::a(); helper::b(); }`. Index. Mutate source to remove `helper::b()`. Re-index. Count refs in entry's file. Expected: count drops by exactly 1. If unchanged or higher, claim false. | Manually count call expressions in the new source via inspection; SQL `COUNT(*)` against the post-mutation DB. | 5 min | **passed** (probe_refs_bug.sh observed 2 → 3 → 1) | New integration test `refs_cascade_on_call_removal` in `reindex_cascade.rs`. | +| 2 | After a file's source is mutated to remove an entire function definition that carried `#[some_attr]`, re-indexing without DB reset removes BOTH the symbol row AND its `attributes` row (`attributes` cascades via `symbols(id) ON DELETE CASCADE`). | Fixture with `#[allow(dead_code)] fn target() {}`. Index. Capture `(symbol_count, attribute_count)`. Remove the `target` fn from source. Re-index. Capture again. Expected: both decrease by exactly 1. | Direct SQL count: `SELECT COUNT(*) FROM symbols WHERE name = 'target'` and `SELECT COUNT(*) FROM attributes a JOIN symbols s ON s.id = a.symbol_id WHERE s.name = 'target'`. | 5 min | pending | New integration test `attributes_cascade_on_symbol_removal` in `reindex_cascade.rs`. | +| 3 | Re-indexing an unchanged workspace produces stable `call_edges` and `file_deps` counts (the existing `clear_all_X` discipline works; lcb6 + sibling). | Fixture with two files, one calling the other (so call_edges and file_deps have rows). Index twice with no source change. Compare counts. Expected: equal across runs. If second run is greater, the clear_all is missing or not called. | Direct SQL count: `SELECT COUNT(*) FROM call_edges` and `SELECT COUNT(*) FROM file_deps`. | 5 min | pending | New integration test `clear_all_tables_stable_under_reindex` in `reindex_cascade.rs`. Already partially covered by `file_deps_idempotency.rs` (rivets-lcb6) — this test adds the call_edges twin and the joint stability assertion. | + +## Self-review (per the v1.0.3 falsifiable-design checklist) + +### 1. Claim count +3 claims. In the 3-15 healthy band. Each defends a distinct invariant. + +### 2. Falsifier independence +- Claim 1's oracle is direct SQL `COUNT(*)` and tree-sitter-independent manual call enumeration. Probe used Bash + sqlite3 CLI; the test will use Rust + rusqlite. Different mechanisms. +- Claim 2's oracle is direct SQL queries against `symbols` and `attributes`. Indexer is the SUT; the SQL is independent. +- Claim 3's oracle is direct SQL `COUNT(*)`. Independent of indexer. + +### 3. Falsifier non-vacuity +For each claim, a concrete buggy implementation that would make the fence fail: + +- **Claim 1**: if `refs.in_symbol_id REFERENCES symbols(id) ON DELETE CASCADE` were silently relaxed to `... ON DELETE SET NULL` (or the FK dropped entirely) in a future migration, the cascade wouldn't fire and removed refs would persist. The test asserts a specific count decrease — would fail directly. +- **Claim 2**: if a future schema migration changed `attributes.symbol_id REFERENCES symbols(id) ON DELETE CASCADE` to `ON DELETE NO ACTION`, removed symbols would leave orphan attribute rows. Test would observe `attribute_count` unchanged after symbol removal. +- **Claim 3**: if a future change to `index_with_options` accidentally removed the `clear_all_file_deps()` call at `indexing.rs:139`, file_deps would accumulate across runs and the test's count-stability assertion would fail. + +Vacuity checks (per the v1.0.3 anti-pattern list): +- No `column LIKE '%...%' AND symbol_id IS NOT NULL` filters (the I1 shape from PR #74). +- No disjunctive assertions where one disjunct is also asserted standalone (the R2-1 shape). +- All assertions are direct count equalities. TDD inversion: each test would fail under exactly one specific code mutation (the cascade FK removal for Claim 1/2, the `clear_all` removal for Claim 3). None passes-when-bug-present. + +### 4. Per-claim verification distinctness +- Claim 1's failure message: "refs count for src/lib.rs did not decrease after removing helper::b()". Localizes to the in_symbol_id cascade. +- Claim 2's failure message: "attributes count for symbol 'target' did not decrease after removing the function definition". Localizes to the symbols→attributes cascade. +- Claim 3's failure message: "call_edges count or file_deps count grew across unchanged-source re-index runs". Localizes to the clear_all discipline. +Each test has a distinct failure mode that names the failing invariant. + +### 5. Cost distribution +All three falsifiers are ≤ 5 min each. Cheap, deterministic, CI-friendly. No expensive falsifiers in the design. + +### 6. Negative space (what this design deliberately does NOT do) + +- **Does not fix any bug.** The audit found zero. This is a regression-fence-only design. +- **Does not cover the orphan-file-from-disk case.** Files deleted from disk leave `files` rows behind; their cascade-dependent rows persist. Tracked separately as **rivets-dhxo**. This design's fixtures all retain the original file paths to isolate the per-file re-index path from the orphan-file path. +- **Does not test the streaming-mode (`IndexOptions::with_streaming()`) indexing variant.** Default full-index path only. Streaming has divergent behavior per dhxo's analysis. +- **Does not add an architectural meta-fence** (e.g., "every new INSERT site in `db/*.rs` must have a paired clear path"). That would be a more invasive test (parsing source code at test time) and is properly its own follow-up issue. If a future PR introduces a new bug-class table, the dhxo-style review process should catch it — but a meta-fence would be belt-and-braces. Filed considerations: + - Could be a separate rivets task post-merge if the team wants it. + - As written, the three fences here are surface-only: they catch cascade or clear_all *regressions* on the audited tables. They do not catch new bug-class tables added in the future. + +### 7. Tracker references + +- This design references **rivets-wsix** (this issue) and **rivets-dhxo** (orphan-file boundary) and **rivets-lcb6** (the file_deps fix that established the precedent). All three are real tracker entries verified by `rivets show`. +- No "defer to follow-up" language in the design body. The architectural meta-fence consideration in #6 is explicitly out of scope, not deferred — meaning it can be picked up later if desired, but is not a tracked follow-up obligation. If the user decides post-review to file it, it becomes a new issue ID; otherwise it lives in this design as out-of-scope rationale. + +## What's NOT in this design (consolidated) + +1. No production code changes — fence-only. +2. No coverage of `IndexOptions::with_streaming()` mode (dhxo territory). +3. No architectural meta-fence for new INSERT sites (see #6). +4. No coverage of cross-crate or `--lsp` Pass-3 cascade behavior (Pass 2 and earlier only). + +## Output of falsifiable-design (hard gate) + +- [x] Probe and oracle agreed on the smallest slice (see `what-i-learned.md`) +- [x] Each claim has a falsifier +- [x] At least one falsifier has been run (Claim 1 via probe_refs_bug.sh — passed) +- [x] Self-review checklist applied (sections 1-7 above) +- [x] All claims falsifiable +- [x] All tracker references verified to exist + +Ready for budgeted-plan. diff --git a/.rivets-wsix/oracle.sh b/.rivets-wsix/oracle.sh new file mode 100644 index 0000000..3df3fcb --- /dev/null +++ b/.rivets-wsix/oracle.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# rivets-wsix oracle: independent enumeration of tethys's tables via schema +# DDL, then audit ALL insert sites in crates/tethys/src/ for each table. +# +# Different mechanism from probe.sh: +# - Probe assumed 1 table per file (db/.rs convention). +# - Oracle reads actual CREATE TABLE statements from schema.rs, then +# greps for INSERT INTO
across the whole tethys crate. +# - Catches tables that don't follow the file naming convention +# (subordinate tables like enum_variants, struct_fields per PR #58). +# +# Output: for each schema-declared table, list every INSERT site found +# and flag UPSERT shape. + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +SCHEMA_FILE="crates/tethys/src/db/schema.rs" + +# Extract table names from CREATE TABLE statements (handle quoted, IF NOT EXISTS, etc.) +tables=$(grep -oE "CREATE TABLE (IF NOT EXISTS )?[a-z_]+" "$SCHEMA_FILE" \ + | awk '{print $NF}' | sort -u) + +echo "=== Tables declared in $SCHEMA_FILE ===" +echo "$tables" +echo + +echo "=== INSERT sites per table ===" +for t in $tables; do + # Find INSERT INTO
( or INSERT OR REPLACE INTO
+ sites=$(grep -rnE "INSERT( OR REPLACE)? INTO ${t}\b" \ + crates/tethys/src --include="*.rs" 2>/dev/null || true) + if [[ -z "$sites" ]]; then + printf "%-30s [NO INSERT SITES FOUND]\n" "$t" + continue + fi + while IFS= read -r line; do + file_line=$(echo "$line" | cut -d: -f1-2) + sql=$(echo "$line" | cut -d: -f3- | sed 's/^[[:space:]]*//' \ + | cut -c1-100) + upsert_marker="" + # Check if the line OR a nearby line (within 5 lines) has UPSERT shape + loc=$(echo "$line" | cut -d: -f1-2) + fpath=$(echo "$loc" | cut -d: -f1) + lnum=$(echo "$loc" | cut -d: -f2) + end=$((lnum + 5)) + nearby=$(sed -n "${lnum},${end}p" "$fpath" 2>/dev/null || true) + if echo "$nearby" | grep -qE "ON CONFLICT.*DO UPDATE|INSERT OR REPLACE"; then + upsert_marker=" [UPSERT]" + fi + printf "%-30s %s%s\n" "$t" "$file_line" "$upsert_marker" + done <<< "$sites" +done + +echo +echo "=== clear_all_X function definitions ===" +grep -rnE "fn clear_all_[a-z_]+" crates/tethys/src --include="*.rs" \ + | sed 's|crates/tethys/src/||' diff --git a/.rivets-wsix/plan.md b/.rivets-wsix/plan.md new file mode 100644 index 0000000..993370a --- /dev/null +++ b/.rivets-wsix/plan.md @@ -0,0 +1,328 @@ +# rivets-wsix budgeted plan + +Three slices, one per design claim. All slices are pure-test additions (no production code changes). Single new file `crates/tethys/tests/reindex_cascade.rs`. Per-slice budgets reflect test execution time, not production runtime. + +## Slice 1: refs cascade on call removal + +**Claim:** [design C1] After a file's source is mutated to remove a function-body call, re-indexing without DB reset removes the corresponding row from `refs`. + +**Oracle:** Direct SQL `SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id WHERE f.path='src/lib.rs'`. Independent of the indexer: the assertion reads SQLite directly, not through any tethys API layer. + +**Stress fixture:** +Starting source: `pub fn entry() { helper::a(); helper::b(); helper::c(); }` → 3 cross-file call refs. +Mutated source: `pub fn entry() { helper::a(); helper::c(); }` → 2 calls (removed the MIDDLE one). +Adversarial intent: +- If the cascade chain were "wipe all refs and re-insert," count goes 3 → 2. ✓ test passes. +- If the cascade missed the middle ref specifically (some weird "first match wins" bug), count goes 3 → 3 or 3 → 1. ✗ test fails. +- If no clearing happened, count goes 3 → 5 (2 new + 3 stale). ✗ test fails. +The exact-count assertion `== 2` distinguishes all three. The middle-removal shape defeats a hypothetical "head-only" or "tail-only" cascade bug. + +Also assert: the surviving refs target `helper::a` and `helper::c` specifically (not `helper::b`). Catches the "cascade ran but kept the wrong refs" failure mode. + +**Loop budget:** N/A — the slice adds no production loops; only test code. + +**Wall budget:** ≤ 5s per test (target for nextest CI). Each `Tethys::index()` call on this fixture is sub-second on the laptop in earlier probes. + +**Files:** +- `crates/tethys/tests/reindex_cascade.rs` (new, this slice creates it) + +**Code (advisory):** +```rust +//! Regression fences for rivets-wsix: cascade-correctness across re-index runs. +//! +//! The wsix audit (see .rivets-wsix/what-i-learned.md) found that re-index +//! correctness for `refs`, `attributes`, and `symbols` relies on the schema's +//! ON DELETE CASCADE chain, not the `clear_all_X` pattern. These tests lock +//! in that cascade-correctness so a future schema change (e.g., relaxing a +//! cascade FK to SET NULL or NO ACTION) is caught in CI. + +use rusqlite::params; + +mod common; +use common::{open_db, workspace_with_files}; + +/// Pin claim 1: removing a function-body call from a file's source produces +/// exactly one row removal from `refs` after re-index, via the +/// `refs.in_symbol_id REFERENCES symbols(id) ON DELETE CASCADE` chain. +#[test] +fn refs_cascade_on_call_removal() { + let (dir, mut tethys) = workspace_with_files(&[ + ("Cargo.toml", r#" +[package] +name = "wsix_refs" +version = "0.0.0" +edition = "2021" +"#), + ("src/lib.rs", r" +mod helper; + +pub fn entry() { + helper::a(); + helper::b(); + helper::c(); +} +"), + ("src/helper.rs", r" +pub fn a() {} +pub fn b() {} +pub fn c() {} +"), + ]); + + tethys.index().expect("initial index"); + + let conn = open_db(&tethys); + let refs_pre: i64 = conn.query_row( + "SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id JOIN symbols s ON s.id=r.symbol_id + WHERE f.path='src/lib.rs' AND s.name IN ('a','b','c')", + params![], |row| row.get(0), + ).expect("count pre"); + + // Sanity: indexer produced what we expected. + assert_eq!(refs_pre, 3, "fixture should produce 3 cross-file call refs"); + + // Mutate source: remove the MIDDLE call. Belt-and-braces mtime bump so + // tethys doesn't skip-by-hash. + std::fs::write(dir.path().join("src/lib.rs"), r" +mod helper; + +pub fn entry() { + helper::a(); + helper::c(); +} +").expect("rewrite"); + std::thread::sleep(std::time::Duration::from_secs(1)); + filetime::set_file_mtime(dir.path().join("src/lib.rs"), + filetime::FileTime::now()).expect("touch"); + + tethys.index().expect("re-index"); + + let conn = open_db(&tethys); + let refs_post: i64 = conn.query_row( + "SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id JOIN symbols s ON s.id=r.symbol_id + WHERE f.path='src/lib.rs' AND s.name IN ('a','b','c')", + params![], |row| row.get(0), + ).expect("count post"); + + assert_eq!(refs_post, 2, "expected 2 refs (a,c) after removing b()"); + + // Stronger: the surviving refs target a and c specifically. + let b_refs: i64 = conn.query_row( + "SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id JOIN symbols s ON s.id=r.symbol_id + WHERE f.path='src/lib.rs' AND s.name='b'", + params![], |row| row.get(0), + ).expect("count b"); + assert_eq!(b_refs, 0, "ref to helper::b must be gone after source removal"); +} +``` + +If `filetime` is not already a dev-dependency, fall back to deleting + recreating the file (which definitively changes mtime). If even that doesn't trigger re-index, use `Tethys::index_with_options(IndexOptions::rebuild())` — but that defeats the purpose since `--rebuild` does `db.reset()`. The first option (filetime) is the surgical answer. + +**Verification:** +- [ ] `cargo nextest run -p tethys --test reindex_cascade refs_cascade_on_call_removal` passes +- [ ] Mutate the assertion to `assert_eq!(refs_post, 3)` (the "bug present" case) and confirm the test fails — TDD-inversion proves non-vacuity +- [ ] `cargo clippy -p tethys --tests -- -D warnings` clean + +--- + +## Slice 2: symbols → attributes cascade on symbol removal + +**Claim:** [design C2] Removing an attributed function from source removes both the symbol row AND its attribute rows; cascade `attributes.symbol_id REFERENCES symbols(id) ON DELETE CASCADE` fires. + +**Oracle:** Direct SQL count against `symbols` and `attributes` tables, joined by `s.id = a.symbol_id`. Independent of indexer. + +**Stress fixture:** +Two attributed symbols. `#[allow(dead_code)] fn target() {}` (the one we'll remove) and `#[allow(dead_code)] fn keep() {}` (the one that stays). Remove `target` from source. + +Adversarial intent: +- Correct cascade: target gone, target's attributes gone, keep still there, keep's attributes still there. Final counts: `symbols where name='target'` = 0, `attributes joined on symbols where s.name='target'` = 0, `symbols where name='keep'` = 1, `attributes joined on symbols where s.name='keep'` ≥ 1. +- Cascade too aggressive (clears all attributes for the file, not just for the deleted symbol): keep's attributes also drop to 0. Test catches. +- Cascade too narrow / not firing: target's attribute rows persist with stale `symbol_id`. Test catches via the target-attribute count assertion. + +**Loop budget:** N/A. + +**Wall budget:** ≤ 5s. + +**Files:** +- `crates/tethys/tests/reindex_cascade.rs` (modify — append a new `#[test] fn attributes_cascade_on_symbol_removal`) + +**Code (advisory):** +```rust +/// Pin claim 2: removing an attributed symbol from source removes the symbol +/// AND its attribute rows via `attributes.symbol_id ON DELETE CASCADE`. The +/// keep-symbol's attributes must remain. +#[test] +fn attributes_cascade_on_symbol_removal() { + let (dir, mut tethys) = workspace_with_files(&[ + ("Cargo.toml", /* same as slice 1 */ ), + ("src/lib.rs", r" +#[allow(dead_code)] +pub fn target() {} + +#[allow(dead_code)] +pub fn keep() {} +"), + ]); + + tethys.index().expect("initial"); + + let conn = open_db(&tethys); + let target_attrs_pre: i64 = conn.query_row( + "SELECT COUNT(*) FROM attributes a JOIN symbols s ON s.id=a.symbol_id WHERE s.name='target'", + params![], |row| row.get(0)).expect("count"); + let keep_attrs_pre: i64 = conn.query_row( + "SELECT COUNT(*) FROM attributes a JOIN symbols s ON s.id=a.symbol_id WHERE s.name='keep'", + params![], |row| row.get(0)).expect("count"); + + assert!(target_attrs_pre >= 1, "fixture should index target's #[allow] attribute"); + assert!(keep_attrs_pre >= 1, "fixture should index keep's #[allow] attribute"); + + // Remove target from source. + std::fs::write(dir.path().join("src/lib.rs"), r" +#[allow(dead_code)] +pub fn keep() {} +").expect("rewrite"); + std::thread::sleep(std::time::Duration::from_secs(1)); + filetime::set_file_mtime(dir.path().join("src/lib.rs"), + filetime::FileTime::now()).expect("touch"); + tethys.index().expect("re-index"); + + let conn = open_db(&tethys); + let target_attrs_post: i64 = conn.query_row( + "SELECT COUNT(*) FROM attributes a JOIN symbols s ON s.id=a.symbol_id WHERE s.name='target'", + params![], |row| row.get(0)).expect("count post"); + let keep_attrs_post: i64 = conn.query_row( + "SELECT COUNT(*) FROM attributes a JOIN symbols s ON s.id=a.symbol_id WHERE s.name='keep'", + params![], |row| row.get(0)).expect("count post"); + let target_sym_post: i64 = conn.query_row( + "SELECT COUNT(*) FROM symbols WHERE name='target'", + params![], |row| row.get(0)).expect("count"); + + assert_eq!(target_sym_post, 0, "target symbol must be gone after source removal"); + assert_eq!(target_attrs_post, 0, "target's attributes must cascade-delete with the symbol"); + assert_eq!(keep_attrs_post, keep_attrs_pre, + "keep's attributes must NOT cascade-delete (cascade was too aggressive)"); +} +``` + +**Verification:** +- [ ] Test passes on current main +- [ ] TDD-inversion: temporarily change `target_attrs_post` assert to `== keep_attrs_pre` (i.e., expect bug); confirm test fails +- [ ] Clippy clean + +--- + +## Slice 3: clear_all stability on unchanged-source re-index + +**Claim:** [design C3] Running `Tethys::index()` twice on an unchanged workspace produces identical row counts in `call_edges` and `file_deps` (the existing `clear_all_X` discipline holds). + +**Oracle:** Direct SQL `SELECT COUNT(*) FROM call_edges` and `SELECT COUNT(*) FROM file_deps`. Independent. + +**Stress fixture:** +Two-file workspace where lib.rs calls helper.rs's function (producing one call_edge AND one file_dep). Index twice. Assert both counts EQUAL between runs. + +Adversarial intent: +- Working `clear_all`: count1 == count2. Test passes. +- `clear_all_file_deps` removed from `index_with_options`: file_deps doubles to 2. Test fails. +- `clear_all_call_edges` removed: call_edges doubles to 2. Test fails. +- A UPSERT bug where ref_count keeps incrementing without new rows: row counts equal, but per-row `ref_count` doubles. Add a third assertion on `SELECT SUM(ref_count) FROM file_deps` to catch this. + +**Loop budget:** N/A. + +**Wall budget:** ≤ 5s. + +**Files:** +- `crates/tethys/tests/reindex_cascade.rs` (modify — append `#[test] fn clear_all_tables_stable_under_reindex`) + +**Code (advisory):** +```rust +/// Pin claim 3: re-indexing an unchanged workspace produces stable counts in +/// `call_edges` and `file_deps`. Catches regression of the `clear_all_X` +/// discipline (rivets-lcb6's fix for file_deps, plus call_edges). +#[test] +fn clear_all_tables_stable_under_reindex() { + let (_dir, mut tethys) = workspace_with_files(&[ + ("Cargo.toml", /* ... */ ), + ("src/lib.rs", r" +mod helper; + +pub fn entry() { + helper::do_thing(); +} +"), + ("src/helper.rs", r"pub fn do_thing() {}"), + ]); + + tethys.index().expect("first"); + + let conn = open_db(&tethys); + let ce1: i64 = conn.query_row("SELECT COUNT(*) FROM call_edges", params![], |row| row.get(0)).expect("ce1"); + let fd1: i64 = conn.query_row("SELECT COUNT(*) FROM file_deps", params![], |row| row.get(0)).expect("fd1"); + let fd_sum1: i64 = conn.query_row( + "SELECT COALESCE(SUM(ref_count), 0) FROM file_deps", params![], |row| row.get(0) + ).expect("fd_sum1"); + + assert!(ce1 >= 1, "fixture should produce at least one call_edge"); + assert!(fd1 >= 1, "fixture should produce at least one file_dep"); + + // Re-index without source change. Same fixture, same files, no mtime bump. + // tethys may skip-by-hash, which is FINE for this test — the assertion + // we care about is that *if* the indexer touches these tables again, the + // clear_all_X path runs before re-insertion. Force a re-index by calling + // index() again; under the current implementation, even skipped files' + // dependencies are recomputed. + tethys.index().expect("second"); + + let conn = open_db(&tethys); + let ce2: i64 = conn.query_row("SELECT COUNT(*) FROM call_edges", params![], |row| row.get(0)).expect("ce2"); + let fd2: i64 = conn.query_row("SELECT COUNT(*) FROM file_deps", params![], |row| row.get(0)).expect("fd2"); + let fd_sum2: i64 = conn.query_row( + "SELECT COALESCE(SUM(ref_count), 0) FROM file_deps", params![], |row| row.get(0) + ).expect("fd_sum2"); + + assert_eq!(ce1, ce2, "call_edges count must not grow across unchanged-source re-index"); + assert_eq!(fd1, fd2, "file_deps count must not grow across unchanged-source re-index"); + assert_eq!(fd_sum1, fd_sum2, "file_deps SUM(ref_count) must not grow either (UPSERT-aggregate fence)"); +} +``` + +**Verification:** +- [ ] Test passes on current main +- [ ] TDD-inversion: comment out `self.db.clear_all_file_deps()` in `indexing.rs:139`; confirm `fd_sum1 == fd_sum2` assertion fails (this is the lcb6 regression check) +- [ ] Clippy clean + +--- + +## Plan Self-Review + +### 1. Every loop in the plan +- None of the 3 slices adds a new production loop. All slices are test-only. Test-execution loops (over fixture files, over assertions) are bounded by fixture size (≤ 5 files, ≤ 5 assertions per test). No budget concern. + +### 2. Every fixture +- Slice 1: middle-removal stress, designed to fail under "wrong-cascade-row removal" bugs. Not happy-path. +- Slice 2: two-symbol-different-attribute-status, designed to fail under both "cascade missed" and "cascade too wide" bugs. Not happy-path. +- Slice 3: cross-file call (covers both call_edges and file_deps) plus a `SUM(ref_count)` assertion, designed to fail under both "clear_all missing" and "UPSERT-aggregate growth without new rows." Not happy-path. + +### 3. Every doc-comment precondition +None of the slices introduces a doc-commented precondition. The slices ARE the regression fences for an already-documented (in `what-i-learned.md`) invariant. No new contracts; no enforcement-strength classification needed. + +### 4. Every write target +- All asserts go through `panic!` (via the `assert*!` macros) and `nextest` captures them to stderr. That's diagnostic. No `println!`/`eprintln!` introduced. + +### 5. Every tracker reference +The plan references: +- **rivets-wsix** (the active issue — verified open and in-progress) +- **rivets-lcb6** (closed, the precedent fix — referenced for context, no deferred work) +- **rivets-dhxo** (open, orphan-file boundary — explicit out-of-scope per design.md, not deferred FROM this work) + +No "TODO" or "follow-up" trigger phrases in the plan body. All slices ship in this PR or get rejected; none are deferred. + +## Hard gate + +- [x] Every slice has all mandatory fields filled in (Claim, Oracle, Stress fixture, Loop budget, Wall budget, Files, Code, Verification) +- [x] Every loop has a complexity statement (N/A noted explicitly for each slice — no new production loops) +- [x] Every slice has a stress fixture +- [x] Plan's claim coverage matches design's claim list (3 slices ↔ 3 design claims, 1:1) +- [x] Every tracker reference resolves to an existing issue + +Ready for checkpointed-build. diff --git a/.rivets-wsix/probe.sh b/.rivets-wsix/probe.sh new file mode 100644 index 0000000..da25263 --- /dev/null +++ b/.rivets-wsix/probe.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# rivets-wsix probe: enumerate UPSERT-shaped writes in crates/tethys/src/db/ +# and pair each table with whether it has a clear_all_* function defined. +# +# Mechanism: regex grep over .rs source. Tables come from the file name +# (db/
.rs convention); UPSERT shape detected by SQL pattern in source. +# +# Output columns: +# FILE | HAS_UPSERT | HAS_CLEAR_FN | CLEAR_FN_NAME + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +DB_DIR="crates/tethys/src/db" + +for f in "$DB_DIR"/*.rs; do + base=$(basename "$f" .rs) + [[ "$base" == "mod" ]] && continue + + has_upsert="no" + if grep -qE "INSERT[[:space:]]+OR[[:space:]]+REPLACE|ON[[:space:]]+CONFLICT.*DO[[:space:]]+UPDATE" "$f"; then + has_upsert="yes" + fi + + clear_fn=$(grep -oE "fn clear_all_[a-z_]+" "$f" | head -1 || true) + has_clear="no" + if [[ -n "$clear_fn" ]]; then + has_clear="yes" + fi + + printf '%-30s upsert=%-4s clear_fn=%-4s %s\n' "$base" "$has_upsert" "$has_clear" "${clear_fn:--}" +done diff --git a/.rivets-wsix/probe_null_in_symbol.sh b/.rivets-wsix/probe_null_in_symbol.sh new file mode 100644 index 0000000..6adc0c3 --- /dev/null +++ b/.rivets-wsix/probe_null_in_symbol.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# rivets-wsix probe slice 3: do refs with in_symbol_id IS NULL accumulate +# across re-index runs? These are file-scope refs (mod declarations, top-level +# type annotations) not contained in any function/struct/impl, so they don't +# cascade-delete via the symbol DELETE that catches in-function refs. + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +WORK=$(mktemp -d) +trap "rm -rf $WORK" EXIT + +mkdir -p "$WORK/src" +cat > "$WORK/Cargo.toml" << 'EOF' +[package] +name = "probe_null_in_symbol" +version = "0.0.0" +edition = "2021" +EOF + +# Run 1: file with mod declarations and use statements (file-scope refs). +cat > "$WORK/src/lib.rs" << 'EOF' +mod helper_a; +mod helper_b; +EOF +cat > "$WORK/src/helper_a.rs" << 'EOF' +pub fn do_a() {} +EOF +cat > "$WORK/src/helper_b.rs" << 'EOF' +pub fn do_b() {} +EOF + +TETHYS="$(pwd)/target/release/tethys" +cd "$WORK" + +count_null_refs() { + sqlite3 .rivets/index/tethys.db \ + "SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id + WHERE f.path='src/lib.rs' AND r.in_symbol_id IS NULL;" +} +count_total_lib_refs() { + sqlite3 .rivets/index/tethys.db \ + "SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id + WHERE f.path='src/lib.rs';" +} + +"$TETHYS" index --workspace . > /dev/null 2>&1 +N1_NULL=$(count_null_refs) +N1_TOTAL=$(count_total_lib_refs) + +# Run 2: REMOVE mod helper_b; (delete one of the two mod-level refs). +cat > src/lib.rs << 'EOF' +mod helper_a; +EOF +sleep 1 +touch src/lib.rs + +"$TETHYS" index --workspace . > /dev/null 2>&1 +N2_NULL=$(count_null_refs) +N2_TOTAL=$(count_total_lib_refs) + +# Run 3: change to a DIFFERENT module name entirely. +cat > src/lib.rs << 'EOF' +mod helper_a; +mod helper_c; +EOF +cat > src/helper_c.rs << 'EOF' +pub fn do_c() {} +EOF +sleep 1 +touch src/lib.rs + +"$TETHYS" index --workspace . > /dev/null 2>&1 +N3_NULL=$(count_null_refs) +N3_TOTAL=$(count_total_lib_refs) + +echo "=== src/lib.rs refs (null in_symbol_id only) across re-index runs ===" +echo "Run 1 (mod helper_a + mod helper_b): null=$N1_NULL total=$N1_TOTAL expected null≥2" +echo "Run 2 (only mod helper_a): null=$N2_NULL total=$N2_TOTAL expected null=1 if cleared, 2 if buggy" +echo "Run 3 (mod helper_a + mod helper_c): null=$N3_NULL total=$N3_TOTAL expected null=2 if cleared, ≥3 if buggy" +echo +echo "=== dump of in_symbol_id NULL refs in src/lib.rs after run 3 ===" +sqlite3 .rivets/index/tethys.db \ + "SELECT r.reference_name, s.name AS symbol_name + FROM refs r JOIN files f ON f.id=r.file_id + LEFT JOIN symbols s ON s.id=r.symbol_id + WHERE f.path='src/lib.rs' AND r.in_symbol_id IS NULL;" +echo + +if [[ "$N2_NULL" -gt 1 || "$N3_NULL" -gt 2 ]]; then + echo "BUG CONFIRMED: file-scope refs (in_symbol_id IS NULL) persist after source removal." +else + echo "OK: file-scope refs also reflect current source state." +fi diff --git a/.rivets-wsix/probe_refs_bug.sh b/.rivets-wsix/probe_refs_bug.sh new file mode 100644 index 0000000..9c3dbb7 --- /dev/null +++ b/.rivets-wsix/probe_refs_bug.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# rivets-wsix probe (expanded slice): does `refs` actually accumulate across +# re-index runs? +# +# Smallest factual question: if I index a fixture twice without changing +# anything, does the total number of refs in the DB double? +# +# Mechanism (probe): run `tethys index` twice on a tiny tempdir workspace, +# count rows in `refs` table via direct SQLite query between runs. +# +# Oracle: count refs from the indexing.rs trace logs themselves +# (RUST_LOG=tethys=trace). Independent because the trace fires once per +# extracted ref, regardless of what the DB does on insert. + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +WORK=$(mktemp -d) +trap "rm -rf $WORK" EXIT + +# Tiny fixture: one file, two refs (one resolved same-file, one cross-file). +mkdir -p "$WORK/src" +cat > "$WORK/Cargo.toml" << 'EOF' +[package] +name = "probe_refs_bug" +version = "0.0.0" +edition = "2021" +EOF +cat > "$WORK/src/lib.rs" << 'EOF' +mod helper; + +pub fn entry() { + helper::do_thing(); + let x = make_thing(); +} + +fn make_thing() -> i32 { 42 } +EOF +cat > "$WORK/src/helper.rs" << 'EOF' +pub fn do_thing() {} +EOF + +# Build tethys release binary if not already +TETHYS="$(pwd)/target/release/tethys" +if [[ ! -x "$TETHYS" ]]; then + cargo build -p tethys --release --quiet 2>&1 | tail -5 +fi + +cd "$WORK" + +count_refs() { + sqlite3 .rivets/index/tethys.db \ + "SELECT COUNT(*) FROM refs r JOIN files f ON f.id=r.file_id + WHERE f.path='src/lib.rs';" +} + +# Run 1: initial index. 3 refs in lib.rs: helper, do_thing, make_thing. +"$TETHYS" index --workspace . > /dev/null 2>&1 +N1=$(count_refs) + +# Modify src/lib.rs and re-index. Force a content change AND a mtime bump. +cat > src/lib.rs << 'EOF' +mod helper; + +pub fn entry() { + helper::do_thing(); + let x = make_thing(); + let y = make_thing(); +} + +fn make_thing() -> i32 { 42 } +EOF +# Belt-and-braces mtime bump (some filesystems have 1s mtime resolution). +sleep 1 +touch src/lib.rs + +# Run 2: now lib.rs has an EXTRA call to make_thing (4 refs total expected). +"$TETHYS" index --workspace . > /dev/null 2>&1 +N2=$(count_refs) + +# Now REMOVE both make_thing calls and re-index. +cat > src/lib.rs << 'EOF' +mod helper; + +pub fn entry() { + helper::do_thing(); +} + +fn make_thing() -> i32 { 42 } +EOF +sleep 1 +touch src/lib.rs + +"$TETHYS" index --workspace . > /dev/null 2>&1 +N3=$(count_refs) + +echo "=== refs in src/lib.rs across re-index runs ===" +echo "Run 1 (helper::do_thing + 1× make_thing call): $N1 expected ~3 (mod, fn, fn)" +echo "Run 2 (helper::do_thing + 2× make_thing call): $N2 expected ~4 (mod, fn, fn, fn)" +echo "Run 3 (helper::do_thing only, no make_thing): $N3 expected ~2 if cleared, ~5+ if buggy" +echo +if [[ "$N3" -gt "$N2" || "$N3" -gt 3 ]]; then + echo "BUG CONFIRMED: removing refs from source leaves stale rows in refs table." +elif [[ "$N3" -lt "$N2" ]]; then + echo "OK: refs table correctly reflects current source state (some clear path exists)." +else + echo "AMBIGUOUS: refs count unchanged. Source change may not have been re-indexed." +fi diff --git a/.rivets-wsix/related-issues.md b/.rivets-wsix/related-issues.md new file mode 100644 index 0000000..32ecac2 --- /dev/null +++ b/.rivets-wsix/related-issues.md @@ -0,0 +1,23 @@ +# Related issues — rivets-wsix + +Tracker scan (5-min cap per prove-it-prototype step 0): + +## Direct ancestors + +- **rivets-lcb6** (closed, PR #65) — the *parent* fix. Discovered file_deps was UPSERT-only and never cleared, causing stale resolver edges to accumulate. Established the `clear_all_X` pattern and applied it to `file_deps`. The wsix audit asks: "what other tables have this shape?" +- **rivets-zoi3** (open, P2) — also surfaced during PR #65 review. Asks for *expanded test coverage* of the file_deps clear pattern (4 specific tests). Adjacent but different work; wsix is breadth-first (which tables), zoi3 is depth-first (one table, many fixtures). + +## Sibling / related + +- **rivets-dhxo** (open, P3) — orphan file_deps re-insertion in streaming-mode indexing. Different bug class (orphan file_ids producing phantom edges *from* deleted source files), but lives in the same "file_deps coherence" problem space. Not displaced by wsix. + +## Closed but informative + +- **rivets-itz7** (closed, P1) — added imports indexing for Rust + C#. +- **rivets-lxbg** (closed, P1) — added imports table to schema. + +Both are about *creating* the imports table; neither audited its write pattern for the UPSERT-stale-row bug class. The imports table is a wsix candidate (it appears in my broader grep). + +## No prior art found for + +The wsix-specific question — "which UPSERT-only tables in `crates/tethys/src/db/` are missing a `clear_all_X` call from `index_with_options`" — has no existing issue. wsix is the right tracker entry; no displacement. diff --git a/.rivets-wsix/what-i-learned.md b/.rivets-wsix/what-i-learned.md new file mode 100644 index 0000000..002b738 --- /dev/null +++ b/.rivets-wsix/what-i-learned.md @@ -0,0 +1,59 @@ +# What I learned — rivets-wsix prove-it-prototype + +## One-sentence summary + +The schema's `ON DELETE CASCADE` chain plus per-file `DELETE FROM symbols WHERE file_id` is the actual safety net for re-index correctness across most tables — not the `clear_all_X` pattern lcb6 established for `file_deps` and `call_edges`. The wsix issue's mental model ("UPSERT-only ⇒ needs `clear_all_X`") was a special case, not the general principle. + +## Per-table inventory after three probe expansions + +| Table | Insert shape | Re-index safety mechanism | Audit result | +|---|---|---|---| +| `call_edges` | UPSERT `ON CONFLICT DO UPDATE` | `clear_all_call_edges` at `indexing.rs:439` | ✓ Safe | +| `file_deps` | UPSERT `ON CONFLICT DO UPDATE` | `clear_all_file_deps` at `indexing.rs:139` (lcb6 fix) | ✓ Safe | +| `imports` | UPSERT `INSERT OR REPLACE` | `DELETE FROM imports WHERE file_id` at `files.rs:146` before per-file re-insert | ✓ Safe | +| `symbols` | Plain `INSERT` | `DELETE FROM symbols WHERE file_id` at `files.rs:145` | ✓ Safe | +| `attributes` | Plain `INSERT` | Cascade from `symbols(id) ON DELETE CASCADE` when symbols are wiped per file | ✓ Safe | +| `refs` | Plain `INSERT`, no explicit clear | Cascade via `refs.in_symbol_id REFERENCES symbols(id) ON DELETE CASCADE` (and the `symbol_id` FK for same-file refs) when the containing file's symbols are wiped | ✓ Safe per probe | +| `files` | `INSERT` after `SELECT id WHERE path = ?` existence check | UPSERT-by-existence-check; row id is reused so cascade-dependent rows keep stable file_id | ✓ Per-file safe | +| `arch_packages` | Plain `INSERT` | `DELETE FROM arch_packages` at start of `repopulate_architecture` (architecture.rs:61); cascade clears the two child tables | ✓ Safe | +| `arch_file_packages` | Plain `INSERT` | Cascade from `arch_packages(id)` | ✓ Safe | +| `arch_package_deps` | Plain `INSERT` | Cascade from `arch_packages(id)` | ✓ Safe | + +**Zero new bugs found.** wsix's mental model expected the file_deps shape (monotonic aggregate growth) on other tables. Empirically, none of them exhibit it. + +## What surprised me + +1. **The `refs` table has no explicit clear and no UPSERT, yet handles re-indexing correctly.** My initial code reading said "this should accumulate." Probe disproved that. The cascade chain via `in_symbol_id → symbols(id)` is wider than I appreciated — when a file's symbols are wiped, *every* ref contained in those symbols cascade-deletes, regardless of whether the ref's target is in the same file. + +2. **Tethys's rust extractor doesn't generate refs for `mod X;` declarations.** I expected `mod helper_a; mod helper_b;` to produce 2 file-scope refs (in_symbol_id IS NULL). The probe showed 0. So the in_symbol_id IS NULL edge case — which would have been the only path stale refs could persist — doesn't exist in practice. (This may be worth a separate audit if `extern crate X;` or top-level type aliases produce file-scope refs that mod declarations don't; out of scope for wsix.) + +3. **`imports` uses BOTH `INSERT OR REPLACE` AND a per-file `DELETE`.** Belt-and-suspenders. The DELETE alone would be sufficient; the `OR REPLACE` covers a narrower edge case (same-file duplicate `use` statements). Not a bug, just redundant. Worth noting if anyone simplifies. + +4. **The wsix issue's classification (a/b/c) missed a fourth class**: tables that are correct *because of schema cascade*, not because of explicit clear-or-UPSERT logic. The `refs`, `attributes`, and `symbols`-via-files-cascade tables all live there. + +## Probe / oracle independence + +- **Initial probe**: regex grep on `db/*.rs` for `INSERT ... ON CONFLICT DO UPDATE` paired with `fn clear_all_*`. Assumed 1 table per file. +- **Oracle 1**: enumerate `CREATE TABLE` statements from `schema.rs`, then grep `INSERT INTO
` across `crates/tethys/src/`. Caught the multi-table `files.rs`, the `refs`-vs-`references.rs` naming mismatch, and the `arch_*` family. +- **Probe 2 (empirical, refs accumulation)**: run `tethys index --workspace .` twice on a tempdir fixture, then thrice with source mutations, comparing `SELECT COUNT(*) FROM refs WHERE file_id = ?`. Disproved my "refs accumulates" hypothesis. +- **Probe 3 (empirical, in_symbol_id IS NULL case)**: same shape, fixture with `mod helper_a; mod helper_b;` at file scope. Showed extractor doesn't produce these refs. + +Probe-vs-oracle disagreement on the *table inventory question*: probe under-detected (file-name assumption). Resolved by adopting the oracle's view as canonical for the inventory. Then probes 2 and 3 directly verified empirical behavior on the tables the oracle inventory raised concerns about. + +## Implication for the gilfoyle loop + +The audit found no bugs to fix. The remaining value is **regression fences** that lock in the audited correctness properties so they survive future schema or indexing changes. Candidates: + +- Test: re-index without source change → `refs` count for that file is unchanged (not doubled). Pins the cascade. +- Test: remove a function from a file, re-index → its refs disappear (not orphan). Pins the per-file cascade chain. +- Test: remove `mod X;` then re-index → no orphan refs to X. Pins the (currently empty) file-scope case. +- Test: each non-UPSERT INSERT site (`refs`, `attributes`, `symbols`, `files`) has either a documented cascade path OR a per-file clear. Could be an architecture test that fails CI if a new INSERT is added without a corresponding clear/cascade declaration. + +The falsifiable-design step should choose which of these fences to ship. + +## What's NOT in this audit + +- **Orphan-file behavior** (file deleted from disk, files.id persists): out of scope for wsix per its explicit framing as a sibling of lcb6 (UPSERT growth class). Tracked separately as **rivets-dhxo**. +- **Streaming-mode (`IndexOptions::with_streaming()`) indexing**: only tested the default full-index path. dhxo's analysis suggests streaming has its own divergent behavior here. +- **Concurrent re-index**: only single-threaded `tethys index` runs. SQLite's `busy_timeout` (per CLAUDE.md) protects against concurrent writers but cascade ordering under concurrent reads/writes wasn't probed. +- **Schema migration scenarios**: a future schema change that drops or alters a cascade FK could silently introduce the bug class wsix was looking for. The regression fences should be schema-aware enough to catch that. diff --git a/.rivets/issues.jsonl b/.rivets/issues.jsonl index 5fc89e7..47c7c05 100644 --- a/.rivets/issues.jsonl +++ b/.rivets/issues.jsonl @@ -1,302 +1,302 @@ -{"id":"rivets-95l","title":"Add local quality gates using cargo-husky","description":"Add cargo husky and formatting, linting, and testing as pre-commit gates","status":"closed","priority":2,"issue_type":"task","assignee":"Claude","labels":[],"design":"Implemented pre-commit hook combining beads sync and cargo quality gates:\n\n**Hook Location**: `.git/hooks/pre-commit`\n\n**Quality Checks**:\n1. Beads sync flush (if in beads workspace)\n2. `cargo fmt -- --check` - Code formatting\n3. `cargo clippy --all-targets --all-features -- -D warnings` - Linting\n4. `cargo test --quiet` - All tests\n\n**Stub Code Handling**: Added `#[allow(dead_code)]` attributes to placeholder types/functions to prevent false positives during development.\n\n**Manual Execution**: Hook can be tested by running `.git/hooks/pre-commit` directly.","acceptance_criteria":"✓ Pre-commit hook installed at .git/hooks/pre-commit\n✓ Runs cargo fmt check before commits\n✓ Runs cargo clippy with -D warnings before commits\n✓ Runs cargo test before commits\n✓ Integrates with existing beads sync hook\n✓ Documentation added to README.md\n✓ Successfully passes all checks on current codebase","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T21:55:30.160010922Z","updated_at":"2025-11-17T22:04:29.413337487Z","closed_at":"2025-11-17T22:04:29.413337487Z"} -{"id":"rivets-3l14","title":"Compute content hash for indexed files","description":"The schema supports `content_hash` but it's always `None`. See `batch_writer.rs:238`, `lib.rs:643`, `lib.rs:803`.\n\nContent hashing would provide more reliable change detection than mtime/size alone, useful for:\n- Detecting changes after file touch without content change\n- Cache key generation\n- Detecting identical content in different files","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:21.382922447Z","updated_at":"2026-01-30T00:18:21.382922447Z","closed_at":null} +{"id":"rivets-limz","title":"Audit tethys tracing log messages for structured-field consistency","description":"Project convention (per CLAUDE.md 'Structured Logging with tracing') is structured fields, not string interpolation in the message. PR #63 added some logs that bake the operation name into prose ('compute_dependencies: file not in any known crate; skipping') rather than using a structured field like operation = \"compute_dependencies\". Codebase has a mix of both styles.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Concrete examples flagged in PR #63 review\n\nThe three new trace/debug sites added in PR #63 use prose-style operation names:\n\n- resolve.rs::resolve_refs_for_file: 'File not in any known crate; skipping Pass-2-imports'\n- indexing.rs::compute_dependencies: 'compute_dependencies: file not in any known crate; skipping'\n- indexing.rs::compute_dependencies_from_stored: 'compute_dependencies_from_stored: file not in any known crate; skipping'\n\nProject convention would prefer:\n\n```rust\ndebug!(\n operation = \"compute_dependencies\",\n file = %current_file.display(),\n \"File not in any known crate; skipping\"\n);\n```\n\n## Scope\n\nThis is broader than just PR #63 sites. The whole tracing surface in tethys should be audited. CLAUDE.md provides examples of the desired pattern; grep for tracing macros and identify call sites that don't match.\n\n## Why P4\n\nCosmetic. Doesn't affect correctness. Worth doing during a quiet maintenance window, not as part of a bug fix PR.","acceptance_criteria":"- [ ] Inventory all tracing call sites in crates/tethys/src/\n- [ ] For each, classify: matches structured-field convention OR uses string interpolation\n- [ ] Update string-interpolated sites to use structured fields per CLAUDE.md\n- [ ] Bonus: add a lint or test that flags the pattern (optional)","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:40:44.092737600Z","updated_at":"2026-05-12T23:40:44.092737600Z","closed_at":null} +{"id":"rivets-d06","title":"Write tests for MCP tools","description":"Integration and end-to-end tests for the MCP server:\n- Integration tests with actual MCP protocol messages\n- Multi-workspace scenario tests\n- Error response format verification\n- Full workflow tests (create -> update -> close)\n- Test with real InMemoryStorage (not mocks)","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Integration test harness for MCP protocol\n- [ ] Tests for complete issue lifecycle via MCP\n- [ ] Multi-workspace context switching tests\n- [ ] Error response format matches MCP spec\n- [ ] All tools tested with real storage backend","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:45.284084681Z","updated_at":"2025-11-30T18:09:34.327560198Z","closed_at":"2025-11-30T18:09:34.327560198Z"} +{"id":"rivets-bxom","title":"tethys: implement proper incremental update and staleness check","description":"CodeIndex::update() currently re-indexes everything instead of doing a proper incremental update. CodeIndex::needs_update() always returns true. Implement proper staleness detection and incremental re-indexing.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:04.826229488Z","updated_at":"2026-03-19T01:41:04.826229488Z","closed_at":null} +{"id":"rivets-j20","title":"Verify path traversal protection in rivets directory resolution","description":"Audit and verify that the rivets directory resolution logic properly handles path traversal attempts like \"../\" in user inputs. Add tests to confirm that malicious paths cannot escape the intended directory boundaries.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["pr-feedback","security"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:42.751822201Z","updated_at":"2025-11-30T04:11:42.751822201Z","closed_at":null} +{"id":"rivets-6yc","title":"Evaluate lock holding duration in MCP tools","description":"Review the lock holding patterns in rivets-mcp tools to ensure we're not holding locks longer than necessary.\n\nContext from PR review:\n- Current pattern: Acquire context read lock, then acquire storage write lock for operations\n- Potential optimization: Clone the Arc early and release the context lock immediately\n- Trade-off: Earlier lock release vs additional Arc clone overhead\n\nEvaluation should consider:\n1. Whether the current lock ordering prevents deadlocks (context -> storage)\n2. If cloning Arc early provides meaningful concurrency benefits\n3. Actual contention patterns in typical MCP usage (single client vs multiple)\n4. Whether read locks on context could be held across storage operations safely","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["concurrency","performance","rivets-mcp"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T07:29:38.632289389Z","updated_at":"2025-11-29T07:29:38.632289389Z","closed_at":null} +{"id":"rivets-08u","title":"Implement map transformations for JsonlQuery","description":"Add map() method to JsonlQuery for transforming records during streaming.\n\nThis enables type transformations and projections on JSONL data.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\npub struct JsonlQuery {\n predicates: Vec bool + Send + Sync>>,\n transform: Option U + Send + Sync>>,\n _phantom: PhantomData<(T, U)>,\n}\n\nimpl JsonlQuery\nwhere\n T: DeserializeOwned + 'static,\n U: 'static,\n{\n pub fn map(self, transform: F) -> JsonlQuery\n where\n F: Fn(U) -> V + Send + Sync + 'static,\n V: 'static,\n {\n JsonlQuery {\n predicates: self.predicates,\n transform: Some(Box::new(move |t| transform(\n self.transform.as_ref()\n .map(|f| f(t))\n .unwrap_or(t)\n ))),\n _phantom: PhantomData,\n }\n }\n}\n```\n\nNote: This requires refactoring JsonlQuery to support type transformations.","acceptance_criteria":"- map() method implemented\n- Supports type transformations\n- Can chain multiple map() calls\n- Transformations applied during streaming\n- Unit tests verify transformations\n- Integration test with filter + map pipeline","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-83j","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T23:17:01.111641956Z","updated_at":"2025-11-27T23:17:01.111641956Z","closed_at":null} +{"id":"rivets-lye5","title":"Snapshot and diff: track structural graph changes over time","description":"Save lightweight snapshots of the graph and compare them to current state. Useful before/after refactors, in CI to audit what a PR added or removed structurally, and as a baseline for regression detection.\n\n**Inspired by:** KiroGraph's snapshot save / snapshot diff / kirograph_diff MCP tool. Storage is just JSON arrays of node IDs and edge tuples; diff is set-difference, O(n) regardless of codebase size.\n\n**Implementation:**\n- Storage: .rivets/snapshots/{label}.json with serialized list of (qualified_name, kind) for nodes and (caller_qn, callee_qn, kind) for edges. JSON over native binary because they're small and human-readable.\n- Default snapshot label is the timestamp; named labels (e.g. 'pre-refactor') are first-class.\n- Diff is computed as two set differences: added = current - snapshot, removed = snapshot - current.\n- Public API:\n - Tethys::save_snapshot(label) -> Result\n - Tethys::list_snapshots() -> Result>\n - Tethys::diff_snapshot(label) -> Result\n- CLI:\n - tethys snapshot save [LABEL]\n - tethys snapshot list\n - tethys snapshot diff [LABEL] [--format full|summary|json]\n\n**CI use case:**\ngh pr create … then tethys snapshot save pre-pr; merge; tethys snapshot diff pre-pr to audit structural changes.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] save_snapshot, list_snapshots, diff_snapshot API methods\n- [ ] CLI subcommands snapshot save / list / diff\n- [ ] Snapshot file format documented (JSON schema in module-level docs)\n- [ ] Diff returns added_symbols, removed_symbols, added_edges, removed_edges\n- [ ] Default label is timestamp; named labels supported\n- [ ] Round-trip test: save → diff against unchanged graph returns zero changes","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:31:32.968546400Z","updated_at":"2026-05-10T04:31:32.968546400Z","closed_at":null} +{"id":"rivets-6jxv","title":"Extract Tethys::crate_root_for_file helper to dedupe triple-duplicated derivation","description":"The per-file crate_root derivation introduced by rivets-6aoc is now duplicated verbatim across three production sites in tethys. Each duplicate carries the same get_crate_for_file + src_root + file-parent-fallback logic. Extracting to a single helper on Tethys would make each call site a one-liner and ensure the three sites stay in sync.","status":"closed","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Current duplication\n\nThe same ~8-line pattern lives in three places:\n\n1. `crates/tethys/src/resolve.rs::resolve_refs_for_file` (Pass-2-imports)\n2. `crates/tethys/src/indexing.rs::compute_dependencies` (dep-graph from extracted refs)\n3. `crates/tethys/src/indexing.rs::compute_dependencies_from_stored` (streaming-mode dep graph)\n\nEach site has:\n\n```rust\nlet crate_root = if let Some(crate_info) =\n crate::cargo::get_crate_for_file(, &self.crates) // or self.crates()\n{\n crate_info.src_root()\n} else {\n debug!(file = %.display(), \"...; using file parent as sentinel crate_root\");\n .parent()\n .map_or_else(|| self.workspace_root.clone(), Path::to_path_buf)\n};\n```\n\n## Proposed helper\n\n```rust\nimpl Tethys {\n /// Returns the per-file crate_root for use by the resolver and dep-graph\n /// computation. Falls back to the file's parent directory as a sentinel\n /// when the file is outside any known crate (workspace-root example/bench\n /// dirs).\n fn crate_root_for_file(&self, file: &Path, caller: &'static str) -> PathBuf {\n if let Some(crate_info) = crate::cargo::get_crate_for_file(file, &self.crates) {\n crate_info.src_root()\n } else {\n debug!(\n operation = caller,\n file = %file.display(),\n \"File not in any known crate; using file parent as sentinel crate_root\"\n );\n file.parent()\n .map_or_else(|| self.workspace_root.clone(), Path::to_path_buf)\n }\n }\n}\n```\n\nEach call site collapses to:\n\n```rust\nlet crate_root = self.crate_root_for_file(, \"compute_dependencies\");\n```\n\n## Discovered during\n\nPR #63 round-2 claude review (post-merge of round-1 fixes). The reviewer flagged the duplication as a suggestion. Filing per the new gilfoyle tracker discipline (any deferral or 'follow-up' phrase needs a tracker entry filed at write-time).\n\n## Why P4\n\nCosmetic. Three sites are correctly in sync today. The risk is future drift if someone modifies one without the others. The helper is also a natural extension point for the rivets-bjdn BTreeMap optimization (the pre-computed lookup map would live in this helper).","acceptance_criteria":"- [ ] Tethys::crate_root_for_file helper extracted with consistent caller-name parameter\n- [ ] resolve.rs::resolve_refs_for_file uses the helper\n- [ ] indexing.rs::compute_dependencies uses the helper\n- [ ] indexing.rs::compute_dependencies_from_stored uses the helper\n- [ ] All resolver tests still pass (605+ in PR #63)\n- [ ] cargo clippy + cargo fmt clean","notes":"Closed: Fixed in PR #71 (commit f0220d0). Extracted Tethys::src_root_for_file helper, deduped 3 call sites in resolve.rs and indexing.rs. Helper named src_root_for_file (not crate_root_for_file as the issue text suggested) per cross-validation from 3 independent reviewers — matches CrateInfo::src_root() and avoids visual collision with the pre-existing Tethys::get_crate_root_for_file. ResolveContext::crate_root also renamed to src_root for end-to-end consistency. Added unit test for orphan-file fallback branch.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-i8qn","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-13T00:28:31.586298200Z","updated_at":"2026-05-18T18:51:03.322639100Z","closed_at":"2026-05-18T18:51:03.322636200Z"} +{"id":"rivets-2wp","title":"Create architecture document and diagrams for rivets","description":"Based on research from JSONL library design and project structure planning, create comprehensive architecture documentation with diagrams showing component relationships, data flow, and system design.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create comprehensive documentation covering:\n\n**1. High-Level System Architecture**:\n- System architecture diagram showing all components/crates\n- Component interaction diagrams (crate dependency graph)\n- Key design decisions and rationale\n- Technology choices (libraries, patterns, etc.)\n- Future extensibility considerations\n\n**2. Data Flow**:\n- JSONL data flow through system (CLI → RPC → Storage → JSONL)\n- How JSONL operations work across components\n- Module organization within each crate\n\n**3. Storage Layer**:\n- Database schema diagram\n- Table relationships\n- Index strategy\n- Migration system\n\n**4. ID Generation System**:\n- Hash algorithm explanation\n- Collision handling strategy\n- Hierarchical ID format\n\n**5. Dependency System**:\n- 4 dependency types explained\n- Cycle detection algorithm\n- Ready work calculation logic\n\n**6. JSONL Sync**:\n- Export flow diagram\n- Import flow diagram\n- Conflict resolution strategy\n\n**7. RPC Protocol**:\n- Message format specification\n- Operation list\n- Error handling approach\n\nUse mermaid diagrams for visual clarity throughout. Format as ADR or similar structured format.","acceptance_criteria":"- Architecture document created at docs/architecture.md\n- System architecture diagram showing all crates and components\n- Crate dependency graph with clear separation of concerns\n- Data flow diagram showing JSONL operations (CLI → RPC → Storage → JSONL)\n- Database schema diagram with table relationships\n- ID generation algorithm documented with collision handling\n- Dependency system explained (4 types, cycle detection, ready work)\n- JSONL sync flows documented (export/import with conflict resolution)\n- RPC protocol specification (message format, operations, errors)\n- All diagrams in mermaid format (maintainable and version-controllable)\n- Design decisions justified with rationale\n- Future extensibility considerations noted\n- Document integrates findings from rivets-fk9 and rivets-kr3 research","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-fk9","dep_type":"blocks"},{"depends_on_id":"rivets-kr3","dep_type":"blocks"}],"created_at":"2025-11-17T21:08:45.698527855Z","updated_at":"2025-11-28T00:44:22.287918117Z","closed_at":"2025-11-28T00:44:22.287918117Z"} +{"id":"rivets-53zq","title":"Test polish for workspace-crate resolver (PR #62 follow-up)","description":"Three low-priority test polish items surfaced in PR #62 reviews 4 and 5. None are correctness issues; PR #62 was merged with the core fix and the items below tracked here.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Items\n\n### F1 — Hoist `use crate::types::CrateInfo;` to mod tests scope\n\n`use crate::types::CrateInfo;` appears inline inside multiple test functions in `crates/tethys/src/resolver.rs::tests` (e.g., `resolves_workspace_crate_via_new_arm`, `single_segment_falls_back_to_bin_when_lib_path_absent`, `single_segment_returns_none_when_no_entry_point`). The `workspace_with_crates` helper in the same module references it too. Hoisting to a single `use` at the top of `mod tests` would clean up the duplication.\n\nTrivial; ~5 lines removed.\n\n### F2 — Test that lib_path takes priority when both lib_path and bin_paths are set\n\nThe single-segment workspace-crate arm uses:\n\n```rust\ntarget.lib_path\n .as_ref()\n .or_else(|| target.bin_paths.first().map(|(_, p)| p))\n```\n\nCurrently tested:\n- `lib_path: Some(...), bin_paths: []` (single_segment_workspace_crate_resolves_to_entry_point_file)\n- `lib_path: None, bin_paths: [bin]` (single_segment_falls_back_to_bin_when_lib_path_absent)\n- `lib_path: None, bin_paths: []` (single_segment_returns_none_when_no_entry_point)\n\nNOT directly tested: `lib_path: Some(lib), bin_paths: [bin]` should resolve to lib_path, not the bin. The `or_else` ordering implies this but it isn't locked down.\n\nAdd a test constructing a `CrateInfo` with both populated, asserting the resolved path ends with the lib_path file.\n\n### F3 — Prefix-of-crate-name negative test\n\nAsserts that a use-path head that is a strict prefix of a workspace crate name (e.g., `riv` when the workspace has `rivets`) does NOT match. The current `find(|c| c.name.replace('-', \"_\") == head)` uses `==` not `starts_with`, so this is correct by inspection — but an explicit test documents the intentional non-match for future readers.\n\nLow priority; documents semantics already obvious from the code.\n\n## Why one issue, not three\n\nThese are all small (10-line tests or smaller) and all touch the same test module in `resolver.rs`. A single follow-up PR addressing all three is cleaner than three separate ones.","acceptance_criteria":"- [ ] F1: `use crate::types::CrateInfo;` hoisted to mod tests scope; duplicate inline imports removed\n- [ ] F2: New test constructing CrateInfo with both lib_path and bin_paths populated; asserts lib_path wins\n- [ ] F3: New test asserting a use-path head that is a prefix of a workspace crate name returns None\n- [ ] cargo nextest run -p tethys passes\n- [ ] cargo clippy + cargo fmt clean","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T21:57:36.396522300Z","updated_at":"2026-05-12T21:57:36.396522300Z","closed_at":null} {"id":"rivets-0dw","title":"Add debug logging to find_database() for observability","description":"The `find_database()` function has fallback logic but doesn't log which database was selected. This makes debugging harder when users have unexpected database files in `.rivets/`.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["logging","observability"],"design":"Add `tracing::debug!` calls to show which path was taken:\n\n```rust\nfn find_database(rivets_dir: &Path) -> Result {\n let standard = rivets_dir.join(\"issues.jsonl\");\n if standard.exists() {\n tracing::debug!(\"Using standard database: {}\", standard.display());\n return Ok(standard);\n }\n\n // ... search logic ...\n\n match found.len() {\n 0 => {\n tracing::debug!(\"No database found, using default: {}\", standard.display());\n Ok(standard)\n }\n 1 => {\n let path = found.into_iter().next().expect(\"checked len\");\n tracing::debug!(\"Using discovered database: {}\", path.display());\n Ok(path)\n }\n _ => {\n tracing::warn!(\"Multiple databases found: {:?}\", found);\n Err(...)\n }\n }\n}\n```\n\n**Location:** `context.rs:276-314`","acceptance_criteria":"- [ ] Debug log when using standard `issues.jsonl` path\n- [ ] Debug log when using default path for new workspace\n- [ ] Debug log when using discovered non-standard database\n- [ ] Warn log before returning error for multiple databases","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-29T04:33:25.311491982Z","updated_at":"2025-11-29T04:33:25.311491982Z","closed_at":null} -{"id":"rivets-dvsw","title":"Dead-code finder: symbols with zero incoming references","description":"Find unexported / non-public symbols with no incoming references — candidates for removal. Trivial to implement on the existing schema; high developer-value signal.\n\n**Inspired by:** KiroGraph's kirograph_dead_code tool and CLI command.\n\n**Implementation:**\nSingle SQL query:\n SELECT s.* FROM symbols s\n LEFT JOIN call_edges c ON c.callee_symbol_id = s.id\n LEFT JOIN refs r ON r.symbol_id = s.id\n WHERE c.callee_symbol_id IS NULL\n AND r.symbol_id IS NULL\n AND s.visibility != 'public'\n LIMIT ?;\n\nFiltering to non-public is important — public/exported symbols may be used by consumers outside the indexed workspace, so reporting them as dead would generate false positives.\n\n**Public API:**\n- Tethys::find_dead_code(limit: usize) -> Result>\n\n**CLI:**\n- tethys dead-code [--limit N] [--json]\n\n**Caveats to document:**\n- Macro-expanded references aren't tracked, so macro-only call sites can produce false positives.\n- Trait-method dispatch through dyn Trait may not appear as a direct call edge; investigate before reporting.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] find_dead_code() API method\n- [ ] CLI subcommand tethys dead-code with --limit and --json\n- [ ] Filters out public/exported symbols\n- [ ] Documents known false-positive sources in module docs\n- [ ] MCP tool tethys_dead_code (sibling rivets-o4re)\n- [ ] Tests on a fixture with both clearly-dead and clearly-live symbols","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:33:07.744576100Z","updated_at":"2026-05-10T04:33:07.744576100Z","closed_at":null} -{"id":"rivets-cxq","title":"Add priority constants (M-DOCUMENTED-MAGIC)","description":"Priority validation in domain/mod.rs uses magic number `4` without named constants, violating the M-DOCUMENTED-MAGIC guideline.\n\nLocations:\n- Line 133: `if self.priority > 4`\n- Line 269: `if self.priority > 4`\n- Line 500-512: Test using raw numbers 0..=4\n\nCurrent Issues:\n- Magic number repeated in multiple places\n- No single source of truth for valid priority range\n- Unclear to readers what the valid range is","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["code-quality","documentation","refactoring"],"design":"Add public constants in domain/mod.rs after MAX_TITLE_LENGTH (line 207):\n\n```rust\n/// Minimum priority value (highest urgency)\npub const MIN_PRIORITY: u8 = 0;\n\n/// Maximum priority value (lowest urgency)\npub const MAX_PRIORITY: u8 = 4;\n```\n\nUpdate validation code:\n\n```rust\nif self.priority > MAX_PRIORITY {\n return Err(format!(\n \"Priority must be in range {}-{} (got {})\",\n MIN_PRIORITY, MAX_PRIORITY, self.priority\n ));\n}\n```\n\nUpdate tests to use constants.","acceptance_criteria":"- [ ] MIN_PRIORITY and MAX_PRIORITY constants defined\n- [ ] All validation code uses constants instead of magic numbers\n- [ ] Error messages reference constants\n- [ ] Tests use constants (e.g., `for priority in MIN_PRIORITY..=MAX_PRIORITY`)\n- [ ] Constants are documented\n- [ ] All tests pass","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-27T22:48:58.565187568Z","updated_at":"2025-11-28T15:00:31.345213766Z","closed_at":"2025-11-28T15:00:31.345213766Z"} -{"id":"rivets-dr0b","title":"Refactor: Add structured logging per M-LOG-STRUCTURED","description":"The code uses raw println!/eprintln! instead of structured logging:\n\n```rust\neprintln!(\"Warning: Failed to reload after save error: {}\", reload_err);\n```\n\nPer M-LOG-STRUCTURED guideline, should use a logging crate with named properties:\n\n```rust\ntracing::warn!(\n name: \"storage.reload.failed\",\n error = %reload_err,\n \"Failed to reload after save error: {{error}}\"\n);\n```\n\n**Scope:** Review all eprintln! calls in execute.rs and convert to structured tracing events.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["M-LOG-STRUCTURED","execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:40.909394132Z","updated_at":"2025-12-28T17:26:55.645651081Z","closed_at":"2025-12-28T17:26:55.645651081Z"} -{"id":"rivets-0jv","title":"Consider path canonicalization in find_rivets_root for symlink support","description":"The find_rivets_root function traverses parent directories to find a .rivets directory. If the start path contains symlinks, this might behave unexpectedly since it operates on the logical path rather than the resolved physical path.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["enhancement","filesystem"],"design":"Current implementation:\\n```rust\\nlet mut current = start_dir.to_path_buf();\\n```\\n\\nPotential enhancement:\\n```rust\\nlet mut current = start_dir.canonicalize().ok()?.to_path_buf();\\n```\\n\\nTrade-offs:\\n- Pro: Correctly handles symlinked directories\\n- Con: canonicalize() fails if path doesn't exist\\n- Con: Adds complexity for edge case\\n\\nOptions:\\n1. Try canonicalize, fall back to original path on failure\\n2. Document current behavior as intentional\\n3. Add optional `follow_symlinks` parameter\\n\\nCurrent implementation is reasonable for most use cases. This is a low-priority enhancement for symlink-heavy workflows.","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T02:54:20.690844991Z","updated_at":"2025-11-30T02:54:20.690844991Z","closed_at":null} -{"id":"rivets-itez","title":"C# parser: detect unsafe modifier and extract generics","description":"Minor metadata gaps in C# function parsing:\n\n1. `csharp.rs:981` - `is_unsafe` is hardcoded to `false`, should detect `unsafe` modifier\n2. `csharp.rs:983` - `generics` is always `None`, should extract type parameters\n\nThese are minor enhancements for completeness of C# symbol metadata.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["csharp-support","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:38.853313385Z","updated_at":"2026-01-30T00:18:38.853313385Z","closed_at":null} -{"id":"rivets-j5e7","title":"Feature: Implement --no-assignee flag for explicit unassignment","description":"In args.rs (lines 149-156), there's a TODO for implementing a --no-assignee flag:\n\n```rust\n/// New assignee\n///\n/// Note: To unassign, use `--no-assignee` flag instead. Clap does not\n/// support empty strings (\"\") as argument values by default.\n///\n/// TODO: Implement --no-assignee flag for explicit unassignment\n#[arg(short, long)]\npub assignee: Option,\n```\n\n**Problem:** Users cannot unassign an issue via the CLI because clap doesn't accept empty strings.\n\n**Solution:** Add a `--no-assignee` boolean flag that explicitly clears the assignee field when updating an issue.\n\n**Implementation notes:**\n- Add `#[arg(long, conflicts_with = \"assignee\")]` for the new flag\n- In execute_update, check if no_assignee is true and set assignee to Some(None) to clear it","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["args.rs","cli","enhancement"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:42:13.371513935Z","updated_at":"2025-12-28T21:37:19.178243668Z","closed_at":"2025-12-28T21:37:19.178243668Z"} -{"id":"rivets-fn7","title":"Consider case-insensitive assignee matching","description":"The current assignee filtering is case-sensitive (\"alice\" does not match \"Alice\"). Consider whether case-insensitive matching would be more user-friendly.\n\nLocation: Documented in test at `crates/rivets-mcp/tests/integration.rs` line 1863-1869\n\nOptions to consider:\n1. Make assignee matching case-insensitive (more user-friendly)\n2. Keep case-sensitive but normalize on input (e.g., lowercase all assignees)\n3. Keep current behavior (explicit, predictable)","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["pr-feedback","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T18:29:37.222874542Z","updated_at":"2025-11-30T18:29:37.222874542Z","closed_at":null} -{"id":"rivets-kuf","title":"Create comprehensive examples","description":"Create examples demonstrating all major rivets-jsonl features.\n\nExamples should be runnable and well-documented.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create in examples/:\n\n1. **basic.rs** - Simple read/write\n2. **streaming.rs** - Large file streaming\n3. **resilient.rs** - Resilient loading with warnings\n4. **query.rs** - Filtering and querying\n5. **atomic_write.rs** - Atomic writes\n6. **integration.rs** - Integration with rivets\n\nEach example should:\n- Be fully runnable with `cargo run --example `\n- Include inline documentation\n- Demonstrate best practices\n- Show error handling","acceptance_criteria":"- All 6 examples created\n- Each example compiles and runs\n- Examples well-documented\n- README lists all examples with descriptions\n- Examples demonstrate key features","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-dhs","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-t0k","dep_type":"blocks"}],"created_at":"2025-11-27T23:17:58.239786888Z","updated_at":"2025-11-27T23:17:58.239786888Z","closed_at":null} -{"id":"rivets-8yl","title":"Implement resilient streaming for JsonlReader","description":"Implement stream_resilient() method that continues reading despite malformed JSON lines, collecting warnings instead of returning errors.\n\nThis provides the same resilient loading behavior as in_memory::load_from_jsonl().","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nimpl JsonlReader {\n pub fn stream_resilient(\n self\n ) -> (impl Stream, WarningCollector)\n where\n T: DeserializeOwned + 'static,\n {\n let collector = WarningCollector::new();\n let collector_clone = collector.clone();\n \n let stream = futures::stream::unfold(\n (self, collector_clone),\n |(mut reader, collector)| async move {\n loop {\n match reader.read_line().await {\n Ok(Some(value)) => return Some((value, (reader, collector))),\n Ok(None) => return None, // EOF\n Err(e) => {\n // Collect warning and continue\n collector.add(Warning::MalformedJson {\n line_number: reader.line_number,\n error: e.to_string(),\n });\n // Continue to next line\n continue;\n }\n }\n }\n },\n );\n \n (stream, collector)\n }\n}\n```","acceptance_criteria":"- stream_resilient() method implemented\n- Returns (Stream, WarningCollector)\n- Continues reading on malformed JSON\n- Warnings collected for each error\n- Stream yields only successfully parsed records\n- Unit tests verify resilient behavior\n- Integration test with mixed valid/invalid JSONL","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-dgt","dep_type":"blocks"}],"created_at":"2025-11-27T23:15:55.303102489Z","updated_at":"2025-11-28T02:05:48.560873847Z","closed_at":"2025-11-28T02:05:48.560873847Z"} -{"id":"rivets-cux","title":"Add interactive prompt tests with mock stdin","description":"Add tests for interactive prompts in execute.rs by mocking stdin input. This ensures the interactive title/description prompts handle various inputs correctly including:\n- Valid inputs\n- Invalid inputs that should be rejected\n- Empty inputs\n- Multiline inputs for descriptions","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:25.383605039Z","updated_at":"2025-11-30T04:11:25.383605039Z","closed_at":null} -{"id":"rivets-sx8j","title":"Rivets Core: Features & Refactoring","description":"Core rivets functionality: query system, filtering, config, daemon, storage backends, structured errors, and codebase refactoring.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:28.807289106Z","updated_at":"2026-02-21T02:28:28.807289106Z","closed_at":null} -{"id":"rivets-n3w","title":"Add smart suggestions after operations","description":"Show context-aware suggestions after operations:\n\n```\n$ rivets close rivets-abc\nClosed issue: rivets-abc\n\nTip: rivets-def is now unblocked. Run 'rivets ready' to see available work.\n```\n\nAlso show warnings for risky operations:\n```\n$ rivets update rivets-abc --status closed\nWarning: Use 'rivets close' instead for proper close workflow.\nContinue anyway? [y/N]:\n```","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:24.182937809Z","updated_at":"2025-11-30T18:36:24.182937809Z","closed_at":null} -{"id":"rivets-40p","title":"Add NO_COLOR environment variable support","description":"Support NO_COLOR environment variable per https://no-color.org/\n\nWhen NO_COLOR is set (to any value), disable all colored output. This ensures compatibility with:\n- CI/CD pipelines\n- Log aggregation systems\n- Users with accessibility needs\n- Piped output\n\nAlso auto-detect if stdout is a TTY and disable colors if not.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["accessibility","phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:58.392325579Z","updated_at":"2025-11-30T18:34:58.392325579Z","closed_at":null} +{"id":"rivets-dn35","title":"tethys: Pass 2 short-circuits on import-less files, leaving workspace-internal refs unresolved","description":"crates/tethys/src/resolve.rs::resolve_refs_for_file returns early when imports.is_empty(), so any unresolved reference in a file with no use statements never reaches Pass-2 import/fallback resolution.\n\nCLAUDE.md flags this explicitly in the 'Tethys resolver internals' section but it's not tracked.\n\nAffected input shape: files that legitimately reference workspace symbols without a use statement — typically:\n - test files using fully-qualified paths (crate_name::Type)\n - mod.rs files that re-export via pub use only\n - integration test harnesses\n\nImpact on resolution coverage: unknown without measurement. The early-return is a speed optimization that traded correctness for cycles; the import-less case may have been judged rare when it shipped, but on the rivets workspace itself it's worth quantifying.\n\nReproduction: index the rivets workspace, then query for refs where r.symbol_id IS NULL AND r.file_id IN (SELECT id FROM files WHERE NOT EXISTS (SELECT 1 FROM imports WHERE file_id = files.id)) AND r.reference_name IN (SELECT name FROM symbols). Any non-zero result is a Pass-2 miss this short-circuit caused.\n\nLikely fix: drop the imports.is_empty() short-circuit entirely, or replace with a per-ref check (don't skip the whole file, just skip refs whose name can't resolve without an import context). Pass 2's fallback search_symbol_by_name path doesn't actually need imports — only the import-resolution path does.","status":"closed","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Probe SQL above returns 0 rows on the rivets workspace\n- [ ] Regression test: a fixture file with no use statements but a fully-qualified workspace reference (e.g., crate_target::Widget) resolves correctly\n- [ ] resolve_refs_for_file no longer returns Ok(()) early when imports.is_empty()\n- [ ] No regression in resolution time on the rivets workspace (measure via tethys index timing)","notes":"Closed: Fixed in PR #69 (commit 21e82e6). Removed the imports.is_empty() short-circuit in resolve_refs_for_file so import-less files now reach fallback_symbol_search. Locked in by tests/pass2_no_imports.rs. Acceptance criterion #2 (qualified-ref test) was partially met: the unqualified-fallback case works; the qualified-path case is a separate gap filed as rivets-044i.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-13T02:48:03.363815600Z","updated_at":"2026-05-18T18:51:03.202003600Z","closed_at":"2026-05-18T18:51:03.202000400Z"} +{"id":"rivets-jwf9","title":"C# namespace resolution for using statements","description":"`resolve_import()` in `csharp.rs:109-111` returns empty vec, meaning C# `using` statements don't resolve to files.\n\nThis was marked \"Task 6\" in the original TODO, suggesting it was planned. Need to implement namespace-to-file resolution for C# projects.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["csharp-support","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:33.059266108Z","updated_at":"2026-01-30T00:18:33.059266108Z","closed_at":null} +{"id":"rivets-4q2","title":"Integrate resilient JSONL loading with in_memory storage","description":"Update rivets in_memory::load_from_jsonl() to use the new rivets-jsonl library's resilient loading functionality.\n\nThis replaces the current manual JSONL parsing with the library implementation.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Update in_memory.rs to use rivets-jsonl:\n\n```rust\nuse rivets_jsonl::read_jsonl_resilient;\n\npub async fn load_from_jsonl(\n path: &Path,\n prefix: String,\n) -> Result<(Box, Vec)> {\n let (issues, warnings) = read_jsonl_resilient::(path).await?;\n \n // Convert rivets_jsonl::Warning to LoadWarning\n let load_warnings: Vec = warnings\n .into_iter()\n .map(|w| match w {\n rivets_jsonl::Warning::MalformedJson { line_number, error } => {\n LoadWarning::MalformedJson { line_number, error }\n }\n rivets_jsonl::Warning::SkippedLine { line_number, reason } => {\n // Map to appropriate LoadWarning variant\n LoadWarning::MalformedJson { line_number, error: reason }\n }\n })\n .collect();\n \n // Continue with existing dependency reconstruction logic\n let storage = Arc::new(Mutex::new(InMemoryStorageInner::new(prefix)));\n // ... existing code\n \n Ok((Box::new(storage), load_warnings))\n}\n```\n\nAdd rivets-jsonl as dependency in rivets/Cargo.toml.","acceptance_criteria":"- in_memory::load_from_jsonl uses rivets-jsonl library\n- Warnings correctly mapped between types\n- All existing tests still pass\n- No performance regression\n- Dependency added to Cargo.toml","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-uyg","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:06.671886113Z","updated_at":"2025-11-28T07:04:14.251308562Z","closed_at":"2025-11-28T07:04:14.251308562Z"} +{"id":"rivets-1zz","title":"Add conflict logging for Automerge merges","description":"Even though CRDTs auto-resolve conflicts, users should be aware when merges happen.\n\nImplement:\n- Log when merge() is called and changes are integrated\n- Track which fields had concurrent modifications\n- Optional: Store merge events in a separate log file or document metadata\n- Surface merge history in `rv log` or similar command","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T21:49:37.307979170Z","updated_at":"2025-12-23T04:45:28.797678904Z","closed_at":"2025-12-23T04:45:28.797678904Z"} +{"id":"rivets-4l2","title":"Implement init command and workspace setup","description":"Implement the init command that creates the .rivets directory structure, initializes the SQLite database, and sets up configuration.","status":"closed","priority":1,"issue_type":"task","assignee":"rust-developer","labels":[],"design":"Based on MVP architecture (in-memory + JSONL):\n\n**Directory Structure**:\n```\n.rivets/\n├── issues.jsonl # JSONL data file\n├── config.yaml # Project config\n└── .gitignore # Ignore metadata files\n```\n\n**Init Process**:\n1. Check if already initialized (error if exists)\n2. Create .rivets/ directory\n3. Create default config.yaml with storage backend settings\n4. Set issue prefix (from flag or prompt)\n5. Create .gitignore\n6. Create empty issues.jsonl file\n\n**Git Integration**:\n- Add .rivets to .gitignore root if not present\n- Suggest git commit for initial setup\n\n**CLI**:\n```bash\nrivets init [--prefix ] [--quiet]\n```\n\n**Config Template**:\n```yaml\nissue-prefix: \"proj\"\nstorage:\n backend: \"memory\"\n data_file: \".rivets/issues.jsonl\"\n```","acceptance_criteria":"- Init creates .rivets directory\n- config.yaml created with storage backend = memory\n- issues.jsonl created (empty initially)\n- .gitignore created\n- Prefix validation (alphanumeric, 2-20 chars)\n- Error if already initialized\n- Integration test verifies complete setup\n- No database files created","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:16:47.684395549Z","updated_at":"2025-11-30T01:41:23.084110706Z","closed_at":"2025-11-30T01:41:23.084110706Z"} +{"id":"rivets-6pi","title":"Implement JsonlWriter::write() and write_all() methods","description":"Implement write methods for JsonlWriter that serialize and write records to the output stream.\n\nIncludes both single-record write() and batch write_all() for efficiency.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse serde::Serialize;\nuse tokio::io::AsyncWriteExt;\n\nimpl JsonlWriter {\n pub async fn write(&mut self, value: &T) -> Result<()> {\n let json = serde_json::to_string(value)?;\n self.writer.write_all(json.as_bytes()).await?;\n self.writer.write_all(b\"\\n\").await?;\n Ok(())\n }\n \n pub async fn write_all(\n &mut self,\n values: impl IntoIterator,\n ) -> Result<()> {\n for value in values {\n self.write(&value).await?;\n }\n Ok(())\n }\n \n pub async fn flush(&mut self) -> Result<()> {\n self.writer.flush().await?;\n Ok(())\n }\n}\n```","acceptance_criteria":"- write() method serializes and writes single record\n- write_all() writes multiple records efficiently\n- flush() method flushes buffered data\n- Each record terminated with newline\n- Proper error handling for serialization failures\n- Unit tests verify writing single/multiple records\n- Integration test with read/write round-trip","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-uo7","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:51.609289345Z","updated_at":"2025-11-28T00:21:22.277922663Z","closed_at":"2025-11-28T00:21:22.277922663Z"} +{"id":"rivets-v465","title":"tethys: import resolver leaks legitimate cross-crate refs to unscoped fallback","description":"Discovered as the root-cause blocker for rivets-3d0s during gilfoyle/checkpointed-build (PR #61 follow-up branch fix/rivets-3d0s-stdlib-symbol-pollution).\n\n**Symptom:** ~80% of legitimate cross-crate refs in Cargo-dep-allowed pairs are being resolved through the unscoped workspace-wide fallback (search_unique_symbol_by_name) instead of via Pass 2's explicit/glob import resolution.\n\n**Quantified on the rivets workspace (post-rivets-0gom, pre-rivets-3d0s):**\n - Total cross-crate refs: 294\n - In FORBIDDEN pairs (phantoms): 174\n - In ALLOWED pairs (legitimate): 120\n - Of those 120 legit_cross, **95 reach unscoped fallback** (would be demoted by an extended rivets-3d0s-style audit)\n - Breakdown of leaks: ~50 function calls, ~48 method calls, ~6 enum_variant constructors\n\n**Why this is a bigger bug than rivets-3d0s:**\nSame code path (unscoped fallback) is currently doing BOTH legitimate cross-crate resolution (when imports leak) and phantom resolution (when no real import exists). Any audit/filter that tries to demote phantom resolutions at this level hits 80% of legitimate refs too. The rivets-3d0s audit-and-demote design is blocked on this — without fixing the import resolver, no kind-compatibility rule can distinguish the two populations.\n\n**Reproduction:**\nOn branch fix/rivets-3d0s-stdlib-symbol-pollution (or main, post rivets-0gom):\n 1. tethys index --rebuild (re-index rivets workspace)\n 2. uv run --no-project --python 3.13 -- python .rivets-3d0s/audit_simulation.py\n - With EXTENDED=True at the top of the script\n 3. Observe: 'legit-cross demoted: 95' — these are the leak signal.\n\n**Discovered during:** gilfoyle/checkpointed-build of slice 1+2 of rivets-3d0s. The audit landed; probe showed 174 -> 164 phantoms (5.7% reduction vs predicted 58.6%). Root cause analysis: the audit's narrowing of candidate sets converts previously-ambiguous names into unique matches, creating new method/function phantoms. Extending the audit to cover call->method via unscoped (Option A) was probed and shown to have an 80% false-positive rate on ALLOWED pairs. Slice 1+2 was reverted.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":"**Investigation entry points:**\n\n1. **crates/tethys/src/resolve.rs::resolve_via_explicit_import** — does it correctly handle all of:\n - 'use rivets::storage::Storage;' (named import, multi-segment path)\n - 'use rivets::storage::*;' (glob import)\n - 'use rivets::storage;' followed by 'storage::Storage' (partial-path use)\n - Re-exports ('pub use crate::storage::Storage;' in lib.rs)\n - Alias imports ('use rivets::storage::Storage as S;')\n\n2. **crates/tethys/src/resolve.rs::resolve_via_glob_import** — same checks for glob.\n\n3. **crates/tethys/src/languages/rust.rs** — does the extractor capture imports correctly? Maybe imports are extracted but with wrong source_module strings.\n\n4. **The 'method' bucket specifically:** even with imports of the TYPE, method calls like 'storage.create_issue()' may bypass import resolution because tree-sitter doesn't know that 'storage's type came from an import. This may require special handling — possibly the right answer is 'don't try; let LSP handle it' (which routes back to rivets-714v).\n\n**Likely findings:**\n- The 50 function calls are probably failing on multi-segment paths or re-exports\n- The 48 method calls likely need type-info (LSP) — separately tracked as rivets-714v\n- The 6 enum_variant constructors likely need qualified-name handling (e.g., 'Error::Variant' resolving when only 'Error' is imported)\n\n**Suggested approach:**\nDo gilfoyle/prove-it-prototype on this issue. Build a probe that:\n 1. Lists the 95 leaked refs by (caller_file, ref_name, target_file)\n 2. For each, inspects the caller_file's imports to determine what SHOULD have matched\n 3. Classifies failures: 're-export', 'multi-segment-path', 'glob-import-miss', 'method-needs-type-info', 'qualified-name-construct'\n\nThen design fixes per category. Some may be cheap (re-export support); some may be expensive (type-info-via-LSP).","acceptance_criteria":"- [ ] Categorize the 95 (or current count) leaks by failure class (re-export miss, multi-segment, glob, method-needs-type-info, etc.)\n- [ ] For at least the top 2 failure classes, design + implement fixes\n- [ ] On the rivets workspace post-fix: legit_cross refs via unscoped drops by >= 50% (95 -> <= 47)\n- [ ] No regression in same-crate or import-based resolution counts\n- [ ] After this lands, re-attempt rivets-3d0s: the extended audit (Option A) should have a manageable false-positive rate (<= 10 ALLOWED-pair demotions)","notes":"\n\n## Checkpointed-build outcome (2026-05-12)\n\nBranch: fix/rivets-v465-import-resolver-leak. Status: slices 1+2 IMPLEMENTED; slice 3 verification done; ACTUAL impact much smaller than design predicted.\n\n**What slice 1+2 actually fixes:** the small-but-real case where a leaked cross-crate ref's bare name matches an explicit workspace-crate import (`use rivets::storage::in_memory::new_in_memory_storage` + bare `new_in_memory_storage()`). resolve_module_path now correctly handles `path[0] == workspace_crate_name`.\n\n**Empirical impact on rivets workspace:** 6 refs migrate from Pass-2-fallback to Pass-2-imports. Pre-fix all 279 cross-crate refs (105 ALLOWED + 174 FORBIDDEN) went through fallback; post-fix 273 still do.\n\n**Why so small:** the refining probe (.rivets-v465/refine_c5_upper_bound.py) revealed that only 5 of 105 ALLOWED-pair leaks have a `ref_name`-matching workspace import. The other 100 are method-on-imported-type calls (`use rivets::Storage; s.create_issue()`) — the type is imported but the method has no import of its own. Pass 2's name-matching resolver fundamentally cannot catch these without type information.\n\n**Re-scoped acceptance criteria:** original \"≥50% fallback reduction\" replaced with \"≥5 refs migrate\" (achievable; 6 measured). The dominant population (100 method-on-imported-type leaks + 174 FORBIDDEN phantoms) now depends on rivets-714v (LSP integration for multi-crate workspaces).\n\n**Diagnostic artifacts retained on the branch:**\n- .rivets-v465/probe.py - initial leak categorization\n- .rivets-v465/check_same_crate_ambiguity.py - same-crate-vs-cross split \n- .rivets-v465/check_pass_provenance.py - resolved ref counts\n- .rivets-v465/source_module_shapes.py - cheapest falsifier (workspace-crate imports shape)\n- .rivets-v465/refine_c5_upper_bound.py - design-correcting probe\n- .rivets-v465/after-fix-counts.txt - final empirical snapshot\n- .rivets-v465/falsifiable-design.md - includes \"Re-design rationale\" section explaining the C5/C6 revision\n\n**Implication for rivets-3d0s:** still blocked. The fallback still handles ~99% of cross-crate resolution post-fix, so any rule at the fallback level (like rivets-3d0s's audit-and-demote) would still produce massive false positives. Audit-and-demote becomes viable only after rivets-714v migrates the method-on-imported-type calls out of fallback.\n\n## Related-issues discovery (2026-05-12, after PR #62 opened)\n\nProcess miss: should have searched the rivets tracker before filing rivets-v465. Two open issues describe the same multi-crate-resolver code class from different angles:\n\n- **[rivets-6aoc](rivets-6aoc)** (P2, open): \"resolve.rs:66 uses `self.workspace_root.join('src')` which is wrong for multi-crate workspaces. Should use per-crate src_root from `self.crates: Vec`. Flagged in PR #55 review.\"\n- **[rivets-34tv](rivets-34tv)** (P2, open): \"Two sites in lib.rs (~1089 and ~1516) still hardcode workspace_root/src/ as crate root in `compute_dependencies` and `resolve_unresolved_references`.\"\n- **[rivets-m4wt](rivets-m4wt)** (P1, closed Feb): \"Parse Cargo.toml to detect actual crate root.\" `CrateInfo` discovery was added but applied incompletely — the three hardcoded sites above survived.\n\n**rivets-v465's relationship to these:** distinct bug, same code class. v465 fixes `path[0]==workspace_crate_name` matching (e.g., `use rivets::Foo`). 6aoc/34tv fix the caller's own `crate_root` being hardcoded (affects `use crate::Foo` resolution for non-root crates in multi-crate workspaces). Both are needed for full multi-crate support; neither subsumes the other.\n\n**Hardcoded `workspace_root.join(\"src\")` still present (as of HEAD of rivets-v465 branch):**\n- `crates/tethys/src/resolve.rs:66` (resolve_cross_file_references)\n- `crates/tethys/src/indexing.rs:857` (compute_dependencies first call site)\n- `crates/tethys/src/indexing.rs:1023` (compute_dependencies second call site)\n\nEmpirical implication: my v465 measurements showed 5971 same-crate refs resolved, but that count is masked by Pass-1 same-file resolution + Pass-2 fallback. Pass-2 cross-file same-crate refs (`use crate::Foo` where `crate` means the caller's actual crate, not `workspace_root/src`) are likely ALSO failing in non-root crates due to 6aoc/34tv. A combined fix would compound the value.\n\nClosed: Merged in PR #62 (commit c1af978)","external_ref":null,"dependencies":[],"created_at":"2026-05-12T04:18:20.703569200Z","updated_at":"2026-05-12T22:01:15.506283400Z","closed_at":"2026-05-12T22:01:15.506276900Z"} +{"id":"rivets-t0k","title":"Add comprehensive tests for resilient loading","description":"Create comprehensive tests for Phase 2 resilient loading functionality.\n\nTests should verify warning collection, error recovery, and integration with in_memory storage.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Test categories:\n\n**Warning collection tests**:\n- Collect warnings for malformed JSON\n- Collect warnings for skipped lines\n- Warning contains correct line numbers\n- Multiple warnings collected\n\n**Resilient streaming tests**:\n- stream_resilient() continues on errors\n- stream_resilient() yields only valid records\n- Mixed valid/invalid records handled correctly\n\n**Integration tests**:\n- read_jsonl_resilient() with corrupted file\n- in_memory::load_from_jsonl with warnings\n- Verify existing in_memory tests still pass","acceptance_criteria":"- Unit tests for warning collection\n- Unit tests for stream_resilient()\n- Integration tests for read_jsonl_resilient()\n- Integration tests for in_memory integration\n- All tests pass\n- Test coverage >80% for Phase 2 code","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4q2","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:12.327924224Z","updated_at":"2025-11-28T07:16:42.034792534Z","closed_at":"2025-11-28T07:16:42.034792534Z"} +{"id":"rivets-e7fp","title":"Implement content hashing for file change detection","description":"The content_hash field exists in the schema and types but is always passed as None. Implement actual content hashing to enable content-based change detection alongside mtime. This would allow skipping re-indexing when mtime changes but content has not. Three TODO sites: lib.rs (2) and batch_writer.rs (1).","status":"closed","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Closed: Duplicate of rivets-3l14 (older, describes the same content_hash TODOs in lib.rs and batch_writer.rs).","external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-05T22:54:56.057264980Z","updated_at":"2026-05-10T04:30:14.008782Z","closed_at":"2026-05-10T04:30:14.008780200Z"} +{"id":"rivets-wsix","title":"tethys: audit other UPSERT-only tables for stale-row growth (sibling of rivets-lcb6)","description":"Surfaced by code-reviewer during PR #65 multi-agent review. The rivets-lcb6 fix establishes the pattern: UPSERT-only tables (INSERT ... ON CONFLICT DO UPDATE) need an explicit clear before full re-index, or 'ref_count'-style aggregates grow monotonically and removed-at-source rows persist as phantoms.\n\nPR #65 fixed file_deps (and noted call_edges already had its clear_all_call_edges). Other tables in crates/tethys/src/db/ using ON CONFLICT DO UPDATE were NOT audited.\n\nLikely candidates to grep (ON CONFLICT...DO UPDATE in crates/tethys/src/db/):\n- imports (db/imports.rs?)\n- refs (db/refs.rs)\n- attributes (db/attributes.rs?)\n- symbols' subordinate tables (enum variants, struct fields — added in PR #58)\n- architecture / coupling tables (added in PR #60)\n- any new table added after rivets-lcb6 lands\n\nFor each: classify as\n (a) UPSERT with monotonically-growing aggregate column → SAME BUG, needs clear_all_X + call from index_with_options\n (b) UPSERT but no growing aggregate → still has the 'stale row persists when source is deleted' subset of the bug\n (c) UPSERT but table is fully rebuilt from another source-of-truth each run → no bug\n\nDeliverable: a comment in this issue listing every UPSERT-only table with its classification, then file follow-up issues for any (a) or (b) cases.\n\nThe 30.7% baseline-vs-resolved gap noted in project_resolver_baseline_2026_05_12.md may have contributions from this class of bug we haven't measured.\n\nSource: PR #65 multi-agent review aggregate (code-reviewer 'Suggestion #3'/Strength).","status":"in_progress","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"* All UPSERT-only tables in crates/tethys/src/db/ enumerated with classification (a/b/c)\n* Follow-up issues filed for each (a) or (b) classification\n* If any (a) is found, regression-fence test added matching the file_deps_idempotency.rs pattern","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-17T02:43:53.022009444Z","updated_at":"2026-05-19T01:31:35.201636100Z","closed_at":null} +{"id":"rivets-dhxo","title":"tethys: streaming compute_all_dependencies re-inserts file_deps from orphan files (deleted-from-disk but still in DB)","description":"Streaming-mode indexing (IndexOptions::with_streaming()) calls compute_all_dependencies (crates/tethys/src/indexing.rs:943) which iterates self.db.list_all_files() — the FULL set of files in the DB, including files that have been deleted from disk since their last index. For each such orphan, it loads the stale stored imports + refs and calls compute_dependencies_from_stored, which re-inserts file_deps rows with the orphan''s file_id as from_file_id.\n\nDownstream queries (coupling Ce/Ca, callers, cycles, impact analysis) then see the orphan as a real source of cross-file edges, producing phantom contributions.\n\n**Asymmetries — which paths are affected:**\n\n| Path | Status |\n|---|---|\n| tethys index --rebuild (any mode) | Not affected — db.reset() wipes DB; orphans gone |\n| tethys index non-streaming (default) | Not affected — compute_dependencies runs per disk-file in the parse loop; orphans never seen |\n| tethys index streaming | **Affected** |\n\n**Root cause:** index_with_options has no orphan-cleanup pass. db/files.rs:145-146 only DELETEs symbols/imports when an EXISTING file is RE-indexed (still on disk). Files deleted from disk are never processed, so no DELETE fires for them. FileChange::Deleted is detected in reindex.rs:122 but only by get_stale_files() (observation API, not called from indexing).\n\n**How discovered:** Gemini code review on PR #65 flagged \"compute_all_dependencies may re-calculate dependencies for stale entries (files in DB but deleted from disk).\" Initial round-1 verdict was \"reject (not a real bug)\" on cascade-cleanup grounds; pressure-test revealed the cascade only applies to re-indexed files, not orphans. See .rivets-lcb6/review-decisions-round-1.md for the corrected verdict.\n\n**Not caused by rivets-lcb6.** Pre-existing in streaming mode. rivets-lcb6''s clear_all_file_deps even partially mitigates by wiping file_deps each run, but compute_all_dependencies immediately re-inserts from orphans.\n\n**Likely fix shape:** before compute_all_dependencies runs, delete file rows whose disk path doesn''t resolve (and let FK cascades clean up dependents). Either:\n1. New cleanup_orphan_files that classifies via the same staleness check as reindex.rs::classify_indexed_file and DELETEs orphan rows, called from index_with_options before resolver/dep passes.\n2. Filter compute_all_dependencies to skip files whose path doesn''t exist on disk (lighter touch, leaves orphan rows in files table).\n\nOption 1 is cleaner — once orphan files are gone from the DB, all downstream queries are correct without per-callsite filtering.","status":"open","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Regression test: index a 2-file workspace in streaming mode, delete one file from disk, re-index (no --rebuild). Verify (a) the orphan no longer appears in list_all_files, OR (b) file_deps contains no rows with from_file_id == orphan's id.\n- [ ] Equivalent test for non-streaming mode confirms the bug doesn't manifest there (regression fence).\n- [ ] No perf regression: indexing a workspace with N files and 0 orphans takes the same time before and after.","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T03:26:07.176733800Z","updated_at":"2026-05-13T03:26:37.855664300Z","closed_at":null} +{"id":"rivets-w0qw","title":"tethys: define and document --depth 0 semantics for impact analysis","description":"After PR #57, `tethys impact --depth N` is wired through. `--depth 0` is currently undefined behavior: it would call `get_transitive_dependents(file_id, Some(0))` which depends on whatever the SQLite recursive CTE does at depth 0 (likely returns nothing).\n\nDecide and document the contract: either treat 0 as 'direct dependents only and no transitive search', or reject it at the CLI layer with a friendly error. Add a test pinning the behavior either way.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-04-27T21:56:00.600464844Z","updated_at":"2026-04-27T21:56:00.600464844Z","closed_at":null} +{"id":"rivets-zoi3","title":"tethys: expand file_deps test coverage (rename, target deletion, DB-unit, rebuild idempotency)","description":"Surfaced by pr-test-analyzer during multi-agent review of PR #65. The 2 regression tests added in rivets-lcb6 fence the specific reported bug well, but four adjacent code paths are uncovered.\n\nTests to add (in crates/tethys/tests/file_deps_idempotency.rs unless noted):\n\n1. file_deps_swapped_when_use_target_changes (rating 8 / high). Replace 'use crate_target::Widget' with 'use crate_target_alt::Gadget' in the fixture. Re-index. Assert the edge to crate_target is GONE and the edge to crate_target_alt EXISTS — check by exact FileId lookup, not just row count (counts could net to zero coincidentally and hide the bug).\n\n2. clear_all_file_deps unit test (rating 6 / medium). crates/tethys/src/db/file_deps.rs has no #[cfg(test)] mod tests block. clear_all_file_deps is pub and per CLAUDE.md 'every public method needs tests' (rust-best-practices Rule 39). Add: insert 2 deps via insert_file_dependency, call clear_all_file_deps, assert get_file_dependencies returns [] for both source FileIds. Catches SQL typos / schema renames the integration tests would miss.\n\n3. file_deps_target_file_deletion_removes_edge (rating 8 / high but DEFERRED to rivets-dhxo). Delete crate_target/src/lib.rs from disk, re-index, assert no edges reference the orphan file. This test would currently FAIL in streaming mode — that's the whole point. Defer landing this until rivets-dhxo is fixed so it doesn't break CI. Cross-reference here so the regression-fence test gets added alongside the fix.\n\n4. rebuild_with_options idempotency (rating 7 / medium). The design doc notes rebuild is 'correct by accident' because db.reset() wipes the table first. Add a #[case::rebuild] variant or separate test calling rebuild_with_options(IndexOptions::default()) twice and assert FileDepsSnapshot stability. Defends against someone 'optimizing' rebuild to skip the reset later.\n\n5. (Suggestion, rating 4) file with 2 uses of same target → remove one → row remains, ref_count drops. Probes the multi-ref-per-edge code path the existing tests don't exercise.\n\nSource: PR #65 multi-agent review aggregate (pr-test-analyzer findings 1-5).","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"* file_deps_swapped_when_use_target_changes added and passing\n* clear_all_file_deps unit test added to db/file_deps.rs #[cfg(test)] mod tests\n* rebuild_with_options idempotency test added (as #[case::rebuild] or standalone)\n* (deferred) file_deps_target_file_deletion_removes_edge added once rivets-dhxo lands","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-17T02:43:32.040071906Z","updated_at":"2026-05-17T02:43:32.040071906Z","closed_at":null} +{"id":"rivets-yip","title":"Write comprehensive API documentation","description":"Write comprehensive rustdoc documentation for all public APIs with usage examples.\n\nDocumentation should match or exceed the quality outlined in research document.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Documentation requirements:\n\n1. **Module-level docs** (lib.rs):\n - Overview of library\n - Quick start guide\n - Feature comparison with other libraries\n - Performance characteristics\n\n2. **Type documentation**:\n - JsonlReader with examples\n - JsonlWriter with examples\n - JsonlQuery with examples\n - Warning types\n - Error types\n\n3. **Method documentation**:\n - All public methods documented\n - Include # Example blocks\n - Document edge cases\n - Document performance characteristics\n\n4. **README.md**:\n - Installation instructions\n - Quick start\n - Feature list\n - Performance benchmarks\n - Comparison with alternatives\n\nGenerate docs with `cargo doc --no-deps --open`.","acceptance_criteria":"- All public APIs documented\n- Module-level docs comprehensive\n- README.md complete\n- All doc examples compile (tested with cargo test --doc)\n- Documentation coverage >90%\n- cargo doc generates without warnings","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6640","dep_type":"parent-child"},{"depends_on_id":"rivets-kuf","dep_type":"blocks"}],"created_at":"2025-11-27T23:18:03.937459711Z","updated_at":"2025-11-27T23:18:03.937459711Z","closed_at":null} +{"id":"rivets-v21s","title":"Implement cycle detection for file dependencies","description":"Implement the `detect_cycles()` method that currently returns \"not implemented\".\n\n**What already exists:**\n- ✅ `FileGraphOps::detect_cycles()` trait method (graph/mod.rs:88)\n- ✅ `FileGraphOps::detect_cycles_involving(file_id)` trait method\n- ✅ `tethys cycles` CLI command (cli/cycles.rs)\n- ✅ `file_dependencies` table with all edges\n- ❌ Actual implementation (returns error at graph/sql.rs:318)\n\n**Implementation approach (~100 lines):**\n\nOption A: **Tarjan's SCC** (finds all cycles efficiently)\n```rust\nfn detect_cycles(&self) -> Result> {\n // Standard Tarjan's algorithm for strongly connected components\n // Any SCC with size > 1 is a cycle\n}\n```\n\nOption B: **Simple DFS** (easier to understand)\n```rust\nfn detect_cycles(&self) -> Result> {\n let mut cycles = Vec::new();\n let mut visited = HashSet::new();\n let mut rec_stack = HashSet::new();\n \n for file_id in self.get_all_file_ids()? {\n self.dfs_find_cycles(file_id, &mut visited, &mut rec_stack, &mut cycles)?;\n }\n Ok(cycles)\n}\n```\n\n**The CLI already handles output:**\n```rust\n// cli/cycles.rs already formats output nicely:\n// Cycle 1: a.rs -> b.rs -> c.rs -> a.rs\n```\n\n**Estimated effort: Low-Medium (~100 lines, 2-3 hours)**\n\nJust need to implement the algorithm - all the plumbing exists.","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":["drift-inspired","feature","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:53:28.542509377Z","updated_at":"2026-01-29T15:00:39.567609143Z","closed_at":"2026-01-29T15:00:39.567609143Z"} {"id":"rivets-ugg6","title":"Add integration tests for cross-file resolution","description":"Add tests in tests/indexing.rs:\n\n- cross_file_callers_via_explicit_import\n- cross_file_callers_via_qualified_path \n- cross_file_callers_via_glob_import\n- csharp_cross_file_via_namespace\n\nEach test creates a multi-file workspace and verifies callers() finds cross-file references.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-rndz","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:52.150595727Z","updated_at":"2026-01-28T18:11:03.344673588Z","closed_at":"2026-01-28T18:11:03.344673588Z"} -{"id":"rivets-2uv","title":"Evaluate rstest for rivets-jsonl/src/warning.rs and atomic.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in warning.rs and atomic.rs.\n\nFiles:\n- crates/rivets-jsonl/src/warning.rs (lines ~392+)\n- crates/rivets-jsonl/src/atomic.rs (lines ~209+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:19.453806850Z","updated_at":"2025-11-29T01:28:55.618500868Z","closed_at":"2025-11-29T01:28:55.618500868Z"} -{"id":"rivets-xov3","title":"C# parser doesn't extract nested types (classes, records, structs)","description":"The C# parser's `extract_class_members` function only extracts methods and constructors from type declarations. It does NOT recurse into nested type declarations (classes, records, structs, interfaces).\n\n**Impact**: References like `Distribution.Percentage` can't be resolved because `Percentage` (a nested record) is never indexed.\n\n**Root cause**: `extract_class_members()` in `csharp.rs:663-696` only handles `METHOD_DECLARATION` and `CONSTRUCTOR_DECLARATION`. It should also handle `CLASS_DECLARATION`, `RECORD_DECLARATION`, `STRUCT_DECLARATION`, `INTERFACE_DECLARATION`.\n\n**Test added**: `extracts_nested_types` test in `csharp.rs` (currently failing) demonstrates the issue.\n\n**Example**:\n```csharp\npublic abstract record Distribution\n{\n public sealed record Percentage : Distribution { } // NOT extracted\n public sealed record FixedAmount : Distribution { } // NOT extracted\n}\n```\n\nOnly `Distribution` is extracted, not the nested `Percentage` or `FixedAmount` types.","status":"closed","priority":1,"issue_type":"bug","assignee":null,"labels":["csharp-support","parser","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-30T03:40:30.208935600Z","updated_at":"2026-01-30T03:46:50.536075597Z","closed_at":"2026-01-30T03:46:50.536075597Z"} -{"id":"rivets-vdi","title":"Wire StorageBackend::Jsonl to use InMemoryStorage::load_from_jsonl","description":"The `StorageBackend::Jsonl(path)` variant exists but returns an error \"JSONL storage backend not yet implemented\". \n\nPer architecture doc, JSONL is persistence for InMemory, not a separate backend. Wire this variant to use `InMemoryStorage::load_from_jsonl(path)`.\n\nThis unblocks the MCP server which already uses `StorageBackend::Jsonl(db_path)` in `context.rs:109`.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Update `create_storage()` in `crates/rivets/src/storage/mod.rs`:\n\n```rust\nStorageBackend::Jsonl(path) => {\n let (storage, warnings) = in_memory::load_from_jsonl(&path, \"rivets\".to_string()).await?;\n if !warnings.is_empty() {\n // Log warnings - storage still usable\n for w in &warnings {\n tracing::warn!(\"JSONL load warning: {:?}\", w);\n }\n }\n Ok(storage)\n}\n```\n\nIf file doesn't exist, create empty InMemoryStorage (first-run case).","acceptance_criteria":"- [ ] `StorageBackend::Jsonl(path)` returns working storage\n- [ ] Loads existing JSONL file if present\n- [ ] Creates empty storage if file doesn't exist (first run)\n- [ ] Warnings logged but don't fail load\n- [ ] MCP server can successfully use Jsonl backend\n- [ ] Unit test for Jsonl backend creation","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:36:46.342699192Z","updated_at":"2025-11-29T23:40:44.382872584Z","closed_at":"2025-11-29T23:40:44.382872584Z"} -{"id":"rivets-3hs","title":"Add indicatif dependency for progress indicators","description":"Add indicatif crate to workspace dependencies for progress bars and spinners.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:19.476455174Z","updated_at":"2025-11-30T18:35:19.476455174Z","closed_at":null} -{"id":"rivets-8fe","title":"Add edge case filter tests for MCP integration tests","description":"Add comprehensive edge case tests for the filter functionality in MCP integration tests to ensure robust handling of boundary conditions and special scenarios.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["mcp","testing"],"design":"Add test cases to `crates/rivets-mcp/tests/integration.rs` covering:\n\n1. **Empty filter results**: Create issues that don't match any filter combination, verify empty vec returned without errors\n\n2. **Multiple labels**: Create issue with labels [\"frontend\", \"backend\", \"urgent\"], filter by \"backend\" should include it\n\n3. **Case sensitivity**: Test whether \"Alice\" matches assignee filter \"alice\" - document expected behavior\n\n4. **Unicode support**: Test with:\n - Japanese title: \"バグ修正\"\n - Emoji label: \"🔥hotfix\"\n - Accented assignee: \"José García\"","acceptance_criteria":"- [ ] Empty filter results - verify graceful handling when no issues match filters\n- [ ] Multiple labels - test filtering when an issue has multiple labels and filter matches one\n- [ ] Case sensitivity - verify label/assignee filters handle case correctly (e.g., \"Alice\" vs \"alice\")\n- [ ] Unicode/special characters - ensure filters work with non-ASCII issue titles, labels, assignees","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T01:06:15.222446228Z","updated_at":"2025-11-30T18:09:22.615118183Z","closed_at":"2025-11-30T18:09:22.615118183Z"} +{"id":"rivets-gut","title":"Implement rivets done workflow command","description":"Add `rivets done` command for completing work:\n\n```\n$ rivets done rivets-abc\nClosing rivets-abc...\nAdd completion notes (optional, Ctrl+D to finish):\n> Fixed authentication flow, added token refresh.\n> PR: #42\n\nClosed: rivets-abc\n - Added close reason\n - Status: in_progress -> closed\n\n2 issues are now unblocked:\n - rivets-def Add OAuth support\n - rivets-ghi User profile update\n```","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","workflow"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:56.117458877Z","updated_at":"2025-11-30T18:37:56.117458877Z","closed_at":null} +{"id":"rivets-o4re","title":"MCP server for tethys tools","description":"Expose tethys functionality as MCP tools for AI agent integration. AI assistants like Claude/Cursor/Copilot can then query the indexed graph instead of grepping files, dramatically cutting tool-call count and context usage on long sessions.\n\nInspired by both Drift (50+ MCP tools across 7 layers) and KiroGraph (16 MCP tools, all auto-approved). KiroGraph's design observation drives most of the value: ''Kiro literally queries the graph instead of grepping files,'' so the index becomes context the model uses for every prompt.\n\n**MVP tools (wrap existing Tethys API methods):**\n\n| Tool | Description | Underlying API |\n|------|-------------|----------------|\n| tethys_status | Index stats, language breakdown | get_stats |\n| tethys_symbol | Get symbol by qualified name | get_symbol |\n| tethys_search | Search symbols by name/kind | search_symbols |\n| tethys_callers | Who calls this symbol? | get_callers |\n| tethys_callees | What does this symbol call? | get_symbol_dependencies |\n| tethys_impact | Impact analysis for a symbol | get_symbol_impact |\n| tethys_file_impact | Impact analysis for a file | get_impact |\n| tethys_reachable | Forward/backward reachability | get_forward_reachable / get_backward_reachable |\n| tethys_cycles | Detect dependency cycles | detect_cycles |\n| tethys_panic_points | Find .unwrap()/.expect() calls | get_panic_points |\n| tethys_affected_tests | Tests affected by changed files | get_affected_tests |\n\n**v2 tools (depend on new analytics — see sibling issues):**\n\n| Tool | Description | Depends on |\n|------|-------------|------------|\n| tethys_path | Shortest path between two symbols | new path-query feature |\n| tethys_dead_code | Symbols with zero incoming refs | new dead-code feature |\n| tethys_hotspots | Most-connected symbols | new hotspots feature |\n| tethys_surprising | Non-obvious cross-file edges | new surprising-connections feature |\n| tethys_type_hierarchy | Up/down extends-implements walk | new type-hierarchy feature |\n| tethys_coupling | Per-package Ca/Ce/instability | new architecture-metrics feature |\n| tethys_diff | Graph diff vs. saved snapshot | new snapshot/diff feature |\n\n**Implementation plan:**\n- New crates/tethys-mcp crate (binary + thin lib).\n- Use rmcp (already in workspace via rivets-mcp).\n- Each tool is a thin async wrapper around a Tethys method.\n- Tools should default to auto-approved in the Claude MCP config so the model can call them freely.\n- Coexists with rivets-mcp on a single MCP host.\n\n**Benefits:**\n- AI agents query codebase structure instead of grepping files.\n- Integrates with Claude Code, Cursor, Copilot, and any MCP-compatible client.\n- Complements rivets-mcp (issue tracker) and tethys's CLI.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["drift-inspired","feature","mcp","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:26.753055176Z","updated_at":"2026-05-10T04:30:37.025624100Z","closed_at":null} +{"id":"rivets-dhs","title":"Add query performance benchmarks","description":"Create benchmarks to measure query/filter performance and verify the \"1M records in <5s\" target from research.\n\nUses criterion for benchmarking.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create benches/query_benchmarks.rs:\n\n```rust\nuse criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};\nuse rivets_jsonl::*;\n\nfn query_benchmarks(c: &mut Criterion) {\n let mut group = c.benchmark_group(\"query\");\n \n // Benchmark: filter 1M records\n group.bench_function(\"filter_1m_records\", |b| {\n b.iter(|| {\n // ... benchmark code\n });\n });\n \n // Benchmark: filter + map pipeline\n group.bench_function(\"filter_map_pipeline\", |b| {\n b.iter(|| {\n // ... benchmark code\n });\n });\n \n // Benchmark: complex multi-predicate filter\n group.bench_function(\"complex_filter\", |b| {\n b.iter(|| {\n // ... benchmark code\n });\n });\n}\n\ncriterion_group!(benches, query_benchmarks);\ncriterion_main!(benches);\n```\n\nAdd criterion as dev-dependency.","acceptance_criteria":"- Benchmarks created in benches/\n- Measures filter performance\n- Measures map performance \n- Measures complex query pipelines\n- 1M records benchmark runs\n- Results documented in comments\n- cargo bench runs successfully","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-08u","dep_type":"blocks"},{"depends_on_id":"rivets-6p4","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T23:17:06.817308399Z","updated_at":"2025-11-27T23:17:06.817308399Z","closed_at":null} +{"id":"rivets-qeb","title":"Implement ready work algorithm with recursive blocking","description":"Implement the ready work detection algorithm that finds unblocked issues by recursively propagating blocks through parent-child hierarchies using CTEs.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on beads ready.go, adapted for Phase 1 (in-memory + petgraph):\n\n**Algorithm** (Phase 1 - petgraph):\n1. Find directly blocked issues (via 'blocks' deps to open/in_progress issues)\n2. Recursively propagate blockage through parent-child deps (depth limit 50)\n3. Exclude all blocked issues from results\n\n**Phase 1 Implementation**:\n```rust\nimpl InMemoryStorage {\n fn find_ready(&self, filter: Option<&IssueFilter>) -> Result> {\n use petgraph::Direction;\n \n // Find all blocked issues\n let mut blocked = HashSet::new();\n \n // Direct blocks: issues with blocking dependencies\n for (id, issue) in &self.issues {\n if issue.status == Status::Closed {\n continue;\n }\n \n for dep in &issue.dependencies {\n if dep.dep_type == DependencyType::Blocks {\n let blocker = self.issues.get(&dep.depends_on_id)?;\n if blocker.status == Status::Open || blocker.status == Status::InProgress {\n blocked.insert(id.clone());\n }\n }\n }\n }\n \n // Transitive blocking via parent-child (BFS with depth limit)\n let mut to_process: VecDeque<(IssueId, usize)> = blocked.iter()\n .map(|id| (id.clone(), 0))\n .collect();\n \n while let Some((id, depth)) = to_process.pop_front() {\n if depth >= 50 { continue; }\n \n // Find children (issues that depend on this one via parent-child)\n let node = self.node_map.get(&id)?;\n for edge in self.graph.edges_directed(*node, Direction::Incoming) {\n if edge.weight() == &DependencyType::ParentChild {\n let child_node = edge.source();\n let child_id = &self.graph[child_node];\n if blocked.insert(child_id.clone()) {\n to_process.push_back((child_id.clone(), depth + 1));\n }\n }\n }\n }\n \n // Filter out blocked issues\n let mut ready: Vec = self.issues.values()\n .filter(|issue| {\n issue.status != Status::Closed \n && !blocked.contains(&issue.id)\n })\n .cloned()\n .collect();\n \n // Apply additional filter if provided\n if let Some(filter) = filter {\n ready = self.apply_filter(ready, filter)?;\n }\n \n // Sort by policy (hybrid default)\n self.sort_by_policy(&mut ready, SortPolicy::Hybrid);\n \n Ok(ready)\n }\n}\n```\n\n**Sort Policies**:\n- `hybrid` (default): Recent (48h) by priority, older by age\n- `priority`: P0→P1→P2→P3→P4 strict\n- `oldest`: Creation date ascending\n\n**Phase 3 (PostgreSQL)**: Will use recursive CTEs for blocking propagation (see design notes)","acceptance_criteria":"- Ready issues query excludes blocked work using petgraph\n- Recursive parent-child blocking via BFS traversal\n- All 3 sort policies implemented\n- Depth limit (50) prevents infinite loops\n- Performance: <10ms for 1000 issues\n- Unit tests for complex blocking scenarios\n- Integration test with various dependency graphs","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:15:21.592668862Z","updated_at":"2025-11-28T19:38:24.156494963Z","closed_at":"2025-11-28T19:38:24.156494963Z"} +{"id":"rivets-uvv","title":"Fix: Blocked status not counted in execute_info","description":"In execute_info (lines 61-66), the fold counting issue statuses doesn't track Blocked issues separately. They're counted in total but have no dedicated counter, inconsistent with execute_stats which tracks blocked separately.\n\n**Current code:**\n```rust\nlet (total, open, in_progress, closed) =\n all_issues.iter().fold((0, 0, 0, 0), |(t, o, ip, c), issue| match issue.status {\n IssueStatus::Blocked => (t + 1, o, ip, c), // ← Not tracked\\!\n });\n```\n\n**Fix:** Add a blocked counter to the tuple and output.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":["bug","execute.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:17.787185220Z","updated_at":"2025-12-28T16:04:20.692554031Z","closed_at":"2025-12-28T16:04:20.692554031Z"} +{"id":"rivets-s3o","title":"Implement context management module","description":"Implement workspace context management:\n- Workspace detection (walk up to find .rivets/)\n- Path canonicalization\n- Per-workspace storage instance management\n- Context state storage (Arc>)","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Workspace detection walks up to find .rivets/\n- [ ] Path canonicalization handles symlinks\n- [ ] Storage instances cached per workspace\n- [ ] Unit tests: discover_workspace, set_workspace, storage caching","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-cfx","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:11.298145823Z","updated_at":"2025-11-29T14:41:24.606759038Z","closed_at":"2025-11-29T14:41:24.606759038Z"} {"id":"rivets-uqv","title":"Add JSONL export mirroring to AutomergeStorage","description":"Enable optional JSONL file generation on save() for human readability and git diff friendliness.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:38.970619144Z","updated_at":"2025-12-23T04:45:11.249724356Z","closed_at":"2025-12-23T04:45:11.249724356Z"} -{"id":"rivets-0gc","title":"Design and implement storage trait abstraction","description":"Create the Storage trait abstraction that will allow multiple backend implementations (in-memory, JSONL, PostgreSQL). This is the foundation for the phased storage approach.\n\nPhase 1: Storage Trait + In-Memory Graph (MVP)\n- Define Storage trait with all required methods\n- In-memory implementation using HashMap + petgraph\n- JSONL persistence as backup/export format\n\nPhase 2: PostgreSQL with Recursive CTEs (Production)\n- PostgreSQL implementation with recursive CTEs for dependency queries\n- Migration path from in-memory to PostgreSQL\n\nThis task focuses on the trait definition and abstraction layer.\n\n## Clarifications\n\n### Session 2025-11-17\n\n- Q: Should the IssueStorage trait be async or synchronous? → A: Make trait async from start using async-trait crate (future-proof for PostgreSQL, enables non-blocking I/O)\n- Q: How should storage operations handle concurrent access in the in-memory backend? → A: Wrap in Arc> for thread-safe access (standard pattern, simple for MVP)\n- Q: How should JSONL file corruption be handled during load? → A: Skip invalid lines with warning logs, continue loading valid entries (practical, allows recovery from partial corruption)\n- Q: Which tokio runtime should the CLI use? → A: Current-thread runtime (simpler for CLI, lower overhead, easier debugging)\n- Q: What should happen to dependencies when an issue is deleted? → A: Delete issue's dependencies, fail if issue has dependents with warning (safe, prevents orphaned references)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"## Storage Trait Design\n\n### Core Trait Definition\n\n**Async trait using async-trait crate:**\n\n```rust\nuse async_trait::async_trait;\n\n#[async_trait]\npub trait IssueStorage: Send + Sync {\n // CRUD operations\n async fn create(&mut self, issue: NewIssue) -> Result;\n async fn get(&self, id: &IssueId) -> Result>;\n async fn update(&mut self, id: &IssueId, updates: IssueUpdate) -> Result;\n async fn delete(&mut self, id: &IssueId) -> Result<()>;\n \n // Dependency management\n async fn add_dependency(&mut self, from: &IssueId, to: &IssueId, dep_type: DependencyType) -> Result<()>;\n async fn remove_dependency(&mut self, from: &IssueId, to: &IssueId) -> Result<()>;\n async fn get_dependencies(&self, id: &IssueId) -> Result>;\n async fn get_dependents(&self, id: &IssueId) -> Result>;\n async fn has_cycle(&self, from: &IssueId, to: &IssueId) -> Result;\n \n // Queries\n async fn list(&self, filter: &IssueFilter) -> Result>;\n async fn ready_to_work(&self, filter: Option<&IssueFilter>) -> Result>;\n async fn blocked_issues(&self) -> Result)>>;\n \n // Batch operations for JSONL import/export\n async fn import_issues(&mut self, issues: Vec) -> Result<()>;\n async fn export_all(&self) -> Result>;\n \n // Persistence (for backends that need explicit save)\n async fn save(&self) -> Result<()>;\n}\n```\n\n**Note**: In-memory implementation will use blocking operations wrapped in `async` blocks for Phase 1. PostgreSQL will use true async I/O with sqlx in Phase 2. The save() method allows backends to persist state - InMemoryStorage saves to JSONL, PostgreSQL is a no-op.\n\n### Async Runtime Configuration\n\n**Use current-thread tokio runtime for CLI:**\n\n```rust\n// main.rs\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<()> {\n // CLI initialization and execution\n let cli = Cli::parse();\n let config = Config::load().await?;\n let storage = create_storage(config.storage_backend).await?;\n \n cli.execute(storage).await\n}\n```\n\n**Rationale**:\n- Simpler runtime for sequential CLI operations\n- Lower memory and CPU overhead\n- Easier to debug with single-threaded execution\n- Sufficient for I/O-bound storage operations\n- Can upgrade to multi-threaded if parallelism becomes necessary\n\n### Concurrency Model\n\n**Thread-safe wrapper for in-memory storage:**\n\n```rust\n// Storage implementation (not thread-safe)\nstruct InMemoryStorageInner {\n issues: HashMap,\n graph: DiGraph,\n node_map: HashMap,\n}\n\n// Thread-safe wrapper\npub type InMemoryStorage = Arc>;\n\n// CLI usage\npub struct App {\n storage: Box, // Already thread-safe via Arc>\n config: Config,\n}\n```\n\n**Rationale**: \n- Simple standard pattern for Rust CLI applications\n- Prevents data races with minimal complexity\n- Easy to test and reason about\n- PostgreSQL backend will handle concurrency at database level\n\n### Delete Semantics & Referential Integrity\n\n**Safe deletion with dependent check:**\n\n```rust\nasync fn delete(&mut self, id: &IssueId) -> Result<()> {\n // Check for dependents\n let dependents = self.get_dependents(id).await?;\n if !dependents.is_empty() {\n let dependent_ids: Vec<_> = dependents.iter()\n .map(|d| d.issue_id.to_string())\n .collect();\n return Err(Error::HasDependents {\n issue_id: id.clone(),\n dependents: dependent_ids,\n message: format!(\n \"Cannot delete {}: {} other issues depend on it. Delete or update those issues first.\",\n id, dependents.len()\n )\n });\n }\n \n // Remove all outgoing dependencies from this issue\n let dependencies = self.get_dependencies(id).await?;\n for dep in dependencies {\n self.remove_dependency(id, &dep.depends_on_id).await?;\n }\n \n // Remove issue from storage\n self.issues.remove(id);\n if let Some(node_idx) = self.node_map.remove(id) {\n self.graph.remove_node(node_idx);\n }\n \n Ok(())\n}\n```\n\n**Strategy**: \n- Fail with clear error if issue has dependents\n- Error message lists dependent issue IDs\n- Remove all outgoing dependencies automatically\n- Prevents orphaned dependency references\n- User can use `--force` flag to batch delete dependents first (future enhancement)\n\n### Error Handling & Recovery\n\n**JSONL Corruption Handling:**\n\n```rust\npub async fn load_from_jsonl(path: &Path) -> Result<(Self, Vec)> {\n let file = BufReader::new(File::open(path).await?);\n let mut storage = Self::new();\n let mut errors = Vec::new();\n \n for (line_num, line) in file.lines().enumerate() {\n match line {\n Ok(content) => {\n match serde_json::from_str::(&content) {\n Ok(issue) => {\n if let Err(e) = storage.import_issue(issue).await {\n errors.push(LoadError::ImportFailed { line: line_num, reason: e });\n log::warn!(\"Line {}: Failed to import issue: {}\", line_num, e);\n }\n }\n Err(e) => {\n errors.push(LoadError::InvalidJson { line: line_num, reason: e });\n log::warn!(\"Line {}: Invalid JSON, skipping: {}\", line_num, e);\n }\n }\n }\n Err(e) => {\n errors.push(LoadError::ReadError { line: line_num, reason: e });\n log::warn!(\"Line {}: Read error, skipping: {}\", line_num, e);\n }\n }\n }\n \n if !errors.is_empty() {\n log::warn!(\"Loaded with {} errors/warnings. {} issues imported.\", \n errors.len(), storage.issue_count());\n }\n \n Ok((storage, errors))\n}\n```\n\n**Strategy**: Skip corrupted lines, log warnings, continue loading. Returns both the loaded storage and collection of errors for caller to handle.\n\n### Backend Factory\n\n```rust\npub enum StorageBackend {\n InMemory,\n Jsonl(PathBuf),\n PostgreSQL(String), // connection string\n}\n\npub async fn create_storage(backend: StorageBackend) -> Result> {\n match backend {\n StorageBackend::InMemory => Ok(Box::new(InMemoryStorage::new())),\n StorageBackend::Jsonl(path) => {\n let (storage, errors) = InMemoryStorage::load_from_jsonl(&path).await?;\n if !errors.is_empty() {\n eprintln!(\"Warning: Loaded with {} errors. Check logs.\", errors.len());\n }\n Ok(Box::new(storage))\n }\n StorageBackend::PostgreSQL(conn_str) => {\n Ok(Box::new(PostgresStorage::new(&conn_str).await?))\n }\n }\n}\n```\n\n### Architecture Benefits\n\n1. **Testability**: Use in-memory storage for fast unit tests\n2. **Flexibility**: Easy to add new backends (SQLite, cloud storage, etc.)\n3. **Performance**: Can optimize each backend independently\n4. **Migration**: Clear path from simple to complex storage\n5. **Future-proof**: Async trait enables efficient PostgreSQL and network I/O\n6. **Thread-safety**: Arc> wrapper prevents data races\n7. **Resilience**: Graceful degradation on partial JSONL corruption\n8. **Simplicity**: Current-thread runtime reduces complexity for CLI use case\n9. **Data integrity**: Safe deletion prevents orphaned dependency references\n10. **Explicit persistence**: save() method allows control over when data is persisted","acceptance_criteria":"- Storage trait defined with async methods using async-trait in src/storage/mod.rs\n- Backend factory pattern implemented for creating storage instances (async)\n- Trait includes save() method for explicit persistence (async fn save(&self) -> Result<()>)\n- Trait documentation includes usage examples and backend comparison\n- All trait methods have clear error semantics\n- Trait is object-safe (can use Box)\n- Unit tests for trait object usage with tokio test runtime\n- Documentation examples compile and run\n- async-trait dependency added to Cargo.toml\n- tokio dependency with current_thread flavor configured in main.rs\n- JSONL load handles corruption gracefully (skip invalid lines, log warnings)\n- Load function returns both storage and error collection\n- Delete operation checks for dependents, fails with clear error message if found\n- Delete operation removes all outgoing dependencies automatically\n- Error type includes HasDependents variant with issue list","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:15:21.281073361Z","updated_at":"2025-11-18T00:26:22.170944324Z","closed_at":"2025-11-18T00:26:22.170944324Z"} +{"id":"rivets-cbm","title":"Add vim-style keyboard navigation to TUI","description":"Implement vim-style keyboard navigation:\n- h/j/k/l for movement\n- / for search\n- c for create\n- e for edit\n- d for delete (with confirmation)\n- Enter for details\n- Space for toggle status\n- Tab for switch views\n- q for quit\n- ? for help\n\nAlso support arrow keys and mouse.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:24.109173058Z","updated_at":"2025-11-30T18:37:24.109173058Z","closed_at":null} +{"id":"rivets-9o82","title":"Add gated integration tests for LSP","description":"Add #[ignore] tests that require rust-analyzer installed:\n\n- lsp_resolves_trait_method_call\n- lsp_resolves_method_on_inferred_type\n- lsp_index_increases_resolution_count\n\nRun with: `cargo test --ignored`","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1dza","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:29:15.445807729Z","updated_at":"2026-01-29T02:01:00.845691700Z","closed_at":"2026-01-29T02:01:00.845691700Z"} +{"id":"rivets-1xh","title":"Add Unicode status icons with color support","description":"Enhance status icons with Unicode characters and colors:\n- [ ] Open (blue)\n- [>] In Progress (yellow)\n- [→] Ready (green) - new icon for ready-to-work\n- [X] Blocked (red)\n- [✓] Closed (gray)\n\nSupport fallback to ASCII for terminals without Unicode support.","status":"open","priority":1,"issue_type":"feature","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:46.957517194Z","updated_at":"2025-11-30T18:34:46.957517194Z","closed_at":null} {"id":"rivets-34tv","title":"Use cargo discovery for crate root in dependency resolution","description":"Two sites in lib.rs (lines ~1089 and ~1516) still hardcode workspace_root/src/ as the crate root for dependency resolution. Cargo discovery (compute_module_path_for_file) correctly detects crate roots but isn't used in compute_dependencies or resolve_unresolved_references. Stale FIXME comments reference resolved rivets-m4wt but the fix wasn't applied to these code paths.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"\n## Cross-reference (2026-05-12)\n\nRelated to [rivets-v465](rivets-v465) (open, in PR #62) and [rivets-6aoc](rivets-6aoc). Same code class: the resolver does not consistently use cargo-discovered crate roots in multi-crate workspaces. v465 covers `path[0]==workspace_crate_name` resolution; 6aoc covers `resolve.rs:66`; this issue covers the two sites in `indexing.rs` (now at lines 857 and 1023 on the v465 branch).\n\nThe `Tethys::crates()` accessor introduced by rivets-v465 (PR #62) is reusable infrastructure for these fixes — the call sites that currently do `self.workspace_root.join(\"src\")` can be updated to use `get_crate_for_file(current_file, &self.crates).path.join(\"src\")` (or similar) once a caller-aware crate_root lookup is wired in.\n\nClosed: Merged in PR #63 (commit 9a39bd5)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-02-06T01:00:01.537127793Z","updated_at":"2026-05-13T00:29:50.887112300Z","closed_at":"2026-05-13T00:29:50.887110700Z"} -{"id":"rivets-6pi","title":"Implement JsonlWriter::write() and write_all() methods","description":"Implement write methods for JsonlWriter that serialize and write records to the output stream.\n\nIncludes both single-record write() and batch write_all() for efficiency.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse serde::Serialize;\nuse tokio::io::AsyncWriteExt;\n\nimpl JsonlWriter {\n pub async fn write(&mut self, value: &T) -> Result<()> {\n let json = serde_json::to_string(value)?;\n self.writer.write_all(json.as_bytes()).await?;\n self.writer.write_all(b\"\\n\").await?;\n Ok(())\n }\n \n pub async fn write_all(\n &mut self,\n values: impl IntoIterator,\n ) -> Result<()> {\n for value in values {\n self.write(&value).await?;\n }\n Ok(())\n }\n \n pub async fn flush(&mut self) -> Result<()> {\n self.writer.flush().await?;\n Ok(())\n }\n}\n```","acceptance_criteria":"- write() method serializes and writes single record\n- write_all() writes multiple records efficiently\n- flush() method flushes buffered data\n- Each record terminated with newline\n- Proper error handling for serialization failures\n- Unit tests verify writing single/multiple records\n- Integration test with read/write round-trip","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-uo7","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:51.609289345Z","updated_at":"2025-11-28T00:21:22.277922663Z","closed_at":"2025-11-28T00:21:22.277922663Z"} -{"id":"rivets-xwcy","title":"Docs: Add documentation for MAX_LABEL_LENGTH magic number","description":"In validators.rs (line 153), the constant lacks explanation:\n\n```rust\n/// Maximum length for labels\npub const MAX_LABEL_LENGTH: usize = 50;\n```\n\nPer M-DOCUMENTED-MAGIC guideline:\n> Hardcoded magic values in production code must be accompanied by a comment outlining why this value was chosen.\n\n**Fix:** Add reasoning for why 50 was chosen:\n```rust\n/// Maximum length for labels.\n///\n/// Set to 50 characters to:\n/// - Keep labels concise and readable in CLI output\n/// - Allow for descriptive multi-word labels (e.g., \"needs-security-review\")\n/// - Align with common tag/label length limits in other issue trackers\npub const MAX_LABEL_LENGTH: usize = 50;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","validators.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:45:19.530764463Z","updated_at":"2025-12-28T15:45:19.530764463Z","closed_at":null} -{"id":"rivets-hnf","title":"Refactor: Extract batch processing helper to reduce duplication","description":"The batch processing pattern with save-after-success and reload-on-failure is duplicated ~6 times across:\n- execute_update\n- execute_close \n- execute_reopen\n- execute_label (Add)\n- execute_label (Remove)\n\n**Pattern:**\n```rust\nmatch app.storage_mut().update(&issue_id, update).await {\n Ok(issue) => {\n if let Err(save_err) = app.save().await {\n if let Err(reload_err) = app.storage_mut().reload().await {\n eprintln!(\"Warning: Failed to reload...\");\n }\n result.failed.push(BatchError { ... });\n } else {\n result.succeeded.push(issue);\n }\n }\n Err(e) => { result.failed.push(...); }\n}\n```\n\n**Recommendation:** Extract into a helper function like `save_or_record_failure()`.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["DRY","execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:23.533238227Z","updated_at":"2025-12-28T16:15:44.346630821Z","closed_at":"2025-12-28T16:15:44.346630821Z"} -{"id":"rivets-syau","title":"Path queries: shortest path between two symbols","description":"Find the shortest connection between any two symbols, traversing all edge kinds (calls, refs, imports). Answers ''how is X connected to Y?'' for code review and impact analysis.\n\n**Inspired by:** KiroGraph's kirograph_path tool and CLI command. Tethys already has get_dependency_chain for files; the same BFS over the union of edge kinds gives symbol-to-symbol path-finding.\n\n**Implementation:**\n- BFS shortest-path search over the union of call_edges and refs (grouped by symbol_id, in_symbol_id).\n- Public API:\n - Tethys::get_symbol_path(from: &str, to: &str) -> Result>>\n - PathStep { symbol, edge_kind, line, file }\n- CLI: tethys path [--format text|json]\n- Resolve symbol names with the same fuzzy matcher as tethys search; prefer real-symbol matches over file/import nodes.\n- Output shows each hop with file:line and the edge kind used.\n\n**Edge cases:**\n- No path exists → return None / clear CLI message.\n- Symbol not found → standard NotFound error.\n- Self-path (from == to) → empty path with clear semantics.\n\n**Test plan:**\nBuild a fixture workspace with three modules a → b → c plus an unrelated d, and verify path(a, c) returns the two-hop path while path(a, d) returns None.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] get_symbol_path() public API method\n- [ ] CLI subcommand tethys path FROM TO\n- [ ] BFS over union of call_edges and refs\n- [ ] Returns each hop with file:line and edge kind\n- [ ] Disconnected symbols return None / friendly CLI message\n- [ ] MCP tool tethys_path (sibling rivets-o4re)\n- [ ] Tests cover: connected, disconnected, self-path, fuzzy name resolution","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:32:05.780014500Z","updated_at":"2026-05-10T04:32:05.780014500Z","closed_at":null} -{"id":"rivets-kr3","title":"Design rivets project structure with two-crate architecture","description":"Plan the overall project structure splitting rivets into two crates: a JSONL library (general-purpose) and the rivets CLI application (issue tracking). Define workspace layout, dependencies, and inter-crate relationships.","status":"closed","priority":1,"issue_type":"task","assignee":"Claude","labels":[],"design":"## Workspace Design\n\n### Directory Structure\n```\nrivets/\n├── Cargo.toml # Workspace config (resolver=3)\n├── crates/\n│ ├── rivets-jsonl/ # JSONL library crate\n│ │ ├── src/ (lib.rs, reader.rs, writer.rs, query.rs, stream.rs, error.rs)\n│ │ ├── tests/, benches/, examples/\n│ │ └── Cargo.toml\n│ └── rivets/ # CLI application crate\n│ ├── src/ (main.rs, cli.rs, commands/, domain/, storage.rs, config.rs)\n│ ├── tests/\n│ └── Cargo.toml\n├── docs/\n└── target/ # Shared build output\n```\n\n### Separation of Concerns\n\n**rivets-jsonl**: Generic JSONL operations (read, write, stream, query), no domain knowledge\n**rivets**: Issue tracking domain (CLI, commands, business logic), uses rivets-jsonl for storage\n\n### Dependencies\n- One-way: rivets → rivets-jsonl\n- Shared deps via [workspace.dependencies]: serde, serde_json, thiserror, anyhow, clap\n- rivets-jsonl internally referenced via { workspace = true }\n\n### Naming\n- Library: rivets-jsonl (crates.io)\n- Binary: rivets (crates.io), installs 'rivets' command\n- Modules: snake_case, one primary type per file\n\n### Testing\n- Unit: #[cfg(test)] modules\n- Integration: crates/*/tests/\n- Benchmarks: crates/rivets-jsonl/benches/\n- Doc tests: /// examples in public APIs\n\n### Future Extensibility\nWorkspace supports: rivets-tui, rivets-server, rivets-web, rivets-sync","acceptance_criteria":"- Cargo.toml workspace configuration designed\n- Directory structure planned and documented\n- Clear separation of concerns between crates defined\n- Naming conventions established\n- Inter-crate dependency strategy documented","notes":"Design complete with comprehensive workspace structure. Ready for implementation. Directory structure uses flat 'crates/' layout per Rust best practices. Resolver 3 for modern dependency resolution. Clean separation between generic JSONL library and domain-specific CLI application.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-cr9","dep_type":"blocks"}],"created_at":"2025-11-17T21:08:45.002094002Z","updated_at":"2025-11-17T21:47:19.541529556Z","closed_at":"2025-11-17T21:43:27.115215646Z"} -{"id":"rivets-xvlw","title":"tethys: --rebuild flag fails on schema changes","description":"When the database schema changes (e.g., adding new columns like `is_test`), running `tethys index --rebuild` fails because:\n\n1. The existing database file is preserved\n2. `CREATE TABLE IF NOT EXISTS` doesn't add new columns to existing tables\n3. New indexes referencing the missing column fail with \"no such column\" error\n\n**Error observed:**\n```\nerror: database error: no such column: is_test in \nCREATE INDEX IF NOT EXISTS idx_symbols_is_test ON symbols(is_test) WHERE is_test = 1;\n```\n\n**Workaround:** Manually delete `.rivets/index/tethys.db` before running with `--rebuild`.","status":"open","priority":2,"issue_type":"bug","assignee":null,"labels":["database","tethys"],"design":"Options to consider:\n\n1. **Delete and recreate** (simplest): Have `--rebuild` delete the database file entirely before creating a fresh one. This is semantically correct since \"rebuild\" implies starting fresh.\n\n2. **Schema migration**: Add version tracking and migration logic to handle schema evolution. More complex but preserves data when possible.\n\n3. **Hybrid**: Check schema version on open, delete and recreate if incompatible.\n\nRecommended: Option 1 for now (delete on rebuild), with option 3 as future enhancement.","acceptance_criteria":"- [ ] `tethys index --rebuild` works correctly after schema changes\n- [ ] No manual deletion of database required\n- [ ] Clear user feedback when schema is incompatible","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T02:42:14.111981937Z","updated_at":"2026-01-30T02:42:14.111981937Z","closed_at":null} -{"id":"rivets-1dza","title":"Integrate LSP into callers --lsp","description":"When `--lsp` flag is set for callers query:\n\n1. Return stored references (from DB)\n2. Additionally, ask LSP find_references for the symbol\n3. Merge results, deduplicate\n\nThis catches callers that tree-sitter couldn't resolve during indexing.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-k3mv","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:29:02.890552913Z","updated_at":"2026-01-29T01:34:56.775465920Z","closed_at":"2026-01-29T01:34:56.775465920Z"} -{"id":"rivets-tuph","title":"Surface architecture phase stats in tethys index output","description":"The architecture phase populates IndexStats.architecture with packages_recorded/files_assigned/package_deps_recorded counts, but crates/tethys/src/cli/index.rs never displays them. Users running 'tethys index' see 'Indexed 42 files' but no indication that the architecture phase ran or how many packages were detected.\n\n**Fix:** Add a section to the index command output that conditionally prints architecture counts when stats.architecture is Some. Mirror the existing 'Files', 'Symbols', 'References' section style.\n\nFound during final review of rivets-byie.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["tethys","enhancement"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T00:37:37.245100700Z","updated_at":"2026-05-11T00:37:37.245100700Z","closed_at":null} -{"id":"rivets-ml05","title":"tethys: non-transactional clear in index_with_options leaves DB degraded on mid-pipeline panic","description":"Both clear_all_call_edges (indexing.rs:424) and clear_all_file_deps (indexing.rs:138, added in rivets-lcb6) run outside any explicit transaction. If the indexing pipeline panics or is interrupted between the clear and the subsequent re-population:\n\n- file_deps is empty (or partially populated)\n- call_edges is empty (if the panic happens after line 424)\n\nUntil the next complete index run, downstream queries (coupling, callers, cycles, impact analysis) operate on degraded data.\n\nThis is pre-existing for call_edges; rivets-lcb6 makes file_deps adopt the same pattern (the alternative — leaving file_deps stale — has worse properties). So this issue documents a known risk shared by both, not a regression introduced by lcb6.\n\nSurfaced by claude-review on PR #65.\n\nLikely fix shape: wrap the clear + populate work in a transaction so partial failure rolls back. Connection-level: BEGIN / COMMIT around the relevant indexing.rs section. SQLite WAL mode supports this; the question is what 'partial failure' means in the current architecture (batch_writer commits in chunks, so wrapping the whole pipeline in one transaction may not be feasible without changes there).\n\nOut of scope here; file before fix work begins so the design considers it.\n\nReference acceptance criteria for the eventual fix:\n- Test: index a workspace, abort mid-pipeline (panic injection), assert file_deps + call_edges are EITHER both populated from prior state OR both empty — no half-populated states.\n- Test: under normal operation, no perf regression compared to current non-transactional pattern.","status":"open","priority":4,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] file_deps and call_edges remain consistent (both old-state or both new-state) across pipeline abort\n- [ ] No significant indexing perf regression\n- [ ] Both clear_all_file_deps and clear_all_call_edges share the same transactional discipline","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T03:19:35.181862600Z","updated_at":"2026-05-13T03:19:35.181862600Z","closed_at":null} -{"id":"rivets-276h","title":"tethys LSP: verify or extend path_to_uri handling for UNC paths","description":"rivets-714v fixes path_to_uri's \\?\\ extended-length prefix bug for drive-letter paths (C:\\...). UNC paths (\\\\server\\share\\...) follow a different shape and may need explicit handling: canonicalize() returns them as \\\\?\\UNC\\server\\share\\... on Windows, and the RFC 8089 file URI form for UNC is ambiguous between authority-style (file://server/share/...) and path-style (file:////server/share/...).","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Why deferred from rivets-714v\n\nThe rivets-714v fix is bounded to drive-letter paths (the reported and reproduced bug case). UNC paths:\n\n1. Are rare in production for the rivets workspace and typical Cargo users\n2. Have ambiguous RFC 8089 mapping (authority vs path-style)\n3. Would require a separate decision about which form rust-analyzer accepts\n4. Need their own probe + oracle to verify behavior\n\n## Acceptance criteria\n\n- [ ] Add a probe that constructs a fixture on a UNC mount (or simulates one via Path manipulation)\n- [ ] Verify what URI shape rust-analyzer accepts for UNC paths\n- [ ] Extend path_to_uri to produce that shape correctly\n- [ ] Add a regression test covering the UNC case\n\n## Trigger condition for promoting priority\n\nIf a user reports tethys --lsp failing on a UNC-mounted workspace OR if the test_topology suite ever exercises a UNC fixture and surfaces the gap. Otherwise P4 is appropriate.","acceptance_criteria":"- [ ] UNC path probe + oracle added (mirrors .rivets-714v/probe.py shape)\n- [ ] path_to_uri's behavior on UNC paths verified against rust-analyzer's expectations\n- [ ] Regression test for UNC shape added\n- [ ] Documented in path_to_uri's rustdoc what URI form is produced for UNC inputs","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T00:43:18.379629200Z","updated_at":"2026-05-13T00:43:18.379629200Z","closed_at":null} -{"id":"rivets-67uu","title":"Add file-based logging option for debugging","description":"Currently rivets only logs to stderr via tracing. Add an option to write logs to a file for easier debugging in production environments or when troubleshooting issues.\n\nThis would help users capture diagnostic information when reporting bugs or debugging problems without having to manually redirect stderr.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["cli","dx","observability"],"design":"## Proposed Implementation\n\n### CLI Flag\nAdd a `--log-file ` global option that enables file-based logging:\n```bash\nrivets --log-file ./rivets.log list\nrivets --log-file /tmp/debug.log create \"New issue\"\n```\n\n### Environment Variable Alternative\nSupport `RIVETS_LOG_FILE` environment variable as an alternative to the CLI flag.\n\n### Log Format\n- Use the same tracing format as stderr output\n- Include timestamps for file logs (may want to add to stderr too)\n- Consider JSON format option for machine parsing: `--log-format json`\n\n### Log Rotation (future consideration)\nFor long-running use cases (like MCP server), consider:\n- Max file size before rotation\n- Number of rotated files to keep\n\n### Implementation Notes\n- Use `tracing-appender` crate for file writing\n- Layer approach: keep stderr output, add file layer when requested\n- Respect `RUST_LOG` for filtering even when writing to file","acceptance_criteria":"- [ ] `--log-file ` CLI option writes logs to specified file\n- [ ] `RIVETS_LOG_FILE` environment variable works as alternative\n- [ ] File logs include timestamps\n- [ ] `RUST_LOG` filtering still applies to file output\n- [ ] Documentation updated with logging options\n- [ ] Works for both `rivets` CLI and `rivets-mcp` server","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-nz52","dep_type":"parent-child"}],"created_at":"2026-02-05T04:30:32.062115144Z","updated_at":"2026-02-05T04:30:32.062115144Z","closed_at":null} -{"id":"rivets-sv1x","title":"Refactor: Extract App initialization pattern in Cli::execute()","description":"In mod.rs, the pattern `App::from_directory(&std::env::current_dir()?)` is repeated 13 times in the execute() method (lines 186-240):\n\n```rust\nSome(Commands::Info(args)) => {\n let app = App::from_directory(&std::env::current_dir()?).await?;\n execute::execute_info(&app, args, output_mode).await\n}\nSome(Commands::Create(args)) => {\n let mut app = App::from_directory(&std::env::current_dir()?).await?;\n execute::execute_create(&mut app, args, output_mode).await\n}\n// ... repeated 11 more times\n```\n\n**Recommendation:** Consider one of these approaches:\n\n1. **Helper method approach:**\n```rust\nasync fn with_app(&self, f: F) -> Result\nwhere\n F: FnOnce(App) -> Fut,\n Fut: Future>,\n{\n let app = App::from_directory(&std::env::current_dir()?).await?;\n f(app).await\n}\n```\n\n2. **Early initialization:** Initialize the app once before the match (for commands that need it)\n\n3. **Macro approach:** Create a macro to reduce boilerplate\n\nThis reduces duplication and makes the execute method more readable.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","cli","mod.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:45:13.765152589Z","updated_at":"2025-12-28T21:15:09.942608111Z","closed_at":"2025-12-28T21:15:09.942608111Z"} +{"id":"rivets-3l14","title":"Compute content hash for indexed files","description":"The schema supports `content_hash` but it's always `None`. See `batch_writer.rs:238`, `lib.rs:643`, `lib.rs:803`.\n\nContent hashing would provide more reliable change detection than mtime/size alone, useful for:\n- Detecting changes after file touch without content change\n- Cache key generation\n- Detecting identical content in different files","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:21.382922447Z","updated_at":"2026-01-30T00:18:21.382922447Z","closed_at":null} +{"id":"rivets-lyt","title":"Add TOCTOU scenario tests for close command","description":"Add tests that verify behavior when notes are modified between read and write operations in the close command. While the TOCTOU window is documented as acceptable for single-user CLI, tests should verify the append behavior works correctly.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:19.620773682Z","updated_at":"2025-11-30T04:11:19.620773682Z","closed_at":null} +{"id":"rivets-6mi","title":"Add tracing and logging","description":"Integrate tracing for observability:\n- tracing-subscriber setup\n- Request/response logging\n- Debug logging for context and storage operations","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:39.610630301Z","updated_at":"2025-12-08T00:00:49.802877931Z","closed_at":"2025-12-08T00:00:49.802877931Z"} +{"id":"rivets-4tev","title":"Implement Pass 2: resolve references against symbol DB","description":"After all files are indexed, run a second pass to resolve unresolved references:\n\n1. Query all references with symbol_id = NULL\n2. For each, look up import context from imports table\n3. Match reference name against symbols from imported modules\n4. Update reference with resolved symbol_id\n\nResolution priority:\n1. Same-file lookup\n2. Explicit import lookup\n3. Qualified path resolution\n4. Glob import search\n5. Leave unresolved (LSP candidate)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1p0f","dep_type":"blocks"},{"depends_on_id":"rivets-itz7","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:39.509250642Z","updated_at":"2026-01-28T18:09:44.894299963Z","closed_at":"2026-01-28T18:09:44.894299963Z"} +{"id":"rivets-p4n","title":"Implement TUI dependency graph view","description":"Create dependency graph visualization in TUI:\n- ASCII art graph of issue dependencies\n- Color-coded nodes by status\n- Navigate graph with keyboard\n- Show dependency types (blocks, related, parent-child, discovered-from)\n- Highlight current issue and its connections","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:12.590091097Z","updated_at":"2025-11-30T18:37:12.590091097Z","closed_at":null} +{"id":"rivets-ql0","title":"Implement rivets tui subcommand","description":"Add `rivets tui` subcommand that launches the terminal UI.\n\nCreate module structure:\n- src/tui/mod.rs - TUI module entry\n- src/tui/app.rs - Application state (Elm-like Model-View-Update)\n- src/tui/ui/ - View components\n- src/tui/widgets/ - Reusable widgets\n\nUse ratatui with crossterm backend.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:01.065594800Z","updated_at":"2025-11-30T18:37:01.065594800Z","closed_at":null} +{"id":"rivets-rndz","title":"Update get_callers/get_symbol_impact to use resolved refs","description":"The graph operations should now pick up cross-file references since they're stored with proper symbol_id after Pass 2.\n\nVerify that:\n- `get_callers()` returns callers from other files\n- `get_symbol_impact()` shows transitive dependents across files","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4tev","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:45.836727912Z","updated_at":"2026-01-28T18:10:27.543232953Z","closed_at":"2026-01-28T18:10:27.543232953Z"} +{"id":"rivets-5hvt","title":"Backlog","description":"Lower-priority and speculative items. Revisit periodically to promote or close.","status":"open","priority":4,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:32.353090414Z","updated_at":"2026-02-21T02:28:32.353090414Z","closed_at":null} +{"id":"rivets-cjz","title":"Replace println! with tracing::info! in CLI commands","description":"The execute_init function uses println! for user output. Consider using tracing::info! for more flexible logging that can be configured at runtime.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["cli","observability"],"design":"Replace direct println! calls in CLI command execution with tracing macros:\\n\\n- Use tracing::info! for success messages\\n- Use tracing::warn! for warnings\\n- Use tracing::error! for errors\\n\\nThis enables:\\n- Structured logging\\n- Log level filtering\\n- Integration with observability tools\\n- Consistent logging across all commands","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-0dw","dep_type":"related"},{"depends_on_id":"rivets-nz52","dep_type":"parent-child"}],"created_at":"2025-11-30T02:48:28.716762764Z","updated_at":"2025-11-30T02:48:28.716762764Z","closed_at":null} +{"id":"rivets-yis","title":"Implement storage backend selection via configuration","description":"Add configuration options to select storage backend (in-memory, PostgreSQL, SQLite) and pass connection parameters. Enables switching backends without code changes.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Extend config system:\n\n```yaml\n# .rivets/config.yaml\nstorage:\n backend: \"memory\" # Options: memory, postgres, sqlite\n \n # For memory backend\n data_file: \".rivets/issues.jsonl\"\n \n # For postgres backend (future)\n postgres:\n host: \"localhost\"\n port: 5432\n database: \"rivets\"\n user: \"rivets\"\n password_env: \"RIVETS_DB_PASSWORD\"\n \n # For sqlite backend (future)\n sqlite:\n path: \".rivets/rivets.db\"\n```\n\n**Config struct**:\n```rust\n#[derive(Deserialize)]\npub struct StorageConfig {\n pub backend: StorageBackend,\n pub data_file: Option,\n pub postgres: Option,\n pub sqlite: Option,\n}\n\npub enum StorageBackend {\n Memory,\n Postgres, // Future\n Sqlite, // Future\n}\n```\n\n**Factory pattern**:\n```rust\npub fn create_storage(config: &StorageConfig) -> Result> {\n match config.backend {\n StorageBackend::Memory => {\n let path = config.data_file.as_ref()\n .ok_or(Error::ConfigMissing(\"data_file\"))?;\n \n let storage = if path.exists() {\n InMemoryStorage::load_from_jsonl(path)?\n } else {\n InMemoryStorage::new()\n };\n \n Ok(Box::new(storage))\n }\n StorageBackend::Postgres => {\n unimplemented!(\"PostgreSQL backend coming soon\")\n }\n StorageBackend::Sqlite => {\n unimplemented!(\"SQLite backend coming soon\")\n }\n }\n}\n```","acceptance_criteria":"- Config supports storage backend selection\n- Factory creates correct storage implementation\n- In-memory backend configurable\n- Config validation with helpful errors\n- Environment variable support for secrets\n- Default backend is memory + JSONL\n- Integration test verifies backend switching","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-cgl","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:41:42.380662548Z","updated_at":"2025-11-17T22:41:42.380662548Z","closed_at":null} {"id":"rivets-6op","title":"Implement dependency system with cycle detection","description":"Implement the 4-type dependency system (blocks, related, parent-child, discovered-from) with recursive CTE-based cycle detection to prevent circular dependencies.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on beads dependencies.go, adapted for Phase 1 (in-memory + petgraph):\n\n**Dependency Types**:\n1. blocks: Hard blocker (prevents work)\n2. related: Soft link (informational)\n3. parent-child: Hierarchical (epics→tasks)\n4. discovered-from: Found during work\n\n**Phase 1 Implementation (In-Memory + petgraph)**:\n```rust\nimpl InMemoryStorage {\n fn add_dependency(\n &mut self, \n from: &IssueId, \n to: &IssueId, \n dep_type: DependencyType\n ) -> Result<()> {\n // Cycle detection using petgraph\n if self.has_cycle(from, to)? {\n return Err(Error::CircularDependency);\n }\n \n // Add edge to graph\n let from_node = self.node_map.get(from)?;\n let to_node = self.node_map.get(to)?;\n self.graph.add_edge(*from_node, *to_node, dep_type);\n \n // Update issue's dependency list\n let issue = self.issues.get_mut(from)?;\n issue.dependencies.push(Dependency { \n depends_on_id: to.clone(), \n dep_type \n });\n \n Ok(())\n }\n \n fn has_cycle(&self, from: &IssueId, to: &IssueId) -> Result {\n use petgraph::algo;\n let from_node = self.node_map.get(from)?;\n let to_node = self.node_map.get(to)?;\n \n // Check if adding edge creates cycle\n Ok(algo::has_path_connecting(&self.graph, *to_node, *from_node, None))\n }\n}\n```\n\n**Phase 3 (PostgreSQL)**: Will use recursive CTEs for cycle detection:\n```sql\nWITH RECURSIVE paths AS (\n SELECT issue_id, depends_on_id, 1 as depth\n FROM dependencies WHERE issue_id = ?\n UNION ALL\n SELECT d.issue_id, d.depends_on_id, p.depth + 1\n FROM dependencies d JOIN paths p \n ON d.issue_id = p.depends_on_id\n WHERE p.depth < 100\n)\nSELECT EXISTS(SELECT 1 FROM paths WHERE depends_on_id = ?)\n```","acceptance_criteria":"- All 4 dependency types supported\n- Add dependency with cycle pre-check using petgraph\n- Remove dependency operation\n- List dependencies (forward/reverse) \n- Cycle detection via petgraph has_path_connecting\n- Find dependency tree via graph traversal\n- Unit tests for cycles, trees, all dep types\n- Benchmark: cycle check for 1000 issues in <10ms","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:15:21.434804051Z","updated_at":"2025-11-28T15:07:46.772048093Z","closed_at":"2025-11-28T15:07:46.772048093Z"} -{"id":"rivets-3911","title":"Refactor lib.rs into focused modules (<500 lines each)","description":"lib.rs is 3,299 lines, exceeding the project guideline of <500 lines per file. Split into focused modules: indexing.rs (Phase 1a/1b), file_ops.rs (discovery and metadata), dependency_resolution.rs (Pass 2 cross-file), lsp_integration.rs (Pass 3). PR review flagged this as critical.","status":"open","priority":2,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:46.087604018Z","updated_at":"2026-02-06T00:54:46.087604018Z","closed_at":null} -{"id":"rivets-cfx","title":"Implement MCP server with stdio transport","description":"Implement the basic MCP server skeleton using rmcp:\n- main.rs entry point with tokio runtime\n- ServerHandler implementation\n- stdio transport setup\n- Graceful shutdown handling","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"**Tool Registration Pattern:**\nUse rmcp's tool registration with handler functions:\n```rust\nserver\n .tool(\"set_context\", |params| {\n // Deserialize params, call tools.set_context()\n })\n .tool(\"ready\", |params| {\n // Call tools.ready()\n })\n // ... etc\n```\n\n**Schema Generation:**\nUse the `JsonSchema` derives in `models.rs` to auto-generate tool input/output schemas. rmcp can use these for MCP tool discovery.\n\n**Server Structure:**\n```rust\npub struct RivetsServer {\n context: Arc>,\n tools: Tools,\n}\n\nimpl ServerHandler for RivetsServer {\n // Tool dispatch\n}\n```\n\n**Transport:**\n- stdio only for MVP\n- Use rmcp's stdio transport utilities","acceptance_criteria":"- [ ] MCP server starts with stdio transport\n- [ ] ServerHandler implementation compiles\n- [ ] Graceful shutdown on SIGINT/SIGTERM\n- [ ] Unit tests: server creation, basic lifecycle","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-rg0","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:05.632602712Z","updated_at":"2025-11-29T05:29:56.252223505Z","closed_at":"2025-11-29T05:29:56.252223505Z"} +{"id":"rivets-8yl","title":"Implement resilient streaming for JsonlReader","description":"Implement stream_resilient() method that continues reading despite malformed JSON lines, collecting warnings instead of returning errors.\n\nThis provides the same resilient loading behavior as in_memory::load_from_jsonl().","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nimpl JsonlReader {\n pub fn stream_resilient(\n self\n ) -> (impl Stream, WarningCollector)\n where\n T: DeserializeOwned + 'static,\n {\n let collector = WarningCollector::new();\n let collector_clone = collector.clone();\n \n let stream = futures::stream::unfold(\n (self, collector_clone),\n |(mut reader, collector)| async move {\n loop {\n match reader.read_line().await {\n Ok(Some(value)) => return Some((value, (reader, collector))),\n Ok(None) => return None, // EOF\n Err(e) => {\n // Collect warning and continue\n collector.add(Warning::MalformedJson {\n line_number: reader.line_number,\n error: e.to_string(),\n });\n // Continue to next line\n continue;\n }\n }\n }\n },\n );\n \n (stream, collector)\n }\n}\n```","acceptance_criteria":"- stream_resilient() method implemented\n- Returns (Stream, WarningCollector)\n- Continues reading on malformed JSON\n- Warnings collected for each error\n- Stream yields only successfully parsed records\n- Unit tests verify resilient behavior\n- Integration test with mixed valid/invalid JSONL","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-dgt","dep_type":"blocks"}],"created_at":"2025-11-27T23:15:55.303102489Z","updated_at":"2025-11-28T02:05:48.560873847Z","closed_at":"2025-11-28T02:05:48.560873847Z"} +{"id":"rivets-241","title":"Add validator edge case tests","description":"Add comprehensive edge case tests to validators.rs for:\n- Multiple hyphens in issue IDs (proj-abc-123-def)\n- Unicode characters and emojis in titles\n- Max length boundary conditions\n- Empty and whitespace-only inputs","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:13.839324597Z","updated_at":"2025-11-30T04:11:13.839324597Z","closed_at":null} +{"id":"rivets-08k1","title":"Incrementally add #[errors] doc to public Tethys methods","description":"The entire impl Tethys block has #[expect(clippy::missing_errors_doc)]. As the API stabilizes, chip away at this by documenting errors on individual methods and narrowing the expect scope. Flagged in PR #55 review.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:17:57.715181098Z","updated_at":"2026-03-20T22:17:57.715181098Z","closed_at":null} +{"id":"rivets-k3mv","title":"Integrate LSP into index --lsp","description":"When `--lsp` flag is set during indexing:\n\n1. After Pass 2, identify still-unresolved references\n2. For each, call LSP goto_definition to find the target\n3. Update reference with resolved symbol_id\n\nLSP is spawned lazily, kept alive for batch queries, shutdown on completion.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-7uw0","dep_type":"blocks"},{"depends_on_id":"rivets-oeu5","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:56.529556093Z","updated_at":"2026-01-29T00:57:33.035836992Z","closed_at":"2026-01-29T00:57:33.035836992Z"} +{"id":"rivets-l66","title":"Implement JSONL persistence for InMemoryStorage","description":"Add save/load functionality to InMemoryStorage to persist data to JSONL files. This provides durability for the in-memory backend.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Extend InMemoryStorage with async persistence (matches rivets-0gc async trait pattern):\n\n**Async Implementation:**\n\n```rust\nimpl InMemoryStorage {\n pub async fn load_from_jsonl(path: &Path) -> Result<(Self, Vec)> {\n use tokio::fs::File;\n use tokio::io::{AsyncBufReadExt, BufReader};\n \n let file = BufReader::new(File::open(path).await?);\n let storage = Arc::new(Mutex::new(InMemoryStorageInner::new()));\n let mut warnings = Vec::new();\n let mut lines = file.lines();\n \n // First pass: import all issues (without dependencies)\n while let Some(line) = lines.next_line().await? {\n match serde_json::from_str::(&line) {\n Ok(issue) => {\n let mut inner = storage.lock().await;\n let id = issue.id.clone();\n inner.issues.insert(id.clone(), issue);\n let node = inner.graph.add_node(id.clone());\n inner.node_map.insert(id, node);\n }\n Err(e) => {\n warnings.push(LoadWarning::MalformedJson { line, error: e });\n log::warn!(\"Skipping malformed JSON: {}\", e);\n }\n }\n }\n \n // Second pass: add dependency edges with orphan/cycle detection\n let mut inner = storage.lock().await;\n for issue in inner.issues.values() {\n for dep in &issue.dependencies {\n // Check if dependency target exists\n if !inner.issues.contains_key(&dep.depends_on_id) {\n warnings.push(LoadWarning::OrphanedDependency {\n from: issue.id.clone(),\n to: dep.depends_on_id.clone(),\n });\n log::warn!(\"Skipping orphaned dependency: {} -> {}\", \n issue.id, dep.depends_on_id);\n continue;\n }\n \n // Check for cycles before adding edge\n if inner.has_cycle_internal(&issue.id, &dep.depends_on_id)? {\n warnings.push(LoadWarning::CircularDependency {\n from: issue.id.clone(),\n to: dep.depends_on_id.clone(),\n });\n log::warn!(\"Skipping circular dependency: {} -> {}\", \n issue.id, dep.depends_on_id);\n continue;\n }\n \n // Add edge to graph\n inner.add_dependency_edge_internal(dep)?;\n }\n }\n \n Ok((storage, warnings))\n }\n \n pub async fn save_to_jsonl(&self, path: &Path) -> Result<()> {\n use tokio::fs::File;\n use tokio::io::{AsyncWriteExt, BufWriter};\n \n // Atomic write: temp file + rename\n let temp_path = path.with_extension(\"tmp\");\n let file = BufWriter::new(File::create(&temp_path).await?);\n \n let inner = self.lock().await;\n for issue in inner.issues.values() {\n let json = serde_json::to_string(issue)?;\n file.write_all(json.as_bytes()).await?;\n file.write_all(b\"\\n\").await?; // Newline-delimited\n }\n file.flush().await?;\n \n // Atomic rename\n tokio::fs::rename(&temp_path, path).await?;\n Ok(())\n }\n}\n```\n\n**Error Handling:**\n- Malformed JSON: Skip line, log warning, continue loading\n- Orphaned dependencies: Skip edge, log warning, import issue without that dependency\n- Circular dependencies: Skip edge, log warning, prevent cycle creation\n- Returns (Storage, Vec) to communicate warnings to caller\n\n**File format**: Standard JSONL (newline-delimited JSON)\n**Atomicity**: Write to temp file, then rename (atomic on POSIX systems)\n**Dependencies**: tokio with fs feature for async file I/O","acceptance_criteria":"- load_from_jsonl reads and reconstructs storage\n- save_to_jsonl writes all issues\n- Dependency graph correctly rebuilt on load\n- Atomic writes (temp file + rename)\n- Round-trip test: save → load → verify\n- Handles large files (streaming read)\n- Error handling for malformed JSON","notes":"## Clarifications\n\n### Session 2025-11-17\n\n- Q: The design shows synchronous methods (`pub fn load_from_jsonl`, `pub fn save_to_jsonl`) but rivets-0gc defines an async storage trait. Should these persistence methods be async? → A: Make persistence methods async (matches async trait pattern from rivets-0gc, enables non-blocking file I/O for large JSONL files)\n- Q: During JSONL import, what should happen if an issue references dependencies that don't exist in the file (orphaned dependency references)? → A: Skip orphaned dependencies with warning (allows partial recovery from corrupted/incomplete files, resilient approach)\n- Q: If the JSONL file contains issues that form circular dependencies, how should load_from_jsonl handle this? → A: Detect and reject circular dependencies during import (ensures loaded data has no cycles, maintains data integrity)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-bz5","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:41.842178005Z","updated_at":"2025-11-27T22:55:23.560905657Z","closed_at":"2025-11-27T22:55:23.560905657Z"} +{"id":"rivets-7q6","title":"Add Send assertions for async futures (M-TYPES-SEND)","description":"The codebase uses async/await extensively but lacks explicit compile-time assertions that futures are Send, violating the M-TYPES-SEND guideline.\n\nCurrent State:\n- IssueStorage trait requires Send + Sync\n- Methods return futures implicitly\n- No compile-time verification that futures are actually Send\n- Could break multi-threaded async runtimes\n\nRisk:\n- If a future accidentally captures non-Send types, it won't be detected until runtime\n- Makes the library unsafe to use with work-stealing executors","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["async","code-quality","testing"],"design":"Add Send assertions in storage/mod.rs tests (after line 453):\n\n```rust\n#[test]\nfn assert_storage_futures_are_send() {\n const fn assert_send() {}\n \n // Assert that all IssueStorage methods return Send futures\n assert_send::>();\n \n // Can also test specific future types if needed\n // This ensures futures work with multi-threaded runtimes\n}\n```\n\nThis pattern:\n- Runs at compile time (const fn)\n- Catches non-Send futures immediately\n- Documents the Send requirement\n- Zero runtime cost","acceptance_criteria":"- [ ] assert_send helper function added\n- [ ] Test verifies Box is Send\n- [ ] Test verifies key future types are Send\n- [ ] Code compiles (proving assertion holds)\n- [ ] All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-27T22:49:09.918623993Z","updated_at":"2025-11-27T22:49:09.918623993Z","closed_at":null} +{"id":"rivets-8fe","title":"Add edge case filter tests for MCP integration tests","description":"Add comprehensive edge case tests for the filter functionality in MCP integration tests to ensure robust handling of boundary conditions and special scenarios.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["mcp","testing"],"design":"Add test cases to `crates/rivets-mcp/tests/integration.rs` covering:\n\n1. **Empty filter results**: Create issues that don't match any filter combination, verify empty vec returned without errors\n\n2. **Multiple labels**: Create issue with labels [\"frontend\", \"backend\", \"urgent\"], filter by \"backend\" should include it\n\n3. **Case sensitivity**: Test whether \"Alice\" matches assignee filter \"alice\" - document expected behavior\n\n4. **Unicode support**: Test with:\n - Japanese title: \"バグ修正\"\n - Emoji label: \"🔥hotfix\"\n - Accented assignee: \"José García\"","acceptance_criteria":"- [ ] Empty filter results - verify graceful handling when no issues match filters\n- [ ] Multiple labels - test filtering when an issue has multiple labels and filter matches one\n- [ ] Case sensitivity - verify label/assignee filters handle case correctly (e.g., \"Alice\" vs \"alice\")\n- [ ] Unicode/special characters - ensure filters work with non-ASCII issue titles, labels, assignees","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T01:06:15.222446228Z","updated_at":"2025-11-30T18:09:22.615118183Z","closed_at":"2025-11-30T18:09:22.615118183Z"} +{"id":"rivets-81c","title":"Implement IssueDocument type for Automerge","description":"Create the core IssueDocument struct that wraps AutoCommit and provides methods for issue CRUD operations on the document.\n\nIncludes:\n- Add autosurgeon Reconcile/Hydrate derives to domain types (Issue, Dependency, IssueStatus, IssueType, DependencyType)\n- Create IssueDocument wrapper struct\n- Implement hydrate/reconcile methods for document ↔ struct conversion","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:27.719020626Z","updated_at":"2025-12-23T04:44:59.580445491Z","closed_at":"2025-12-23T04:44:59.580445491Z"} +{"id":"rivets-cux","title":"Add interactive prompt tests with mock stdin","description":"Add tests for interactive prompts in execute.rs by mocking stdin input. This ensures the interactive title/description prompts handle various inputs correctly including:\n- Valid inputs\n- Invalid inputs that should be rejected\n- Empty inputs\n- Multiline inputs for descriptions","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:25.383605039Z","updated_at":"2025-11-30T04:11:25.383605039Z","closed_at":null} +{"id":"rivets-26b1","title":"Verify symlink resolution stays within workspace_root","description":"canonicalize() resolves symlinks, which could resolve to paths outside workspace_root. Add a check that resolved paths remain within the workspace boundary to prevent directory traversal via symlinks.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:19.994222263Z","updated_at":"2026-02-06T00:35:19.994222263Z","closed_at":null} +{"id":"rivets-2c5","title":"Optimize ready_to_work filter pre-application in find_blocked_issues","description":"Investigate potential optimization to apply IssueFilter early in find_blocked_issues() to reduce the number of issues checked for blocking status.\n\nCurrently, find_blocked_issues checks ALL non-closed issues in Phase 1, then ready_to_work applies the user filter afterward. For selective filters, this does unnecessary work.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["optimization","performance"],"design":"**Current Flow:**\n1. `find_blocked_issues` checks all non-closed issues\n2. `ready_to_work` filters out blocked issues\n3. User filter applied last\n\n**Proposed Optimization:**\nPass filter hint to `find_blocked_issues` to skip checking issues that won't be in final result.\n\n**Correctness Constraint:**\nCannot skip issues that might be transitive blockers via ParentChild. If Parent A (priority=1) is blocked, Child C (priority=2) must also be marked blocked even if filter.priority=2 would skip A.\n\n**Safe Optimization Approaches:**\n1. Only apply filter in Phase 1 for \"leaf-only\" filters (labels, assignee) that don't affect blocking propagation\n2. Lazy per-issue blocking check - trade BFS O(n+e) for O(filtered × edges), better only for very selective filters\n3. Two-pass: first find all potentially blocking parents, then filter\n\n**When to Implement:**\nProfile ready_to_work with large datasets (10k+ issues) to determine if optimization is worthwhile.","acceptance_criteria":"- [ ] Profile ready_to_work performance with 10k+ issues\n- [ ] Determine if optimization provides measurable benefit\n- [ ] If implementing, ensure correctness with parent-child transitive blocking\n- [ ] Add benchmark tests for ready_to_work with various filter selectivity levels","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-28T21:22:06.362877015Z","updated_at":"2025-11-28T21:22:06.362877015Z","closed_at":null} +{"id":"rivets-dzn8","title":"Consolidate three copies of workspace_with_files test helper","description":"Three independent copies of the workspace_with_files test fixture helper exist in the tethys integration test suite. PR #63 (rivets-6aoc) had to update all three with the same Cargo.toml auto-write logic; future test-infrastructure changes will keep paying this triplicate cost.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Current state\n\nThree near-identical `workspace_with_files` functions:\n\n1. `crates/tethys/tests/common/mod.rs` (shared, intended canonical location)\n2. `crates/tethys/tests/indexing.rs` (local copy, 77 callers in this one file)\n3. `crates/tethys/tests/test_topology.rs` (local copy)\n\nAll three now (post-PR #63) have similar auto-write-default-Cargo.toml logic with slight string differences:\n- common writes \"test_workspace\"\n- indexing writes \"test_workspace\"\n- test_topology writes \"test_topology\"\n\n## Folds in S2 from PR #63 review\n\nThe Cargo.toml exact-string match in the helpers (`files.iter().any(|(p, _)| *p == \"Cargo.toml\")`) is fragile: a caller passing \"./Cargo.toml\" would trigger a silent double-write. Normalize via `Path::file_name()` once during consolidation.\n\n## Proposed work\n\n1. Make `tests/common/mod.rs::workspace_with_files` the canonical helper.\n2. Delete the local copies in `tests/indexing.rs` and `tests/test_topology.rs`.\n3. Update all callers in those files to `use common::workspace_with_files;` (add `mod common;`).\n4. Normalize Cargo.toml detection in the canonical helper via `Path::file_name()` rather than exact-string comparison.\n5. Verify all integration tests still pass.","acceptance_criteria":"- [ ] Single workspace_with_files in tests/common/mod.rs\n- [ ] indexing.rs and test_topology.rs use the common helper via mod common\n- [ ] Cargo.toml detection uses Path::file_name(), not exact-string match\n- [ ] cargo nextest run -p tethys passes\n- [ ] cargo clippy + cargo fmt clean","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:40:36.828342600Z","updated_at":"2026-05-12T23:40:36.828342600Z","closed_at":null} +{"id":"rivets-pae","title":"Block ANSI escape sequences in title validation","description":"Explicitly block ANSI escape sequences in validate_title() to prevent terminal manipulation attacks. Current validation blocks control characters 0x00-0x1F and 0x7F-0x9F, but ANSI sequences starting with ESC (0x1B) followed by [ could still be used for terminal manipulation.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","security"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:36.937708941Z","updated_at":"2025-11-30T04:11:36.937708941Z","closed_at":null} +{"id":"rivets-i8qn","title":"Add entry_point_file() / src_root() accessors on CrateInfo","description":"Inline expressions in tethys resolver code leak CrateInfo internals (lib_path -> bin_paths fallback chain, src/ hardcode, hyphen->underscore normalization). Encapsulate behind accessors on the type.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"## Suggested accessors\n\nThree accessors on `CrateInfo` that consolidate currently-leaked behavior:\n\n1. **`entry_point_file(&self) -> Option`** - returns `lib_path` joined onto `path`, falling back to first `bin_paths` entry, `None` if neither set. Replaces the `or_else` chain in `resolver.rs` single-segment arm (~4 lines -> 1).\n\n2. **`src_root(&self) -> PathBuf`** - returns the crate's source root directory. Should NOT be implemented as `self.path.join(\"src\")` - that's the bug being fixed by rivets-6aoc/rivets-34tv. The correct implementation derives from `lib_path.parent()` with hardcoded `src/` only as the None fallback. Introducing this accessor IS the rivets-6aoc fix; this issue and rivets-6aoc should likely be done together.\n\n3. **`module_name(&self) -> Cow`** - returns the name normalized for Rust module paths (hyphens->underscores). Marginal win (one use site today), but pre-pays the design tax if more callers need to compare crate names to module path heads.\n\n## Discovered during\n\nPR #62 round-4 review feedback from type-design-analyzer agent. Ratings reported: encapsulation 3/5, invariant expression 2/5, usefulness 3/5, enforcement 2/5.\n\n## Out-of-scope justification for PR #62\n\n`src_root()` would relocate the `src/` hardcode to the type without fixing it - misleading encapsulation. Best done in/with rivets-6aoc where the implementation is the actual fix. `module_name()` is marginal at a single use site. `entry_point_file()` alone is real value but was deferred to keep PR #62 narrowly scoped (its tests already lock down the inline behavior; the accessor refactor is a clean follow-up).","acceptance_criteria":"- [ ] entry_point_file() accessor on CrateInfo with doc-comment explaining fallback order\n- [ ] resolver.rs single-segment arm uses the accessor\n- [ ] src_root() accessor introduced as part of rivets-6aoc work (lib_path-derived implementation, not hardcoded src/)\n- [ ] module_name() accessor only if a second call site appears; otherwise drop\n- [ ] Tests cover all three accessors' contracts","notes":"Closed: Fixed in PR #70 (commit 33639ac). Added CrateInfo::entry_point_file() accessor with 4 unit tests; replaced inline lib_path/bin_paths fallback chain in resolver.rs single-segment arm. CrateInfo::src_root() pre-existed from rivets-6aoc. module_name() accessor dropped per conditional acceptance #4 (only one CrateInfo.name normalization call site exists).","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gom","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-12T21:47:17.334291900Z","updated_at":"2026-05-18T18:51:03.262363Z","closed_at":"2026-05-18T18:51:03.262360900Z"} +{"id":"rivets-6aoc","title":"Hardcoded crate_root in resolve_cross_file_references breaks multi-crate workspaces","description":"resolve.rs:66 uses self.workspace_root.join(\"src\") which is wrong for multi-crate workspaces. Should use per-crate src_root from self.crates: Vec. Flagged in PR #55 review.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"## Cross-reference (2026-05-12)\n\nRelated to [rivets-v465](rivets-v465) (open, in PR #62) — a sibling multi-crate-resolver bug. v465 fixes `use other_crate::Foo` paths (workspace-crate name at `path[0]`); this issue covers `use crate::Foo` paths where the caller's own crate_root is hardcoded to `workspace_root/src` instead of per-crate src.\n\n**Confirmed `src/` hardcoding sites on `fix/rivets-v465-import-resolver-leak` HEAD:**\n- `crates/tethys/src/resolve.rs:66` (called out in PR #55 review per this issue)\n- `crates/tethys/src/indexing.rs:857` and `:1023` (also covered by [rivets-34tv](rivets-34tv))\n- `crates/tethys/src/resolver.rs:61` (new site introduced by the v465 fix's workspace-crate arm — same bug class)\n\nThese should be fixed together with v465 for full multi-crate-resolver coverage. The `self.crates()` accessor and `&[CrateInfo]` plumbing introduced in v465 PR #62 are reusable infrastructure for this fix.\n\nClosed: Merged in PR #63 (commit 9a39bd5)","external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:16:15.036553022Z","updated_at":"2026-05-13T00:29:50.838403200Z","closed_at":"2026-05-13T00:29:50.838401700Z"} +{"id":"rivets-9s4","title":"Make blocked issues assertion more precise in tests","description":"In the MCP integration tests, the dependency chain test uses a loose assertion:\n\n```rust\n// Line 2716\nassert!(blocked.len() >= 2, \"At least 2 issues should be blocked\");\n```\n\nIf the behavior is deterministic, consider making this assertion more precise (e.g., `assert_eq!(blocked.len(), 2)`) to catch regressions more reliably.\n\nLocation: `crates/rivets-mcp/tests/integration.rs:2716`","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T18:29:43.002652297Z","updated_at":"2025-11-30T18:29:43.002652297Z","closed_at":null} +{"id":"rivets-azq","title":"Support multiple issue IDs in update/close/show commands","description":"Modify update, close, and show commands to accept multiple issue IDs. Examples:\\n- rivets update id1 id2 id3 --status in_progress\\n- rivets close id1 id2 --reason 'Batch completion'\\n- rivets show id1 id2 --json","status":"closed","priority":1,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:45.022427513Z","updated_at":"2025-12-28T00:38:18.113840018Z","closed_at":"2025-12-28T00:38:18.113840018Z"} +{"id":"rivets-cgl","title":"Integrate storage trait with CLI commands","description":"Update CLI commands to use the storage trait instead of direct storage access. Add storage initialization and lifecycle management.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Update rivets CLI to use async storage trait (following rivets-0gc architecture):\n\n**Async CLI Structure:**\n\n```rust\npub struct App {\n storage: Box,\n config: Config,\n}\n\nimpl App {\n pub async fn new(config: Config) -> Result {\n let storage = Self::create_storage(&config).await?;\n Ok(Self { storage, config })\n }\n \n async fn create_storage(config: &Config) -> Result> {\n match config.storage_backend {\n StorageBackend::InMemory => {\n let storage = if let Some(path) = &config.data_file {\n if path.exists() {\n let (storage, warnings) = InMemoryStorage::load_from_jsonl(path).await?;\n if !warnings.is_empty() {\n eprintln!(\"Warning: Loaded with {} warnings. Check logs.\", warnings.len());\n }\n storage\n } else {\n InMemoryStorage::new()\n }\n } else {\n InMemoryStorage::new()\n };\n Ok(Box::new(storage))\n }\n // Future: PostgreSQL, SQLite\n }\n }\n}\n```\n\n**Update IssueStorage trait to include save():**\n\nAdd to rivets-0gc trait definition:\n```rust\n#[async_trait]\npub trait IssueStorage: Send + Sync {\n // ... existing methods ...\n \n /// Persist storage state (for backends that need explicit persistence)\n async fn save(&self) -> Result<()>;\n}\n```\n\n**Command execution with auto-save:**\n\n```rust\nimpl Commands {\n pub async fn execute(&self, app: &mut App) -> Result<()> {\n match self {\n Commands::Create(args) => {\n let issue = app.storage.create(args.into()).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Created: {}\", issue.id);\n }\n Commands::Update(args) => {\n let issue = app.storage.update(&args.id, args.into()).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Updated: {}\", issue.id);\n }\n Commands::Close(args) => {\n app.storage.delete(&args.id).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Closed: {}\", args.id);\n }\n Commands::Delete(args) => {\n app.storage.delete(&args.id).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Deleted: {}\", args.id);\n }\n Commands::List(args) => {\n let filter = args.into();\n let issues = app.storage.list(&filter).await?;\n // ... display (no save needed for read-only)\n }\n Commands::Show(args) => {\n let issue = app.storage.get(&args.id).await?;\n // ... display (no save needed for read-only)\n }\n // ... other commands\n }\n Ok(())\n }\n}\n```\n\n**Main.rs with async runtime:**\n\n```rust\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<()> {\n let cli = Cli::parse();\n let config = Config::load().await?;\n let mut app = App::new(config).await?;\n \n cli.command.execute(&mut app).await?;\n \n Ok(())\n}\n```\n\n**Backend-specific save() implementations:**\n\n```rust\n// InMemoryStorage\n#[async_trait]\nimpl IssueStorage for InMemoryStorage {\n async fn save(&self) -> Result<()> {\n if let Some(path) = &self.data_file {\n self.save_to_jsonl(path).await?;\n }\n Ok(())\n }\n}\n\n// PostgreSQL (future)\n#[async_trait]\nimpl IssueStorage for PostgresStorage {\n async fn save(&self) -> Result<()> {\n Ok(()) // No-op: PostgreSQL commits on each operation\n }\n}\n```\n\n**Key Design Points:**\n- All App methods are async to match storage trait\n- save() added to IssueStorage trait (each backend implements appropriately)\n- Auto-save after every mutating command (create, update, delete, close)\n- Read-only commands (list, show) don't trigger save\n- JSONL load warnings displayed to user\n- Uses tokio current-thread runtime as specified in rivets-0gc","acceptance_criteria":"- CLI uses storage trait, not concrete type\n- Storage initialized on CLI startup\n- Data loaded from JSONL at startup (if exists)\n- Data saved to JSONL on exit\n- All commands work via trait methods\n- Error handling for storage failures\n- Integration tests with InMemoryStorage","notes":"## Clarifications\n\n### Session 2025-11-17\n\n- Q: The storage trait from rivets-0gc is async, but the design shows synchronous App methods. Should the CLI App and command execution be async? → A: Make App and command execution async (matches storage trait pattern, use #[tokio::main] as specified in rivets-0gc)\n- Q: With InMemoryStorage as Arc>, the design's downcast pattern won't work. How should the CLI trigger save on exit for in-memory backend? → A: Add save() method to IssueStorage trait (trait-based approach, each backend implements appropriately, works with Box)\n- Q: When should the in-memory backend persist to JSONL - only on exit, after every command, or periodically? → A: Save after every mutating command (resilient to crashes, better durability guarantee)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gc","dep_type":"blocks"},{"depends_on_id":"rivets-bz5","dep_type":"blocks"},{"depends_on_id":"rivets-l66","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:42.102424622Z","updated_at":"2025-11-30T03:09:08.310600427Z","closed_at":"2025-11-30T03:09:08.310600427Z"} +{"id":"rivets-d71","title":"Implement PostgreSQL storage backend with recursive CTEs","description":"Create PostgresStorage implementation using sqlx with connection pooling. Use recursive CTEs for cycle detection and ready work queries. This is the production multi-user backend.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"Implement PostgreSQL backend:\n\n```rust\npub struct PostgresStorage {\n pool: PgPool,\n}\n\nimpl IssueStorage for PostgresStorage {\n fn create_issue(&mut self, new: NewIssue) -> Result {\n let id = generate_hash_id(&new);\n sqlx::query!(\n \"INSERT INTO issues (id, title, description, ...) VALUES ($1, $2, $3, ...)\",\n id, new.title, new.description\n ).execute(&self.pool).await?;\n \n self.get_issue(&id)?.ok_or(Error::NotFound)\n }\n \n fn check_cycle(&self, from: &IssueId, to: &IssueId) -> Result {\n let exists = sqlx::query_scalar!(\n r#\"\n WITH RECURSIVE paths AS (\n SELECT issue_id, depends_on_id, 1 as depth\n FROM dependencies WHERE issue_id = $1\n UNION ALL\n SELECT d.issue_id, d.depends_on_id, p.depth + 1\n FROM dependencies d JOIN paths p \n ON d.issue_id = p.depends_on_id\n WHERE p.depth < 100\n )\n SELECT EXISTS(SELECT 1 FROM paths WHERE depends_on_id = $2)\n \"#,\n to, from\n ).fetch_one(&self.pool).await?;\n \n Ok(exists.unwrap_or(false))\n }\n \n fn find_ready(&self, filter: &IssueFilter) -> Result> {\n // Recursive CTE from beads ready.go\n // ... implementation\n }\n}\n```\n\n**Schema**: Similar to beads SQLite schema, optimized for PostgreSQL\n**Dependencies**: `sqlx` with `runtime-tokio-rustls`, `postgres` features\n**Migrations**: Use sqlx migrations","acceptance_criteria":"- PostgresStorage implements all trait methods\n- Connection pooling configured\n- Recursive CTEs for cycle detection and ready work\n- All CRUD operations functional\n- Indexes for performance\n- Migration system working\n- Integration tests with test database\n- Performance: <50ms for complex queries","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gc","dep_type":"blocks"},{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-17T22:41:42.669213538Z","updated_at":"2025-11-17T22:41:42.669213538Z","closed_at":null} +{"id":"rivets-ml05","title":"tethys: non-transactional clear in index_with_options leaves DB degraded on mid-pipeline panic","description":"Both clear_all_call_edges (indexing.rs:424) and clear_all_file_deps (indexing.rs:138, added in rivets-lcb6) run outside any explicit transaction. If the indexing pipeline panics or is interrupted between the clear and the subsequent re-population:\n\n- file_deps is empty (or partially populated)\n- call_edges is empty (if the panic happens after line 424)\n\nUntil the next complete index run, downstream queries (coupling, callers, cycles, impact analysis) operate on degraded data.\n\nThis is pre-existing for call_edges; rivets-lcb6 makes file_deps adopt the same pattern (the alternative — leaving file_deps stale — has worse properties). So this issue documents a known risk shared by both, not a regression introduced by lcb6.\n\nSurfaced by claude-review on PR #65.\n\nLikely fix shape: wrap the clear + populate work in a transaction so partial failure rolls back. Connection-level: BEGIN / COMMIT around the relevant indexing.rs section. SQLite WAL mode supports this; the question is what 'partial failure' means in the current architecture (batch_writer commits in chunks, so wrapping the whole pipeline in one transaction may not be feasible without changes there).\n\nOut of scope here; file before fix work begins so the design considers it.\n\nReference acceptance criteria for the eventual fix:\n- Test: index a workspace, abort mid-pipeline (panic injection), assert file_deps + call_edges are EITHER both populated from prior state OR both empty — no half-populated states.\n- Test: under normal operation, no perf regression compared to current non-transactional pattern.","status":"open","priority":4,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] file_deps and call_edges remain consistent (both old-state or both new-state) across pipeline abort\n- [ ] No significant indexing perf regression\n- [ ] Both clear_all_file_deps and clear_all_call_edges share the same transactional discipline","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T03:19:35.181862600Z","updated_at":"2026-05-13T03:19:35.181862600Z","closed_at":null} +{"id":"rivets-9el","title":"Add multi-select for batch operations","description":"Add interactive multi-select for batch operations:\n\n```\n$ rivets close --interactive\nSelect issues to close:\n> [x] rivets-abc Fix authentication bug\n [ ] rivets-def Add dark mode\n> [x] rivets-ghi Update dependencies\n```\n\nUse dialoguer's MultiSelect widget.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:18.376893061Z","updated_at":"2025-11-30T18:36:18.376893061Z","closed_at":null} {"id":"rivets-3r7","title":"Add comprehensive unit tests for Phase 1","description":"Create comprehensive unit tests covering all Phase 1 functionality: reading, writing, streaming, and atomic writes.\n\nTests should cover happy paths, error cases, edge cases, and integration scenarios.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Test categories:\n\n**Reader tests**:\n- read_line() with valid JSON\n- read_line() with malformed JSON\n- read_line() with empty lines\n- read_line() EOF handling\n- stream() with multiple records\n- stream() with errors mid-stream\n\n**Writer tests**:\n- write() single record\n- write_all() multiple records\n- flush() behavior\n- Serialization error handling\n\n**Integration tests**:\n- Round-trip: write then read\n- Atomic write success\n- Atomic write failure (temp file cleanup)\n- Large file streaming (memory usage)\n\nCreate tests/ directory with integration_tests.rs.","acceptance_criteria":"- Unit tests for all JsonlReader methods\n- Unit tests for all JsonlWriter methods\n- Integration tests for round-trip scenarios\n- Tests for error conditions\n- Tests for edge cases (empty files, large files)\n- All tests pass\n- Test coverage >80% for Phase 1 code","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-b1n","dep_type":"blocks"},{"depends_on_id":"rivets-zy0","dep_type":"blocks"}],"created_at":"2025-11-27T23:15:03.058434829Z","updated_at":"2025-11-28T01:36:31.113700307Z","closed_at":"2025-11-28T01:36:31.113700307Z"} -{"id":"rivets-7nq","title":"Create shared fixtures module (optional)","description":"Extract common fixtures used across multiple test files into shared modules.\n\nStructure:\n- crates/rivets-jsonl/tests/common/mod.rs\n- crates/rivets/tests/common/mod.rs\n\nThis task should be done after the conversion tasks to see what fixtures emerge as reusable.","status":"closed","priority":3,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Common fixtures identified and extracted\n- Fixtures reused across test files where applicable\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ans","dep_type":"blocks"},{"depends_on_id":"rivets-c0p","dep_type":"blocks"},{"depends_on_id":"rivets-dee","dep_type":"blocks"},{"depends_on_id":"rivets-dev","dep_type":"blocks"},{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T00:57:53.770663946Z","updated_at":"2025-11-29T02:11:28.565585029Z","closed_at":"2025-11-29T02:11:28.565585029Z"} -{"id":"rivets-l8ur","title":"PR #60 polish backlog: cleanup items deferred from review rounds","description":"Collection of low-priority cleanup items surfaced during PR #60's third review round. Each is real but represents diminishing returns relative to the work already done; bundle them for a future hygiene pass.\n\n**Items:**\n\n1. Remove the now-redundant ''suggestions_capped_at_five'' test in cli/coupling.rs — the new ''suggestions_capped_at_five_returns_first_five_by_input_order'' is a strict superset. Both currently exist and assert the same length on identical inputs.\n\n2. Add an ''add_file'' helper to ''package_coupling_tests'' (it already exists in ''repopulate_architecture_tests''). The new sort-order test in particular repeats the inline ''upsert_file'' pattern 7 times.\n\n3. Strengthen ''get_package_coupling_returns_err_for_corrupt_target_source'': currently asserts only ''is_err()''. Add assertions that the error is the expected variant (''Error::Internal'') and that the message contains the corrupt source value (''totally-bogus'') so a future change that drops diagnostic context would fail the test.\n\n4. Corrupt-source tests use hardcoded package ''id=1,2,3'' and rely on ''PRAGMA ignore_check_constraints'' silently succeeding. Add a verification ''SELECT'' after the corrupt INSERT to confirm the bad row landed, so a silent PRAGMA failure produces a clear diagnostic at the injection point instead of a confusing downstream assertion mismatch. Optionally switch the inserts to use ''INSERT ... RETURNING id'' or query back ids rather than hardcoding.\n\n5. The ''completed_path_writes_nothing_to_stdout'' assertion message embeds the ''rivets-tuph'' ticket reference. Move it to a ''// TODO(rivets-tuph)'' comment above the assertion so the failure message stays clean and the editorial intent stays out of test output.\n\n6. Tighten the ''#[expect(clippy::cast_precision_loss, reason = ...)]'' annotation on ''CouplingMetrics::instability'' to explicitly say ''the annotated cast is ; the numerator uses which is infallible widening, exempt from the lint.''\n\n7. Add a reciprocal cross-reference: ''Package.name'' field doc should point at ''PackageId'' (''recommended stable identity; see [] for why the numeric id is unsuitable for that purpose''). Currently the ''PackageId → name'' direction is documented but not the reverse.\n\n8. The ''none_writes_nothing'' assertion is bare ''assert!(buf.is_empty())'' with no contract message. Add an explanation so a failure tells a future maintainer what the ''None'' arm represents.\n\nDiscovered during third-round PR #60 review (the 6 specialist reviewers ran on commits a4949bb..HEAD).","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["tethys","polish","test-quality"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T05:20:38.446776300Z","updated_at":"2026-05-11T05:20:38.446776300Z","closed_at":null} -{"id":"rivets-p9oz","title":"Test Coverage & Quality","description":"Expand test coverage: validator edge cases, TOCTOU scenarios, integration tests, ANSI validation, and async safety assertions.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:30.278033816Z","updated_at":"2026-02-21T02:28:30.278033816Z","closed_at":null} -{"id":"rivets-f5f","title":"Add error handling and MCP error responses","description":"Implement proper error handling:\n- Map rivets errors to MCP error responses\n- Structured error types for MCP\n- Helpful error messages for common failures","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:33.957289146Z","updated_at":"2025-11-29T14:41:30.508649115Z","closed_at":"2025-11-29T14:41:30.508649115Z"} -{"id":"rivets-wsix","title":"tethys: audit other UPSERT-only tables for stale-row growth (sibling of rivets-lcb6)","description":"Surfaced by code-reviewer during PR #65 multi-agent review. The rivets-lcb6 fix establishes the pattern: UPSERT-only tables (INSERT ... ON CONFLICT DO UPDATE) need an explicit clear before full re-index, or 'ref_count'-style aggregates grow monotonically and removed-at-source rows persist as phantoms.\n\nPR #65 fixed file_deps (and noted call_edges already had its clear_all_call_edges). Other tables in crates/tethys/src/db/ using ON CONFLICT DO UPDATE were NOT audited.\n\nLikely candidates to grep (ON CONFLICT...DO UPDATE in crates/tethys/src/db/):\n- imports (db/imports.rs?)\n- refs (db/refs.rs)\n- attributes (db/attributes.rs?)\n- symbols' subordinate tables (enum variants, struct fields — added in PR #58)\n- architecture / coupling tables (added in PR #60)\n- any new table added after rivets-lcb6 lands\n\nFor each: classify as\n (a) UPSERT with monotonically-growing aggregate column → SAME BUG, needs clear_all_X + call from index_with_options\n (b) UPSERT but no growing aggregate → still has the 'stale row persists when source is deleted' subset of the bug\n (c) UPSERT but table is fully rebuilt from another source-of-truth each run → no bug\n\nDeliverable: a comment in this issue listing every UPSERT-only table with its classification, then file follow-up issues for any (a) or (b) cases.\n\nThe 30.7% baseline-vs-resolved gap noted in project_resolver_baseline_2026_05_12.md may have contributions from this class of bug we haven't measured.\n\nSource: PR #65 multi-agent review aggregate (code-reviewer 'Suggestion #3'/Strength).","status":"open","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"* All UPSERT-only tables in crates/tethys/src/db/ enumerated with classification (a/b/c)\n* Follow-up issues filed for each (a) or (b) classification\n* If any (a) is found, regression-fence test added matching the file_deps_idempotency.rs pattern","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-17T02:43:53.022009444Z","updated_at":"2026-05-17T02:43:53.022009444Z","closed_at":null} +{"id":"rivets-i8g","title":"Clarify blocked tool semantics for status-blocked vs dependency-blocked","description":"The MCP integration tests have a comment indicating uncertainty about expected behavior:\n\n```rust\n// Lines 2398-2399\n// Note: Issue is blocked by status, not by dependency, so it may or may not appear\n// in blocked_issues depending on implementation\n```\n\nShould clarify and document the intended semantics:\n- Should `blocked` tool return issues with status=blocked?\n- Should it only return issues blocked by dependencies?\n- Should it return both?\n\nLocation: `crates/rivets-mcp/tests/integration.rs:2398-2399`","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["documentation","pr-feedback"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6640","dep_type":"parent-child"}],"created_at":"2025-11-30T18:29:48.790471346Z","updated_at":"2025-11-30T18:29:48.790471346Z","closed_at":null} +{"id":"rivets-4j2","title":"Update MCP server for CLI parity","description":"Update rivets-mcp to expose new CLI functionality as MCP tools.\n\nRequired new tools:\n- reopen: Reopen closed issues (mirrors CLI reopen command)\n- stale: Find issues not updated recently\n- label_add: Add labels to issues\n- label_remove: Remove labels from issues \n- label_list: List labels for an issue\n- label_list_all: List all labels in use across issues\n\nEnhancements:\n- Enhance where_am_i or add info tool with issue prefix, config details\n- Consider dep_tree tool for dependency visualization\n\nNote: Multi-ID support is optional for MCP since agents can call tools multiple times.\n\nThis should be done after the corresponding CLI commands are implemented.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-cej","dep_type":"blocks"},{"depends_on_id":"rivets-g25","dep_type":"blocks"},{"depends_on_id":"rivets-i2w","dep_type":"blocks"},{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:25:51.377507037Z","updated_at":"2025-12-28T05:16:27.368028795Z","closed_at":"2025-12-28T05:16:27.368028795Z"} +{"id":"rivets-b1n","title":"Implement atomic write functionality","description":"Implement write_jsonl_atomic() convenience function that writes to a temp file then atomically renames it. This provides crash safety for JSONL persistence.\n\nUses the temp file + rename pattern for atomic writes on POSIX systems.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse std::path::Path;\nuse tokio::fs::File;\n\npub async fn write_jsonl_atomic(path: P, values: &[T]) -> Result<()>\nwhere\n T: Serialize,\n P: AsRef,\n{\n let path = path.as_ref();\n let temp_path = path.with_extension(\"tmp\");\n \n // Write to temp file\n {\n let file = File::create(&temp_path).await?;\n let mut writer = JsonlWriter::new(file);\n writer.write_all(values.iter()).await?;\n writer.flush().await?;\n }\n \n // Atomic rename\n tokio::fs::rename(&temp_path, path).await?;\n \n Ok(())\n}\n```","acceptance_criteria":"- write_jsonl_atomic() function implemented\n- Writes to temp file with .tmp extension\n- Atomically renames temp file to target path\n- Temp file cleaned up on success\n- Proper error handling if write fails\n- Integration test verifies atomicity\n- Test verifies crash during write leaves original file intact","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6pi","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:57.333893140Z","updated_at":"2025-11-28T01:04:56.754496296Z","closed_at":"2025-11-28T01:04:56.754496296Z"} +{"id":"rivets-itez","title":"C# parser: detect unsafe modifier and extract generics","description":"Minor metadata gaps in C# function parsing:\n\n1. `csharp.rs:981` - `is_unsafe` is hardcoded to `false`, should detect `unsafe` modifier\n2. `csharp.rs:983` - `generics` is always `None`, should extract type parameters\n\nThese are minor enhancements for completeness of C# symbol metadata.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["csharp-support","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:38.853313385Z","updated_at":"2026-01-30T00:18:38.853313385Z","closed_at":null} +{"id":"rivets-4im","title":"Refactor: Extract notes-building helper for close/reopen","description":"The pattern for building notes with 'Closed: {reason}' or 'Reopened: {reason}' is duplicated in execute_close (lines 431-457) and execute_reopen (lines 527-553).\n\n**Recommendation:** Extract into helper:\n```rust\nfn append_note(existing: Option<&str>, new_note: &str) -> String {\n match existing {\n Some(notes) => format!(\"{}\\n\\n{}\", notes, new_note),\n None => new_note.to_string(),\n }\n}\n```","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:29.340092547Z","updated_at":"2025-12-28T17:01:02.387365270Z","closed_at":"2025-12-28T17:01:02.387365270Z"} +{"id":"rivets-3d0s","title":"tethys: phantom cross-crate edges from stdlib/external symbol-name collisions with workspace impl items","description":"Discovered as the residual phantom-edge population after rivets-0gom's fix landed (gilfoyle/checkpointed-build slice 3).\n\n**Symptom:** even with crate-scoped fallback (rivets-0gom slice 2) and ambiguity rejection (rivets-0gom slice 3), the tethys resolver still creates ~52 phantom cross-crate file_deps edges on the rivets workspace. These edges have a distinct signature:\n\n - Caller's crate has NO same-name symbol (slice 2 fallback misses)\n - The workspace has EXACTLY ONE candidate symbol with that name (slice 3 doesn't reject)\n - The resolver returns the unique match\n - But the caller's source code never imports the target crate\n\nDrilling into the symbols involved (.rivets-0gom/diagnose_residual.py):\n\n tethys -> rivets-jsonl: 48 refs to `len`\n tethys -> rivets: 44 refs to `children`, 17 to `Tree`\n rivets-mcp -> tethys: 19 refs to `display`\n rivets-jsonl -> tethys: 7 refs to `Serialize`, 1 to `Deserialize`\n\nThese are NOT workspace domain symbols. They're:\n- Stdlib methods: `len`, `write`, `display`, `drop`, `flush`, `read_line`\n- Stdlib types: `Metadata`, `Parser`, `Tree` (or maybe tree-sitter `Tree`)\n- External traits: `Serialize`, `Deserialize` (from serde)\n- External methods: `yellow`, `dimmed`, `reverse` (from colored)\n- Generic field/method names: `children`, `output`, `success`, `stdin`, `stdout`\n\n**Root cause hypothesis:** tethys's parser is recording these as \"symbol definitions\" in workspace files when they appear in contexts like:\n- `impl Serialize for Foo` — recording \"Serialize\" as a symbol in the impl'ing file\n- `fn len(&self) -> usize` — recording \"len\" as a defined symbol\n- Derive macros: `#[derive(Serialize)]` may produce phantom \"Serialize\" entries\n- Doc-comment cross-references\n\nThese accidentally collide with workspace-wide refs to the same names from contexts where they SHOULD be resolved to stdlib/external crates.\n\n**Why rivets-0gom's fix doesn't address this:** rivets-0gom's bug was about resolution scoping (use crate::error matching any error.rs). This new bug is about symbol-table HYGIENE — what counts as a \"symbol definition\" in the first place. Both produce cross-crate phantom edges, but the fix sites are different.\n\n**Likely fix locations:**\n- crates/tethys/src/languages/rust.rs (or wherever Rust symbol extraction lives) — filter out names that come from impl items / trait references rather than actual definitions\n- crates/tethys/src/db/symbols.rs — could add an \"external_reference\" flag and skip such symbols from search_symbol_by_name\n- Or: rivets-0gom's slice 3 could be tightened further to check whether the matched symbol's kind/visibility makes sense as a fallback target\n\n**Scoping note:** this issue is separate from but related to:\n- rivets-0gom (resolver scoping; landed first)\n- rivets-aay4 (parent_symbol_id population; might overlap with how impl items are recorded)\n- rivets-itez (C# parser metadata; analogous shape but different language)\n\nQuantitative impact: in the rivets workspace after rivets-0gom lands, 52 of ~74 remaining cross-crate file_deps are phantoms from this issue. The ALLOWED pairs (rivets->rivets-jsonl, rivets-mcp->rivets) have 22 legitimate edges; everything beyond that is this bug.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Decide where the right filter lives (parser, symbol-insert, search-by-name)\n- [ ] Construct a fixture: two crates, one defines `impl Serialize for Foo`, the other has no Cargo dep on serde. Expect: no symbol of name `Serialize` should be findable as a fallback target from the second crate's refs.\n- [ ] On the rivets workspace post-fix, the residual 52 phantom edges in FORBIDDEN ordered pairs drop to <= 5 (allowing for edge cases).\n- [ ] No regression in ALLOWED pair counts.\n- [ ] No regression in claim 6 (ambiguity = 0).","notes":"## Investigation note from rivets-0gom round 3 (2026-05-12, PR #61)\n\nDuring fixture development for the resolver routing integration test\n(crates/tethys/tests/resolver_routing.rs), discovered that\n`resolve_refs_for_file` (crates/tethys/src/resolve.rs:82-85) short-circuits\nwhen a file has no imports:\n\n let imports = self.db.get_imports_for_file(file_id)?;\n if imports.is_empty() {\n return Ok(0);\n }\n\nThis means references in import-less files NEVER go through Pass-2\nresolution. They bypass BOTH the same-crate scoping AND the ambiguity\nrefusal added by rivets-0gom. They remain unresolved unless Pass 1\n(tree-sitter-direct resolution) handled them.\n\n**Possible interaction with the residual ~52 phantom edges:** if some\nphantom edges are produced by Pass 1 in import-less files (and therefore\nnever go through rivets-0gom's hygiene checks), the stdlib-symbol-hygiene\nfix here will need to think about Pass 1 too — not just symbol-table\nfiltering at Pass 2 time.\n\n**When picking this up:** run a prove-it-prototype that splits the residual\nedges by which pass produced them. Don't assume all 52 are stdlib\npollution — some may be Pass-1 import-less bypass, which would need a\ndifferent fix shape (filter at the Pass-1 extractor or run a Pass-2-style\ncheck on Pass-1 results).\n\n## Related issue (filed 2026-05-12)\n\n- [rivets-714v](rivets-714v): tethys --lsp emits 'url is not a file' on multi-crate workspace indexing. Blocks the LSP angle of the audit-and-demote design — without it, demoted refs stay unresolved instead of getting re-resolved via type info. rivets-3d0s is not hard-blocked (the audit alone eliminates phantoms), but full value of the fix depends on rivets-714v being resolved.\n\n## Checkpointed-build outcome (2026-05-12, PR #61 follow-up branch)\n\nBranch: fix/rivets-3d0s-stdlib-symbol-pollution. Status: slice 1+2 implemented, gates ran, REVERTED on oracle drift. Status set back to open.\n\n**What happened:** the kind-compatibility audit (filter on search_unique_symbol_by_name) landed and the probe ran. Predicted 58.6% phantom reduction; actual 5.7%. Root cause: the audit narrows the candidate set, which converts previously-ambiguous names into unique matches — creating new method-call phantoms via un-ambiguation. The falsifiable-design simulation didn't model this dynamic coupling.\n\n**Probed Option A (extend audit to call->method/function cross-crate):** 99.4% phantom reduction BUT 80% false-positive rate on ALLOWED-pair legitimate refs (95 of 120 in rivets workspace).\n\n**Root cause of false-positive rate** — filed as a separate blocker:\n\n- **[rivets-v465](rivets-v465):** tethys: import resolver leaks legitimate cross-crate refs to unscoped fallback. The unscoped fallback is currently doing BOTH phantom resolution and legitimate cross-crate resolution (because imports leak), making any audit at this layer indiscriminate.\n\n**Blocking issues for rivets-3d0s to ship:**\n\n- [rivets-v465](rivets-v465) (NEW) — import-resolver leak. Until imports are caught, no rule at unscoped-fallback can cleanly distinguish phantom from legitimate.\n- [rivets-714v](rivets-714v) — LSP multi-crate path bug. Even with v465 fixed, the call->method type-info residual needs LSP.\n\n**Diagnostic artifacts retained on the branch:**\n\n- .rivets-3d0s/probe.py, oracle.py, lsp_probe.py — prove-it-prototype outputs\n- .rivets-3d0s/audit_simulation.py — falsifiable-design cheapest falsifier (now with EXTENDED mode that demonstrates the 80% legit_cross false-positive on Option A)\n- .rivets-3d0s/design.md, plan.md — falsifiable-design + budgeted-plan outputs\n\n**When picking this back up:** start by fixing rivets-v465. Then re-run audit_simulation.py with EXTENDED=True against the post-v465 DB. If false-positive rate drops to <= 10 ALLOWED-pair refs, the rivets-3d0s audit-and-demote design becomes viable. Otherwise, escalate to rivets-714v LSP path.\n\nClosed: Implemented as K-hybrid filter in PR #67 (slice 1 commit 58c659c, slice 2 commit dbba5b7). Empirical impact on rivets workspace: total cross-crate file_deps 74->8 (-89%), FORBIDDEN-pair phantoms 14->0 (acceptance target was <=5; exceeded by margin), MISMATCH-pair phantoms 38->0, ambiguity violations preserved at 0 (rivets-0gom claim 6 holds). All 8 surviving cross-crate edges are import-corroborated and verifiable as legitimate. Filter applies at file_deps aggregation time (populate_file_deps_from_call_edges) rather than at resolution time, sidestepping the un-ambiguation dynamic that broke the prior v2 attempt (see .rivets-3d0s/slice2-drift-evidence-2026-05-17.md). Approach inspired by kirograph's coupling-from-imports-only model; adapted to preserve tethys's intra-crate call-derived value.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gom","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-12T01:13:22.383367900Z","updated_at":"2026-05-17T14:41:46.440171932Z","closed_at":"2026-05-17T14:41:46.440170168Z"} +{"id":"rivets-95l","title":"Add local quality gates using cargo-husky","description":"Add cargo husky and formatting, linting, and testing as pre-commit gates","status":"closed","priority":2,"issue_type":"task","assignee":"Claude","labels":[],"design":"Implemented pre-commit hook combining beads sync and cargo quality gates:\n\n**Hook Location**: `.git/hooks/pre-commit`\n\n**Quality Checks**:\n1. Beads sync flush (if in beads workspace)\n2. `cargo fmt -- --check` - Code formatting\n3. `cargo clippy --all-targets --all-features -- -D warnings` - Linting\n4. `cargo test --quiet` - All tests\n\n**Stub Code Handling**: Added `#[allow(dead_code)]` attributes to placeholder types/functions to prevent false positives during development.\n\n**Manual Execution**: Hook can be tested by running `.git/hooks/pre-commit` directly.","acceptance_criteria":"✓ Pre-commit hook installed at .git/hooks/pre-commit\n✓ Runs cargo fmt check before commits\n✓ Runs cargo clippy with -D warnings before commits\n✓ Runs cargo test before commits\n✓ Integrates with existing beads sync hook\n✓ Documentation added to README.md\n✓ Successfully passes all checks on current codebase","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T21:55:30.160010922Z","updated_at":"2025-11-17T22:04:29.413337487Z","closed_at":"2025-11-17T22:04:29.413337487Z"} +{"id":"rivets-e8c","title":"Add confirmation dialogs for destructive operations","description":"Require confirmation for destructive operations:\n\n```\n$ rivets delete rivets-abc\nDelete issue 'rivets-abc' (Fix authentication bug)?\nThis will also remove 3 dependencies.\n[y/N]: \n```\n\nAdd --force/-f flag to skip confirmation for scripting.\nSkip prompts when --json flag is set.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["phase-1b","safety","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:42.629554393Z","updated_at":"2025-11-30T18:35:42.629554393Z","closed_at":null} +{"id":"rivets-pex","title":"Add progress spinners for long operations","description":"Add progress spinners for operations that may take time:\n- List command with many issues\n- Dependency tree calculation\n- Ready work calculation\n- Import/export operations\n\nShow elapsed time for operations >500ms.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:25.358698235Z","updated_at":"2025-11-30T18:35:25.358698235Z","closed_at":null} +{"id":"rivets-vabg","title":"Streaming SQLite writes during indexing","description":"Write to SQLite immediately after parsing each file instead of accumulating data in memory.\n\n**Current behavior:**\n- Parse all files, accumulate symbols/references in memory\n- Write to SQLite after parsing completes\n- Memory usage: O(n) where n = codebase size\n\n**Drift's pattern:**\n- Parse file → immediately send to background writer thread\n- Writer batches 100 files per transaction\n- Memory usage: O(1) constant regardless of codebase size\n\n**Benefits:**\n- Handle 10K+ file codebases without OOM\n- Progressive results (can query partial index)\n- Better crash recovery (data already persisted)\n\n**Implementation:**\n```rust\nstruct BatchWriter {\n sender: Sender,\n handle: JoinHandle,\n}\n\nimpl BatchWriter {\n fn new(db_path: PathBuf, batch_size: usize) -> Self;\n fn send(&self, batch: FileBatch) -> Result<()>;\n fn finish(self) -> Result;\n}\n```\n\nThis pairs with rayon parallelization - parsing threads send to writer thread.","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:15.225967341Z","updated_at":"2026-01-29T19:04:45.646948276Z","closed_at":"2026-01-29T19:04:45.646948276Z"} +{"id":"rivets-nkjd","title":"tethys: resolve_super_path uses filesystem-walk semantics, diverges from Rust super:: scoping","description":"tethys's `resolver::resolve_super_path` interprets `super::X` from a file at `src/parent/child.rs` as a filesystem grandparent walk: `current_file.parent().parent()` then joins X, reaching `src/X.rs`. Rust's actual semantics: `super::X` from `mod parent::child` refers to `crate::parent::X`, which lives at `src/parent/X.rs` (or methods on the parent module file).\n\nConcrete example, surfaced during rivets-044i (PR #74):\n\n src/lib.rs -> mod parent;\n src/parent.rs -> mod child; pub mod sibling;\n src/parent/child.rs:\n pub fn entry() {\n super::sibling::foo(); // Rust: src/parent/sibling.rs ; tethys: src/sibling.rs\n }\n\nThe new `self_and_super_paths_resolve_via_as_written` test in pass2_qualified_paths.rs documents this divergence (fixture lays sibling at the location tethys's resolver expects) but doesn't fix it.\n\nImpact: any qualified ref using `super::X::method()` style from a depth-2+ file resolves to the wrong file via the qualified_module_fallback path (or fails to resolve at all if the wrong-level file doesn't exist). The use-statement form `use super::X;` goes through a different code path (Pass-2 explicit imports) and may have the same issue — needs verification.\n\nSurfaced by claude-code-review #4 on PR #74 during round-3 review-feedback assessment, with the explicit suggestion to file a tracker entry rather than leave the divergence documented-only.","status":"open","priority":4,"issue_type":"bug","assignee":null,"labels":[],"design":"Two candidate fixes:\n\n(a) Build a module-tree map at index time: for each `.rs` adjacent to a `/` directory, record that the .rs file is the parent-module file of the subdirectory's contents. Then `resolve_super_path` walks the module tree instead of the filesystem.\n\n(b) Special-case the parent-module pattern in resolve_super_path: when `current_file.parent()` contains a sibling `.rs` (e.g. for `src/parent/child.rs`, check whether `src/parent.rs` exists), treat that .rs file as the parent module and resolve `super::X` relative to *its* directory (which is `src/parent/`, not `src/`).\n\nOption (b) is narrower and matches the common-case Rust idiom (parent.rs + parent/ pattern). Option (a) handles edge cases like `parent/mod.rs` + sibling files. Either way, `resolve_self_path` may need parallel review since it's the inverse semantics question.\n\nRisk: any test or workspace currently relying on the filesystem-walk semantics breaks. The rivets workspace itself doesn't seem to use this shape (otherwise PR #74's rivets-workspace re-index would have surfaced differently).","acceptance_criteria":"- [ ] resolve_super_path returns `src/parent/X.rs` for ref `super::X` from `src/parent/child.rs` when `src/parent.rs` exists\n- [ ] Test in pass2_qualified_paths.rs (or new test file) pins the Rust-spec semantics\n- [ ] Existing rivets workspace re-index post-fix: no phantom-rate regression (claim 7 of rivets-3d0s fence holds)\n- [ ] Update self_and_super_paths_resolve_via_as_written's docstring to remove the divergence-documentation language (or replace with a comment that says \"this is what Rust does, this is what we do, and they now agree\")\n- [ ] resolve_self_path reviewed for parallel issues; either confirmed correct or follow-up issue filed","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-19T01:04:21.198957500Z","updated_at":"2026-05-19T01:04:21.198957500Z","closed_at":null} +{"id":"rivets-34i","title":"Add missing filter parameters to MCP list and ready tools","description":"The MCP tools don't expose all filter options that the storage layer supports.\n\n**Missing filters:**\n- `list()`: Missing `label` filter\n- `ready()`: Missing `label` and `issue_type` filters\n\nThe storage layer (`IssueFilter`) supports all these fields and applies them correctly. The MCP tools just need to accept and pass these additional parameters.","status":"closed","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":"**list() changes:**\nAdd `label: Option` parameter and include in IssueFilter construction.\n\n**ready() changes:**\nAdd `label: Option` and `issue_type: Option<&str>` parameters.\nValidate issue_type like list() does before passing to storage.\n\n**Server changes:**\nUpdate `ListParams` and `ReadyParams` structs in models.rs to include new fields.\nUpdate server.rs tool handlers to pass new parameters.","acceptance_criteria":"- [ ] list() accepts label parameter\n- [ ] ready() accepts label and issue_type parameters\n- [ ] MCP tool schemas updated with new parameters\n- [ ] Integration tests for new filter combinations","notes":"Progress: Core implementation complete (models, tools, server updated). Integration tests need fixing and refactoring to rstest. See child tasks: rivets-jfs, rivets-wfb, rivets-1yi, rivets-vvp","external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:45:00.665218723Z","updated_at":"2025-11-30T00:03:50.414931004Z","closed_at":"2025-11-30T00:03:50.414931004Z"} +{"id":"rivets-dp5","title":"Add nu-ansi-term dependency for colored output","description":"Add nu-ansi-term crate to workspace dependencies for terminal color support.\n\nThis is the foundation for all colored output features.","status":"open","priority":1,"issue_type":"task","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:29.450981296Z","updated_at":"2025-11-30T18:34:29.450981296Z","closed_at":null} {"id":"rivets-x1e","title":"Implement hash-based ID generation system","description":"Implement the adaptive hash-based ID generation system from beads, which creates collision-resistant IDs using SHA256 and base36 encoding. The system should support adaptive length (4-6 chars based on database size) and hierarchical IDs for parent-child relationships.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on beads ids.go (288 LOC):\n\n**Algorithm**:\n1. Combine title, description, creator, timestamp, nonce\n2. SHA256 hash the content\n3. Base36 encode to desired length (4-6 chars)\n4. Format: {prefix}-{hash} (e.g., \"rivets-a3f8\")\n\n**Adaptive Length**:\n- 0-500 issues: 4 chars\n- 500-1,500: 5 chars \n- 1,500-10,000: 6 chars\n\n**Collision Handling**:\n- Try nonces 0-99\n- If all collide, increase length\n\n**Hierarchical IDs**:\n- Parent-child: \"rivets-a3f8.1\", \"rivets-a3f8.1.2\"\n- Use child_counters table for sequence\n\n**Implementation**:\n- Use `sha2` crate for hashing\n- Custom base36 encoding function\n- Validation regex for ID format","acceptance_criteria":"- Hash ID generation function implemented\n- Adaptive length selection based on DB size\n- Collision detection and handling\n- Hierarchical ID support with dot notation\n- Base36 encoding/decoding\n- ID validation function\n- Unit tests for all scenarios (collisions, hierarchical, validation)","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:15:21.124541589Z","updated_at":"2025-11-18T01:58:56.628735110Z","closed_at":"2025-11-18T01:58:56.628735110Z"} -{"id":"rivets-uvv","title":"Fix: Blocked status not counted in execute_info","description":"In execute_info (lines 61-66), the fold counting issue statuses doesn't track Blocked issues separately. They're counted in total but have no dedicated counter, inconsistent with execute_stats which tracks blocked separately.\n\n**Current code:**\n```rust\nlet (total, open, in_progress, closed) =\n all_issues.iter().fold((0, 0, 0, 0), |(t, o, ip, c), issue| match issue.status {\n IssueStatus::Blocked => (t + 1, o, ip, c), // ← Not tracked\\!\n });\n```\n\n**Fix:** Add a blocked counter to the tuple and output.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":["bug","execute.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:17.787185220Z","updated_at":"2025-12-28T16:04:20.692554031Z","closed_at":"2025-12-28T16:04:20.692554031Z"} -{"id":"rivets-fe4f","title":"Implement parent_symbol_id for nested symbol tracking","description":"Two TODO sites in lib.rs (lines 691, 823) pass None for parent_symbol_id. Implementing this would enable tracking nested symbols (methods within impl blocks, nested functions) for more precise dependency graphs.","status":"closed","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Closed: Duplicate of rivets-aay4 (older, describes the same parent_symbol_id TODOs in lib.rs).","external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:46.456516141Z","updated_at":"2026-05-10T04:30:14.626611600Z","closed_at":"2026-05-10T04:30:14.626609700Z"} -{"id":"rivets-bxom","title":"tethys: implement proper incremental update and staleness check","description":"CodeIndex::update() currently re-indexes everything instead of doing a proper incremental update. CodeIndex::needs_update() always returns true. Implement proper staleness detection and incremental re-indexing.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:04.826229488Z","updated_at":"2026-03-19T01:41:04.826229488Z","closed_at":null} -{"id":"rivets-iwsc","title":"Docs: Add rationale for prefix length and traversal depth constants","description":"In commands/init.rs, the constants at lines 57-63 have minimal documentation:\n\n```rust\n/// Minimum prefix length\npub const MIN_PREFIX_LENGTH: usize = 2;\n\n/// Maximum prefix length\npub const MAX_PREFIX_LENGTH: usize = 20;\n\n/// Maximum directory depth to traverse when searching for rivets root\npub const MAX_TRAVERSAL_DEPTH: usize = 256;\n```\n\nPer M-DOCUMENTED-MAGIC guideline, these should explain WHY these values were chosen:\n\n- Why 2 minimum? (single char too ambiguous?)\n- Why 20 maximum? (readability in issue IDs?)\n- Why 256 traversal depth? (matches typical filesystem limits? performance?)\n\n**Example fix:**\n```rust\n/// Minimum prefix length.\n///\n/// Set to 2 to ensure prefixes are meaningful and distinguishable.\n/// Single-character prefixes would be too ambiguous and harder to type.\npub const MIN_PREFIX_LENGTH: usize = 2;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","init.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:50:43.658996271Z","updated_at":"2025-12-28T15:50:43.658996271Z","closed_at":null} -{"id":"rivets-9el","title":"Add multi-select for batch operations","description":"Add interactive multi-select for batch operations:\n\n```\n$ rivets close --interactive\nSelect issues to close:\n> [x] rivets-abc Fix authentication bug\n [ ] rivets-def Add dark mode\n> [x] rivets-ghi Update dependencies\n```\n\nUse dialoguer's MultiSelect widget.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:18.376893061Z","updated_at":"2025-11-30T18:36:18.376893061Z","closed_at":null} -{"id":"rivets-02t","title":"Implement modification tools (create, update, close, dep)","description":"Implement issue modification tools:\n- create(title, description, priority, type, ...) - Create issue\n- update(issue_id, status, priority, ...) - Update fields\n- close(issue_id, reason) - Mark complete (update status to Closed)\n- dep(issue_id, depends_on_id, dep_type) - Add dependency","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] create tool creates issue and saves\n- [ ] update tool modifies fields correctly\n- [ ] close tool sets status to Closed\n- [ ] dep tool adds dependency with cycle detection\n- [ ] Unit tests: each tool with MockStorage (happy path + error cases)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-6u6","dep_type":"blocks"},{"depends_on_id":"rivets-vdi","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:28.277217069Z","updated_at":"2025-11-30T01:19:22.951737809Z","closed_at":"2025-11-30T01:19:22.951737809Z"} -{"id":"rivets-vpny","title":"Docs: Add rationale for MAX_BLOCKING_DEPTH constant","description":"In storage/in_memory/graph.rs (line 18), the constant has minimal documentation:\n\n```rust\n/// Maximum depth for BFS traversal in blocking detection.\n///\n/// This limit prevents infinite loops and handles extremely deep hierarchies gracefully.\nconst MAX_BLOCKING_DEPTH: usize = 50;\n```\n\nPer M-DOCUMENTED-MAGIC guideline, should explain WHY 50 was chosen:\n\n**Suggested documentation:**\n```rust\n/// Maximum depth for BFS traversal in blocking detection.\n///\n/// This limit prevents infinite loops and handles extremely deep hierarchies gracefully.\n/// Set to 50 to:\n/// - Handle practical issue hierarchies (most projects have < 10 levels)\n/// - Provide headroom for complex epics with nested sub-tasks\n/// - Cap worst-case traversal time for malformed graphs\n/// - Balance between completeness and performance\nconst MAX_BLOCKING_DEPTH: usize = 50;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","graph.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:51:04.569965544Z","updated_at":"2025-12-28T15:51:04.569965544Z","closed_at":null} -{"id":"rivets-c540","title":"tethys: fuzzy fallback for --package suggestions (edit distance)","description":"tethys coupling --package uses substring match (case-insensitive) for the \"Did you mean:\" suggestions when the name isn't found. A user who types rivets_mcp (underscore) for rivets-mcp (hyphen), or rivetsmcp (no separator), gets no suggestion. cargo's own suggestion logic uses Levenshtein-distance fallback in this case.\n\nReviewer flagged this as \"known limitation, could be quality-of-life follow-up\" across multiple PR 60 review rounds.\n\nScope: extend collect_suggestions in crates/tethys/src/cli/coupling.rs to fall back to edit-distance ranking when substring match yields nothing. Use the strsim crate (already in the rust ecosystem) or a hand-rolled DP impl.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Substring match still used first (preserves current behavior for partial matches)\n- [ ] Edit-distance fallback when substring yields zero results\n- [ ] Configurable distance threshold (default ~3)\n- [ ] Unit tests covering: snake-vs-kebab mismatch, missing separator, single-char typo, very distant name yields no suggestion","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:21:28.989377900Z","updated_at":"2026-05-11T21:21:28.989377900Z","closed_at":null} -{"id":"rivets-fxy","title":"Implement semantic color theme","description":"Implement color theme with semantic colors:\n\nStatus colors:\n- Open: Blue\n- In Progress: Yellow/Gold\n- Ready: Green (new status indicator)\n- Blocked: Red\n- Closed: Gray/dim\n\nPriority colors:\n- P0: Red (critical)\n- P1: Orange\n- P2: Yellow\n- P3: Blue\n- P4: Gray\n\nIssue type colors:\n- Bug: Red\n- Feature: Green\n- Task: Blue\n- Epic: Purple\n- Chore: Gray","status":"open","priority":1,"issue_type":"feature","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:41.241090815Z","updated_at":"2025-11-30T18:34:41.241090815Z","closed_at":null} -{"id":"rivets-08k1","title":"Incrementally add #[errors] doc to public Tethys methods","description":"The entire impl Tethys block has #[expect(clippy::missing_errors_doc)]. As the API stabilizes, chip away at this by documenting errors on individual methods and narrowing the expect scope. Flagged in PR #55 review.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:17:57.715181098Z","updated_at":"2026-03-20T22:17:57.715181098Z","closed_at":null} {"id":"rivets-e55","title":"Implement JSONL to Automerge migration","description":"Create migration utilities to convert existing JSONL databases to Automerge format.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:44.625658946Z","updated_at":"2025-12-23T04:45:17.074168648Z","closed_at":"2025-12-23T04:45:17.074168648Z"} -{"id":"rivets-ljj","title":"Refactor output.rs into module structure","description":"Refactor output.rs into a module structure:\n- output/mod.rs - Main exports\n- output/color.rs - Color theme and utilities\n- output/tree.rs - ASCII tree rendering\n- output/table.rs - Table formatting (future)","status":"open","priority":1,"issue_type":"task","assignee":null,"labels":["phase-1a","refactor","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:35.368560579Z","updated_at":"2025-11-30T18:34:35.368560579Z","closed_at":null} -{"id":"rivets-zk2q","title":"Cross-File Reference Resolution","description":"Enable tethys to resolve references across files, so `tethys callers` and `tethys impact` work for symbols defined in other files.\n\nTwo phases:\n1. **Tree-sitter resolution (~85% accuracy)** - resolve via import paths ✅ COMPLETE\n2. **LSP refinement (~98% accuracy)** - delegate ambiguous cases to rust-analyzer/csharp-ls\n\nDesign doc: docs/plans/2026-01-28-cross-file-resolution.md","status":"closed","priority":1,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"## Phase 1 Status: COMPLETE (2026-01-28)\n\nAll Phase 1 tasks completed:\n- ✅ imports table added to schema\n- ✅ Import extraction for Rust (`use`) and C# (`using`)\n- ✅ Unresolved references stored with symbol_id = NULL\n- ✅ Pass 2 resolution implemented\n- ✅ Integration tests passing\n- ✅ Performance benchmarks documented in BENCHMARKS.md\n\n## Phase 2 Status: IN PROGRESS\n\nCompleted tasks:\n- ✅ rivets-8j9u: Add lsp-types dependency\n- ✅ rivets-h1va: Implement LspClient with JSON-RPC transport\n- ✅ rivets-nwwm: Implement LspProvider trait + RustAnalyzerProvider\n\nRemaining tasks:\n- rivets-oeu5: Add --lsp flag to CLI commands\n- rivets-k3mv: Integrate LSP into index --lsp\n- rivets-1dza: Integrate LSP into callers --lsp\n- rivets-6x7g: Add CSharpLsProvider\n- rivets-9o82: Add gated integration tests\n\nKey commit: feat(tethys): add LSP client infrastructure for Phase 2","external_ref":null,"dependencies":[],"created_at":"2026-01-28T17:26:53.486115898Z","updated_at":"2026-01-29T12:41:00.975901770Z","closed_at":"2026-01-29T12:41:00.975901770Z"} -{"id":"rivets-9iwc","title":"tethys: add format_uri_percent_encodes_unicode_unix to complete symmetric test matrix","description":"The format_uri test matrix has a Unix gap: format_uri_percent_encodes_unicode_as_utf8_bytes is #[cfg(windows)]-only. The Windows-side spaces test (format_uri_percent_encodes_spaces) has a Unix counterpart (format_uri_percent_encodes_spaces_unix), but the Unicode variant doesn't.\n\nWithout the Unix counterpart, a future refactor that accidentally moves percent_encode_path or PATH_PERCENT_ENCODE_SET behind #[cfg(windows)] would silently regress Unix non-ASCII path support — CI on Linux/macOS runners wouldn't catch it.\n\nTrivial add: ~5 lines mirroring the existing Windows test, asserting that /home/user/日本/foo.rs encodes to file:///home/user/%E6%97%A5%E6%9C%AC/foo.rs.\n\nSurfaced by PR #64 (closed rivets-714v) — recognized post-merge that the symmetric matrix was incomplete.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test format_uri_percent_encodes_unicode_unix added to crates/tethys/src/lsp/transport.rs tests module\n- [ ] Gated #[cfg(not(windows))]\n- [ ] Asserts UTF-8 byte-level encoding of CJK characters on a Unix-shaped path\n- [ ] cargo nextest run -p tethys passes on Linux and macOS","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:47:05.852969200Z","updated_at":"2026-05-13T02:47:05.852969200Z","closed_at":null} -{"id":"rivets-1p0f","title":"Store unresolved references with symbol_id = NULL","description":"Currently we skip references we can't resolve. Instead, store them with `symbol_id = NULL` and enough context to resolve later.\n\nThis requires allowing nullable symbol_id in the refs table and updating `store_references()` to handle unresolved refs.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:33.176960624Z","updated_at":"2026-01-28T17:44:36.816956542Z","closed_at":"2026-01-28T17:44:36.816956542Z"} -{"id":"rivets-6x7g","title":"Add CSharpLsProvider","description":"Fast follow after rust-analyzer is working:\n\n```rust\npub struct CSharpLsProvider;\nimpl LspProvider for CSharpLsProvider {\n fn command(&self) -> &str { \"csharp-ls\" }\n}\n```\n\nWire up to use csharp-ls for .cs files when --lsp is specified.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["csharp","lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1dza","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:29:09.273111083Z","updated_at":"2026-01-29T01:42:23.934741997Z","closed_at":"2026-01-29T01:42:23.934741997Z"} -{"id":"rivets-3gey","title":"Reachability analysis (forward/backward data flow)","description":"Add data flow analysis capabilities for security auditing.\n\n**UPDATED: Lower effort than originally estimated - primitives already exist!**\n\n**Existing primitives we can wrap:**\n- `get_callers(symbol)` - direct callers (graph/sql.rs:332)\n- `get_callees(symbol)` - direct callees (graph/sql.rs:368)\n- `get_dependents(file)` - files that depend on this file\n- `get_dependencies(file)` - files this file depends on\n- `get_dependency_chain(from, to)` - path between two files\n\n**What's needed:** ~50 lines of recursive wrapper code:\n\n```rust\npub fn get_reachable(&self, symbol: &str, max_depth: usize) -> Result> {\n let mut visited = HashSet::new();\n let mut results = Vec::new();\n let mut queue = VecDeque::new();\n \n queue.push_back((symbol_id, vec![], 0));\n \n while let Some((current, path, depth)) = queue.pop_front() {\n if depth > max_depth || visited.contains(¤t) {\n continue;\n }\n visited.insert(current);\n \n // Use existing get_callees() primitive\n for callee in self.symbol_graph.get_callees(current)? {\n let mut new_path = path.clone();\n new_path.push(callee.symbol.clone());\n results.push(ReachablePath { target: callee.symbol, path: new_path.clone(), depth: depth + 1 });\n queue.push_back((callee.symbol.id, new_path, depth + 1));\n }\n }\n Ok(results)\n}\n```\n\n**Use cases:**\n- Forward: \"What can this function access?\" (BFS on get_callees)\n- Backward: \"Who can reach this?\" (BFS on get_callers)\n\n**Estimated effort: Low (~50 lines, 1-2 hours)**","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","feature","security","tethys"],"design":null,"acceptance_criteria":null,"notes":"Primitives already exist - just needs BFS wrapper. Removed dependency on call_edges table since we can use existing get_callers/get_callees.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-gvmh","dep_type":"blocks"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:32.479277091Z","updated_at":"2026-01-29T20:15:21.061394775Z","closed_at":"2026-01-29T20:15:21.061394775Z"} -{"id":"rivets-24av","title":"Track failed files in WriteStats for debugging","description":"Currently `WriteStats` only tracks successful writes. When files fail to write, that information is logged but lost from the final result.\n\n**Current struct:**\n```rust\npub struct WriteStats {\n pub files_written: usize,\n pub symbols_written: usize,\n pub references_written: usize,\n pub batches_committed: usize,\n}\n```\n\n**Suggested addition:**\n```rust\npub struct WriteStats {\n // ... existing fields ...\n /// Files that failed to write with their error messages\n pub failed_files: Vec<(PathBuf, String)>,\n}\n```\n\n**Benefits:**\n- Users can see which files need attention without grepping logs\n- Enables retry logic for failed files\n- Makes partial success explicit rather than hidden\n\n**Trade-off:** Increases memory usage if many files fail (unlikely in practice).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["batch-writer","error-handling","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:06:51.687980987Z","updated_at":"2026-01-29T19:06:51.687980987Z","closed_at":null} -{"id":"rivets-6ep","title":"Investigate lock contention in multi-workspace MCP scenarios","description":"The current `Arc>` design serializes all storage access across workspaces. For typical single-workspace usage this is fine, but could become a bottleneck with many concurrent MCP requests to different workspaces.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["future","optimization","performance"],"design":"**Current behavior:**\n- Every tool call acquires context read lock to get storage\n- Lock ordering (release context before storage) is correct but adds overhead\n\n**Potential optimization:**\nReplace with lock-free workspace lookup using `DashMap`:\n```rust\nstorage_cache: Arc>>>>\n```\n\nThis allows concurrent reads from different workspaces without contention. Only `set_context` would need exclusive access.\n\n**Locations:**\n- `tools.rs:96-100`\n- `context.rs:32`","acceptance_criteria":"- [ ] Profile with realistic multi-workspace workload to confirm contention exists\n- [ ] If confirmed, implement DashMap-based approach\n- [ ] Benchmark before/after to verify improvement\n- [ ] No regression in single-workspace performance","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T04:28:36.847262094Z","updated_at":"2025-11-29T04:28:36.847262094Z","closed_at":null} -{"id":"rivets-9tff","title":"Refactor: Split execute_dep and execute_label into smaller functions","description":"Per the principle of keeping functions focused and manageable:\n\n| Function | Lines | Issue |\n|----------|-------|-------|\n| execute_dep | ~174 | Too long, handles 4 different actions |\n| execute_label | ~234 | Too long, handles 4 different actions |\n\n**Recommendation:**\n- Split execute_dep into: execute_dep_add, execute_dep_remove, execute_dep_list, execute_dep_tree\n- Split execute_label into: execute_label_add, execute_label_remove, execute_label_list, execute_label_list_all\n\nEach action handler should be a separate function called from the main dispatch function.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:46.704194257Z","updated_at":"2025-12-28T19:14:11.042762217Z","closed_at":"2025-12-28T19:14:11.042762217Z"} +{"id":"rivets-gvmh","title":"Pre-computed call graph edges table","description":"Store explicit caller→callee edges in a dedicated table for faster graph queries.\n\n**Current approach:**\n- `get_callers` joins references → symbols → files\n- Complex query with multiple joins\n- Re-computed on every query\n\n**Drift's approach:**\n- Dedicated `call_edges` table: `(caller_id, callee_id, call_count)`\n- Pre-computed during indexing\n- Simple lookups for callers/callees\n\n**Proposed schema:**\n```sql\nCREATE TABLE call_edges (\n caller_symbol_id INTEGER NOT NULL,\n callee_symbol_id INTEGER NOT NULL,\n call_count INTEGER DEFAULT 1,\n PRIMARY KEY (caller_symbol_id, callee_symbol_id),\n FOREIGN KEY (caller_symbol_id) REFERENCES symbols(id),\n FOREIGN KEY (callee_symbol_id) REFERENCES symbols(id)\n);\n\nCREATE INDEX idx_call_edges_callee ON call_edges(callee_symbol_id);\n```\n\n**Benefits:**\n- O(1) lookup for \"who calls X?\" instead of join-heavy query\n- Enables efficient reachability analysis\n- Pre-computed call counts for hotspot detection\n\n**Trade-off:**\n- Slightly larger database\n- Need to update edges during incremental re-index","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:21.017980416Z","updated_at":"2026-01-29T19:32:12.190603274Z","closed_at":"2026-01-29T19:32:12.190603274Z"} {"id":"rivets-nwwm","title":"Implement LspProvider trait + RustAnalyzerProvider","description":"Create abstraction for language-specific LSP configuration:\n\n```rust\npub trait LspProvider: Send + Sync {\n fn command(&self) -> &str;\n fn initialize_options(&self) -> Value;\n}\n\npub struct RustAnalyzerProvider;\nimpl LspProvider for RustAnalyzerProvider {\n fn command(&self) -> &str { \"rust-analyzer\" }\n}\n```\n\nThis allows adding csharp-ls later without changing LspClient.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-h1va","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:43.848216481Z","updated_at":"2026-01-29T00:00:15.773628040Z","closed_at":"2026-01-29T00:00:15.773628040Z"} +{"id":"rivets-bi2","title":"Implement RPC system for daemon communication","description":"Implement the RPC server/client system using Unix domain sockets (or named pipes on Windows) for CLI-daemon communication with JSON-RPC protocol.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads internal/rpc/:\n\n**Architecture**:\n- Server runs in daemon process\n- Client in each CLI invocation\n- Transport: Unix sockets (Linux/macOS), Named pipes (Windows)\n- Protocol: JSON-RPC over newline-delimited messages\n\n**Operations Exposed**:\n- Full CRUD (create, read, update, delete)\n- Queries (list, ready, blocked, stats)\n- Import/export triggers\n- Health checks\n\n**Rust Stack**:\n- `tokio` for async I/O\n- `serde_json` for JSON-RPC\n- `interprocess` crate for IPC\n- Custom error types for RPC failures\n\n**Features**:\n- Version compatibility checks\n- Connection timeouts\n- Graceful shutdown\n- Request/response correlation","acceptance_criteria":"- RPC server accepts connections\n- RPC client can call server methods\n- JSON-RPC protocol implemented\n- All CRUD operations exposed\n- Health check endpoint works\n- Connection errors handled gracefully\n- Concurrent requests supported\n- Integration tests for RPC round-trips","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:07.159548647Z","updated_at":"2025-11-17T22:16:07.159548647Z","closed_at":null} {"id":"rivets-xy9","title":"Implement configuration system with YAML and environment variables","description":"Implement the hierarchical configuration system that merges CLI flags, environment variables, YAML config files, and defaults with proper precedence.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads internal/config/:\n\n**Precedence (high → low)**:\n1. CLI flags (--db, --json, etc.)\n2. Environment (BD_ACTOR, BD_JSON, BEADS_DIR, etc.)\n3. Config files (.beads/config.yaml, ~/.config/bd/config.yaml)\n4. Defaults\n\n**Config Schema**:\n```yaml\nissue-prefix: \"rivets\"\njson: false\nno-daemon: false\nrouting:\n mode: auto\n default: \".\"\nrepos:\n primary: \".\"\n additional: []\ndaemon:\n auto-start: true\n flush-debounce: \"30s\"\n```\n\n**Rust Stack**:\n- `figment` or `config` crate for merging\n- `serde` for YAML parsing\n- `dirs` for XDG config paths\n\n**Features**:\n- Walk up directory tree to find .beads/config.yaml\n- Merge multiple sources\n- Type-safe access","acceptance_criteria":"- Config merging from all sources works\n- Precedence order enforced correctly\n- YAML parsing functional\n- Environment variable support\n- CLI flag overrides working\n- Default values applied\n- Config validation with helpful errors\n- Unit tests for merge scenarios","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:06.995329820Z","updated_at":"2025-11-17T22:16:06.995329820Z","closed_at":null} -{"id":"rivets-7qr","title":"Fix limit parameter not working in list and ready tools","description":"The `limit` parameter in `list()` and `ready()` tools is not being applied. Tests create 5 issues, request limit=2, but get 5 results back.\n\nFailing tests:\n- `test_list_with_limit` - expected 2, got 5\n- `test_ready_with_limit` - expected 2, got 5\n\nFound during rivets-vdi implementation.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:40:57.038151966Z","updated_at":"2025-11-29T23:42:49.399935566Z","closed_at":"2025-11-29T23:42:49.399935566Z"} -{"id":"rivets-lyt","title":"Add TOCTOU scenario tests for close command","description":"Add tests that verify behavior when notes are modified between read and write operations in the close command. While the TOCTOU window is documented as acceptable for single-user CLI, tests should verify the append behavior works correctly.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:19.620773682Z","updated_at":"2025-11-30T04:11:19.620773682Z","closed_at":null} -{"id":"rivets-cgl","title":"Integrate storage trait with CLI commands","description":"Update CLI commands to use the storage trait instead of direct storage access. Add storage initialization and lifecycle management.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Update rivets CLI to use async storage trait (following rivets-0gc architecture):\n\n**Async CLI Structure:**\n\n```rust\npub struct App {\n storage: Box,\n config: Config,\n}\n\nimpl App {\n pub async fn new(config: Config) -> Result {\n let storage = Self::create_storage(&config).await?;\n Ok(Self { storage, config })\n }\n \n async fn create_storage(config: &Config) -> Result> {\n match config.storage_backend {\n StorageBackend::InMemory => {\n let storage = if let Some(path) = &config.data_file {\n if path.exists() {\n let (storage, warnings) = InMemoryStorage::load_from_jsonl(path).await?;\n if !warnings.is_empty() {\n eprintln!(\"Warning: Loaded with {} warnings. Check logs.\", warnings.len());\n }\n storage\n } else {\n InMemoryStorage::new()\n }\n } else {\n InMemoryStorage::new()\n };\n Ok(Box::new(storage))\n }\n // Future: PostgreSQL, SQLite\n }\n }\n}\n```\n\n**Update IssueStorage trait to include save():**\n\nAdd to rivets-0gc trait definition:\n```rust\n#[async_trait]\npub trait IssueStorage: Send + Sync {\n // ... existing methods ...\n \n /// Persist storage state (for backends that need explicit persistence)\n async fn save(&self) -> Result<()>;\n}\n```\n\n**Command execution with auto-save:**\n\n```rust\nimpl Commands {\n pub async fn execute(&self, app: &mut App) -> Result<()> {\n match self {\n Commands::Create(args) => {\n let issue = app.storage.create(args.into()).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Created: {}\", issue.id);\n }\n Commands::Update(args) => {\n let issue = app.storage.update(&args.id, args.into()).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Updated: {}\", issue.id);\n }\n Commands::Close(args) => {\n app.storage.delete(&args.id).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Closed: {}\", args.id);\n }\n Commands::Delete(args) => {\n app.storage.delete(&args.id).await?;\n app.storage.save().await?; // Auto-save after mutation\n println!(\"Deleted: {}\", args.id);\n }\n Commands::List(args) => {\n let filter = args.into();\n let issues = app.storage.list(&filter).await?;\n // ... display (no save needed for read-only)\n }\n Commands::Show(args) => {\n let issue = app.storage.get(&args.id).await?;\n // ... display (no save needed for read-only)\n }\n // ... other commands\n }\n Ok(())\n }\n}\n```\n\n**Main.rs with async runtime:**\n\n```rust\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<()> {\n let cli = Cli::parse();\n let config = Config::load().await?;\n let mut app = App::new(config).await?;\n \n cli.command.execute(&mut app).await?;\n \n Ok(())\n}\n```\n\n**Backend-specific save() implementations:**\n\n```rust\n// InMemoryStorage\n#[async_trait]\nimpl IssueStorage for InMemoryStorage {\n async fn save(&self) -> Result<()> {\n if let Some(path) = &self.data_file {\n self.save_to_jsonl(path).await?;\n }\n Ok(())\n }\n}\n\n// PostgreSQL (future)\n#[async_trait]\nimpl IssueStorage for PostgresStorage {\n async fn save(&self) -> Result<()> {\n Ok(()) // No-op: PostgreSQL commits on each operation\n }\n}\n```\n\n**Key Design Points:**\n- All App methods are async to match storage trait\n- save() added to IssueStorage trait (each backend implements appropriately)\n- Auto-save after every mutating command (create, update, delete, close)\n- Read-only commands (list, show) don't trigger save\n- JSONL load warnings displayed to user\n- Uses tokio current-thread runtime as specified in rivets-0gc","acceptance_criteria":"- CLI uses storage trait, not concrete type\n- Storage initialized on CLI startup\n- Data loaded from JSONL at startup (if exists)\n- Data saved to JSONL on exit\n- All commands work via trait methods\n- Error handling for storage failures\n- Integration tests with InMemoryStorage","notes":"## Clarifications\n\n### Session 2025-11-17\n\n- Q: The storage trait from rivets-0gc is async, but the design shows synchronous App methods. Should the CLI App and command execution be async? → A: Make App and command execution async (matches storage trait pattern, use #[tokio::main] as specified in rivets-0gc)\n- Q: With InMemoryStorage as Arc>, the design's downcast pattern won't work. How should the CLI trigger save on exit for in-memory backend? → A: Add save() method to IssueStorage trait (trait-based approach, each backend implements appropriately, works with Box)\n- Q: When should the in-memory backend persist to JSONL - only on exit, after every command, or periodically? → A: Save after every mutating command (resilient to crashes, better durability guarantee)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gc","dep_type":"blocks"},{"depends_on_id":"rivets-bz5","dep_type":"blocks"},{"depends_on_id":"rivets-l66","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:42.102424622Z","updated_at":"2025-11-30T03:09:08.310600427Z","closed_at":"2025-11-30T03:09:08.310600427Z"} -{"id":"rivets-2ub","title":"Implement Cargo workspace structure with directory layout","description":"Create the physical workspace structure including workspace Cargo.toml, crates/ directory, and initial Cargo.toml files for both rivets-jsonl and rivets crates. Set up the directory tree according to the design from rivets-kr3.","status":"closed","priority":1,"issue_type":"task","assignee":"Claude","labels":[],"design":"Follow the design from rivets-kr3:\n- Create workspace root Cargo.toml with resolver=3 and workspace configuration\n- Create crates/ directory\n- Create crates/rivets-jsonl/ with Cargo.toml, src/, tests/, benches/, examples/ directories\n- Create crates/rivets/ with Cargo.toml, src/, tests/ directories\n- Create docs/ directory\n- Set up workspace dependencies in root Cargo.toml\n- Configure individual crate Cargo.toml files with workspace inheritance\n- Add README.md files for each crate","acceptance_criteria":"- Workspace Cargo.toml exists with correct resolver and members\n- crates/rivets-jsonl/Cargo.toml configured with workspace inheritance\n- crates/rivets/Cargo.toml configured with workspace inheritance and binary target\n- All required directories created\n- Project builds successfully with `cargo build`\n- README.md files created for workspace root and both crates","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-kr3","dep_type":"blocks"}],"created_at":"2025-11-17T21:45:32.798758607Z","updated_at":"2025-11-17T21:51:12.910655343Z","closed_at":"2025-11-17T21:51:12.910655343Z"} -{"id":"rivets-1xh","title":"Add Unicode status icons with color support","description":"Enhance status icons with Unicode characters and colors:\n- [ ] Open (blue)\n- [>] In Progress (yellow)\n- [→] Ready (green) - new icon for ready-to-work\n- [X] Blocked (red)\n- [✓] Closed (gray)\n\nSupport fallback to ASCII for terminals without Unicode support.","status":"open","priority":1,"issue_type":"feature","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:46.957517194Z","updated_at":"2025-11-30T18:34:46.957517194Z","closed_at":null} -{"id":"rivets-jwf9","title":"C# namespace resolution for using statements","description":"`resolve_import()` in `csharp.rs:109-111` returns empty vec, meaning C# `using` statements don't resolve to files.\n\nThis was marked \"Task 6\" in the original TODO, suggesting it was planned. Need to implement namespace-to-file resolution for C# projects.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["csharp-support","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:33.059266108Z","updated_at":"2026-01-30T00:18:33.059266108Z","closed_at":null} -{"id":"rivets-exy4","title":"Auto-sync via dirty-marker + deferred flush","description":"Add a low-overhead pattern for keeping the tethys index fresh during active editing: file-save writes a tiny dirty marker (cheap, no parse), and the actual re-sync runs at session boundaries (e.g. agent stop, pre-prompt).\n\n**Inspired by:** KiroGraph's hook system. The pattern avoids per-keystroke overhead while keeping the graph fresh enough that AI assistants always see current code structure.\n\n**Two-phase flow:**\n\nPhase 1 — mark dirty (cheap):\n- A new tethys mark-dirty subcommand writes /.rivets/index/dirty.txt (one path per line, append-only).\n- O(1) — no parsing, no SQL writes, suitable for per-save invocation from an editor or hook.\n\nPhase 2 — sync if dirty (deferred):\n- A new tethys sync-if-dirty subcommand reads dirty.txt, re-indexes only the listed files (incremental), and clears the marker.\n- Suitable for invocation at agent stop, pre-prompt, or other session boundaries.\n\n**Hook integration:**\n- Claude Code: PostToolUse hook on Edit/Write writes via mark-dirty; Stop hook runs sync-if-dirty.\n- Editors: file-save hooks invoke mark-dirty.\n- CI: a pre-test step runs sync-if-dirty to ensure the index reflects HEAD.\n\n**Why this depends on rivets-q8qw and rivets-3l14:**\n- rivets-q8qw (incremental updates): without it, the sync re-indexes the whole workspace, defeating the deferred-flush optimization.\n- rivets-3l14 (content hash): mtime alone is not a reliable change indicator (touch updates mtime without changing content). Content hash makes ''actually changed'' the trigger.\n\n**Public API:**\n- Tethys::mark_dirty(paths: &[Path]) -> Result<()>\n- Tethys::sync_if_dirty() -> Result","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] mark_dirty and sync_if_dirty API methods\n- [ ] CLI subcommands tethys mark-dirty PATH... and tethys sync-if-dirty [--quiet]\n- [ ] Dirty marker format documented (newline-separated relative paths, dedup on read)\n- [ ] sync-if-dirty is a no-op when marker is absent or empty\n- [ ] Race-safe: mark-dirty appends with lock; sync-if-dirty reads-then-truncates atomically\n- [ ] Example Claude Code hook config in tethys README\n- [ ] Tests cover: empty marker, deleted file in marker, concurrent mark + sync","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-3l14","dep_type":"blocks"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"},{"depends_on_id":"rivets-q8qw","dep_type":"blocks"}],"created_at":"2026-05-10T04:32:52.798004500Z","updated_at":"2026-05-10T04:32:52.798004500Z","closed_at":null} -{"id":"rivets-gk6y","title":"Document import collision last-write-wins semantics in resolve.rs","description":"In resolve.rs around line 139-147, when import names collide the last import wins. The trace-level log is good but the overwrite semantics are implicit. Add a short comment explaining the behavior. Flagged in PR #55 review.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:17:55.277462665Z","updated_at":"2026-03-20T22:17:55.277462665Z","closed_at":null} -{"id":"rivets-i8g","title":"Clarify blocked tool semantics for status-blocked vs dependency-blocked","description":"The MCP integration tests have a comment indicating uncertainty about expected behavior:\n\n```rust\n// Lines 2398-2399\n// Note: Issue is blocked by status, not by dependency, so it may or may not appear\n// in blocked_issues depending on implementation\n```\n\nShould clarify and document the intended semantics:\n- Should `blocked` tool return issues with status=blocked?\n- Should it only return issues blocked by dependencies?\n- Should it return both?\n\nLocation: `crates/rivets-mcp/tests/integration.rs:2398-2399`","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["documentation","pr-feedback"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6640","dep_type":"parent-child"}],"created_at":"2025-11-30T18:29:48.790471346Z","updated_at":"2025-11-30T18:29:48.790471346Z","closed_at":null} +{"id":"rivets-q8qw","title":"Implement incremental index updates","description":"Currently `update()` in `lib.rs:2095-2104` re-indexes everything. The schema stores `mtime_ns` and `size_bytes` which could enable proper incremental updates:\n\n1. Query files from DB\n2. Compare mtime/size with filesystem\n3. Only re-parse changed files\n4. Remove deleted files from index\n\nThis would significantly improve performance for large codebases.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-gkt2","dep_type":"related"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:03.674170427Z","updated_at":"2026-01-30T00:18:03.674170427Z","closed_at":null} +{"id":"rivets-xov3","title":"C# parser doesn't extract nested types (classes, records, structs)","description":"The C# parser's `extract_class_members` function only extracts methods and constructors from type declarations. It does NOT recurse into nested type declarations (classes, records, structs, interfaces).\n\n**Impact**: References like `Distribution.Percentage` can't be resolved because `Percentage` (a nested record) is never indexed.\n\n**Root cause**: `extract_class_members()` in `csharp.rs:663-696` only handles `METHOD_DECLARATION` and `CONSTRUCTOR_DECLARATION`. It should also handle `CLASS_DECLARATION`, `RECORD_DECLARATION`, `STRUCT_DECLARATION`, `INTERFACE_DECLARATION`.\n\n**Test added**: `extracts_nested_types` test in `csharp.rs` (currently failing) demonstrates the issue.\n\n**Example**:\n```csharp\npublic abstract record Distribution\n{\n public sealed record Percentage : Distribution { } // NOT extracted\n public sealed record FixedAmount : Distribution { } // NOT extracted\n}\n```\n\nOnly `Distribution` is extracted, not the nested `Percentage` or `FixedAmount` types.","status":"closed","priority":1,"issue_type":"bug","assignee":null,"labels":["csharp-support","parser","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-30T03:40:30.208935600Z","updated_at":"2026-01-30T03:46:50.536075597Z","closed_at":"2026-01-30T03:46:50.536075597Z"} +{"id":"rivets-fn7","title":"Consider case-insensitive assignee matching","description":"The current assignee filtering is case-sensitive (\"alice\" does not match \"Alice\"). Consider whether case-insensitive matching would be more user-friendly.\n\nLocation: Documented in test at `crates/rivets-mcp/tests/integration.rs` line 1863-1869\n\nOptions to consider:\n1. Make assignee matching case-insensitive (more user-friendly)\n2. Keep case-sensitive but normalize on input (e.g., lowercase all assignees)\n3. Keep current behavior (explicit, predictable)","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["pr-feedback","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T18:29:37.222874542Z","updated_at":"2025-11-30T18:29:37.222874542Z","closed_at":null} +{"id":"rivets-5an","title":"Add config file validation on load","description":"When loading config.yaml in commands, validate that manually-edited values (like prefix) are still valid. Currently, if a user edits the config file to have an invalid prefix, it won't be caught until issue creation.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["config","validation"],"design":"Add validation in RivetsConfig::load():\\n\\n1. After deserializing, call validate_prefix() on issue_prefix\\n2. Validate storage.backend is a known value\\n3. Validate storage.data_file path is reasonable\\n\\nReturn helpful error messages indicating which field is invalid and what the requirements are.","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-30T02:48:34.570179736Z","updated_at":"2025-11-30T02:48:34.570179736Z","closed_at":null} +{"id":"rivets-m4wt","title":"Parse Cargo.toml to detect actual crate root","description":"The code currently hardcodes `workspace_root/src/` as the crate root in `lib.rs:1016` and `lib.rs:1443`. This breaks for:\n- Workspaces with custom `[lib].path` in Cargo.toml\n- Binary crates vs library crates\n- Multi-crate workspaces\n\nShould parse Cargo.toml to find the actual crate entry point.","status":"closed","priority":1,"issue_type":"bug","assignee":null,"labels":["correctness","rust-support","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-30T00:17:51.864317941Z","updated_at":"2026-02-01T20:51:40.487368476Z","closed_at":"2026-02-01T20:51:40.487368476Z"} +{"id":"rivets-67uu","title":"Add file-based logging option for debugging","description":"Currently rivets only logs to stderr via tracing. Add an option to write logs to a file for easier debugging in production environments or when troubleshooting issues.\n\nThis would help users capture diagnostic information when reporting bugs or debugging problems without having to manually redirect stderr.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["cli","dx","observability"],"design":"## Proposed Implementation\n\n### CLI Flag\nAdd a `--log-file ` global option that enables file-based logging:\n```bash\nrivets --log-file ./rivets.log list\nrivets --log-file /tmp/debug.log create \"New issue\"\n```\n\n### Environment Variable Alternative\nSupport `RIVETS_LOG_FILE` environment variable as an alternative to the CLI flag.\n\n### Log Format\n- Use the same tracing format as stderr output\n- Include timestamps for file logs (may want to add to stderr too)\n- Consider JSON format option for machine parsing: `--log-format json`\n\n### Log Rotation (future consideration)\nFor long-running use cases (like MCP server), consider:\n- Max file size before rotation\n- Number of rotated files to keep\n\n### Implementation Notes\n- Use `tracing-appender` crate for file writing\n- Layer approach: keep stderr output, add file layer when requested\n- Respect `RUST_LOG` for filtering even when writing to file","acceptance_criteria":"- [ ] `--log-file ` CLI option writes logs to specified file\n- [ ] `RIVETS_LOG_FILE` environment variable works as alternative\n- [ ] File logs include timestamps\n- [ ] `RUST_LOG` filtering still applies to file output\n- [ ] Documentation updated with logging options\n- [ ] Works for both `rivets` CLI and `rivets-mcp` server","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-nz52","dep_type":"parent-child"}],"created_at":"2026-02-05T04:30:32.062115144Z","updated_at":"2026-02-05T04:30:32.062115144Z","closed_at":null} +{"id":"rivets-4dw","title":"rivets-mcp: Rust MCP Server for Rivets","description":"Create a Rust-native MCP server that exposes rivets issue tracking functionality to AI assistants like Claude. This is a port of the beads-mcp Python server to Rust.\n\nKey decisions:\n- Library: rmcp v0.8.1 (official Rust MCP SDK)\n- Structure: New rivets-mcp crate in workspace\n- Storage: Direct IssueStorage trait access\n- Transport: stdio only (MVP)","status":"closed","priority":1,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Core MCP server implementation complete with all 10 tools functional. Remaining work is testing enhancements (rivets-8fe, rivets-2pn) and optional refactoring (rivets-o7o, rivets-1wn).","external_ref":null,"dependencies":[],"created_at":"2025-11-29T01:15:28.493045843Z","updated_at":"2025-12-08T00:02:28.855597106Z","closed_at":"2025-12-08T00:02:28.855597106Z"} {"id":"rivets-z45p","title":"tethys: C# parser enhancements — unsafe modifier and type parameters","description":"The C# function signature extraction is missing: (1) detection of the unsafe modifier (is_unsafe always false), (2) extraction of generic type parameters (generics always None).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:05.770967251Z","updated_at":"2026-03-19T01:41:05.770967251Z","closed_at":null} -{"id":"rivets-nqbg","title":"Warn at parse time when CrateInfo bin_paths have divergent parents","description":"CrateInfo::src_root() falls back to bin_paths.first() for bin-only crates. If a crate has multiple [[bin]] entries with paths in different parent directories (e.g., src/bin/a.rs and tools/b.rs), 'first' is non-deterministic ordering-wise and the chosen src_root won't correctly resolve modules under both bins.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Scenario\n\nA Cargo.toml like:\n\n```toml\n[[bin]]\nname = \"a\"\npath = \"src/bin/a.rs\"\n\n[[bin]]\nname = \"b\"\npath = \"tools/b.rs\"\n```\n\nProduces CrateInfo with bin_paths in declaration order. src_root() picks the first one's parent (`src/bin`). If the user's code uses crate:: paths referencing modules under tools/, those won't resolve.\n\n## Empirical occurrence\n\nDoesn't exist in the rivets workspace (single-bin crates only). May exist in larger Cargo workspaces.\n\n## Proposed mitigation\n\nIn cargo.rs::parse_crate_from_manifest, after populating bin_paths, check whether all entries have the same parent directory. If not, emit a warn! with the crate name and the divergent paths. Doesn't change resolution behavior (still picks first), just surfaces the ambiguity to the operator.\n\nOptionally: store a 'has_divergent_bin_parents' flag on CrateInfo and have src_root() return None or pick by some deterministic rule (lexicographic-first parent?). Out of scope for this issue without empirical evidence of harm.\n\n## Why P4\n\nNo known harm in any workspace tethys currently indexes. Defensive observability addition.","acceptance_criteria":"- [ ] In cargo.rs::parse_crate_from_manifest, after bin_paths population, check for divergent parents\n- [ ] If divergent, log warn! with crate name and the bin paths\n- [ ] Unit test in cargo.rs::tests with a multi-bin CrateInfo where bins have different parents\n- [ ] Decide whether to also change src_root() behavior or leave it picking first","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:40:54.732817Z","updated_at":"2026-05-12T23:40:54.732817Z","closed_at":null} -{"id":"rivets-fwy","title":"Add skim dependency for fuzzy search","description":"Add skim crate to workspace dependencies for fuzzy finding functionality.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:06.747113999Z","updated_at":"2025-11-30T18:36:06.747113999Z","closed_at":null} -{"id":"rivets-dee","title":"Convert in_memory_resilient_loading.rs to rstest","description":"Extract JSON construction into fixtures. Parameterize multi-issue loading tests.\n\nFile: crates/rivets/tests/in_memory_resilient_loading.rs","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- JSON construction fixtures created\n- Multi-issue loading tests parameterized\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:36.382550325Z","updated_at":"2025-11-29T02:10:43.723213826Z","closed_at":"2025-11-29T02:10:43.723213826Z"} -{"id":"rivets-b1n","title":"Implement atomic write functionality","description":"Implement write_jsonl_atomic() convenience function that writes to a temp file then atomically renames it. This provides crash safety for JSONL persistence.\n\nUses the temp file + rename pattern for atomic writes on POSIX systems.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse std::path::Path;\nuse tokio::fs::File;\n\npub async fn write_jsonl_atomic(path: P, values: &[T]) -> Result<()>\nwhere\n T: Serialize,\n P: AsRef,\n{\n let path = path.as_ref();\n let temp_path = path.with_extension(\"tmp\");\n \n // Write to temp file\n {\n let file = File::create(&temp_path).await?;\n let mut writer = JsonlWriter::new(file);\n writer.write_all(values.iter()).await?;\n writer.flush().await?;\n }\n \n // Atomic rename\n tokio::fs::rename(&temp_path, path).await?;\n \n Ok(())\n}\n```","acceptance_criteria":"- write_jsonl_atomic() function implemented\n- Writes to temp file with .tmp extension\n- Atomically renames temp file to target path\n- Temp file cleaned up on success\n- Proper error handling if write fails\n- Integration test verifies atomicity\n- Test verifies crash during write leaves original file intact","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6pi","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:57.333893140Z","updated_at":"2025-11-28T01:04:56.754496296Z","closed_at":"2025-11-28T01:04:56.754496296Z"} -{"id":"rivets-6640","title":"Documentation","description":"API documentation, README improvements, and clarification of tool semantics.","status":"open","priority":3,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:31.659249103Z","updated_at":"2026-02-21T02:28:31.659249103Z","closed_at":null} -{"id":"rivets-yis","title":"Implement storage backend selection via configuration","description":"Add configuration options to select storage backend (in-memory, PostgreSQL, SQLite) and pass connection parameters. Enables switching backends without code changes.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Extend config system:\n\n```yaml\n# .rivets/config.yaml\nstorage:\n backend: \"memory\" # Options: memory, postgres, sqlite\n \n # For memory backend\n data_file: \".rivets/issues.jsonl\"\n \n # For postgres backend (future)\n postgres:\n host: \"localhost\"\n port: 5432\n database: \"rivets\"\n user: \"rivets\"\n password_env: \"RIVETS_DB_PASSWORD\"\n \n # For sqlite backend (future)\n sqlite:\n path: \".rivets/rivets.db\"\n```\n\n**Config struct**:\n```rust\n#[derive(Deserialize)]\npub struct StorageConfig {\n pub backend: StorageBackend,\n pub data_file: Option,\n pub postgres: Option,\n pub sqlite: Option,\n}\n\npub enum StorageBackend {\n Memory,\n Postgres, // Future\n Sqlite, // Future\n}\n```\n\n**Factory pattern**:\n```rust\npub fn create_storage(config: &StorageConfig) -> Result> {\n match config.backend {\n StorageBackend::Memory => {\n let path = config.data_file.as_ref()\n .ok_or(Error::ConfigMissing(\"data_file\"))?;\n \n let storage = if path.exists() {\n InMemoryStorage::load_from_jsonl(path)?\n } else {\n InMemoryStorage::new()\n };\n \n Ok(Box::new(storage))\n }\n StorageBackend::Postgres => {\n unimplemented!(\"PostgreSQL backend coming soon\")\n }\n StorageBackend::Sqlite => {\n unimplemented!(\"SQLite backend coming soon\")\n }\n }\n}\n```","acceptance_criteria":"- Config supports storage backend selection\n- Factory creates correct storage implementation\n- In-memory backend configurable\n- Config validation with helpful errors\n- Environment variable support for secrets\n- Default backend is memory + JSONL\n- Integration test verifies backend switching","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-cgl","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:41:42.380662548Z","updated_at":"2025-11-17T22:41:42.380662548Z","closed_at":null} +{"id":"rivets-zy0","title":"Implement streaming support for JsonlReader","description":"Implement the stream() method that returns an async Stream of deserialized records. This enables efficient processing of large JSONL files with constant memory usage.\n\nUses futures::Stream for the return type.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse futures::stream::{Stream, StreamExt};\nuse std::pin::Pin;\n\nimpl JsonlReader {\n pub fn stream(self) -> impl Stream>\n where\n T: DeserializeOwned + 'static,\n {\n futures::stream::unfold(self, |mut reader| async move {\n match reader.read_line().await {\n Ok(Some(value)) => Some((Ok(value), reader)),\n Ok(None) => None, // EOF\n Err(e) => Some((Err(e), reader)),\n }\n })\n }\n}\n```\n\nAdd futures dependency to Cargo.toml.","acceptance_criteria":"- stream() method implemented returning impl Stream>\n- Uses futures::stream::unfold for lazy evaluation\n- Properly handles EOF (returns None to terminate stream)\n- Errors propagated through stream\n- Memory usage constant regardless of file size\n- Unit tests verify streaming behavior\n- Integration test with large file (1000+ records)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-len","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:45.895377787Z","updated_at":"2025-11-28T00:40:17.811744371Z","closed_at":"2025-11-28T00:40:17.811744371Z"} +{"id":"rivets-l56","title":"Replace #[allow] with #[expect] (M-LINT-OVERRIDE-EXPECT)","description":"storage/mod.rs:233 uses `#[allow(dead_code)]` on the PostgreSQL enum variant, violating the M-LINT-OVERRIDE-EXPECT guideline.\n\nCurrent Code (line 233):\n```rust\n#[allow(dead_code)]\nPostgreSQL(String),\n```\n\nIssues:\n- #[allow] silently suppresses warnings\n- No explanation for why the warning is acceptable\n- Will continue to suppress warning even if PostgreSQL gets implemented\n\nGuideline Requirement:\n- Use #[expect] with reason to document intent\n- Fails loudly if expectation becomes invalid","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["code-quality","lints"],"design":"Replace #[allow] with #[expect] and add reason:\n\n```rust\n#[expect(dead_code, reason = \"PostgreSQL backend not yet implemented (tracked in TODO)\")]\nPostgreSQL(String),\n```\n\nThis:\n- Documents why the variant is currently unused\n- Will cause a warning if PostgreSQL gets implemented but attribute isn't removed\n- Makes the temporary nature explicit","acceptance_criteria":"- [ ] #[allow(dead_code)] replaced with #[expect]\n- [ ] Reason documented explaining temporary nature\n- [ ] Code compiles without warnings\n- [ ] All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T22:49:04.233675714Z","updated_at":"2025-11-27T22:49:04.233675714Z","closed_at":null} +{"id":"rivets-0gom","title":"tethys: resolver creates phantom cross-crate edges on shared filenames","description":"**Refined reproduction (via gilfoyle prove-it-prototype, 2026-05-11):**\n\nBuilt an independent probe (.rivets-0gom/probe.py, stdlib sqlite3) that reads tethys's file_deps table directly. Built an independent oracle (.rivets-0gom/oracle.sh, grep + Cargo.toml). Probe and oracle disagree on 149 of 170 cross-crate edges (88%).\n\nThe phantom edges are not random. Drilling into the 72 tethys -> rivets phantoms:\n- 46 of 72 (64%) target a single file: `crates/rivets/src/error.rs`\n- 9 target `mod.rs`, 6 target `color.rs`, 3 each target `args.rs` and `lib.rs`\n- All other targets are similarly common Rust filenames\n\n**The bug, sharpened:** the resolver's `use crate::::*` path resolution is NOT crate-scoped. It collapses to a workspace-wide filename lookup: any file in any crate named `.rs` is a candidate target. Every crate using common module names (`error`, `mod`, `lib`, `color`, `types`) generates phantom edges into every other crate with a file of the same name.\n\nThis is more specific than the original hypothesis. It locates the bug in `crate::` path resolution, not in cross-crate `use foo_crate::` resolution.\n\n**Artifacts committed:** `.rivets-0gom/probe.py`, `.rivets-0gom/probe2.py`, `.rivets-0gom/oracle.sh`. Re-runnable; the fix's design and tests should reuse them as the persistent oracle.\n\n**Workspace state at reproduction:**\n- PR 60 (tethys-coupling-metrics) branch at 8a3fe8a\n- Probe: 559 file_deps rows total, 170 cross-crate, 149 phantom\n- Oracle: 2 ordered pairs legitimately have cross-crate edges (rivets->rivets-jsonl, rivets-mcp->rivets); 10 ordered pairs should have ZERO edges\n\n**Implications for PR 60:**\nThe coupling metrics are computed correctly from the file_deps table. The file_deps table itself is approximately 88% noise on this workspace. The headline output of PR 60 is therefore noise plus a small amount of signal. This issue must land before any downstream consumer treats coupling output as authoritative.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] `tethys coupling` on a workspace whose crates have files with identical names (e.g. multiple `error.rs`, `mod.rs`, `types.rs`) produces edges only for actual `use foreign_crate::...` statements\n- [ ] Regression test: a 2-crate fixture where both crates have a file named `shared_name.rs` with internally-referenced symbols, and neither crate has a Cargo dep on the other, must produce Ca=Ce=0 for both\n- [ ] `tethys cycles` no longer flags Rust-legitimate sibling-module references (`crate::a` used from `b.rs` and vice versa within the same crate) as cycles\n- [ ] Coupling metrics on the rivets workspace itself match the Cargo dependency graph (tethys.Ce=0, rivets-jsonl.Ce=0)","notes":"Closed: Designed slices 1-3 shipped in PR #61 (merge commit 0d4ecf5): search_symbol_by_name_in_path_prefix (slice 1), same-crate preference in fallback_symbol_search (slice 2), ambiguity rejection via LIMIT 2 + refuse (slice 3). Post-lcb6 verification (.rivets-0gom/after-lcb6-merge.txt + .rivets-0gom/what-i-learned-post-lcb6.md) confirmed residuals are rivets-3d0s class, which was subsequently fixed in PR #67 (merge commit 7def342) — closing the resolver-correctness chain lcb6 -> 0gom -> 3d0s.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-lcb6","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-11T21:46:24.655439300Z","updated_at":"2026-05-17T23:08:54.487319350Z","closed_at":"2026-05-17T23:08:54.487318077Z"} +{"id":"rivets-gkt2","title":"Implement proper staleness check","description":"Currently `needs_update()` in `lib.rs:2108-2110` always returns `true`. Should check if any indexed files have changed by comparing stored `mtime_ns`/`size_bytes` against filesystem.\n\nRelated to incremental updates but useful independently for skip-if-fresh optimization.","status":"in_progress","priority":2,"issue_type":"feature","assignee":null,"labels":["performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:09.590472097Z","updated_at":"2026-04-27T22:35:53.633428039Z","closed_at":null} +{"id":"rivets-aaw","title":"Add compact vs detailed view modes","description":"Add view modes for issue listing:\n\nCompact (default): Single line per issue\n```\n[ ] rivets-abc Fix auth bug P1 @alice\n[>] rivets-def Add OAuth P2\n```\n\nDetailed (--detailed/-d): Multi-line with description preview\n```\n[ ] rivets-abc Fix authentication bug\n P1 | task | @alice | urgent, backend\n Authentication fails when token expires...\n```","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:30.019940230Z","updated_at":"2025-11-30T18:36:30.019940230Z","closed_at":null} +{"id":"rivets-js1","title":"Implement issue templates system","description":"Add YAML-based issue templates:\n\n```yaml\n# .rivets/templates/bug.yaml\ntype: bug\npriority: 1\nlabels: [bug, needs-triage]\ndescription: |\n ## Bug Description\n [Describe the bug]\n \n ## Steps to Reproduce\n 1. \n 2. \n 3. \n \n ## Expected Behavior\n [What should happen]\n \n ## Actual Behavior\n [What actually happens]\n```\n\nUsage:\n```\n$ rivets create --template bug \"Login fails on Safari\"\nCreated: rivets-xyz\n Type: bug\n Priority: P1\n Labels: bug, needs-triage\n \nOpen in editor to fill description? [Y/n]: \n```","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","workflow"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:38:01.993057250Z","updated_at":"2025-11-30T18:38:01.993057250Z","closed_at":null} +{"id":"rivets-276h","title":"tethys LSP: verify or extend path_to_uri handling for UNC paths","description":"rivets-714v fixes path_to_uri's \\?\\ extended-length prefix bug for drive-letter paths (C:\\...). UNC paths (\\\\server\\share\\...) follow a different shape and may need explicit handling: canonicalize() returns them as \\\\?\\UNC\\server\\share\\... on Windows, and the RFC 8089 file URI form for UNC is ambiguous between authority-style (file://server/share/...) and path-style (file:////server/share/...).","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Why deferred from rivets-714v\n\nThe rivets-714v fix is bounded to drive-letter paths (the reported and reproduced bug case). UNC paths:\n\n1. Are rare in production for the rivets workspace and typical Cargo users\n2. Have ambiguous RFC 8089 mapping (authority vs path-style)\n3. Would require a separate decision about which form rust-analyzer accepts\n4. Need their own probe + oracle to verify behavior\n\n## Acceptance criteria\n\n- [ ] Add a probe that constructs a fixture on a UNC mount (or simulates one via Path manipulation)\n- [ ] Verify what URI shape rust-analyzer accepts for UNC paths\n- [ ] Extend path_to_uri to produce that shape correctly\n- [ ] Add a regression test covering the UNC case\n\n## Trigger condition for promoting priority\n\nIf a user reports tethys --lsp failing on a UNC-mounted workspace OR if the test_topology suite ever exercises a UNC fixture and surfaces the gap. Otherwise P4 is appropriate.","acceptance_criteria":"- [ ] UNC path probe + oracle added (mirrors .rivets-714v/probe.py shape)\n- [ ] path_to_uri's behavior on UNC paths verified against rust-analyzer's expectations\n- [ ] Regression test for UNC shape added\n- [ ] Documented in path_to_uri's rustdoc what URI form is produced for UNC inputs","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T00:43:18.379629200Z","updated_at":"2026-05-13T00:43:18.379629200Z","closed_at":null} +{"id":"rivets-1v2b","title":"tethys: plumb CancellationToken through index_with_options","description":"Surfaced by silent-failure-hunter during PR #65 multi-agent review. Pre-existing gap exposed by the rivets-lcb6 fix, not a regression introduced by it.\n\nindex_with_options has NO cancellation plumbing today. Before rivets-lcb6, a user Ctrl-C mid-index left the DB with stale-but-valid file_deps from the prior run. After rivets-lcb6, the same Ctrl-C between line 140 (clear_all_file_deps) and the repopulate phases leaves file_deps EMPTY. From the user's perspective, cancellation is now strictly worse: downstream queries (get_file_dependencies, impact analysis) return [] until the next successful index.\n\nTwo issues are linked but distinct:\n1. (this issue) Plumb a CancellationToken (or std::sync::atomic::AtomicBool / tokio::sync::CancellationToken) through index_with_options. Check it at well-defined yield points: after workspace discovery, after each batch_writer.finish(), before each clear_all_X.\n2. (rivets-ml05) Wrap the clear+repopulate in a transaction so abort restores prior state.\n\nEither approach mitigates the worst-case 'empty DB after Ctrl-C' UX. Doing both gives belt + suspenders: transaction handles panics and SQLite errors; cancellation handles user-initiated abort cleanly.\n\nSuggested API surface:\n- index_with_options(&self, options: IndexOptions) → unchanged caller-facing default; internally checks an Arc\n- index_with_options_cancellable(&self, options: IndexOptions, cancel: &CancellationToken) → new variant for CLI / MCP callers\n\nSource: PR #65 multi-agent review aggregate (silent-failure-hunter Suggestion #4).","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":"* CancellationToken (or equivalent) accepted by indexing entry points\n* Cancellation checks at: post-discovery, between batches, before each clear_all_X\n* On cancel: structured warn! log noting which phase aborted\n* Test: spawn an index, cancel mid-flight, assert clean error return (not panic) and DB state is consistent with rivets-ml05 (either fully prior-state or fully cleared)","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-17T02:44:13.339026957Z","updated_at":"2026-05-17T02:44:13.339026957Z","closed_at":null} +{"id":"rivets-azn","title":"Document rivets architecture and API design","description":"Create comprehensive architecture documentation for rivets including crate structure, module organization, data flow diagrams, and public API documentation.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"Create docs/architecture.md covering:\n\n**1. System Architecture**:\n- Crate dependency graph\n- Component interaction diagram\n- Data flow (CLI → RPC → Storage → JSONL)\n\n**2. Storage Layer**:\n- Database schema diagram\n- Table relationships\n- Index strategy\n- Migration system\n\n**3. ID Generation**:\n- Hash algorithm explanation\n- Collision handling\n- Hierarchical ID format\n\n**4. Dependency System**:\n- 4 dependency types\n- Cycle detection algorithm\n- Ready work calculation\n\n**5. JSONL Sync**:\n- Export flow diagram\n- Import flow diagram\n- Conflict resolution\n\n**6. RPC Protocol**:\n- Message format\n- Operation list\n- Error handling\n\nUse mermaid diagrams for visual clarity","acceptance_criteria":"- architecture.md created in docs/\n- All major components documented\n- Mermaid diagrams included\n- Data flow clearly explained\n- API surfaces documented\n- Design decisions justified\n- Future extensibility noted","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:16:47.848274620Z","updated_at":"2025-11-28T00:37:44.421525809Z","closed_at":"2025-11-28T00:37:44.421525809Z"} +{"id":"rivets-sx8j","title":"Rivets Core: Features & Refactoring","description":"Core rivets functionality: query system, filtering, config, daemon, storage backends, structured errors, and codebase refactoring.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:28.807289106Z","updated_at":"2026-02-21T02:28:28.807289106Z","closed_at":null} +{"id":"rivets-e3j","title":"Implement TUI Kanban board view","description":"Create Kanban board view for TUI with columns:\n- Open\n- In Progress\n- Blocked\n- Closed\n\nFeatures:\n- Issue cards with title, priority, assignee\n- Move issues between columns\n- Color-coded by priority\n- Scrollable columns","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:06.816842488Z","updated_at":"2025-11-30T18:37:06.816842488Z","closed_at":null} +{"id":"rivets-7nq","title":"Create shared fixtures module (optional)","description":"Extract common fixtures used across multiple test files into shared modules.\n\nStructure:\n- crates/rivets-jsonl/tests/common/mod.rs\n- crates/rivets/tests/common/mod.rs\n\nThis task should be done after the conversion tasks to see what fixtures emerge as reusable.","status":"closed","priority":3,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Common fixtures identified and extracted\n- Fixtures reused across test files where applicable\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ans","dep_type":"blocks"},{"depends_on_id":"rivets-c0p","dep_type":"blocks"},{"depends_on_id":"rivets-dee","dep_type":"blocks"},{"depends_on_id":"rivets-dev","dep_type":"blocks"},{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T00:57:53.770663946Z","updated_at":"2025-11-29T02:11:28.565585029Z","closed_at":"2025-11-29T02:11:28.565585029Z"} +{"id":"rivets-0ib","title":"Implement TUI list and detail views","description":"Create list and detail views for TUI:\n\nList view:\n- Sortable/filterable issue list\n- Compact single-line display\n- Keyboard navigation (j/k or arrows)\n\nDetail view:\n- Full issue details in side panel\n- Dependencies and dependents\n- Edit capabilities","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:18.308663319Z","updated_at":"2025-11-30T18:37:18.308663319Z","closed_at":null} +{"id":"rivets-2pn","title":"Add integration tests for invalid filter values and error paths","description":"Add integration tests that exercise error handling when invalid values are passed to MCP tool filters. The validation logic exists but isn't covered by integration tests.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["mcp","testing"],"design":"Add error path tests to `crates/rivets-mcp/tests/integration.rs`:\n\n```rust\n// ============================================================================\n// Error Path Tests - Invalid Filter Values\n// ============================================================================\n\n#[tokio::test]\nasync fn test_list_invalid_status_returns_error() {\n let workspace = create_temp_workspace();\n let tools = create_tools();\n set_context(&tools, workspace.path()).await;\n\n let result = tools\n .list(Some(\"invalid_status\"), None, None, None, None, None, None)\n .await;\n\n assert!(result.is_err());\n let err = result.unwrap_err();\n assert!(matches!(err, Error::InvalidArgument { field: \"status\", .. }));\n assert!(err.to_string().contains(\"invalid_status\"));\n assert!(err.to_string().contains(\"open, in_progress, blocked, closed\"));\n}\n\n#[tokio::test]\nasync fn test_list_invalid_issue_type_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_ready_invalid_issue_type_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_create_invalid_issue_type_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_update_invalid_status_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_dep_invalid_dep_type_returns_error() { ... }\n```\n\nValidation logic locations:\n- `tools.rs:32-38` - validate_status()\n- `tools.rs:40-47` - validate_issue_type()\n- `tools.rs:49-56` - validate_dep_type()","acceptance_criteria":"- [ ] Test invalid status values (e.g., \"invalid\", \"pending\", \"done\")\n- [ ] Test invalid issue_type values (e.g., \"invalid\", \"story\", \"spike\")\n- [ ] Test invalid dep_type values (e.g., \"invalid\", \"requires\", \"depends\")\n- [ ] Test invalid priority values (e.g., 5, 10, 255)\n- [ ] Verify appropriate error types are returned (InvalidArgument)\n- [ ] Verify error messages include the invalid value and valid options","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T01:13:42.028936963Z","updated_at":"2025-11-30T18:09:28.465454953Z","closed_at":"2025-11-30T18:09:28.465454953Z"} +{"id":"rivets-nsli","title":"tethys: test format_uri percent-encodes literal % in paths","description":"PATH_PERCENT_ENCODE_SET includes .add(b'%'), so a literal % in a path encodes as %25. No test exercises this. A regression removing .add(b'%') would produce a URI that parses but resolves to the wrong file (or double-decodes downstream).\n\nRare in Rust source paths but load-bearing for the RFC 3986 round-trip claim.\n\nSurfaced by PR #64 round-3 review (pr-test-analyzer).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test added to crates/tethys/src/lsp/transport.rs#tests\n- [ ] Input path contains a literal % character\n- [ ] Asserts the % is encoded as %25 in the resulting URI","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:23.849650300Z","updated_at":"2026-05-13T02:25:23.849650300Z","closed_at":null} +{"id":"rivets-xi4","title":"Add rstest dependency to both crates","description":"Add rstest = \"0.26\" to dev-dependencies in both workspace crates.\n\nFiles to modify:\n- crates/rivets/Cargo.toml\n- crates/rivets-jsonl/Cargo.toml","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- rstest = \"0.26\" added to dev-dependencies in both Cargo.toml files\n- cargo build succeeds\n- cargo test succeeds (no changes to tests yet)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T00:55:59.605072624Z","updated_at":"2025-11-29T02:07:41.450391325Z","closed_at":"2025-11-29T02:07:41.450391325Z"} +{"id":"rivets-lryg","title":"Warn when closing/reopening already closed/open issues","description":"Running `rivets close` on an already-closed issue or `rivets reopen` on an already-open issue silently succeeds. Should inform the user the issue is already in that state.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-11T22:47:34.156430368Z","updated_at":"2026-01-11T22:58:47.138653682Z","closed_at":"2026-01-11T22:58:47.138653682Z"} +{"id":"rivets-j3a","title":"Evaluate rstest for rivets-jsonl/src/reader.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in reader.rs.\n\nFile: crates/rivets-jsonl/src/reader.rs (lines ~448-568)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:02.499885060Z","updated_at":"2025-11-29T01:25:34.366773480Z","closed_at":"2025-11-29T01:25:34.366773480Z"} +{"id":"rivets-p9oz","title":"Test Coverage & Quality","description":"Expand test coverage: validator edge cases, TOCTOU scenarios, integration tests, ANSI validation, and async safety assertions.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:30.278033816Z","updated_at":"2026-02-21T02:28:30.278033816Z","closed_at":null} +{"id":"rivets-9iwc","title":"tethys: add format_uri_percent_encodes_unicode_unix to complete symmetric test matrix","description":"The format_uri test matrix has a Unix gap: format_uri_percent_encodes_unicode_as_utf8_bytes is #[cfg(windows)]-only. The Windows-side spaces test (format_uri_percent_encodes_spaces) has a Unix counterpart (format_uri_percent_encodes_spaces_unix), but the Unicode variant doesn't.\n\nWithout the Unix counterpart, a future refactor that accidentally moves percent_encode_path or PATH_PERCENT_ENCODE_SET behind #[cfg(windows)] would silently regress Unix non-ASCII path support — CI on Linux/macOS runners wouldn't catch it.\n\nTrivial add: ~5 lines mirroring the existing Windows test, asserting that /home/user/日本/foo.rs encodes to file:///home/user/%E6%97%A5%E6%9C%AC/foo.rs.\n\nSurfaced by PR #64 (closed rivets-714v) — recognized post-merge that the symmetric matrix was incomplete.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test format_uri_percent_encodes_unicode_unix added to crates/tethys/src/lsp/transport.rs tests module\n- [ ] Gated #[cfg(not(windows))]\n- [ ] Asserts UTF-8 byte-level encoding of CJK characters on a Unix-shaped path\n- [ ] cargo nextest run -p tethys passes on Linux and macOS","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:47:05.852969200Z","updated_at":"2026-05-13T02:47:05.852969200Z","closed_at":null} +{"id":"rivets-ck11","title":"tethys: slice 1 tests missed Windows path-separator divergence; harden either contract or callee","description":"Discovered during rivets-0gom fix work (gilfoyle/checkpointed-build, slice 2): a Windows-specific path-normalization bug existed in the slice 2 implementation that was not caught by slice 1's unit tests, despite slice 1 explicitly testing path-prefix behavior.\n\n**The miss:**\n`search_symbol_by_name_in_path_prefix` was tested in slice 1 with synthetic file paths inserted via `Path::new(\"crate_a/src/lib.rs\")`. On Windows, those literals contain forward slashes and are stored that way in the DB. Slice 1's tests all passed.\n\nIn slice 2, the resolver computes the prefix from `CrateInfo::path` (canonical absolute Windows path, e.g. `\\\\?\\C:\\...\\crates\\rivets`) via `relative_path` and `to_string_lossy`. The result on Windows is `crates\\rivets` (native BACKslashes). Without normalization, the LIKE query in slice 1's function returned zero rows for every prefix on every Windows system, silently breaking the entire fix.\n\nThe integration gate (probe vs oracle) caught it. The unit tests did not.\n\n**Why the gap exists:**\nslice 1's stress fixture happened to be path-shape-uniform — both `insert_file` calls used forward-slash literals, so the function's normalization assumption was never challenged. A correctly designed adversarial fixture would have used at least one backslash path (representing what real Windows callers might pass).\n\n**Two follow-up options:**\n\n1. Add a Windows-specific regression test to slice 1's unit tests that calls `search_symbol_by_name_in_path_prefix` with a backslash-containing prefix and asserts None (i.e., document that the function requires forward-slash prefixes). This pins the contract but doesn't prevent caller bugs.\n\n2. Make `search_symbol_by_name_in_path_prefix` defensively normalize its prefix argument. This shifts the contract: \"any path-ish string is acceptable; we'll forward-slash it internally.\" Lower caller burden, slight performance cost (one string allocation).\n\nI lean (2). The single call site in fallback_symbol_search ALSO normalizes; that's duplication. Centralizing in the DB-layer function is the right place.\n\n**Broader lesson for gilfoyle/budgeted-plan:** the \"stress fixture\" rule should specifically require coverage of platform-divergent value shapes when the function deals with paths, strings with multibyte chars, or anything that has OS-level non-determinism. Worth a skill-level update.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Decide: option 1 (test pinning) or option 2 (defensive normalization)\n- [ ] If option 2: `search_symbol_by_name_in_path_prefix` accepts both `crates\\rivets` and `crates/rivets` and returns identical results on Windows\n- [ ] Regression test: same function call with backslash prefix and forward-slash prefix produces identical Symbol output on Windows\n- [ ] gilfoyle/budgeted-plan stress-fixture rule augmented with a \"platform-divergent value shapes\" sub-rule for path/string args","notes":"Closed: Resolved by in-function normalization in search_symbol_by_name_in_path_prefix (PR #61 round 2)","external_ref":null,"dependencies":[],"created_at":"2026-05-12T01:05:06.350162900Z","updated_at":"2026-05-12T02:45:41.003064500Z","closed_at":"2026-05-12T02:45:41.003001700Z"} +{"id":"rivets-xjam","title":"Docs: Add rationale for ID length threshold constants (500, 1500)","description":"In storage/in_memory/inner.rs (lines 74-82), the magic numbers 500 and 1500 lack explanation:\n\n```rust\nlet needs_update = match (old_size, current_size) {\n // Crossing 500 boundary (4 -> 5 chars)\n (0..=500, 501..) => true,\n // Crossing 1500 boundary (5 -> 6 chars)\n (0..=1500, 1501..) => true,\n ...\n};\n```\n\nThe comments say WHAT happens (4->5 chars, 5->6 chars) but not WHY these thresholds were chosen.\n\nPer M-DOCUMENTED-MAGIC guideline, should explain the rationale:\n\n**Suggested documentation:**\n```rust\n/// ID length thresholds for adaptive ID generation.\n///\n/// These values are derived from the ID generator's collision probability calculations:\n/// - At 500 issues with 4-char IDs: collision probability exceeds 1%\n/// - At 1500 issues with 5-char IDs: collision probability exceeds 1%\n/// - 6-char IDs support >50,000 issues before similar collision rates\n///\n/// Thresholds chosen to balance ID brevity with uniqueness guarantees.\nconst ID_LENGTH_THRESHOLD_4_TO_5: usize = 500;\nconst ID_LENGTH_THRESHOLD_5_TO_6: usize = 1500;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","inner.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:51:10.352427140Z","updated_at":"2025-12-28T15:51:10.352427140Z","closed_at":null} +{"id":"rivets-zp3","title":"Implement rivets-jsonl library skeleton with core modules","description":"Create the initial implementation of the rivets-jsonl library including lib.rs and stub modules for reader, writer, query, stream, and error. Set up the public API structure and basic error types.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Based on rivets-kr3 design:\n- Create src/lib.rs with public API exports\n- Create src/error.rs with thiserror-based error types\n- Create src/reader.rs stub for JSONL reading operations\n- Create src/writer.rs stub for JSONL writing operations\n- Create src/query.rs stub for query/filter operations\n- Create src/stream.rs stub for streaming operations\n- Add module-level documentation\n- Create basic integration test in tests/\n- Create simple example in examples/basic.rs","acceptance_criteria":"- All module files created with proper exports from lib.rs\n- Error types defined using thiserror\n- Each module has doc comments explaining its purpose\n- Basic integration test compiles and runs\n- Example code compiles and demonstrates usage\n- `cargo test --package rivets-jsonl` passes\n- `cargo doc --package rivets-jsonl` generates without warnings","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-fk9","dep_type":"blocks"}],"created_at":"2025-11-17T21:45:32.940600766Z","updated_at":"2025-11-27T23:13:36.884206539Z","closed_at":"2025-11-27T23:13:36.884206539Z"} +{"id":"rivets-9e3u","title":"Style: Standardize use statement placement in execute.rs","description":"Some functions have imports at the top of the function body, others inline. This is inconsistent.\n\n**Example of current inconsistency:**\n```rust\npub async fn execute_init(args: &InitArgs) -> Result<()> {\n use crate::commands::init; // ← Inside function\n ...\n}\n```\n\n**Options:**\n1. Move all module-level imports to the top of the file\n2. Keep scoped imports but document the convention\n\n**Recommendation:** Evaluate which approach is preferred for this codebase and apply consistently.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["execute.rs","style"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:55.496327128Z","updated_at":"2025-12-28T15:37:55.496327128Z","closed_at":null} +{"id":"rivets-f07","title":"Add trace logging for workspace cache operations","description":"Add observability to the workspace storage cache to help debug multi-workspace scenarios.\n\nSuggested trace logging points:\n- Cache hits: when a workspace is found in the cache\n- Cache misses: when a workspace needs to be loaded from disk\n- Cache evictions: when a workspace is removed due to capacity limits\n- Cache size: periodic or on-change logging of current cache utilization\n\nThis would help with:\n1. Debugging issues in multi-workspace agent scenarios\n2. Validating cache eviction strategy effectiveness (relates to rivets-zkb)\n3. Understanding real-world usage patterns\n4. Identifying potential performance bottlenecks\n\nImplementation notes:\n- Use tracing crate with appropriate log levels (trace/debug for hits, info for evictions)\n- Consider structured logging with workspace paths and cache stats\n- Ensure logging doesn't impact performance (use trace! level for high-frequency events)","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["observability","rivets-mcp"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"},{"depends_on_id":"rivets-zkb","dep_type":"related"}],"created_at":"2025-11-29T07:30:55.370591744Z","updated_at":"2025-11-29T07:30:55.370591744Z","closed_at":null} +{"id":"rivets-sv1x","title":"Refactor: Extract App initialization pattern in Cli::execute()","description":"In mod.rs, the pattern `App::from_directory(&std::env::current_dir()?)` is repeated 13 times in the execute() method (lines 186-240):\n\n```rust\nSome(Commands::Info(args)) => {\n let app = App::from_directory(&std::env::current_dir()?).await?;\n execute::execute_info(&app, args, output_mode).await\n}\nSome(Commands::Create(args)) => {\n let mut app = App::from_directory(&std::env::current_dir()?).await?;\n execute::execute_create(&mut app, args, output_mode).await\n}\n// ... repeated 11 more times\n```\n\n**Recommendation:** Consider one of these approaches:\n\n1. **Helper method approach:**\n```rust\nasync fn with_app(&self, f: F) -> Result\nwhere\n F: FnOnce(App) -> Fut,\n Fut: Future>,\n{\n let app = App::from_directory(&std::env::current_dir()?).await?;\n f(app).await\n}\n```\n\n2. **Early initialization:** Initialize the app once before the match (for commands that need it)\n\n3. **Macro approach:** Create a macro to reduce boilerplate\n\nThis reduces duplication and makes the execute method more readable.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","cli","mod.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:45:13.765152589Z","updated_at":"2025-12-28T21:15:09.942608111Z","closed_at":"2025-12-28T21:15:09.942608111Z"} +{"id":"rivets-7v2","title":"Add interactive prompts for missing required fields","description":"When creating issues without required fields, prompt interactively:\n\n```\n$ rivets create\nTitle: Fix authentication bug\nPriority [2]: 1\nType [task]: bug\nCreated issue: rivets-abc\n```\n\nSkip prompts when --json flag is set (for agent compatibility).","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:36.843222609Z","updated_at":"2025-11-30T18:35:36.843222609Z","closed_at":null} +{"id":"rivets-byie","title":"Architecture analysis: package detection and coupling metrics (Ca/Ce/instability)","description":"Detect high-level project structure (packages and architectural layers) and compute coupling metrics between them. Useful for refactor planning, codebase health monitoring, and PR review.\n\n**Inspired by:** KiroGraph's architecture/coupling/package commands. The metrics (afferent coupling, efferent coupling, instability) are Robert C. Martin classics — well-established and immediately legible to seasoned engineers.\n\n**What we already have:**\n- Cargo.toml discovery via cargo::discover_crates → CrateInfo with name + path.\n- file_deps table with from_file_id → to_file_id edges.\n- Both feed naturally into per-crate rollups; no new parsing required.\n\n**What this adds:**\n- arch_packages table: id, name, path, source ('manifest'|'directory'), language, version, manifest_path.\n- arch_file_packages: file_id → package_id mapping.\n- arch_package_deps: package_id → package_id with import_count, rolled up from file_deps.\n- arch_coupling: per-package Ca, Ce, instability = Ce / (Ca + Ce).\n\nDefinitions:\n- Ca (afferent coupling): how many other packages depend on this one. Higher = more stable / load-bearing.\n- Ce (efferent coupling): how many packages this one depends on. Higher = more unstable.\n- Instability: 0 = maximally stable (everyone depends on it, it depends on nothing); 1 = maximally unstable.\n\n**Public API:**\n- Tethys::get_packages() -> Vec\n- Tethys::get_coupling_metrics(package_name: Option<&str>) -> Vec\n- Tethys::get_package_dependencies(package_id) -> Vec\n\n**CLI:**\n- tethys architecture [--format table|json]\n- tethys coupling [--package NAME] [--sort instability|ca|ce|name]\n- tethys package [--no-files]\n\n**Layer detection (lower priority, follow-up):**\nKiroGraph also classifies files into layers (api/service/data/ui/shared) by path patterns. Could be a v2; the package-level metrics are the high-value primary feature.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] Schema migration adds arch_packages, arch_file_packages, arch_package_deps, arch_coupling\n- [ ] Architecture analysis runs as a phase during indexing (only when enabled — opt-in flag)\n- [ ] CLI: tethys architecture, tethys coupling, tethys package\n- [ ] Coupling metrics correctly compute Ca, Ce, instability for the rivets workspace itself\n- [ ] MCP tool tethys_coupling and tethys_architecture (sibling rivets-o4re)\n- [ ] Documented in tethys README\n- [ ] Tests verify metrics on a multi-crate fixture workspace","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:31:51.584272600Z","updated_at":"2026-05-10T04:31:51.584272600Z","closed_at":null} +{"id":"rivets-jhv4","title":"tethys: test format_uri returns Ok for nonexistent path (I/O-free seam)","description":"The path_to_uri / format_uri split exposes a contract that format_uri is I/O-free (its rustdoc says 'without performing any filesystem I/O'). The existing test path_to_uri_returns_invalid_path_for_nonexistent only verifies the wrapper still surfaces canonicalize errors — it doesn't pin down the seam itself.\n\nAdd a test asserting format_uri(Path::new(r'C:\\does\\not\\exist\\foo.rs')) returns Ok. If someone later 'fixes' format_uri by re-introducing a metadata() or canonicalize() call, that test catches the regression and forces them to explain.\n\nSurfaced by PR #64 round-3 review (pr-test-analyzer).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test added to crates/tethys/src/lsp/transport.rs#tests\n- [ ] Asserts format_uri returns Ok for a deliberately-nonexistent path\n- [ ] cfg-gated appropriately for Windows/Unix path shapes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:20.304477900Z","updated_at":"2026-05-13T02:25:20.304477900Z","closed_at":null} +{"id":"rivets-gk6y","title":"Document import collision last-write-wins semantics in resolve.rs","description":"In resolve.rs around line 139-147, when import names collide the last import wins. The trace-level log is good but the overwrite semantics are implicit. Add a short comment explaining the behavior. Flagged in PR #55 review.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:17:55.277462665Z","updated_at":"2026-03-20T22:17:55.277462665Z","closed_at":null} {"id":"rivets-fayv","title":"workspace: decide fate of orphan bruno-examples crate (rivets-format)","description":"The repo has `bruno-examples/` at the workspace root containing its own `Cargo.toml` declaring package name `rivets-format` plus 5 source files (lib.rs, parser.rs, serializer.rs, storage.rs, types.rs).\n\nIt is NOT listed in the workspace `[workspace] members` array in the root Cargo.toml. So it's an orphan: `cargo build` doesn't compile it, `cargo test` doesn't test it, but its source files are still on disk and get picked up by anything that walks the workspace tree.\n\nConcretely:\n- `tethys index` walks past it and indexes its 5 .rs files (no Cargo.toml found by `discover_crates` for a non-member, so the files become \"outside any crate\")\n- The files appear in `tethys cycles` output but not in `tethys coupling` metrics\n- Anyone grepping the repo trips over symbols defined there that aren't reachable from any real build target\n\nOptions for resolution:\nA. Add it to `[workspace] members` and treat it as a real crate. If it implements a useful parser, it should be a first-class crate.\nB. Move it under `crates/` if option A is chosen, for layout consistency.\nC. Delete it if it was scaffolding from an earlier design exploration and is no longer needed.\nD. Add an explicit `[workspace] exclude` entry if it's intentionally outside the workspace for some reason (then document why in a README).\n\nNeed to decide which one applies. Looking at git history would help establish intent.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Decision recorded: option A, B, C, or D above\n- [ ] If A/B: bruno-examples is in [workspace] members and builds cleanly with `cargo build`\n- [ ] If C: bruno-examples is removed; nothing in the repo references the `rivets-format` symbols it defined\n- [ ] If D: README at `bruno-examples/README.md` explains why it's outside the workspace","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:46:41.277092600Z","updated_at":"2026-05-11T21:46:41.277092600Z","closed_at":null} -{"id":"rivets-241","title":"Add validator edge case tests","description":"Add comprehensive edge case tests to validators.rs for:\n- Multiple hyphens in issue IDs (proj-abc-123-def)\n- Unicode characters and emojis in titles\n- Max length boundary conditions\n- Empty and whitespace-only inputs","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:13.839324597Z","updated_at":"2025-11-30T04:11:13.839324597Z","closed_at":null} -{"id":"rivets-3q4","title":"Add throughput benchmarks","description":"Create throughput benchmarks to verify \"100MB file in <1s\" read and write targets.\n\nMeasures actual I/O performance with realistic data.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create benches/throughput_benchmarks.rs:\n\n```rust\nuse criterion::{criterion_group, criterion_main, Criterion, Throughput};\n\nfn throughput_benchmarks(c: &mut Criterion) {\n let mut group = c.benchmark_group(\"throughput\");\n \n // Read benchmarks\n group.throughput(Throughput::Bytes(100 * 1024 * 1024)); // 100MB\n group.bench_function(\"read_100mb\", |b| {\n b.iter(|| {\n // Stream read 100MB JSONL file\n });\n });\n \n // Write benchmarks\n group.throughput(Throughput::Bytes(100 * 1024 * 1024));\n group.bench_function(\"write_100mb\", |b| {\n b.iter(|| {\n // Write 100MB JSONL file\n });\n });\n \n // Atomic write benchmarks\n group.throughput(Throughput::Bytes(100 * 1024 * 1024));\n group.bench_function(\"write_100mb_atomic\", |b| {\n b.iter(|| {\n // Atomic write 100MB\n });\n });\n}\n```\n\nTargets from research:\n- Read: 100MB in <1s (>100MB/s)\n- Write: 100MB in <1.5s (>66MB/s)","acceptance_criteria":"- Throughput benchmarks created\n- Read 100MB benchmark passes <1s target\n- Write 100MB benchmark passes <1.5s target\n- Atomic write performance measured\n- Results documented in README\n- cargo bench runs successfully","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-t0k","dep_type":"blocks"}],"created_at":"2025-11-27T23:17:52.519008590Z","updated_at":"2025-11-27T23:17:52.519008590Z","closed_at":null} -{"id":"rivets-dev","title":"Convert in_memory_storage.rs to rstest","description":"Create test_issue fixture to replace create_test_issue() helpers. Parameterize priority/status tests with #[values].\n\nFile: crates/rivets/tests/in_memory_storage.rs","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- test_issue fixture created\n- Priority/status tests use #[values] for matrix testing\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:30.726310933Z","updated_at":"2025-11-29T02:09:45.806681348Z","closed_at":"2025-11-29T02:09:45.806681348Z"} -{"id":"rivets-7nko","title":"Consider StorageResult type alias if helper pattern expands","description":"From PR #34 review: Consider adding a type alias for Result if more helper functions emerge that use this pattern. Currently only save_or_record_failure uses it, so not needed yet.\n\n```rust\ntype StorageResult = Result;\n```\n\nTrigger: Add this when a second helper function needs the same type signature.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T16:53:22.966528697Z","updated_at":"2025-12-28T16:53:22.966528697Z","closed_at":null} -{"id":"rivets-5nz","title":"Add property-based tests with proptest for validators","description":"Use proptest crate to add property-based tests for CLI validators. This would help catch edge cases by generating random inputs:\n- validate_prefix: arbitrary alphanumeric strings of varying lengths\n- validate_issue_id: arbitrary prefix-suffix combinations\n- validate_title: arbitrary strings with control characters\n- validate_description: arbitrary multiline strings","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:31.127884203Z","updated_at":"2025-11-30T04:11:31.127884203Z","closed_at":null} -{"id":"rivets-1u7n","title":"Enforce parent-child dependency type is epic-to-task","description":"Help text says parent-child dependency is \"Hierarchical - epic to task\" but we don't actually enforce this. Either enforce the constraint or update the help text.","status":"open","priority":2,"issue_type":"bug","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-01-11T22:47:15.926596806Z","updated_at":"2026-01-11T22:47:15.926596806Z","closed_at":null} -{"id":"rivets-26b1","title":"Verify symlink resolution stays within workspace_root","description":"canonicalize() resolves symlinks, which could resolve to paths outside workspace_root. Add a check that resolved paths remain within the workspace boundary to prevent directory traversal via symlinks.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:19.994222263Z","updated_at":"2026-02-06T00:35:19.994222263Z","closed_at":null} -{"id":"rivets-e8c","title":"Add confirmation dialogs for destructive operations","description":"Require confirmation for destructive operations:\n\n```\n$ rivets delete rivets-abc\nDelete issue 'rivets-abc' (Fix authentication bug)?\nThis will also remove 3 dependencies.\n[y/N]: \n```\n\nAdd --force/-f flag to skip confirmation for scripting.\nSkip prompts when --json flag is set.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["phase-1b","safety","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:42.629554393Z","updated_at":"2025-11-30T18:35:42.629554393Z","closed_at":null} +{"id":"rivets-uyg","title":"Implement read_jsonl_resilient() convenience function","description":"Implement the read_jsonl_resilient() convenience function that reads an entire JSONL file into a Vec while collecting warnings.\n\nThis provides an easy-to-use API for resilient loading from file paths.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse std::path::Path;\nuse tokio::fs::File;\nuse futures::StreamExt;\n\npub async fn read_jsonl_resilient(\n path: P\n) -> Result<(Vec, Vec)>\nwhere\n T: DeserializeOwned + 'static,\n P: AsRef,\n{\n let file = File::open(path).await?;\n let reader = JsonlReader::new(file);\n let (stream, collector) = reader.stream_resilient();\n \n let values: Vec = stream.collect().await;\n let warnings = collector.into_warnings();\n \n Ok((values, warnings))\n}\n```","acceptance_criteria":"- read_jsonl_resilient() function implemented\n- Returns Result<(Vec, Vec)>\n- Reads entire file into memory\n- Collects all warnings\n- Integration test with corrupted JSONL file\n- Verifies warnings contain correct line numbers","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-8yl","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:01.004393308Z","updated_at":"2025-11-28T03:25:03.360959904Z","closed_at":"2025-11-28T03:25:03.360959904Z"} +{"id":"rivets-2y50","title":"tethys: compute and store content hash for indexed files","description":"Content hash is currently passed as None when indexing files (in both lib.rs and batch_writer.rs). Implement content hash computation to enable cache invalidation based on file content rather than mtime alone.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:02.264719080Z","updated_at":"2026-03-19T01:41:02.264719080Z","closed_at":null} +{"id":"rivets-2ub","title":"Implement Cargo workspace structure with directory layout","description":"Create the physical workspace structure including workspace Cargo.toml, crates/ directory, and initial Cargo.toml files for both rivets-jsonl and rivets crates. Set up the directory tree according to the design from rivets-kr3.","status":"closed","priority":1,"issue_type":"task","assignee":"Claude","labels":[],"design":"Follow the design from rivets-kr3:\n- Create workspace root Cargo.toml with resolver=3 and workspace configuration\n- Create crates/ directory\n- Create crates/rivets-jsonl/ with Cargo.toml, src/, tests/, benches/, examples/ directories\n- Create crates/rivets/ with Cargo.toml, src/, tests/ directories\n- Create docs/ directory\n- Set up workspace dependencies in root Cargo.toml\n- Configure individual crate Cargo.toml files with workspace inheritance\n- Add README.md files for each crate","acceptance_criteria":"- Workspace Cargo.toml exists with correct resolver and members\n- crates/rivets-jsonl/Cargo.toml configured with workspace inheritance\n- crates/rivets/Cargo.toml configured with workspace inheritance and binary target\n- All required directories created\n- Project builds successfully with `cargo build`\n- README.md files created for workspace root and both crates","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-kr3","dep_type":"blocks"}],"created_at":"2025-11-17T21:45:32.798758607Z","updated_at":"2025-11-17T21:51:12.910655343Z","closed_at":"2025-11-17T21:51:12.910655343Z"} +{"id":"rivets-fe4f","title":"Implement parent_symbol_id for nested symbol tracking","description":"Two TODO sites in lib.rs (lines 691, 823) pass None for parent_symbol_id. Implementing this would enable tracking nested symbols (methods within impl blocks, nested functions) for more precise dependency graphs.","status":"closed","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Closed: Duplicate of rivets-aay4 (older, describes the same parent_symbol_id TODOs in lib.rs).","external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:46.456516141Z","updated_at":"2026-05-10T04:30:14.626611600Z","closed_at":"2026-05-10T04:30:14.626609700Z"} +{"id":"rivets-anc","title":"Improve publishing instructions visibility in README","description":"The crates.io publishing instructions mention waiting for indexing after the code block. Consider moving or inlining this note with the commands for better visibility.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["documentation"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6640","dep_type":"parent-child"}],"created_at":"2025-12-18T02:29:24.701631594Z","updated_at":"2025-12-18T02:29:24.701631594Z","closed_at":null} +{"id":"rivets-0srv","title":"Add rayon for parallel file parsing","description":"Use rayon to parallelize file parsing during indexing. Currently tethys processes files sequentially, using only one CPU core.\n\n**Drift's pattern:**\n```rust\n// Parallel parsing with rayon\nlet results: Vec<_> = files\n .par_iter()\n .filter_map(|file| process_file(file).ok())\n .collect();\n```\n\n**Key considerations:**\n- Parse in parallel, write to SQLite sequentially (SQLite doesn't like concurrent writes)\n- Use MPSC channel to send parsed data to a background writer thread\n- Batch SQLite writes (100 files per transaction) to amortize overhead\n- Use atomic counters for thread-safe progress tracking\n\n**Expected improvement:**\n- 1,000 files: ~2-3s → ~0.5-1s\n- 10,000 files: ~20-30s → ~3-5s\n- Full CPU utilization instead of single core\n\n**Implementation steps:**\n1. Add `rayon = \"1.10\"` to Cargo.toml\n2. Create `BatchWriter` struct with MPSC channel\n3. Refactor `index_workspace` to use `par_iter()` for Pass 1\n4. Keep Pass 2 (import resolution) and Pass 3 (LSP) sequential","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"},{"depends_on_id":"rivets-vabg","dep_type":"related"}],"created_at":"2026-01-29T12:48:28.808680006Z","updated_at":"2026-01-29T17:55:56.037736620Z","closed_at":"2026-01-29T17:55:56.037736620Z"} +{"id":"rivets-3yxn","title":"tethys: expose --depth flag on `callers --transitive` for parity with impact","description":"PR #57 wired `--depth` through `tethys impact`, but `tethys callers --transitive` still calls `get_symbol_impact(symbol, None)` unconditionally. Users will reasonably expect the same flag on both commands.\n\nAdd a `--depth` CLI flag on `callers` and pass it through to `get_symbol_impact`.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-04-27T22:15:19.841176315Z","updated_at":"2026-04-27T22:15:19.841176315Z","closed_at":null} +{"id":"rivets-h3mv","title":"Surprising connections: cross-file edges scored by file-path distance","description":"Find non-obvious cross-file connections — direct edges between symbols in structurally distant files. High-score pairs indicate unexpected coupling worth investigating.\n\n**Inspired by:** KiroGraph's kirograph_surprising tool. The clever insight: rank cross-file edges by path_distance(source_file, target_file) * edge_kind_weight to surface coupling that a human reviewer would never spot in casual inspection.\n\n**Scoring:**\n- Path distance: number of directory hops between source and target file paths.\n- Edge kind weights (suggested initial values): calls=1.0, references=0.8, type_of=0.7, instantiates=0.7, imports=0.3 (low — imports are expected).\n- Score = path_distance * weight; return top-N unique source-target pairs.\n\n**Why this depends on rivets-puyl:**\nThe call_edges table currently has no kind column. Surprising-connection scoring needs to weight different edge kinds differently, so we need rivets-puyl (Add reference kind tracking to call_edges table) to land first. Without it, we can only score on path distance, which is much weaker signal.\n\n**Implementation:**\n- Public API: Tethys::find_surprising_connections(limit) -> Vec\n- CLI: tethys surprising [--limit N] [--json]","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] find_surprising_connections() API method\n- [ ] CLI subcommand with --limit and --json\n- [ ] Path-distance helper: count distinct directory components between two relative paths\n- [ ] Edge-kind weights configurable or at least documented as constants\n- [ ] Filters out same-file edges\n- [ ] Unit tests on a multi-directory fixture workspace","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"},{"depends_on_id":"rivets-puyl","dep_type":"blocks"}],"created_at":"2026-05-10T04:31:04.459957300Z","updated_at":"2026-05-10T04:31:04.459957300Z","closed_at":null} +{"id":"rivets-cej","title":"Implement stale command","description":"Add 'stale' command to find issues not updated recently.\\n\\nFlags:\\n- --days N (default: 30)\\n- --status (filter by status)\\n- --limit N\\n- --json\\n\\nExample: rivets stale --days 30 --status in_progress --json","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:56.751583711Z","updated_at":"2025-12-28T00:44:18.799743031Z","closed_at":"2025-12-28T00:44:18.799743031Z"} +{"id":"rivets-6bc","title":"Human-first UX Differentiation","description":"Position Rivets as \"equally powerful for both humans AND agents\" - addressing the gap in Beads' positioning where humans feel like secondary citizens.\n\nTarget: Solo developers and small teams using AI agents","status":"open","priority":1,"issue_type":"epic","assignee":null,"labels":["differentiation","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T18:34:01.044494126Z","updated_at":"2025-11-30T18:34:01.044494126Z","closed_at":null} +{"id":"rivets-9tff","title":"Refactor: Split execute_dep and execute_label into smaller functions","description":"Per the principle of keeping functions focused and manageable:\n\n| Function | Lines | Issue |\n|----------|-------|-------|\n| execute_dep | ~174 | Too long, handles 4 different actions |\n| execute_label | ~234 | Too long, handles 4 different actions |\n\n**Recommendation:**\n- Split execute_dep into: execute_dep_add, execute_dep_remove, execute_dep_list, execute_dep_tree\n- Split execute_label into: execute_label_add, execute_label_remove, execute_label_list, execute_label_list_all\n\nEach action handler should be a separate function called from the main dispatch function.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:46.704194257Z","updated_at":"2025-12-28T19:14:11.042762217Z","closed_at":"2025-12-28T19:14:11.042762217Z"} +{"id":"rivets-tuph","title":"Surface architecture phase stats in tethys index output","description":"The architecture phase populates IndexStats.architecture with packages_recorded/files_assigned/package_deps_recorded counts, but crates/tethys/src/cli/index.rs never displays them. Users running 'tethys index' see 'Indexed 42 files' but no indication that the architecture phase ran or how many packages were detected.\n\n**Fix:** Add a section to the index command output that conditionally prints architecture counts when stats.architecture is Some. Mirror the existing 'Files', 'Symbols', 'References' section style.\n\nFound during final review of rivets-byie.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["tethys","enhancement"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T00:37:37.245100700Z","updated_at":"2026-05-11T00:37:37.245100700Z","closed_at":null} +{"id":"rivets-vdi","title":"Wire StorageBackend::Jsonl to use InMemoryStorage::load_from_jsonl","description":"The `StorageBackend::Jsonl(path)` variant exists but returns an error \"JSONL storage backend not yet implemented\". \n\nPer architecture doc, JSONL is persistence for InMemory, not a separate backend. Wire this variant to use `InMemoryStorage::load_from_jsonl(path)`.\n\nThis unblocks the MCP server which already uses `StorageBackend::Jsonl(db_path)` in `context.rs:109`.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Update `create_storage()` in `crates/rivets/src/storage/mod.rs`:\n\n```rust\nStorageBackend::Jsonl(path) => {\n let (storage, warnings) = in_memory::load_from_jsonl(&path, \"rivets\".to_string()).await?;\n if !warnings.is_empty() {\n // Log warnings - storage still usable\n for w in &warnings {\n tracing::warn!(\"JSONL load warning: {:?}\", w);\n }\n }\n Ok(storage)\n}\n```\n\nIf file doesn't exist, create empty InMemoryStorage (first-run case).","acceptance_criteria":"- [ ] `StorageBackend::Jsonl(path)` returns working storage\n- [ ] Loads existing JSONL file if present\n- [ ] Creates empty storage if file doesn't exist (first run)\n- [ ] Warnings logged but don't fail load\n- [ ] MCP server can successfully use Jsonl backend\n- [ ] Unit test for Jsonl backend creation","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:36:46.342699192Z","updated_at":"2025-11-29T23:40:44.382872584Z","closed_at":"2025-11-29T23:40:44.382872584Z"} {"id":"rivets-5vz","title":"Automerge CRDT Storage Backend","description":"Add Automerge-based storage backend to enable true concurrent access by multiple agents without file locking or merge conflicts. See docs/design/automerge-storage.md for full design.","status":"closed","priority":1,"issue_type":"epic","assignee":null,"labels":[],"design":"Architecture: IssueStorage trait abstraction remains unchanged. AutomergeStorage implements the trait with Automerge document as source of truth. JSONL remains as optional human-readable export.\n\nKey decisions:\n- Dual persistence: Automerge primary, JSONL secondary\n- Text fields use Automerge Text for character-level merging\n- Status/priority use last-writer-wins registers\n- Lists (labels, deps) merge additively\n- petgraph rebuilt from document on load/merge\n- Custom git merge driver for .automerge files","acceptance_criteria":"- [ ] AutomergeStorage implements IssueStorage trait\n- [ ] Multiple agents can modify issues concurrently without corruption (integration tests)\n- [ ] Git merge driver resolves .automerge conflicts automatically\n- [ ] JSONL export regenerated on save\n- [ ] Migration path from existing JSONL works\n- [ ] No performance regression for single-agent use\n- [ ] petgraph is rebuilt correctly after hydration/merge","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T19:10:01.278226020Z","updated_at":"2025-12-23T04:44:47.833139894Z","closed_at":"2025-12-23T04:44:47.833139894Z"} -{"id":"rivets-zp3","title":"Implement rivets-jsonl library skeleton with core modules","description":"Create the initial implementation of the rivets-jsonl library including lib.rs and stub modules for reader, writer, query, stream, and error. Set up the public API structure and basic error types.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Based on rivets-kr3 design:\n- Create src/lib.rs with public API exports\n- Create src/error.rs with thiserror-based error types\n- Create src/reader.rs stub for JSONL reading operations\n- Create src/writer.rs stub for JSONL writing operations\n- Create src/query.rs stub for query/filter operations\n- Create src/stream.rs stub for streaming operations\n- Add module-level documentation\n- Create basic integration test in tests/\n- Create simple example in examples/basic.rs","acceptance_criteria":"- All module files created with proper exports from lib.rs\n- Error types defined using thiserror\n- Each module has doc comments explaining its purpose\n- Basic integration test compiles and runs\n- Example code compiles and demonstrates usage\n- `cargo test --package rivets-jsonl` passes\n- `cargo doc --package rivets-jsonl` generates without warnings","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-fk9","dep_type":"blocks"}],"created_at":"2025-11-17T21:45:32.940600766Z","updated_at":"2025-11-27T23:13:36.884206539Z","closed_at":"2025-11-27T23:13:36.884206539Z"} -{"id":"rivets-h1va","title":"Implement LspClient with JSON-RPC transport","description":"Create `src/lsp/mod.rs` with:\n\n- `LspClient` struct (process handle, stdin/stdout, request_id)\n- `send_request()` - generic JSON-RPC request\n- `read_response()` - parse Content-Length header + JSON body\n- `start()` - spawn process, do initialize handshake\n- `shutdown()` - graceful shutdown\n\nUse lsp-types for all protocol types, just implement the transport (~100 lines).","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-8j9u","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:37.453815533Z","updated_at":"2026-01-29T00:00:09.362735526Z","closed_at":"2026-01-29T00:00:09.362735526Z"} -{"id":"rivets-wzgg","title":"tethys: add exhaustive-byte invariant test for PATH_PERCENT_ENCODE_SET","description":"PATH_PERCENT_ENCODE_SET in crates/tethys/src/lsp/transport.rs is a hand-maintained chain of 27 .add(b'X') calls. A typo (duplicate, missing entry) is silent — the existing format_uri_* tests only spot-check specific inputs (spaces, %20, Unicode).\n\nAdd a test that iterates 0u8..=127 and asserts: for every byte b, set.contains(b) == is_must_encode(b), where is_must_encode is the inverse RFC-3986 definition:\n !b.is_ascii_alphanumeric() && !matches!(b, b'-'|b'.'|b'_'|b'~'|b'/'|b':') || b.is_ascii_control()\n\n~10 lines, runs in microseconds, catches every list-drift bug at CI time.\n\nSurfaced by PR #64 round-3 review (type-design-analyzer).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test added to crates/tethys/src/lsp/transport.rs#tests module\n- [ ] Iterates 0..=127 and asserts set membership matches inverse definition\n- [ ] cargo nextest run -p tethys passes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:11.247389900Z","updated_at":"2026-05-13T02:25:11.247389900Z","closed_at":null} -{"id":"rivets-lmh","title":"Documentation and integration guide","description":"Write documentation:\n- README.md with usage instructions\n- Integration guide for Claude Code\n- API documentation for tools","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:50.949175150Z","updated_at":"2025-12-08T00:02:07.288716558Z","closed_at":"2025-12-08T00:02:07.288716558Z"} -{"id":"rivets-m4wt","title":"Parse Cargo.toml to detect actual crate root","description":"The code currently hardcodes `workspace_root/src/` as the crate root in `lib.rs:1016` and `lib.rs:1443`. This breaks for:\n- Workspaces with custom `[lib].path` in Cargo.toml\n- Binary crates vs library crates\n- Multi-crate workspaces\n\nShould parse Cargo.toml to find the actual crate entry point.","status":"closed","priority":1,"issue_type":"bug","assignee":null,"labels":["correctness","rust-support","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-30T00:17:51.864317941Z","updated_at":"2026-02-01T20:51:40.487368476Z","closed_at":"2026-02-01T20:51:40.487368476Z"} -{"id":"rivets-3d0s","title":"tethys: phantom cross-crate edges from stdlib/external symbol-name collisions with workspace impl items","description":"Discovered as the residual phantom-edge population after rivets-0gom's fix landed (gilfoyle/checkpointed-build slice 3).\n\n**Symptom:** even with crate-scoped fallback (rivets-0gom slice 2) and ambiguity rejection (rivets-0gom slice 3), the tethys resolver still creates ~52 phantom cross-crate file_deps edges on the rivets workspace. These edges have a distinct signature:\n\n - Caller's crate has NO same-name symbol (slice 2 fallback misses)\n - The workspace has EXACTLY ONE candidate symbol with that name (slice 3 doesn't reject)\n - The resolver returns the unique match\n - But the caller's source code never imports the target crate\n\nDrilling into the symbols involved (.rivets-0gom/diagnose_residual.py):\n\n tethys -> rivets-jsonl: 48 refs to `len`\n tethys -> rivets: 44 refs to `children`, 17 to `Tree`\n rivets-mcp -> tethys: 19 refs to `display`\n rivets-jsonl -> tethys: 7 refs to `Serialize`, 1 to `Deserialize`\n\nThese are NOT workspace domain symbols. They're:\n- Stdlib methods: `len`, `write`, `display`, `drop`, `flush`, `read_line`\n- Stdlib types: `Metadata`, `Parser`, `Tree` (or maybe tree-sitter `Tree`)\n- External traits: `Serialize`, `Deserialize` (from serde)\n- External methods: `yellow`, `dimmed`, `reverse` (from colored)\n- Generic field/method names: `children`, `output`, `success`, `stdin`, `stdout`\n\n**Root cause hypothesis:** tethys's parser is recording these as \"symbol definitions\" in workspace files when they appear in contexts like:\n- `impl Serialize for Foo` — recording \"Serialize\" as a symbol in the impl'ing file\n- `fn len(&self) -> usize` — recording \"len\" as a defined symbol\n- Derive macros: `#[derive(Serialize)]` may produce phantom \"Serialize\" entries\n- Doc-comment cross-references\n\nThese accidentally collide with workspace-wide refs to the same names from contexts where they SHOULD be resolved to stdlib/external crates.\n\n**Why rivets-0gom's fix doesn't address this:** rivets-0gom's bug was about resolution scoping (use crate::error matching any error.rs). This new bug is about symbol-table HYGIENE — what counts as a \"symbol definition\" in the first place. Both produce cross-crate phantom edges, but the fix sites are different.\n\n**Likely fix locations:**\n- crates/tethys/src/languages/rust.rs (or wherever Rust symbol extraction lives) — filter out names that come from impl items / trait references rather than actual definitions\n- crates/tethys/src/db/symbols.rs — could add an \"external_reference\" flag and skip such symbols from search_symbol_by_name\n- Or: rivets-0gom's slice 3 could be tightened further to check whether the matched symbol's kind/visibility makes sense as a fallback target\n\n**Scoping note:** this issue is separate from but related to:\n- rivets-0gom (resolver scoping; landed first)\n- rivets-aay4 (parent_symbol_id population; might overlap with how impl items are recorded)\n- rivets-itez (C# parser metadata; analogous shape but different language)\n\nQuantitative impact: in the rivets workspace after rivets-0gom lands, 52 of ~74 remaining cross-crate file_deps are phantoms from this issue. The ALLOWED pairs (rivets->rivets-jsonl, rivets-mcp->rivets) have 22 legitimate edges; everything beyond that is this bug.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Decide where the right filter lives (parser, symbol-insert, search-by-name)\n- [ ] Construct a fixture: two crates, one defines `impl Serialize for Foo`, the other has no Cargo dep on serde. Expect: no symbol of name `Serialize` should be findable as a fallback target from the second crate's refs.\n- [ ] On the rivets workspace post-fix, the residual 52 phantom edges in FORBIDDEN ordered pairs drop to <= 5 (allowing for edge cases).\n- [ ] No regression in ALLOWED pair counts.\n- [ ] No regression in claim 6 (ambiguity = 0).","notes":"## Investigation note from rivets-0gom round 3 (2026-05-12, PR #61)\n\nDuring fixture development for the resolver routing integration test\n(crates/tethys/tests/resolver_routing.rs), discovered that\n`resolve_refs_for_file` (crates/tethys/src/resolve.rs:82-85) short-circuits\nwhen a file has no imports:\n\n let imports = self.db.get_imports_for_file(file_id)?;\n if imports.is_empty() {\n return Ok(0);\n }\n\nThis means references in import-less files NEVER go through Pass-2\nresolution. They bypass BOTH the same-crate scoping AND the ambiguity\nrefusal added by rivets-0gom. They remain unresolved unless Pass 1\n(tree-sitter-direct resolution) handled them.\n\n**Possible interaction with the residual ~52 phantom edges:** if some\nphantom edges are produced by Pass 1 in import-less files (and therefore\nnever go through rivets-0gom's hygiene checks), the stdlib-symbol-hygiene\nfix here will need to think about Pass 1 too — not just symbol-table\nfiltering at Pass 2 time.\n\n**When picking this up:** run a prove-it-prototype that splits the residual\nedges by which pass produced them. Don't assume all 52 are stdlib\npollution — some may be Pass-1 import-less bypass, which would need a\ndifferent fix shape (filter at the Pass-1 extractor or run a Pass-2-style\ncheck on Pass-1 results).\n\n## Related issue (filed 2026-05-12)\n\n- [rivets-714v](rivets-714v): tethys --lsp emits 'url is not a file' on multi-crate workspace indexing. Blocks the LSP angle of the audit-and-demote design — without it, demoted refs stay unresolved instead of getting re-resolved via type info. rivets-3d0s is not hard-blocked (the audit alone eliminates phantoms), but full value of the fix depends on rivets-714v being resolved.\n\n## Checkpointed-build outcome (2026-05-12, PR #61 follow-up branch)\n\nBranch: fix/rivets-3d0s-stdlib-symbol-pollution. Status: slice 1+2 implemented, gates ran, REVERTED on oracle drift. Status set back to open.\n\n**What happened:** the kind-compatibility audit (filter on search_unique_symbol_by_name) landed and the probe ran. Predicted 58.6% phantom reduction; actual 5.7%. Root cause: the audit narrows the candidate set, which converts previously-ambiguous names into unique matches — creating new method-call phantoms via un-ambiguation. The falsifiable-design simulation didn't model this dynamic coupling.\n\n**Probed Option A (extend audit to call->method/function cross-crate):** 99.4% phantom reduction BUT 80% false-positive rate on ALLOWED-pair legitimate refs (95 of 120 in rivets workspace).\n\n**Root cause of false-positive rate** — filed as a separate blocker:\n\n- **[rivets-v465](rivets-v465):** tethys: import resolver leaks legitimate cross-crate refs to unscoped fallback. The unscoped fallback is currently doing BOTH phantom resolution and legitimate cross-crate resolution (because imports leak), making any audit at this layer indiscriminate.\n\n**Blocking issues for rivets-3d0s to ship:**\n\n- [rivets-v465](rivets-v465) (NEW) — import-resolver leak. Until imports are caught, no rule at unscoped-fallback can cleanly distinguish phantom from legitimate.\n- [rivets-714v](rivets-714v) — LSP multi-crate path bug. Even with v465 fixed, the call->method type-info residual needs LSP.\n\n**Diagnostic artifacts retained on the branch:**\n\n- .rivets-3d0s/probe.py, oracle.py, lsp_probe.py — prove-it-prototype outputs\n- .rivets-3d0s/audit_simulation.py — falsifiable-design cheapest falsifier (now with EXTENDED mode that demonstrates the 80% legit_cross false-positive on Option A)\n- .rivets-3d0s/design.md, plan.md — falsifiable-design + budgeted-plan outputs\n\n**When picking this back up:** start by fixing rivets-v465. Then re-run audit_simulation.py with EXTENDED=True against the post-v465 DB. If false-positive rate drops to <= 10 ALLOWED-pair refs, the rivets-3d0s audit-and-demote design becomes viable. Otherwise, escalate to rivets-714v LSP path.\n\nClosed: Implemented as K-hybrid filter in PR #67 (slice 1 commit 58c659c, slice 2 commit dbba5b7). Empirical impact on rivets workspace: total cross-crate file_deps 74->8 (-89%), FORBIDDEN-pair phantoms 14->0 (acceptance target was <=5; exceeded by margin), MISMATCH-pair phantoms 38->0, ambiguity violations preserved at 0 (rivets-0gom claim 6 holds). All 8 surviving cross-crate edges are import-corroborated and verifiable as legitimate. Filter applies at file_deps aggregation time (populate_file_deps_from_call_edges) rather than at resolution time, sidestepping the un-ambiguation dynamic that broke the prior v2 attempt (see .rivets-3d0s/slice2-drift-evidence-2026-05-17.md). Approach inspired by kirograph's coupling-from-imports-only model; adapted to preserve tethys's intra-crate call-derived value.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gom","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-12T01:13:22.383367900Z","updated_at":"2026-05-17T14:41:46.440171932Z","closed_at":"2026-05-17T14:41:46.440170168Z"} +{"id":"rivets-7qb","title":"Memory profiling and optimization","description":"Profile memory usage of rivets-jsonl and optimize to meet the <10MB target regardless of file size.\n\nUses memory profiling tools and techniques to verify streaming guarantees.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Memory profiling approach:\n\n1. **Create test files**:\n - 1MB (1K records)\n - 10MB (10K records)\n - 100MB (100K records)\n - 1GB (1M records)\n\n2. **Profile streaming operations**:\n ```rust\n #[cfg(test)]\n mod memory_tests {\n use std::alloc::{GlobalAlloc, Layout, System};\n use std::sync::atomic::{AtomicUsize, Ordering};\n \n struct TrackingAllocator;\n \n static ALLOCATED: AtomicUsize = AtomicUsize::new(0);\n \n unsafe impl GlobalAlloc for TrackingAllocator {\n unsafe fn alloc(&self, layout: Layout) -> *mut u8 {\n ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);\n System.alloc(layout)\n }\n \n unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {\n ALLOCATED.fetch_sub(layout.size(), Ordering::SeqCst);\n System.dealloc(ptr, layout);\n }\n }\n }\n ```\n\n3. **Optimization targets**:\n - Buffer size tuning\n - Reduce allocations in hot paths\n - Reuse String buffers where possible\n\n4. **Verification**:\n - Peak memory usage <10MB for all file sizes\n - Document peak memory in README","acceptance_criteria":"- Memory profiling tests created\n- Tested with 1MB, 10MB, 100MB, 1GB files\n- Peak memory <10MB for all sizes\n- Buffer sizes optimized\n- Allocations minimized in hot paths\n- Results documented","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-t0k","dep_type":"blocks"}],"created_at":"2025-11-27T23:17:46.767467084Z","updated_at":"2025-11-27T23:17:46.767467084Z","closed_at":null} +{"id":"rivets-ljj","title":"Refactor output.rs into module structure","description":"Refactor output.rs into a module structure:\n- output/mod.rs - Main exports\n- output/color.rs - Color theme and utilities\n- output/tree.rs - ASCII tree rendering\n- output/table.rs - Table formatting (future)","status":"open","priority":1,"issue_type":"task","assignee":null,"labels":["phase-1a","refactor","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:35.368560579Z","updated_at":"2025-11-30T18:34:35.368560579Z","closed_at":null} +{"id":"rivets-ghip","title":"Add error threshold to BatchWriter.write_batch() for systemic failures","description":"Currently `write_batch()` catches ALL database errors as warnings and continues. This hides systemic failures (disk full, database corruption) that will affect every subsequent file.\n\n**Current behavior:**\n```rust\nErr(e) => {\n warn!(file = %data.relative_path.display(), error = %e, \"Failed to write file\");\n}\n```\n\n**Problem:** If disk fills up after 50% of files, user sees 5000 warnings instead of one clear error.\n\n**Suggested improvement:**\n```rust\nconst MAX_CONSECUTIVE_FAILURES: usize = 5;\nlet mut consecutive_failures = 0;\n\nfor data in batch.drain(..) {\n match Self::write_single_file(db, &data) {\n Ok(_) => { consecutive_failures = 0; }\n Err(e) => {\n consecutive_failures += 1;\n if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {\n return Err(e); // Systemic failure, stop\n }\n }\n }\n}\n```\n\n**Trade-off:** Need to distinguish file-specific errors (bad data) from systemic errors (disk full, corruption).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["batch-writer","error-handling","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:06:46.053326512Z","updated_at":"2026-01-29T19:06:46.053326512Z","closed_at":null} +{"id":"rivets-ealr","title":"tethys: detect actual crate root from Cargo.toml instead of assuming src/","description":"Two places in lib.rs assume crate root is workspace_root/src/. This breaks for crates with non-standard main/lib locations. Needs Cargo.toml parsing to detect the actual crate root.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Closed: Duplicate of rivets-34tv (same hardcoded workspace_root/src/ assumption at the same lib.rs sites). rivets-6aoc remains open as it covers a separate code path in resolve.rs.","external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:03.504824959Z","updated_at":"2026-05-10T04:30:16.386071800Z","closed_at":"2026-05-10T04:30:16.386070Z"} +{"id":"rivets-hnf","title":"Refactor: Extract batch processing helper to reduce duplication","description":"The batch processing pattern with save-after-success and reload-on-failure is duplicated ~6 times across:\n- execute_update\n- execute_close \n- execute_reopen\n- execute_label (Add)\n- execute_label (Remove)\n\n**Pattern:**\n```rust\nmatch app.storage_mut().update(&issue_id, update).await {\n Ok(issue) => {\n if let Err(save_err) = app.save().await {\n if let Err(reload_err) = app.storage_mut().reload().await {\n eprintln!(\"Warning: Failed to reload...\");\n }\n result.failed.push(BatchError { ... });\n } else {\n result.succeeded.push(issue);\n }\n }\n Err(e) => { result.failed.push(...); }\n}\n```\n\n**Recommendation:** Extract into a helper function like `save_or_record_failure()`.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["DRY","execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:23.533238227Z","updated_at":"2025-12-28T16:15:44.346630821Z","closed_at":"2025-12-28T16:15:44.346630821Z"} +{"id":"rivets-84i","title":"Add Automerge sync protocol support","description":"Implement generate_sync_message and receive_sync_message for future P2P and server sync capabilities.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:55.978563339Z","updated_at":"2025-12-23T04:45:40.567472509Z","closed_at":"2025-12-23T04:45:40.567472509Z"} +{"id":"rivets-6640","title":"Documentation","description":"API documentation, README improvements, and clarification of tool semantics.","status":"open","priority":3,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:31.659249103Z","updated_at":"2026-02-21T02:28:31.659249103Z","closed_at":null} +{"id":"rivets-wfb","title":"Refactor list filter tests to use rstest with struct-based test cases","description":"Replace individual list filter tests with a single rstest parameterized test using a ListFilterCase struct.\n\nCurrent tests to consolidate:\n- test_list_filter_by_status\n- test_list_filter_by_priority\n- test_list_filter_by_type\n- test_list_filter_by_assignee\n- test_list_with_limit\n\nNew structure:\n```rust\nstruct ListFilterCase {\n name: &'static str,\n status: Option<&'static str>,\n priority: Option,\n issue_type: Option<&'static str>,\n assignee: Option<&'static str>,\n label: Option<&'static str>,\n limit: Option,\n expected_count: usize,\n}\n```\n\nCreate a shared fixture that sets up diverse issues, then parameterize filter assertions.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-jfs","dep_type":"blocks"}],"created_at":"2025-11-29T23:57:39.855684150Z","updated_at":"2025-11-30T00:03:27.331176053Z","closed_at":"2025-11-30T00:03:27.331176053Z"} {"id":"rivets-714v","title":"tethys: --lsp emits 'url is not a file' on multi-crate workspace indexing","description":"Symptom: running 'tethys index --lsp' against a multi-crate Cargo workspace emits\n\n WARN LSP goto_definition failed (further errors logged at trace level) ref_id=1 error=LSP error -32603: url is not a file\n WARN Failed to send exit notification to LSP server during cleanup error=LSP communication error: The pipe is being closed. (os error 232)\n\nPass 3 (LSP) then resolves zero references. Indexing still completes (with degraded results).\n\nReproduction (~5 lines of Python + a 2-file fixture): .rivets-3d0s/lsp_probe.py on branch fix/rivets-3d0s-stdlib-symbol-pollution. The fixture is a virtual Cargo workspace with members crate_caller and crate_target; running '.\\target\\release\\tethys.exe index --rebuild --lsp -w ' produces the warnings above on Windows.\n\nCounter-evidence (rules out 'LSP entirely broken'): crates/tethys/tests/lsp_resolution.rs::lsp_resolves_method_on_inferred_type passes cleanly with the same rust-analyzer install. That test uses a SINGLE-crate Cargo.toml and a fresh tempdir, without --rebuild. So the bug appears specific to multi-crate workspaces and/or the --rebuild path.\n\nDiscovered: 2026-05-12 during rivets-3d0s prove-it-prototype work. Documented in .rivets-3d0s/design.md as a side finding.\n\nImpact: blocks the LSP angle of the rivets-3d0s 'audit-and-demote' fix design. Without LSP working on multi-crate workspaces, the audit-and-demote approach demotes wrongly-resolved refs to NULL but Pass 3 can't re-resolve them via type info — leaving them unresolved instead of correctly resolved. Still strictly better than the current phantom state, so rivets-3d0s is not blocked, but the full value of audit-and-demote is gated on this.\n\nEnvironment: Windows 11, rust-analyzer 1.94.0 (rustup component), tethys release build off branch fix/rivets-3d0s-stdlib-symbol-pollution at commit e57af7e.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":"Two leading hypotheses for the URL malformation:\n\n1. Path serialization uses backslashes. Tethys may construct file URIs like 'file:///C:\\Users\\...' (Windows native separator) instead of 'file:///C:/Users/...' (URI standard). rust-analyzer rejects the former as invalid.\n\n2. Workspace-root mismatch on multi-crate setup. The LSP client's initialize message may pass workspace_root that doesn't match the file URLs sent in subsequent didOpen requests. The single-crate test fixture wouldn't trigger this because workspace_root and the file's containing crate are the same.\n\nStart investigation in crates/tethys/src/lsp/transport.rs and crates/tethys/src/lsp/provider.rs — wherever URI construction lives. Also check resolve.rs:resolve_via_lsp around the goto_definition request site for the ref_id=1 case in the probe.","acceptance_criteria":"- [ ] Reproduce the probe failure: '.rivets-3d0s/lsp_probe.py' on the branch shows the warning and zero LSP resolutions\n- [ ] Identify the URL-construction or workspace-root code path responsible\n- [ ] Fix so that '.rivets-3d0s/lsp_probe.py' completes the --lsp pass with HashMap and String type refs resolved to stdlib (not UNRESOLVED)\n- [ ] No regression in tests/lsp_resolution.rs (lsp_resolves_method_on_inferred_type, lsp_resolves_trait_method_call, etc.)\n- [ ] Add an integration test using a multi-crate fixture so this bug class is gated by CI going forward","notes":"Closed: Fixed in PR #64 (merged). format_uri now strips \\?\\ extended-length prefix and percent-encodes RFC 3986 non-unreserved chars; explicit UNC rejection added in round-3 review response. Pass-3 LSP confirmed unblocked on multi-crate Windows workspaces.","external_ref":null,"dependencies":[],"created_at":"2026-05-12T03:39:42.264365500Z","updated_at":"2026-05-13T02:47:55.711734400Z","closed_at":"2026-05-13T02:47:55.711664Z"} -{"id":"rivets-o3s8","title":"Stream load_indexed_map to reduce needs_update memory footprint","description":"needs_update currently loads all DB rows into a HashMap before walking the disk. For very large workspaces this is the dominant memory cost; deleted-detection requires the full DB enumeration so this cannot be fully eliminated, but a streaming cursor or per-file SELECT approach could reduce peak memory at the cost of either a second pass for deletes or O(N) round-trips. Likely needs a benchmark first to confirm any pain point justifies the complexity.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Benchmark current memory and latency on a 10k-file workspace - [ ] Decide on streaming-cursor vs per-file SELECT based on benchmark - [ ] Update needs_update implementation, retain consistency tests - [ ] Update needs_update docstring to reflect new memory profile","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-04-27T23:13:41.272913367Z","updated_at":"2026-04-27T23:13:41.272913367Z","closed_at":null} -{"id":"rivets-w0qw","title":"tethys: define and document --depth 0 semantics for impact analysis","description":"After PR #57, `tethys impact --depth N` is wired through. `--depth 0` is currently undefined behavior: it would call `get_transitive_dependents(file_id, Some(0))` which depends on whatever the SQLite recursive CTE does at depth 0 (likely returns nothing).\n\nDecide and document the contract: either treat 0 as 'direct dependents only and no transitive search', or reject it at the CLI layer with a friendly error. Add a test pinning the behavior either way.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-04-27T21:56:00.600464844Z","updated_at":"2026-04-27T21:56:00.600464844Z","closed_at":null} -{"id":"rivets-6tl","title":"Implement issue filtering and query system","description":"Implement the comprehensive filtering system for querying issues by status, priority, type, assignee, labels, dates, and text search.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads IssueFilter struct and query builder:\n\n**Filter Dimensions**:\n- **Status**: open, in_progress, blocked, closed\n- **Priority**: 0-4, ranges (P0-P2), min/max\n- **Type**: bug, feature, task, epic, chore\n- **Assignee**: specific user, empty\n- **Labels**: AND (all required), OR (any match)\n- **Dates**: created_after/before, updated_after/before, closed_after/before\n- **Text**: title_contains, description_contains, notes_contains\n- **Flags**: empty_description, no_assignee, no_labels\n- **IDs**: specific list\n\n**Query Builder Pattern**:\n```rust\nIssueFilter::builder()\n .status(vec![Status::Open, Status::InProgress])\n .priority_range(0..=2)\n .labels_all(vec![\"bug\", \"urgent\"])\n .created_after(date)\n .limit(100)\n .build()\n```\n\n**SQL Generation**:\n- Dynamic WHERE clause construction\n- Parameterized queries (SQL injection safe)\n- Index-friendly query plans","acceptance_criteria":"- All filter dimensions functional\n- Complex multi-filter queries work\n- Query builder API type-safe\n- SQL generation correct and efficient\n- Performance acceptable (1000 issues in <50ms)\n- Unit tests for edge cases\n- Integration tests for complex filters","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:47.522142136Z","updated_at":"2025-11-17T22:16:47.522142136Z","closed_at":null} -{"id":"rivets-7p54","title":"Hotspots: rank symbols by connectivity (in+out degree)","description":"Add a hotspots query that ranks symbols by total edge degree (incoming + outgoing) excluding structural edges, surfacing the most-connected code in the codebase. Useful for identifying core abstractions and high-blast-radius change points before refactoring.\n\n**Inspired by:** KiroGraph's kirograph_hotspots tool and CLI command. See crates/tethys/KIROGRAPH-COMPARISON.md.\n\n**Implementation:**\n- Pure SQL on the existing schema:\n SELECT s.id, COUNT(*) AS degree FROM symbols s\n JOIN (SELECT caller_symbol_id AS sid FROM call_edges\n UNION ALL\n SELECT callee_symbol_id AS sid FROM call_edges\n UNION ALL\n SELECT symbol_id AS sid FROM refs WHERE symbol_id IS NOT NULL\n UNION ALL\n SELECT in_symbol_id AS sid FROM refs WHERE in_symbol_id IS NOT NULL) e\n ON e.sid = s.id\n GROUP BY s.id ORDER BY degree DESC LIMIT ?\n- Public API: Tethys::get_hotspots(limit) -> Vec\n- CLI: tethys hotspots [--limit N] [--json]","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] get_hotspots() public API method on Tethys\n- [ ] CLI subcommand 'tethys hotspots' with --limit and --json flags\n- [ ] Returns each entry with in_degree, out_degree, total_degree\n- [ ] Excludes structural/contains edges (none currently exist, so noop, but documented)\n- [ ] Unit tests on a fixture workspace\n- [ ] Integration tested on the rivets workspace itself","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:30:50.185175300Z","updated_at":"2026-05-10T04:30:50.185175300Z","closed_at":null} -{"id":"rivets-9s4","title":"Make blocked issues assertion more precise in tests","description":"In the MCP integration tests, the dependency chain test uses a loose assertion:\n\n```rust\n// Line 2716\nassert!(blocked.len() >= 2, \"At least 2 issues should be blocked\");\n```\n\nIf the behavior is deterministic, consider making this assertion more precise (e.g., `assert_eq!(blocked.len(), 2)`) to catch regressions more reliably.\n\nLocation: `crates/rivets-mcp/tests/integration.rs:2716`","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T18:29:43.002652297Z","updated_at":"2025-11-30T18:29:43.002652297Z","closed_at":null} -{"id":"rivets-1yi","title":"Refactor ready filter tests to use rstest with struct-based test cases","description":"Replace individual ready filter tests with a single rstest parameterized test using a ReadyFilterCase struct.\n\nCurrent tests to consolidate:\n- test_ready_with_priority_filter\n- test_ready_with_assignee_filter\n- test_ready_with_limit\n- test_ready_excludes_blocked\n\nNew structure:\n```rust\nstruct ReadyFilterCase {\n name: &'static str,\n limit: Option,\n priority: Option,\n issue_type: Option<&'static str>,\n assignee: Option<&'static str>,\n label: Option<&'static str>,\n expected_count: usize,\n}\n```","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-jfs","dep_type":"blocks"}],"created_at":"2025-11-29T23:57:45.611773476Z","updated_at":"2025-11-30T00:03:33.242895114Z","closed_at":"2025-11-30T00:03:33.242895114Z"} -{"id":"rivets-n08s","title":"Prompt for prefix in `rivets init` when not provided","description":"When running `rivets init` without a prefix argument, prompt users to enter one interactively (similar to how we prompt when creating an issue).","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-11T22:47:22.022380598Z","updated_at":"2026-01-11T22:54:29.052167180Z","closed_at":"2026-01-11T22:54:29.052167180Z"} -{"id":"rivets-1os9","title":"tethys: implement depth-limited transitive impact analysis","description":"The --depth flag in impact analysis is accepted but not implemented. Currently, transitive analysis always explores the full dependency graph. Implement depth limiting to control how many levels of indirection to follow.","status":"in_progress","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:06.640405407Z","updated_at":"2026-04-27T21:37:12.185509775Z","closed_at":null} -{"id":"rivets-bjdn","title":"Pre-compute crate-path lookup map for large workspaces","description":"The per-file crate_root lookup added by rivets-6aoc (cargo::get_crate_for_file, called in resolve.rs, indexing.rs x2, and resolver.rs) does an O(crates) linear scan with Path::starts_with per file. At rivets-workspace scale (4 crates) and typical Cargo workspaces (<50 crates) this is comfortably within the planned budget. At >50 crates AND if Pass-2 wall-time becomes non-trivial in profiling, swap to a pre-computed lookup map for O(log crates) or O(1) per-file dispatch.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Current implementation\n\n`crates/tethys/src/cargo.rs`:\n```rust\npub fn get_crate_for_file<'a>(file_path: &Path, crates: &'a [CrateInfo]) -> Option<&'a CrateInfo> {\n crates.iter()\n .filter(|c| file_path.starts_with(&c.path))\n .max_by_key(|c| c.path.components().count())\n}\n```\n\nCalled per file from:\n- `resolve.rs::resolve_refs_for_file` (Pass-2-imports cross-file ref resolution)\n- `indexing.rs::compute_dependencies` (dep-graph from extracted refs)\n- `indexing.rs::compute_dependencies_from_stored` (dep-graph from stored refs)\n- `resolver.rs::resolve_module_path` workspace-crate arm (target crate's src_root lookup)\n\nCost: O(files x crates) per indexing pass, dominated by Path::starts_with comparisons. For rivets (118 files x 4 crates = ~470 ops per pass) negligible.\n\n## Trigger condition\n\nTwo conditions must both hold before this optimization is worth the complexity:\n1. A real workspace appears with >50 crates being indexed by tethys\n2. Profiling shows the linear scan is non-trivial wall-time relative to DB I/O and parsing\n\nUntil then, the simple linear scan reads more clearly and avoids premature optimization.\n\n## Proposed mitigation when triggered\n\nPre-compute a `BTreeMap` once in `Tethys::new` (after `discover_crates` returns) and store it as a field on `Tethys`. Per-file lookups become O(log crates) via `range(..file_path).next_back()` (longest-prefix match via reverse-iteration on the sorted map).\n\nAlternative: `HashMap` with each crate's *exact* path as the key. Per-file dispatch then iterates parent directories of the file (O(path depth) which is ~10-20 for typical Rust files) checking membership. Avoids the longest-prefix complication.\n\n## Discovered during\n\nrivets-6aoc / rivets-34tv PR. Plan-phase budget analysis (`.rivets-6aoc/plan.md` slice 2 \"Justification\") admitted the linear scan exceeds the 10^6 ops budget at extreme scale (50k files x 100 crates = 5x10^6), justified inline by:\n1. Inner op is microsecond-scale (Path::starts_with on canonicalized paths)\n2. Dominated by SQL queries on `imports` and `symbols` tables that fire per-file regardless\n3. Acceptable trade-off until profiling demonstrates the cost matters\n\n## Out-of-scope justification for the rivets-6aoc PR\n\nPremature optimization. The simple linear scan is correct, readable, and tests pass. The mitigation path is documented and reusable when needed.","acceptance_criteria":"- [ ] Decision point: a workspace with >50 crates AND profiling shows the get_crate_for_file linear scan is >5% of indexing wall-time\n- [ ] Pre-computed BTreeMap or HashMap of (path -> CrateInfo) stored on Tethys\n- [ ] All 4 call sites in resolve.rs / indexing.rs / resolver.rs use the pre-computed map\n- [ ] Existing get_crate_for_file behavior tests still pass (longest-prefix match, prefix-of-name disambiguation)\n- [ ] Hyperfine measurement on the triggering workspace shows the optimization actually helps","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:18:23.735449800Z","updated_at":"2026-05-12T23:18:23.735449800Z","closed_at":null} -{"id":"rivets-4tev","title":"Implement Pass 2: resolve references against symbol DB","description":"After all files are indexed, run a second pass to resolve unresolved references:\n\n1. Query all references with symbol_id = NULL\n2. For each, look up import context from imports table\n3. Match reference name against symbols from imported modules\n4. Update reference with resolved symbol_id\n\nResolution priority:\n1. Same-file lookup\n2. Explicit import lookup\n3. Qualified path resolution\n4. Glob import search\n5. Leave unresolved (LSP candidate)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1p0f","dep_type":"blocks"},{"depends_on_id":"rivets-itz7","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:39.509250642Z","updated_at":"2026-01-28T18:09:44.894299963Z","closed_at":"2026-01-28T18:09:44.894299963Z"} +{"id":"rivets-067","title":"Create rivets-automerge crate scaffold","description":"Set up the new crate with Cargo.toml, basic module structure, and dependencies.\n\nDependencies to add:\n- autosurgeon = \"0.9\"\n- automerge = \"0.7\"","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:22.019758791Z","updated_at":"2025-12-23T04:44:53.700012982Z","closed_at":"2025-12-23T04:44:53.700012982Z"} {"id":"rivets-1i67","title":"tethys: snapshot tests for tethys coupling CLI output","description":"PR 60 (per-crate coupling metrics) implements the runtime, but the design spec in docs/design/tethys-architecture-analysis.md calls for CLI-level snapshot tests against a fixture workspace covering: default output, --sort name/ca/ce/instability, --package with hit and miss, and --json shape. The runtime is covered by 566 unit/integration/property tests; what is missing is end-to-end output stability.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"Add insta or similar snapshot crate as dev-dep, build a small two-or-three-crate fixture workspace under crates/tethys/tests/fixtures/, and capture stdout for each invocation. Stripping ANSI is unnecessary because writing to a Vec isn't a TTY and colored elides codes automatically.","acceptance_criteria":"- [ ] Snapshot of default 'tethys coupling' output\n- [ ] Snapshot per --sort variant (instability/ca/ce/name)\n- [ ] Snapshot of --package detail (text + json)\n- [ ] Snapshot of --package showing suggestions on stderr\n- [ ] CI fails on unintentional output drift","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T20:14:33.124143700Z","updated_at":"2026-05-11T20:14:57.472689200Z","closed_at":null} -{"id":"rivets-4l2","title":"Implement init command and workspace setup","description":"Implement the init command that creates the .rivets directory structure, initializes the SQLite database, and sets up configuration.","status":"closed","priority":1,"issue_type":"task","assignee":"rust-developer","labels":[],"design":"Based on MVP architecture (in-memory + JSONL):\n\n**Directory Structure**:\n```\n.rivets/\n├── issues.jsonl # JSONL data file\n├── config.yaml # Project config\n└── .gitignore # Ignore metadata files\n```\n\n**Init Process**:\n1. Check if already initialized (error if exists)\n2. Create .rivets/ directory\n3. Create default config.yaml with storage backend settings\n4. Set issue prefix (from flag or prompt)\n5. Create .gitignore\n6. Create empty issues.jsonl file\n\n**Git Integration**:\n- Add .rivets to .gitignore root if not present\n- Suggest git commit for initial setup\n\n**CLI**:\n```bash\nrivets init [--prefix ] [--quiet]\n```\n\n**Config Template**:\n```yaml\nissue-prefix: \"proj\"\nstorage:\n backend: \"memory\"\n data_file: \".rivets/issues.jsonl\"\n```","acceptance_criteria":"- Init creates .rivets directory\n- config.yaml created with storage backend = memory\n- issues.jsonl created (empty initially)\n- .gitignore created\n- Prefix validation (alphanumeric, 2-20 chars)\n- Error if already initialized\n- Integration test verifies complete setup\n- No database files created","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:16:47.684395549Z","updated_at":"2025-11-30T01:41:23.084110706Z","closed_at":"2025-11-30T01:41:23.084110706Z"} -{"id":"rivets-6u6","title":"Implement set_context and where_am_i tools","description":"Implement context management tools:\n- set_context(workspace_root) - Set workspace and discover .rivets/\n- where_am_i() - Debug: show current workspace context\n\nThese are foundational tools that all other tools depend on.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] set_context tool registers and responds correctly\n- [ ] where_am_i returns current context state\n- [ ] Error handling for missing/invalid workspace\n- [ ] Unit tests: both tools with MockStorage","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-s3o","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:16.966340774Z","updated_at":"2025-11-30T01:19:11.657351433Z","closed_at":"2025-11-30T01:19:11.657351433Z"} -{"id":"rivets-hzul","title":"`rivets update` with no field arguments should error","description":"Currently `rivets update ISSUE-1` succeeds silently even when no fields are updated. Should fail with a message explaining what arguments can be used.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-11T22:47:28.086230193Z","updated_at":"2026-01-11T22:55:43.729972424Z","closed_at":"2026-01-11T22:55:43.729972424Z"} -{"id":"rivets-06w","title":"Implement core domain types (Issue, Dependency, Filter)","description":"Create the core domain types that represent issues, dependencies, filters, and related structures. These are used across all storage implementations and the CLI.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Create domain module in rivets crate (src/domain/mod.rs):\n\n**Module Structure**:\n```\nsrc/domain/\n├── mod.rs # Public exports\n├── issue.rs # Issue, NewIssue, IssueUpdate\n├── dependency.rs # Dependency, DependencyType\n├── filter.rs # IssueFilter, builder\n└── types.rs # IssueId, Priority, Status, IssueType\n```\n\n**Core Types**:\n```rust\n// Issue representation\npub struct Issue {\n pub id: IssueId,\n pub title: String,\n pub description: String,\n pub design: Option,\n pub acceptance_criteria: Option,\n pub notes: Option,\n pub status: Status,\n pub priority: Priority,\n pub issue_type: IssueType,\n pub assignee: Option,\n pub created_at: DateTime,\n pub updated_at: DateTime,\n pub closed_at: Option>,\n pub labels: Vec,\n pub dependencies: Vec,\n}\n\npub struct NewIssue { /* builder fields */ }\npub struct IssueUpdate { /* optional fields */ }\n\n// Enums\npub enum Status { Open, InProgress, Blocked, Closed }\npub enum IssueType { Bug, Feature, Task, Epic, Chore }\npub enum DependencyType { Blocks, Related, ParentChild, DiscoveredFrom }\n\n// Filters\npub struct IssueFilter {\n pub status: Option>,\n pub priority: Option>,\n pub assignee: Option,\n pub labels: Option>,\n // ... all filter dimensions\n}\n\n// Newtypes for type safety\npub struct IssueId(String);\npub struct Priority(u8); // 0-4\n```\n\n**Serde support**: All types derive Serialize/Deserialize for JSONL","acceptance_criteria":"- All core types defined\n- Serde derives for serialization\n- Builder patterns for NewIssue, IssueFilter\n- Validation logic (priority 0-4, ID format)\n- Type safety with newtypes\n- Unit tests for validation\n- Documentation with examples","notes":"Implement as src/domain/ module in rivets crate, NOT a separate rivets-core crate. This keeps the domain types co-located with the CLI application for now. Can be extracted to separate crate later if needed for library reuse.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gc","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:41.346359643Z","updated_at":"2025-11-18T01:25:08.707227678Z","closed_at":"2025-11-18T01:25:08.707227678Z"} +{"id":"rivets-8mze","title":"Expand language support: Python, TypeScript, Go","description":"Add language extractors for the three highest-leverage languages beyond Rust and C#. KiroGraph supports 17 languages; tethys can borrow the same tree-sitter grammars.\n\n**Why these three first:**\n- Python: huge ecosystem; tree-sitter-python is mature; many Rust crates wrap Python.\n- TypeScript: covers frontend; necessary for full-stack repos and any project using Node tooling. Grammar is tree-sitter-typescript (also covers .tsx via tree-sitter-tsx).\n- Go: well-defined module system; grammar is small and stable; popular in infrastructure code.\n\n**Approach:**\nTethys's languages/ module is already structured around per-language extractors (common.rs, csharp.rs, rust.rs, tree_sitter_utils.rs). Each new language gets its own file plus grammar dependency in Cargo.toml.\n\nEach extractor needs to handle:\n- Symbol extraction (functions, classes/structs, methods, types)\n- Visibility (Python lacks formal modifiers; convention is leading underscore)\n- Imports / module resolution (Python: import / from; TS: import { } from; Go: import \"path\")\n- Test detection (Python: pytest test_*, unittest.TestCase; TS: describe/it; Go: func TestXxx)\n- Reference extraction (call expressions, type annotations)\n\n**Dependencies to add:**\n- tree-sitter-python = ''0.23''\n- tree-sitter-typescript = ''0.23''\n- tree-sitter-go = ''0.23''\n\n**Suggested split:**\nThis issue is an epic — break into one child issue per language so they can ship incrementally:\n- Python language support\n- TypeScript language support\n- Go language support\n\nEach child should add: grammar dep, extractor module, common test fixture under crates/tethys/tests/fixtures//, and integration tests. The tree-sitter parsing infrastructure is already done — adding a language is mostly a query-pattern translation exercise.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":["tethys","kirograph-inspired","languages"],"design":null,"acceptance_criteria":"- [ ] Three child issues created (Python, TypeScript, Go), each independently shippable\n- [ ] Each language reaches feature parity with the Rust extractor: symbols, refs, imports, test detection\n- [ ] Integration test on a representative open-source repo per language\n- [ ] tethys index detects language correctly by file extension\n- [ ] README updated with new language list","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:32:34.601715100Z","updated_at":"2026-05-10T04:32:34.601715100Z","closed_at":null} +{"id":"rivets-lxbg","title":"Add imports table to schema","description":"Add `imports` table to track what each file imports:\n\n```sql\nCREATE TABLE imports (\n file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n symbol_name TEXT NOT NULL,\n source_module TEXT NOT NULL,\n alias TEXT,\n PRIMARY KEY (file_id, symbol_name, source_module)\n);\nCREATE INDEX idx_imports_file ON imports(file_id);\nCREATE INDEX idx_imports_symbol ON imports(symbol_name);\n```\n\nUpdate db.rs with methods to insert/query imports.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:20.547791398Z","updated_at":"2026-01-28T17:38:55.129203575Z","closed_at":"2026-01-28T17:38:55.129203575Z"} +{"id":"rivets-rg8","title":"Implement AutomergeStorage: IssueStorage trait","description":"Implement the full IssueStorage trait for AutomergeStorage, including all CRUD, dependency, and query methods.\n\nIncludes:\n- AutomergeStorage struct with in-memory cache (HashMap + petgraph)\n- Implement rebuild_dependency_graph() for post-hydration graph construction\n- All IssueStorage trait methods\n- Wire into storage factory and config (StorageBackend::Automerge variant)","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:33.301178192Z","updated_at":"2025-12-23T04:45:05.425292273Z","closed_at":"2025-12-23T04:45:05.425292273Z"} {"id":"rivets-d8s3","title":"Consider shared test utilities module","description":"From PR #34 review: The create_test_issue helper in execute.rs tests could be moved to a shared test utilities module if other test modules need similar issue creation.\n\nCurrent location: crates/rivets/src/cli/execute.rs (in #[cfg(test)] mod tests)\n\nTrigger: Move to a shared module when a second test file needs to create test Issue instances.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T16:53:28.930793949Z","updated_at":"2025-12-28T16:53:28.930793949Z","closed_at":null} -{"id":"rivets-jhv4","title":"tethys: test format_uri returns Ok for nonexistent path (I/O-free seam)","description":"The path_to_uri / format_uri split exposes a contract that format_uri is I/O-free (its rustdoc says 'without performing any filesystem I/O'). The existing test path_to_uri_returns_invalid_path_for_nonexistent only verifies the wrapper still surfaces canonicalize errors — it doesn't pin down the seam itself.\n\nAdd a test asserting format_uri(Path::new(r'C:\\does\\not\\exist\\foo.rs')) returns Ok. If someone later 'fixes' format_uri by re-introducing a metadata() or canonicalize() call, that test catches the regression and forces them to explain.\n\nSurfaced by PR #64 round-3 review (pr-test-analyzer).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test added to crates/tethys/src/lsp/transport.rs#tests\n- [ ] Asserts format_uri returns Ok for a deliberately-nonexistent path\n- [ ] cfg-gated appropriately for Windows/Unix path shapes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:20.304477900Z","updated_at":"2026-05-13T02:25:20.304477900Z","closed_at":null} -{"id":"rivets-wfb","title":"Refactor list filter tests to use rstest with struct-based test cases","description":"Replace individual list filter tests with a single rstest parameterized test using a ListFilterCase struct.\n\nCurrent tests to consolidate:\n- test_list_filter_by_status\n- test_list_filter_by_priority\n- test_list_filter_by_type\n- test_list_filter_by_assignee\n- test_list_with_limit\n\nNew structure:\n```rust\nstruct ListFilterCase {\n name: &'static str,\n status: Option<&'static str>,\n priority: Option,\n issue_type: Option<&'static str>,\n assignee: Option<&'static str>,\n label: Option<&'static str>,\n limit: Option,\n expected_count: usize,\n}\n```\n\nCreate a shared fixture that sets up diverse issues, then parameterize filter assertions.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-jfs","dep_type":"blocks"}],"created_at":"2025-11-29T23:57:39.855684150Z","updated_at":"2025-11-30T00:03:27.331176053Z","closed_at":"2025-11-30T00:03:27.331176053Z"} -{"id":"rivets-9mh","title":"Implement daemon with auto-sync and RPC server","description":"Implement the background daemon process that provides auto-export (dirty issues to JSONL), auto-import (JSONL to SQLite), and serves RPC requests.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads internal/daemon/:\n\n**Daemon Responsibilities**:\n1. Auto-export with debouncing (5s default)\n2. Auto-import on JSONL mtime changes\n3. RPC server for CLI commands\n4. PID file management\n5. Graceful shutdown on signals\n\n**Lifecycle**:\n- Auto-start on first CLI command (if not running)\n- Background detached process\n- Socket cleanup on exit\n- Lock file prevents multiple daemons\n\n**Rust Stack**:\n- `tokio` runtime for async event loop\n- `notify` crate for file watching (optional)\n- `signal-hook` for signal handling\n- `daemonize` crate for backgrounding\n\n**Features**:\n- Debounced exports (collect changes, flush after timeout)\n- File watcher for imports\n- Health monitoring\n- Log rotation","acceptance_criteria":"- Daemon starts in background\n- Auto-export triggers after changes\n- Auto-import on JSONL modification\n- RPC server responds to requests\n- PID file created and managed\n- Graceful shutdown on SIGTERM\n- Multiple daemon prevention\n- Integration tests verify auto-sync behavior","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:07.330511078Z","updated_at":"2025-11-17T22:16:07.330511078Z","closed_at":null} -{"id":"rivets-js1","title":"Implement issue templates system","description":"Add YAML-based issue templates:\n\n```yaml\n# .rivets/templates/bug.yaml\ntype: bug\npriority: 1\nlabels: [bug, needs-triage]\ndescription: |\n ## Bug Description\n [Describe the bug]\n \n ## Steps to Reproduce\n 1. \n 2. \n 3. \n \n ## Expected Behavior\n [What should happen]\n \n ## Actual Behavior\n [What actually happens]\n```\n\nUsage:\n```\n$ rivets create --template bug \"Login fails on Safari\"\nCreated: rivets-xyz\n Type: bug\n Priority: P1\n Labels: bug, needs-triage\n \nOpen in editor to fill description? [Y/n]: \n```","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","workflow"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:38:01.993057250Z","updated_at":"2025-11-30T18:38:01.993057250Z","closed_at":null} {"id":"rivets-7uw0","title":"Performance benchmarking on rivets codebase","description":"After Phase 1 implementation, benchmark on the rivets codebase:\n\n- Index time with two-pass vs previous single-pass\n- Memory usage during Pass 2\n- Query performance for callers/impact\n\nDocument findings and optimize if needed.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ugg6","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:58.427268019Z","updated_at":"2026-01-28T23:39:21.911663853Z","closed_at":"2026-01-28T23:39:21.911663853Z"} -{"id":"rivets-zoi3","title":"tethys: expand file_deps test coverage (rename, target deletion, DB-unit, rebuild idempotency)","description":"Surfaced by pr-test-analyzer during multi-agent review of PR #65. The 2 regression tests added in rivets-lcb6 fence the specific reported bug well, but four adjacent code paths are uncovered.\n\nTests to add (in crates/tethys/tests/file_deps_idempotency.rs unless noted):\n\n1. file_deps_swapped_when_use_target_changes (rating 8 / high). Replace 'use crate_target::Widget' with 'use crate_target_alt::Gadget' in the fixture. Re-index. Assert the edge to crate_target is GONE and the edge to crate_target_alt EXISTS — check by exact FileId lookup, not just row count (counts could net to zero coincidentally and hide the bug).\n\n2. clear_all_file_deps unit test (rating 6 / medium). crates/tethys/src/db/file_deps.rs has no #[cfg(test)] mod tests block. clear_all_file_deps is pub and per CLAUDE.md 'every public method needs tests' (rust-best-practices Rule 39). Add: insert 2 deps via insert_file_dependency, call clear_all_file_deps, assert get_file_dependencies returns [] for both source FileIds. Catches SQL typos / schema renames the integration tests would miss.\n\n3. file_deps_target_file_deletion_removes_edge (rating 8 / high but DEFERRED to rivets-dhxo). Delete crate_target/src/lib.rs from disk, re-index, assert no edges reference the orphan file. This test would currently FAIL in streaming mode — that's the whole point. Defer landing this until rivets-dhxo is fixed so it doesn't break CI. Cross-reference here so the regression-fence test gets added alongside the fix.\n\n4. rebuild_with_options idempotency (rating 7 / medium). The design doc notes rebuild is 'correct by accident' because db.reset() wipes the table first. Add a #[case::rebuild] variant or separate test calling rebuild_with_options(IndexOptions::default()) twice and assert FileDepsSnapshot stability. Defends against someone 'optimizing' rebuild to skip the reset later.\n\n5. (Suggestion, rating 4) file with 2 uses of same target → remove one → row remains, ref_count drops. Probes the multi-ref-per-edge code path the existing tests don't exercise.\n\nSource: PR #65 multi-agent review aggregate (pr-test-analyzer findings 1-5).","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"* file_deps_swapped_when_use_target_changes added and passing\n* clear_all_file_deps unit test added to db/file_deps.rs #[cfg(test)] mod tests\n* rebuild_with_options idempotency test added (as #[case::rebuild] or standalone)\n* (deferred) file_deps_target_file_deletion_removes_edge added once rivets-dhxo lands","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-17T02:43:32.040071906Z","updated_at":"2026-05-17T02:43:32.040071906Z","closed_at":null} -{"id":"rivets-cbm","title":"Add vim-style keyboard navigation to TUI","description":"Implement vim-style keyboard navigation:\n- h/j/k/l for movement\n- / for search\n- c for create\n- e for edit\n- d for delete (with confirmation)\n- Enter for details\n- Space for toggle status\n- Tab for switch views\n- q for quit\n- ? for help\n\nAlso support arrow keys and mouse.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:24.109173058Z","updated_at":"2025-11-30T18:37:24.109173058Z","closed_at":null} -{"id":"rivets-f07","title":"Add trace logging for workspace cache operations","description":"Add observability to the workspace storage cache to help debug multi-workspace scenarios.\n\nSuggested trace logging points:\n- Cache hits: when a workspace is found in the cache\n- Cache misses: when a workspace needs to be loaded from disk\n- Cache evictions: when a workspace is removed due to capacity limits\n- Cache size: periodic or on-change logging of current cache utilization\n\nThis would help with:\n1. Debugging issues in multi-workspace agent scenarios\n2. Validating cache eviction strategy effectiveness (relates to rivets-zkb)\n3. Understanding real-world usage patterns\n4. Identifying potential performance bottlenecks\n\nImplementation notes:\n- Use tracing crate with appropriate log levels (trace/debug for hits, info for evictions)\n- Consider structured logging with workspace paths and cache stats\n- Ensure logging doesn't impact performance (use trace! level for high-frequency events)","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["observability","rivets-mcp"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"},{"depends_on_id":"rivets-zkb","dep_type":"related"}],"created_at":"2025-11-29T07:30:55.370591744Z","updated_at":"2025-11-29T07:30:55.370591744Z","closed_at":null} -{"id":"rivets-kln","title":"Add dialoguer dependency for interactive prompts","description":"Add dialoguer crate to workspace dependencies for interactive CLI prompts.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:31.001190538Z","updated_at":"2025-11-30T18:35:31.001190538Z","closed_at":null} -{"id":"rivets-gkt2","title":"Implement proper staleness check","description":"Currently `needs_update()` in `lib.rs:2108-2110` always returns `true`. Should check if any indexed files have changed by comparing stored `mtime_ns`/`size_bytes` against filesystem.\n\nRelated to incremental updates but useful independently for skip-if-fresh optimization.","status":"in_progress","priority":2,"issue_type":"feature","assignee":null,"labels":["performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:09.590472097Z","updated_at":"2026-04-27T22:35:53.633428039Z","closed_at":null} -{"id":"rivets-bsp","title":"Implement core CLI commands (create, list, show, update, close, delete)","description":"Implement the essential CLI commands for basic issue management using clap for argument parsing. These are the most frequently used commands.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on beads cmd/bd/:\n\n**Commands**:\n1. **create**: Create new issues (interactive or flags)\n2. **list**: Query issues with filters (status, priority, assignee, labels, etc.)\n3. **show**: Display full issue details with dependencies\n4. **update**: Modify issue fields\n5. **close**: Mark issues as closed with reason\n6. **delete**: Remove issues (with cascade confirmation)\n\n**Clap Structure**:\n```rust\n#[derive(Parser)]\nenum Commands {\n Create(CreateArgs),\n List(ListArgs),\n Show(ShowArgs),\n Update(UpdateArgs),\n Close(CloseArgs),\n Delete(DeleteArgs),\n}\n```\n\n**Features**:\n- Interactive prompts for missing data\n- JSON output mode (--json flag)\n- Batch operations where applicable\n- Validation and error messages","acceptance_criteria":"- All 6 commands implemented and working\n- --help shows usage for each command\n- Filters work correctly (status, priority, etc.)\n- JSON output mode functional\n- Interactive mode for create command\n- Error messages are clear and helpful\n- Integration tests verify command behavior","notes":"## Implementation Notes (2025-11-29)\n\n### Completed Work\n\nAll 6 core CLI commands have been implemented and integrated with the storage trait:\n\n1. **create** - Creates new issues with full option support\n - Interactive prompt for title if not provided\n - Supports all fields: title, description, priority, type, assignee, labels, design, acceptance criteria\n - Dependency specification via --deps flag\n\n2. **list** - Queries issues with filters\n - Status filter with in_progress alias support\n - Priority, type, assignee, label filters\n - Sorting: priority, newest, oldest, updated\n - Limit option\n\n3. **show** - Displays full issue details\n - Shows all fields with formatted output\n - Displays dependencies and dependents\n - Supports JSON output mode\n\n4. **update** - Modifies issue fields\n - All fields updateable\n - Status transitions supported\n\n5. **close** - Marks issues as closed\n - Custom reason support\n - Updates status to closed\n\n6. **delete** - Removes issues\n - Interactive confirmation by default\n - --force flag to skip confirmation\n - Proper error on dependents\n\n### Additional Commands\n\nAlso implemented bonus commands:\n- **ready** - Find ready-to-work issues (no blockers)\n- **dep** - Dependency management (add/remove/list)\n- **blocked** - Show blocked issues\n- **stats** - Project statistics\n\n### Architecture\n\n- Created `App` struct for storage lifecycle management\n- Created `output` module for consistent formatting (text/JSON modes)\n- Commands find rivets root by searching up directory tree\n- Auto-save after mutations\n\n### Testing\n\n- 33 integration tests using rstest\n- All tests pass\n- Tests properly initialize repositories before testing","external_ref":null,"dependencies":[{"depends_on_id":"rivets-cgl","dep_type":"blocks"}],"created_at":"2025-11-17T22:16:06.835605991Z","updated_at":"2025-11-30T03:09:14.214873854Z","closed_at":"2025-11-30T03:09:14.214873854Z"} -{"id":"rivets-ceg","title":"Implement CLI argument parsing with clap validation","description":"Extend the CLI skeleton (rivets-p7v) with complete argument parsing, validation, error handling, and help text generation for all commands.\n\nThis task implements the argument parsing layer that sits between the CLI entry point and command execution. Each command gets proper args structs with validation.\n\n**Commands to implement**:\n- create: --title, --description, --priority, --type, --assignee, --deps\n- list: --status, --priority, --type, --assignee, --label, --limit\n- show: , --json\n- update: , --title, --description, --status, --priority, --assignee\n- close: , --reason\n- delete: , --force\n- init: --prefix, --quiet\n- ready: --assignee, --limit, --sort\n\n**Features**:\n- Validation for all flag values (priority 0-4, valid status enum, etc.)\n- Helpful error messages with suggestions\n- Auto-generated help text via clap derives\n- Subcommand structure with shared global flags\n- Interactive prompts for missing required fields (create command)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Use clap derive API for argument parsing:\n\n```rust\nuse clap::{Parser, Subcommand, ValueEnum};\n\n#[derive(Parser)]\n#[command(name = \"rivets\")]\n#[command(author, version, about, long_about = None)]\npub struct Cli {\n /// Output in JSON format\n #[arg(long, global = true)]\n pub json: bool,\n \n #[command(subcommand)]\n pub command: Option,\n}\n\n#[derive(Subcommand)]\npub enum Commands {\n /// Initialize a new rivets repository\n Init(InitArgs),\n \n /// Create a new issue\n Create(CreateArgs),\n \n /// List issues with optional filters\n List(ListArgs),\n \n /// Show detailed issue information\n Show(ShowArgs),\n \n /// Update an existing issue\n Update(UpdateArgs),\n \n /// Close an issue\n Close(CloseArgs),\n \n /// Delete an issue\n Delete(DeleteArgs),\n \n /// Show ready-to-work issues\n Ready(ReadyArgs),\n}\n\n#[derive(Parser)]\npub struct CreateArgs {\n /// Issue title\n #[arg(short, long)]\n pub title: Option,\n \n /// Issue description\n #[arg(short, long)]\n pub description: Option,\n \n /// Priority (0-4)\n #[arg(short, long, value_parser = clap::value_parser!(u8).range(0..=4))]\n pub priority: Option,\n \n /// Issue type\n #[arg(short = 't', long)]\n pub issue_type: Option,\n \n /// Assignee\n #[arg(short, long)]\n pub assignee: Option,\n}\n\n#[derive(ValueEnum, Clone)]\npub enum IssueTypeArg {\n Bug,\n Feature,\n Task,\n Epic,\n Chore,\n}\n\n// ... more arg structs\n```\n\n**Validation**:\n- Priority range checking (0-4)\n- Issue ID format validation\n- Enum value validation via ValueEnum\n- Required vs optional field handling\n- Custom validators for complex types","acceptance_criteria":"- All CLI commands have argument structs defined\n- Clap derive attributes configured correctly\n- Priority validation enforces 0-4 range\n- Issue ID validation checks format\n- Enum arguments use ValueEnum for type safety\n- Help text auto-generated and informative\n- Error messages are clear with examples\n- Unit tests for argument parsing\n- Integration tests verify --help output\n- Invalid arguments produce helpful error messages","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p7v","dep_type":"blocks"}],"created_at":"2025-11-17T23:04:11.868192762Z","updated_at":"2025-11-28T14:22:51.079089079Z","closed_at":"2025-11-28T14:22:51.079089079Z"} -{"id":"rivets-yip","title":"Write comprehensive API documentation","description":"Write comprehensive rustdoc documentation for all public APIs with usage examples.\n\nDocumentation should match or exceed the quality outlined in research document.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Documentation requirements:\n\n1. **Module-level docs** (lib.rs):\n - Overview of library\n - Quick start guide\n - Feature comparison with other libraries\n - Performance characteristics\n\n2. **Type documentation**:\n - JsonlReader with examples\n - JsonlWriter with examples\n - JsonlQuery with examples\n - Warning types\n - Error types\n\n3. **Method documentation**:\n - All public methods documented\n - Include # Example blocks\n - Document edge cases\n - Document performance characteristics\n\n4. **README.md**:\n - Installation instructions\n - Quick start\n - Feature list\n - Performance benchmarks\n - Comparison with alternatives\n\nGenerate docs with `cargo doc --no-deps --open`.","acceptance_criteria":"- All public APIs documented\n- Module-level docs comprehensive\n- README.md complete\n- All doc examples compile (tested with cargo test --doc)\n- Documentation coverage >90%\n- cargo doc generates without warnings","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6640","dep_type":"parent-child"},{"depends_on_id":"rivets-kuf","dep_type":"blocks"}],"created_at":"2025-11-27T23:18:03.937459711Z","updated_at":"2025-11-27T23:18:03.937459711Z","closed_at":null} -{"id":"rivets-xjam","title":"Docs: Add rationale for ID length threshold constants (500, 1500)","description":"In storage/in_memory/inner.rs (lines 74-82), the magic numbers 500 and 1500 lack explanation:\n\n```rust\nlet needs_update = match (old_size, current_size) {\n // Crossing 500 boundary (4 -> 5 chars)\n (0..=500, 501..) => true,\n // Crossing 1500 boundary (5 -> 6 chars)\n (0..=1500, 1501..) => true,\n ...\n};\n```\n\nThe comments say WHAT happens (4->5 chars, 5->6 chars) but not WHY these thresholds were chosen.\n\nPer M-DOCUMENTED-MAGIC guideline, should explain the rationale:\n\n**Suggested documentation:**\n```rust\n/// ID length thresholds for adaptive ID generation.\n///\n/// These values are derived from the ID generator's collision probability calculations:\n/// - At 500 issues with 4-char IDs: collision probability exceeds 1%\n/// - At 1500 issues with 5-char IDs: collision probability exceeds 1%\n/// - 6-char IDs support >50,000 issues before similar collision rates\n///\n/// Thresholds chosen to balance ID brevity with uniqueness guarantees.\nconst ID_LENGTH_THRESHOLD_4_TO_5: usize = 500;\nconst ID_LENGTH_THRESHOLD_5_TO_6: usize = 1500;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","inner.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:51:10.352427140Z","updated_at":"2025-12-28T15:51:10.352427140Z","closed_at":null} -{"id":"rivets-4q2","title":"Integrate resilient JSONL loading with in_memory storage","description":"Update rivets in_memory::load_from_jsonl() to use the new rivets-jsonl library's resilient loading functionality.\n\nThis replaces the current manual JSONL parsing with the library implementation.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Update in_memory.rs to use rivets-jsonl:\n\n```rust\nuse rivets_jsonl::read_jsonl_resilient;\n\npub async fn load_from_jsonl(\n path: &Path,\n prefix: String,\n) -> Result<(Box, Vec)> {\n let (issues, warnings) = read_jsonl_resilient::(path).await?;\n \n // Convert rivets_jsonl::Warning to LoadWarning\n let load_warnings: Vec = warnings\n .into_iter()\n .map(|w| match w {\n rivets_jsonl::Warning::MalformedJson { line_number, error } => {\n LoadWarning::MalformedJson { line_number, error }\n }\n rivets_jsonl::Warning::SkippedLine { line_number, reason } => {\n // Map to appropriate LoadWarning variant\n LoadWarning::MalformedJson { line_number, error: reason }\n }\n })\n .collect();\n \n // Continue with existing dependency reconstruction logic\n let storage = Arc::new(Mutex::new(InMemoryStorageInner::new(prefix)));\n // ... existing code\n \n Ok((Box::new(storage), load_warnings))\n}\n```\n\nAdd rivets-jsonl as dependency in rivets/Cargo.toml.","acceptance_criteria":"- in_memory::load_from_jsonl uses rivets-jsonl library\n- Warnings correctly mapped between types\n- All existing tests still pass\n- No performance regression\n- Dependency added to Cargo.toml","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-uyg","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:06.671886113Z","updated_at":"2025-11-28T07:04:14.251308562Z","closed_at":"2025-11-28T07:04:14.251308562Z"} -{"id":"rivets-anc","title":"Improve publishing instructions visibility in README","description":"The crates.io publishing instructions mention waiting for indexing after the code block. Consider moving or inlining this note with the commands for better visibility.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["documentation"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6640","dep_type":"parent-child"}],"created_at":"2025-12-18T02:29:24.701631594Z","updated_at":"2025-12-18T02:29:24.701631594Z","closed_at":null} -{"id":"rivets-0ib","title":"Implement TUI list and detail views","description":"Create list and detail views for TUI:\n\nList view:\n- Sortable/filterable issue list\n- Compact single-line display\n- Keyboard navigation (j/k or arrows)\n\nDetail view:\n- Full issue details in side panel\n- Dependencies and dependents\n- Edit capabilities","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:18.308663319Z","updated_at":"2025-11-30T18:37:18.308663319Z","closed_at":null} -{"id":"rivets-djr","title":"Evaluate rstest for rivets/src/cli.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in cli.rs.\n\nFile: crates/rivets/src/cli.rs (lines ~873+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:36.464605033Z","updated_at":"2025-11-29T01:31:25.950082366Z","closed_at":"2025-11-29T01:31:25.950082366Z"} -{"id":"rivets-6jxv","title":"Extract Tethys::crate_root_for_file helper to dedupe triple-duplicated derivation","description":"The per-file crate_root derivation introduced by rivets-6aoc is now duplicated verbatim across three production sites in tethys. Each duplicate carries the same get_crate_for_file + src_root + file-parent-fallback logic. Extracting to a single helper on Tethys would make each call site a one-liner and ensure the three sites stay in sync.","status":"closed","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Current duplication\n\nThe same ~8-line pattern lives in three places:\n\n1. `crates/tethys/src/resolve.rs::resolve_refs_for_file` (Pass-2-imports)\n2. `crates/tethys/src/indexing.rs::compute_dependencies` (dep-graph from extracted refs)\n3. `crates/tethys/src/indexing.rs::compute_dependencies_from_stored` (streaming-mode dep graph)\n\nEach site has:\n\n```rust\nlet crate_root = if let Some(crate_info) =\n crate::cargo::get_crate_for_file(, &self.crates) // or self.crates()\n{\n crate_info.src_root()\n} else {\n debug!(file = %.display(), \"...; using file parent as sentinel crate_root\");\n .parent()\n .map_or_else(|| self.workspace_root.clone(), Path::to_path_buf)\n};\n```\n\n## Proposed helper\n\n```rust\nimpl Tethys {\n /// Returns the per-file crate_root for use by the resolver and dep-graph\n /// computation. Falls back to the file's parent directory as a sentinel\n /// when the file is outside any known crate (workspace-root example/bench\n /// dirs).\n fn crate_root_for_file(&self, file: &Path, caller: &'static str) -> PathBuf {\n if let Some(crate_info) = crate::cargo::get_crate_for_file(file, &self.crates) {\n crate_info.src_root()\n } else {\n debug!(\n operation = caller,\n file = %file.display(),\n \"File not in any known crate; using file parent as sentinel crate_root\"\n );\n file.parent()\n .map_or_else(|| self.workspace_root.clone(), Path::to_path_buf)\n }\n }\n}\n```\n\nEach call site collapses to:\n\n```rust\nlet crate_root = self.crate_root_for_file(, \"compute_dependencies\");\n```\n\n## Discovered during\n\nPR #63 round-2 claude review (post-merge of round-1 fixes). The reviewer flagged the duplication as a suggestion. Filing per the new gilfoyle tracker discipline (any deferral or 'follow-up' phrase needs a tracker entry filed at write-time).\n\n## Why P4\n\nCosmetic. Three sites are correctly in sync today. The risk is future drift if someone modifies one without the others. The helper is also a natural extension point for the rivets-bjdn BTreeMap optimization (the pre-computed lookup map would live in this helper).","acceptance_criteria":"- [ ] Tethys::crate_root_for_file helper extracted with consistent caller-name parameter\n- [ ] resolve.rs::resolve_refs_for_file uses the helper\n- [ ] indexing.rs::compute_dependencies uses the helper\n- [ ] indexing.rs::compute_dependencies_from_stored uses the helper\n- [ ] All resolver tests still pass (605+ in PR #63)\n- [ ] cargo clippy + cargo fmt clean","notes":"Closed: Fixed in PR #71 (commit f0220d0). Extracted Tethys::src_root_for_file helper, deduped 3 call sites in resolve.rs and indexing.rs. Helper named src_root_for_file (not crate_root_for_file as the issue text suggested) per cross-validation from 3 independent reviewers — matches CrateInfo::src_root() and avoids visual collision with the pre-existing Tethys::get_crate_root_for_file. ResolveContext::crate_root also renamed to src_root for end-to-end consistency. Added unit test for orphan-file fallback branch.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-i8qn","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-13T00:28:31.586298200Z","updated_at":"2026-05-18T18:51:03.322639100Z","closed_at":"2026-05-18T18:51:03.322636200Z"} -{"id":"rivets-7q6","title":"Add Send assertions for async futures (M-TYPES-SEND)","description":"The codebase uses async/await extensively but lacks explicit compile-time assertions that futures are Send, violating the M-TYPES-SEND guideline.\n\nCurrent State:\n- IssueStorage trait requires Send + Sync\n- Methods return futures implicitly\n- No compile-time verification that futures are actually Send\n- Could break multi-threaded async runtimes\n\nRisk:\n- If a future accidentally captures non-Send types, it won't be detected until runtime\n- Makes the library unsafe to use with work-stealing executors","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["async","code-quality","testing"],"design":"Add Send assertions in storage/mod.rs tests (after line 453):\n\n```rust\n#[test]\nfn assert_storage_futures_are_send() {\n const fn assert_send() {}\n \n // Assert that all IssueStorage methods return Send futures\n assert_send::>();\n \n // Can also test specific future types if needed\n // This ensures futures work with multi-threaded runtimes\n}\n```\n\nThis pattern:\n- Runs at compile time (const fn)\n- Catches non-Send futures immediately\n- Documents the Send requirement\n- Zero runtime cost","acceptance_criteria":"- [ ] assert_send helper function added\n- [ ] Test verifies Box is Send\n- [ ] Test verifies key future types are Send\n- [ ] Code compiles (proving assertion holds)\n- [ ] All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-27T22:49:09.918623993Z","updated_at":"2025-11-27T22:49:09.918623993Z","closed_at":null} -{"id":"rivets-d71","title":"Implement PostgreSQL storage backend with recursive CTEs","description":"Create PostgresStorage implementation using sqlx with connection pooling. Use recursive CTEs for cycle detection and ready work queries. This is the production multi-user backend.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"Implement PostgreSQL backend:\n\n```rust\npub struct PostgresStorage {\n pool: PgPool,\n}\n\nimpl IssueStorage for PostgresStorage {\n fn create_issue(&mut self, new: NewIssue) -> Result {\n let id = generate_hash_id(&new);\n sqlx::query!(\n \"INSERT INTO issues (id, title, description, ...) VALUES ($1, $2, $3, ...)\",\n id, new.title, new.description\n ).execute(&self.pool).await?;\n \n self.get_issue(&id)?.ok_or(Error::NotFound)\n }\n \n fn check_cycle(&self, from: &IssueId, to: &IssueId) -> Result {\n let exists = sqlx::query_scalar!(\n r#\"\n WITH RECURSIVE paths AS (\n SELECT issue_id, depends_on_id, 1 as depth\n FROM dependencies WHERE issue_id = $1\n UNION ALL\n SELECT d.issue_id, d.depends_on_id, p.depth + 1\n FROM dependencies d JOIN paths p \n ON d.issue_id = p.depends_on_id\n WHERE p.depth < 100\n )\n SELECT EXISTS(SELECT 1 FROM paths WHERE depends_on_id = $2)\n \"#,\n to, from\n ).fetch_one(&self.pool).await?;\n \n Ok(exists.unwrap_or(false))\n }\n \n fn find_ready(&self, filter: &IssueFilter) -> Result> {\n // Recursive CTE from beads ready.go\n // ... implementation\n }\n}\n```\n\n**Schema**: Similar to beads SQLite schema, optimized for PostgreSQL\n**Dependencies**: `sqlx` with `runtime-tokio-rustls`, `postgres` features\n**Migrations**: Use sqlx migrations","acceptance_criteria":"- PostgresStorage implements all trait methods\n- Connection pooling configured\n- Recursive CTEs for cycle detection and ready work\n- All CRUD operations functional\n- Indexes for performance\n- Migration system working\n- Integration tests with test database\n- Performance: <50ms for complex queries","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gc","dep_type":"blocks"},{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-17T22:41:42.669213538Z","updated_at":"2025-11-17T22:41:42.669213538Z","closed_at":null} +{"id":"rivets-qawf","title":"tethys: push name/Ca/Ce sort to SQL ORDER BY (keep instability in Rust)","description":"get_coupling_metrics (architecture.rs) fetches all rows from arch_coupling unsorted, then sorts in Rust. This was a deliberate choice for the Instability variant — the formula lives only in CouplingMetrics::instability() and can't be replicated in SQL without duplicating logic. For the other three sort variants (Name, Afferent, Efferent) the choice was made by consistency, not necessity.\n\nReviewer flagged in PR 60 round 7: \"At scale (monorepos with hundreds of crates) the full fetch + in-memory sort could become noticeable... consider adding a follow-up issue for DB-side sorting of the non-instability variants (name, Ca, Ce) if performance ever becomes a concern.\"\n\nTrigger condition: a workspace with hundreds of crates where get_coupling_metrics becomes a measurable hot path.\n\nScope:\n- Add ORDER BY clauses to the query in get_coupling_metrics for Name/Ca/Ce variants\n- Keep the Rust-side sort for Instability (formula stays in one place)\n- The current secondary sort (by package name) for ties stays in the SQL ORDER BY\n\nNote: SQLite COLLATE NOCASE may be wanted for the Name variant to match the locale-insensitive lexicographic sort Rust's String::cmp provides today.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] get_coupling_metrics emits ORDER BY for CouplingSort::Name, ::Afferent, ::Efferent\n- [ ] Instability sort still happens in Rust (formula stays in one place)\n- [ ] Tie-breaking by name remains deterministic across variants\n- [ ] Benchmark before/after on a synthetic >200-crate workspace","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:21:54.741507500Z","updated_at":"2026-05-11T21:21:54.741507500Z","closed_at":null} +{"id":"rivets-6x7g","title":"Add CSharpLsProvider","description":"Fast follow after rust-analyzer is working:\n\n```rust\npub struct CSharpLsProvider;\nimpl LspProvider for CSharpLsProvider {\n fn command(&self) -> &str { \"csharp-ls\" }\n}\n```\n\nWire up to use csharp-ls for .cs files when --lsp is specified.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["csharp","lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1dza","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:29:09.273111083Z","updated_at":"2026-01-29T01:42:23.934741997Z","closed_at":"2026-01-29T01:42:23.934741997Z"} +{"id":"rivets-kuf","title":"Create comprehensive examples","description":"Create examples demonstrating all major rivets-jsonl features.\n\nExamples should be runnable and well-documented.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create in examples/:\n\n1. **basic.rs** - Simple read/write\n2. **streaming.rs** - Large file streaming\n3. **resilient.rs** - Resilient loading with warnings\n4. **query.rs** - Filtering and querying\n5. **atomic_write.rs** - Atomic writes\n6. **integration.rs** - Integration with rivets\n\nEach example should:\n- Be fully runnable with `cargo run --example `\n- Include inline documentation\n- Demonstrate best practices\n- Show error handling","acceptance_criteria":"- All 6 examples created\n- Each example compiles and runs\n- Examples well-documented\n- README lists all examples with descriptions\n- Examples demonstrate key features","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-dhs","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-t0k","dep_type":"blocks"}],"created_at":"2025-11-27T23:17:58.239786888Z","updated_at":"2025-11-27T23:17:58.239786888Z","closed_at":null} +{"id":"rivets-dev","title":"Convert in_memory_storage.rs to rstest","description":"Create test_issue fixture to replace create_test_issue() helpers. Parameterize priority/status tests with #[values].\n\nFile: crates/rivets/tests/in_memory_storage.rs","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- test_issue fixture created\n- Priority/status tests use #[values] for matrix testing\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:30.726310933Z","updated_at":"2025-11-29T02:09:45.806681348Z","closed_at":"2025-11-29T02:09:45.806681348Z"} +{"id":"rivets-9po8","title":"Rivets MCP Server Hardening","description":"Improve rivets-mcp reliability: cache eviction, lock contention, tracing visibility, and integration test coverage.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:29.669076471Z","updated_at":"2026-02-21T02:28:29.669076471Z","closed_at":null} +{"id":"rivets-4il","title":"Create git merge driver for .automerge files","description":"Implement CLI command and git configuration for automatic CRDT merging during git merge operations.\n\nIncludes:\n- `rv merge ` subcommand\n- .gitattributes configuration for *.automerge files\n- Documentation for git merge driver setup","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:50.322609338Z","updated_at":"2025-12-23T04:45:22.910742278Z","closed_at":"2025-12-23T04:45:22.910742278Z"} +{"id":"rivets-hefr","title":"Scope SQLite busy_timeout based on read vs write workload","description":"The current Index::open sets a 30-second busy_timeout for all operations. This was chosen to handle concurrent test parallelism (rivets-byie's architecture phase writes interact with tests sharing the rivets workspace DB).\n\n**The concern:** if a user runs an interactive command like 'tethys coupling' while a background 'tethys index' is running, the read-side query could silently block for up to 30s before its first SQL operation succeeds. With WAL mode, true read queries don't block on writes, but the schema/pragma writes in Index::open itself can. The user sees no feedback — just a long pause.\n\n**Possible fix shapes:**\n- Set a short busy_timeout (500ms or 1s) by default and override it inside indexing operations only.\n- Add an Index::open_read_only or similar that uses a shorter timeout for query-only commands.\n- Surface a 'waiting for lock' message after 2-3s if the operation is still blocked.\n\n**Why deferred:** the concurrent-interactive scenario is rare in practice (most users run one tethys command at a time). The 30s timeout is conservative but safe. Worth tuning if anyone reports a real UX issue.\n\nDiscovered during second-round PR #60 review.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["tethys","enhancement","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T05:03:24.255671800Z","updated_at":"2026-05-11T05:03:24.255671800Z","closed_at":null} +{"id":"rivets-0vd","title":"Implement dep tree visualization","description":"Add 'dep tree' subcommand to display dependency tree for an issue.\\n\\nShould show both upstream dependencies (what this issue depends on) and downstream dependents (what depends on this issue) in a tree format.\\n\\nExample: rivets dep tree proj-abc","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:24:02.683285758Z","updated_at":"2025-12-28T00:50:58.475771946Z","closed_at":"2025-12-28T00:50:58.475771946Z"} +{"id":"rivets-cz85","title":"Compute module_path for symbols","description":"The schema has a `module_path` column (indexed) but symbols are stored with empty strings. See `lib.rs:624` and `lib.rs:753`.\n\nThe existing `resolve_module_path` in `resolver.rs` computes **file paths**, not **module paths** (like `crate::storage::issue`). Need to compute the actual Rust module path from the file location.\n\nThis would enable queries like \"find all symbols in module X\".","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-m4wt","dep_type":"blocks"}],"created_at":"2026-01-30T00:17:57.789103820Z","updated_at":"2026-02-01T23:54:48.228483332Z","closed_at":"2026-02-01T23:54:48.228483332Z"} +{"id":"rivets-ans","title":"Convert roundtrip.rs to rstest","description":"Replace 11 similar roundtrip tests with parameterized #[rstest] test. Create fixture for buffer setup pattern.\n\nFile: crates/rivets-jsonl/tests/roundtrip.rs\nEstimated reduction: ~150 lines → ~50 lines\n\nThis is the highest-impact conversion - the buffer setup pattern is repeated 11 times.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- All roundtrip tests converted to use #[rstest] with #[case] parameterization\n- Buffer setup extracted to fixture or helper\n- All tests pass\n- Significant line count reduction achieved","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:19.405900112Z","updated_at":"2025-11-29T02:08:24.172376961Z","closed_at":"2025-11-29T02:08:24.172376961Z"} {"id":"rivets-p5rj","title":"tethys: rename format_uri to absolute_path_to_uri","description":"format_uri's rustdoc says 'caller must pass an absolute, canonicalized path' — that precondition lives only in prose. A CanonicalPath newtype would be overkill for two private callsites, but the rename converts a hidden precondition into a name a reviewer can't miss.\n\nTrivial: rename the fn, update the one caller in path_to_uri, update the five format_uri_* test names if desired (or leave them as the shorter form).\n\nIf the function ever goes pub(crate), revisit with CanonicalPath. Defer until that day.\n\nSurfaced by PR #64 round-3 review (type-design-analyzer).","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Rename fn format_uri -> fn absolute_path_to_uri in crates/tethys/src/lsp/transport.rs\n- [ ] Update caller in path_to_uri\n- [ ] Update doc comments referencing the old name\n- [ ] cargo nextest run -p tethys passes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:26.719504Z","updated_at":"2026-05-13T02:25:26.719504Z","closed_at":null} -{"id":"rivets-gvmh","title":"Pre-computed call graph edges table","description":"Store explicit caller→callee edges in a dedicated table for faster graph queries.\n\n**Current approach:**\n- `get_callers` joins references → symbols → files\n- Complex query with multiple joins\n- Re-computed on every query\n\n**Drift's approach:**\n- Dedicated `call_edges` table: `(caller_id, callee_id, call_count)`\n- Pre-computed during indexing\n- Simple lookups for callers/callees\n\n**Proposed schema:**\n```sql\nCREATE TABLE call_edges (\n caller_symbol_id INTEGER NOT NULL,\n callee_symbol_id INTEGER NOT NULL,\n call_count INTEGER DEFAULT 1,\n PRIMARY KEY (caller_symbol_id, callee_symbol_id),\n FOREIGN KEY (caller_symbol_id) REFERENCES symbols(id),\n FOREIGN KEY (callee_symbol_id) REFERENCES symbols(id)\n);\n\nCREATE INDEX idx_call_edges_callee ON call_edges(callee_symbol_id);\n```\n\n**Benefits:**\n- O(1) lookup for \"who calls X?\" instead of join-heavy query\n- Enables efficient reachability analysis\n- Pre-computed call counts for hotspot detection\n\n**Trade-off:**\n- Slightly larger database\n- Need to update edges during incremental re-index","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:21.017980416Z","updated_at":"2026-01-29T19:32:12.190603274Z","closed_at":"2026-01-29T19:32:12.190603274Z"} -{"id":"rivets-zkb","title":"Evaluate cache eviction strategy for workspace storage","description":"The current workspace storage uses FIFO eviction when the cache limit (8 workspaces) is reached. Consider whether LRU eviction would be more appropriate for typical usage patterns.\n\nContext from PR review:\n- Current implementation: FIFO eviction via `workspace_order.remove(0)`\n- Alternative: LRU eviction (move recently-used workspaces to end of queue)\n- Trade-off: FIFO is simpler but may evict frequently-used workspaces\n\nEvaluation should consider:\n1. Typical agent workflow patterns (do they frequently switch between workspaces?)\n2. Memory/performance impact of LRU tracking\n3. Whether the current cache limit (8) is appropriate","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["performance","rivets-mcp"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T07:29:32.898224105Z","updated_at":"2025-11-29T07:29:32.898224105Z","closed_at":null} +{"id":"rivets-yyri","title":"Refactor: Extract shared validation logic between Issue and NewIssue","description":"In domain/mod.rs, `Issue::validate()` (lines 118-138) and `NewIssue::validate()` (lines 321-344) have similar validation logic:\n\n**Issue::validate():**\n```rust\npub fn validate(&self) -> Result<(), String> {\n let trimmed_title = self.title.trim();\n if trimmed_title.is_empty() { return Err(...); }\n if self.title.len() > MAX_TITLE_LENGTH { return Err(...); }\n if self.priority > 4 { return Err(...); }\n Ok(())\n}\n```\n\n**NewIssue::validate():**\n```rust\npub fn validate(&self) -> Result<(), String> {\n let trimmed_title = self.title.trim();\n if trimmed_title.is_empty() { return Err(...); }\n if trimmed_title.len() > MAX_TITLE_LENGTH { return Err(...); }\n if self.priority > 4 { return Err(...); }\n Ok(())\n}\n```\n\n**Recommendation:** Extract to a helper function:\n```rust\nfn validate_title_and_priority(title: &str, priority: u8) -> Result<(), String> {\n let trimmed = title.trim();\n if trimmed.is_empty() { return Err(\"Title cannot be empty\".to_string()); }\n if trimmed.len() > MAX_TITLE_LENGTH { return Err(...); }\n if priority > MAX_PRIORITY { return Err(...); }\n Ok(())\n}\n```\n\nNote: Also addresses the hardcoded `4` vs `MAX_PRIORITY` constant issue.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","domain","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:50:52.984657028Z","updated_at":"2025-12-28T20:57:06.989347788Z","closed_at":"2025-12-28T20:57:06.989347788Z"} +{"id":"rivets-24av","title":"Track failed files in WriteStats for debugging","description":"Currently `WriteStats` only tracks successful writes. When files fail to write, that information is logged but lost from the final result.\n\n**Current struct:**\n```rust\npub struct WriteStats {\n pub files_written: usize,\n pub symbols_written: usize,\n pub references_written: usize,\n pub batches_committed: usize,\n}\n```\n\n**Suggested addition:**\n```rust\npub struct WriteStats {\n // ... existing fields ...\n /// Files that failed to write with their error messages\n pub failed_files: Vec<(PathBuf, String)>,\n}\n```\n\n**Benefits:**\n- Users can see which files need attention without grepping logs\n- Enables retry logic for failed files\n- Makes partial success explicit rather than hidden\n\n**Trade-off:** Increases memory usage if many files fail (unlikely in practice).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["batch-writer","error-handling","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:06:51.687980987Z","updated_at":"2026-01-29T19:06:51.687980987Z","closed_at":null} +{"id":"rivets-044i","title":"tethys: qualified refs from import-less files don't resolve (e.g., helper::do_thing or crate_name::Type)","description":"Discovered while writing the rivets-dn35 regression test (tests/pass2_no_imports.rs).\n\nEven after rivets-dn35 removes the imports.is_empty() short-circuit in Pass 2, qualified refs from import-less files still don't resolve. Example:\n\n // crate_a/src/lib.rs (NO use statements)\n mod helper;\n pub fn entry() { helper::do_thing_q(); }\n\n // crate_a/src/helper.rs\n pub fn do_thing_q() {}\n\ntry_resolve_reference takes the is_qualified=true path:\n 1. explicit imports lookup: 'helper' not in (empty) explicit_imports -> miss\n 2. glob imports: empty list -> skip\n 3. fallback: get_symbol_by_qualified_name('helper::do_thing_q')\n -> looks for symbols.qualified_name = 'helper::do_thing_q'\n -> free functions are stored with qualified_name = name (no module prefix)\n -> miss\n\nSame shape with workspace-crate prefix (e.g. crate_a::widget::Widget from a sibling file) also fails for the same reason.","status":"closed","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":"Two candidate fixes:\n\n(a) In try_resolve_reference's qualified branch (when explicit imports miss), invoke resolver::resolve_module_path on the path segments. That function already handles crate::, self::, super::, and workspace-crate prefixes — returning a module file path. Then call resolve_symbol_in_module on the result.\n\n(b) Walk the symbol's file's module hierarchy: derive an implicit 'crate::SAME_MODULE_PATH::ref_name' from the caller's file location and try get_symbol_by_qualified_name with that.\n\n(a) is more general (handles workspace-crate-prefix style too). (b) is narrower (only same-crate sibling-module shape).\n\nRisk: option (a) adds a new resolution path. If it succeeds where the existing fallback would have returned None, the resolver becomes more permissive — could introduce phantom resolutions analogous to rivets-0gom's un-ambiguation drift. Mitigation: the K-hybrid filter (PR #67) catches cross-crate phantoms at file_deps; the new path could be scoped to crate_root anchors (crate::, self::, super::, own workspace-crate-name) to avoid pulling in unrelated crates.","acceptance_criteria":"- [ ] Test: qualified call helper::do_thing_q() from import-less src/lib.rs (mod helper;) resolves to crate_a/src/helper.rs::do_thing_q\n- [ ] Test: workspace-crate-prefix call crate_a::widget::Widget::new() from import-less integration test resolves\n- [ ] No regression in rivets-3d0s K-hybrid file_deps phantom rate (re-run .rivets-ycaq/probe_phantom_rate.py: must remain 0.00%)\n- [ ] No new ambiguity violations (re-run .rivets-0gom/probe.py Section 3)","notes":"Closed: Implemented in PR #74 (merge commit 9c54b17). Added qualified_module_fallback in resolve.rs that bridges module-stripped stored qualified_name vs as-written qualified ref text. Three rounds of review-feedback applied (see .rivets-044i/review-decisions-round-{1,2,3}.md). Filed rivets-nkjd for the unrelated super:: filesystem-walk-vs-Rust-spec divergence surfaced during test design. Verification: +81 resolved refs, -65 unresolved qualified, -10% wall time, 0.00% phantom rate, 1532/1532 nextest pass.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-ycaq","dep_type":"discovered-from"}],"created_at":"2026-05-18T00:20:09.088221500Z","updated_at":"2026-05-19T01:29:47.149235900Z","closed_at":"2026-05-19T01:29:47.149169200Z"} +{"id":"rivets-zk2q","title":"Cross-File Reference Resolution","description":"Enable tethys to resolve references across files, so `tethys callers` and `tethys impact` work for symbols defined in other files.\n\nTwo phases:\n1. **Tree-sitter resolution (~85% accuracy)** - resolve via import paths ✅ COMPLETE\n2. **LSP refinement (~98% accuracy)** - delegate ambiguous cases to rust-analyzer/csharp-ls\n\nDesign doc: docs/plans/2026-01-28-cross-file-resolution.md","status":"closed","priority":1,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"## Phase 1 Status: COMPLETE (2026-01-28)\n\nAll Phase 1 tasks completed:\n- ✅ imports table added to schema\n- ✅ Import extraction for Rust (`use`) and C# (`using`)\n- ✅ Unresolved references stored with symbol_id = NULL\n- ✅ Pass 2 resolution implemented\n- ✅ Integration tests passing\n- ✅ Performance benchmarks documented in BENCHMARKS.md\n\n## Phase 2 Status: IN PROGRESS\n\nCompleted tasks:\n- ✅ rivets-8j9u: Add lsp-types dependency\n- ✅ rivets-h1va: Implement LspClient with JSON-RPC transport\n- ✅ rivets-nwwm: Implement LspProvider trait + RustAnalyzerProvider\n\nRemaining tasks:\n- rivets-oeu5: Add --lsp flag to CLI commands\n- rivets-k3mv: Integrate LSP into index --lsp\n- rivets-1dza: Integrate LSP into callers --lsp\n- rivets-6x7g: Add CSharpLsProvider\n- rivets-9o82: Add gated integration tests\n\nKey commit: feat(tethys): add LSP client infrastructure for Phase 2","external_ref":null,"dependencies":[],"created_at":"2026-01-28T17:26:53.486115898Z","updated_at":"2026-01-29T12:41:00.975901770Z","closed_at":"2026-01-29T12:41:00.975901770Z"} +{"id":"rivets-c0p","title":"Convert resilient_loading.rs to rstest","description":"Extract common fixtures for temp file creation. Parameterize error frequency tests with #[case].\n\nFile: crates/rivets-jsonl/tests/resilient_loading.rs","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Temp file creation extracted to fixture\n- Error frequency tests parameterized with #[case]\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:25.053725065Z","updated_at":"2025-11-29T02:09:12.153724641Z","closed_at":"2025-11-29T02:09:12.153724641Z"} +{"id":"rivets-tri","title":"Find domain name for Rivets","description":"Research and select an appropriate domain name for the Rivets project. Use the domain name brainstormer skill to generate and evaluate potential domain options that align with the project's identity as a Rust-based project tracking system.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"## Domain Name Research for Rivets (Updated 2025-11-30)\n\n### Project Context\n- Rivets is a Rust-based issue tracking system using JSONL storage\n- CLI-first developer tool\n- Human-readable, version-control friendly\n- Alternative to traditional issue trackers\n\n---\n\n## DECISION: rivets.rs\n\nSelected **rivets.rs** as the official domain for the Rivets project.\n\n### Why This Choice\n- Perfect alignment with Rust ecosystem (.rs = Rust file extension)\n- Follows precedent of other Rust projects (docs.rs, lib.rs, crates.io)\n- Short, memorable, and brandable\n- Available via Serbian registrars (ISTanCo, EuroDNS, etc.)\n- ~€10-18/year pricing\n\n### Registration Notes\n- .rs domains available from: ISTanCo (istanco.rs), SkyHosting, EuroDNS, Gandi\n- No local presence required - open to worldwide registration\n- Note: Some corporate firewalls may block .rs TLD (Serbia)\n\n---\n\n## Research Summary (for reference)\n\n### Domains Checked - TAKEN\n- rivets.dev (Frontier.sh)\n- rivets.io (for sale)\n- rivets.com\n- getrivets.com\n- rivet.dev (Rivet stateful backends)\n\n### Alternative Options Considered\n- gorivets.dev, rivethq.dev, rivetcli.com (potentially available)\n- rivets.rs (SELECTED)","external_ref":null,"dependencies":[],"created_at":"2025-12-01T04:00:32.666023401Z","updated_at":"2025-12-07T23:36:51.172920061Z","closed_at":"2025-12-07T23:36:51.172920061Z"} +{"id":"rivets-mb5x","title":"Docs: Add documentation for MAX_VISUAL_DEPTH magic number","description":"Per M-DOCUMENTED-MAGIC guideline, the constant at line 855 lacks explanation:\n\n```rust\nconst MAX_VISUAL_DEPTH: usize = 10;\n```\n\n**Guideline states:**\n> Hardcoded magic values in production code must be accompanied by a comment outlining why this value was chosen.\n\n**Fix:**\n```rust\n/// Maximum indentation depth for tree visualization.\n/// Set to 10 to prevent extremely wide output on terminals while\n/// still showing meaningful hierarchy for most dependency trees.\nconst MAX_VISUAL_DEPTH: usize = 10;\n```","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","execute.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:35.134166836Z","updated_at":"2025-12-28T15:37:35.134166836Z","closed_at":null} +{"id":"rivets-epmj","title":"Discriminated NotFound variants in tethys::Error (e.g., PackageNotFound)","description":"Currently tethys::Error::NotFound(String) carries an ad-hoc payload format like 'package: foo', 'file: bar', 'symbol: baz'. The string content is a de facto protocol for distinguishing what was not found, but nothing in the type enforces or documents it.\n\nThis becomes a problem when rivets-mcp adds a tethys_coupling tool. If MCP needs to react differently to 'package not found' vs 'file not found' (e.g., suggest re-indexing for the former, suggest checking the path for the latter), it has to parse the string payload — a brittle implicit contract.\n\n**Fix:** Introduce discriminated variants on tethys::Error:\n- PackageNotFound(String)\n- (existing NotFound(String) stays as catch-all for legacy callers)\n\nUpdate get_package_coupling and cli/coupling.rs::run_detail_to to use PackageNotFound.\n\n**Why deferred:** Low urgency today because (a) no MCP consumer of get_package_coupling exists yet, (b) the string format isn't being parsed anywhere, (c) main.rs renders both variants the same way. The right time to fix is just before rivets-o4re wires up tethys_coupling.\n\nDiscovered during the durability review pass on PR #60.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["tethys","enhancement","error-handling"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T02:47:33.676226Z","updated_at":"2026-05-11T02:47:33.676226Z","closed_at":null} +{"id":"rivets-utkl","title":"Add tracing::debug to needs_update() placeholder","description":"needs_update() always returns Ok(true). Add a debug\\! log saying 'change detection not yet implemented, assuming stale' so automated callers can diagnose why re-indexing always runs. Flagged in PR #55 review.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:17:52.888212060Z","updated_at":"2026-03-20T22:17:52.888212060Z","closed_at":null} +{"id":"rivets-q3z","title":"Consider lazy storage initialization","description":"Currently `set_workspace()` creates the storage instance immediately. Consider lazy initialization where storage is only created on first actual use.\n\nThis could reduce startup overhead for multi-workspace scenarios where not all workspaces are actively used.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["future","optimization","performance"],"design":"**Current behavior:**\n- `set_workspace()` calls `create_storage()` immediately\n- Storage is cached but created eagerly\n\n**Potential optimization:**\nStore workspace config without initializing storage until first tool call needs it:\n\n```rust\nenum CachedWorkspace {\n Pending { db_path: PathBuf },\n Initialized { storage: Arc>> },\n}\n```\n\n**Trade-offs:**\n- Pro: Faster `set_context` calls\n- Pro: Less memory for unused workspaces\n- Con: First tool call per workspace has latency\n- Con: More complex code\n\n**When to consider:**\nProfile first - may not matter for typical usage patterns.","acceptance_criteria":"- [ ] Profile to determine if eager initialization is actually a bottleneck\n- [ ] If needed, implement lazy initialization pattern\n- [ ] Ensure first-use latency is acceptable\n- [ ] No regression for single-workspace usage","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-29T04:41:25.388695600Z","updated_at":"2025-11-29T04:41:25.388695600Z","closed_at":null} +{"id":"rivets-1dza","title":"Integrate LSP into callers --lsp","description":"When `--lsp` flag is set for callers query:\n\n1. Return stored references (from DB)\n2. Additionally, ask LSP find_references for the symbol\n3. Merge results, deduplicate\n\nThis catches callers that tree-sitter couldn't resolve during indexing.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-k3mv","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:29:02.890552913Z","updated_at":"2026-01-29T01:34:56.775465920Z","closed_at":"2026-01-29T01:34:56.775465920Z"} {"id":"rivets-v3s","title":"Beads CLI command parity","description":"Implement missing commands and flags from the Beads CLI to achieve feature parity. This epic tracks the high-priority gaps identified in the CLI comparison.","status":"closed","priority":1,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-12-28T00:22:08.108387219Z","updated_at":"2025-12-28T05:17:28.442189690Z","closed_at":"2025-12-28T05:17:28.442189690Z"} +{"id":"rivets-02t","title":"Implement modification tools (create, update, close, dep)","description":"Implement issue modification tools:\n- create(title, description, priority, type, ...) - Create issue\n- update(issue_id, status, priority, ...) - Update fields\n- close(issue_id, reason) - Mark complete (update status to Closed)\n- dep(issue_id, depends_on_id, dep_type) - Add dependency","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] create tool creates issue and saves\n- [ ] update tool modifies fields correctly\n- [ ] close tool sets status to Closed\n- [ ] dep tool adds dependency with cycle detection\n- [ ] Unit tests: each tool with MockStorage (happy path + error cases)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-6u6","dep_type":"blocks"},{"depends_on_id":"rivets-vdi","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:28.277217069Z","updated_at":"2025-11-30T01:19:22.951737809Z","closed_at":"2025-11-30T01:19:22.951737809Z"} +{"id":"rivets-cxq","title":"Add priority constants (M-DOCUMENTED-MAGIC)","description":"Priority validation in domain/mod.rs uses magic number `4` without named constants, violating the M-DOCUMENTED-MAGIC guideline.\n\nLocations:\n- Line 133: `if self.priority > 4`\n- Line 269: `if self.priority > 4`\n- Line 500-512: Test using raw numbers 0..=4\n\nCurrent Issues:\n- Magic number repeated in multiple places\n- No single source of truth for valid priority range\n- Unclear to readers what the valid range is","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["code-quality","documentation","refactoring"],"design":"Add public constants in domain/mod.rs after MAX_TITLE_LENGTH (line 207):\n\n```rust\n/// Minimum priority value (highest urgency)\npub const MIN_PRIORITY: u8 = 0;\n\n/// Maximum priority value (lowest urgency)\npub const MAX_PRIORITY: u8 = 4;\n```\n\nUpdate validation code:\n\n```rust\nif self.priority > MAX_PRIORITY {\n return Err(format!(\n \"Priority must be in range {}-{} (got {})\",\n MIN_PRIORITY, MAX_PRIORITY, self.priority\n ));\n}\n```\n\nUpdate tests to use constants.","acceptance_criteria":"- [ ] MIN_PRIORITY and MAX_PRIORITY constants defined\n- [ ] All validation code uses constants instead of magic numbers\n- [ ] Error messages reference constants\n- [ ] Tests use constants (e.g., `for priority in MIN_PRIORITY..=MAX_PRIORITY`)\n- [ ] Constants are documented\n- [ ] All tests pass","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-27T22:48:58.565187568Z","updated_at":"2025-11-28T15:00:31.345213766Z","closed_at":"2025-11-28T15:00:31.345213766Z"} +{"id":"rivets-dve","title":"Add test-util feature for MockStorage (M-TEST-UTIL)","description":"MockStorage in storage/mod.rs (lines 284-390) should be feature-gated to allow downstream crates to use it for testing their own code that depends on the IssueStorage trait.\n\nCurrent State:\n- MockStorage is only available with #[cfg(test)]\n- Downstream crates cannot access it for their own tests\n\nImpact:\n- Makes library harder to use for external consumers who need to test code using IssueStorage\n- Forces users to write their own mocks","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["code-quality","public-api","testing"],"design":"Add a `test-util` feature to Cargo.toml:\n\n```toml\n[features]\ntest-util = []\n```\n\nMove MockStorage to public API under feature gate:\n\n```rust\n#[cfg(any(test, feature = \"test-util\"))]\npub struct MockStorage;\n\n#[cfg(any(test, feature = \"test-util\"))]\nimpl MockStorage {\n pub fn new() -> Self { Self }\n}\n```\n\nUpdate documentation to explain the feature and its usage.","acceptance_criteria":"- [ ] `test-util` feature added to Cargo.toml\n- [ ] MockStorage is `pub` and available with `feature = \"test-util\"`\n- [ ] MockStorage includes constructor and helper methods\n- [ ] Documentation updated with usage example\n- [ ] Existing tests still pass","notes":"PR #20 created: https://github.com/dwalleck/rivets/pull/20","external_ref":null,"dependencies":[],"created_at":"2025-11-27T22:48:52.858336885Z","updated_at":"2025-12-07T23:55:59.025020672Z","closed_at":"2025-12-07T23:55:59.025020672Z"} +{"id":"rivets-37y","title":"Implement labels and comments system","description":"Implement label tagging and commenting functionality for issues, with many-to-many relationships and filtering support.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads labels.go and comments functionality:\n\n**Labels**:\n- Many-to-many relationship (issues ↔ labels)\n- Operations: add, remove, list labels on issue\n- Query by labels (AND / OR semantics)\n- Label management (create, delete, rename)\n\n**Comments**:\n- One-to-many (issue → comments)\n- Fields: author, created_at, content\n- Operations: add comment, list comments\n- Threaded comments (optional future)\n\n**Schema**:\n```sql\nlabels (\n id INTEGER PRIMARY KEY,\n issue_id TEXT NOT NULL,\n label TEXT NOT NULL,\n UNIQUE(issue_id, label)\n)\n\ncomments (\n id INTEGER PRIMARY KEY,\n issue_id TEXT NOT NULL,\n author TEXT,\n created_at TIMESTAMP,\n content TEXT NOT NULL\n)\n```\n\n**CLI Commands**:\n- `rivets label add `\n- `rivets label remove `\n- `rivets comment add `\n- `rivets comment list `","acceptance_criteria":"- Label add/remove operations work\n- Comment add/list operations work\n- Query issues by labels (--label, --labels-any)\n- Labels embedded in JSONL export\n- Comments embedded in JSONL export\n- Foreign key CASCADE on issue deletion\n- Unit tests for label/comment CRUD","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:47.355222934Z","updated_at":"2025-11-17T22:16:47.355222934Z","closed_at":null} +{"id":"rivets-cr9","title":"Research beads project and create tasks to implement the project in rust as rivets","description":"We are going to convert the project beads to rust and call it rivets. You should have context to the beads project. Create all needed research and development tasks that you become aware of in beads so that we can begin development","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":[],"design":"## Beads Research Complete\n\nConducted comprehensive analysis of beads codebase (~19,243 LOC storage layer alone). \n\n**Key Findings**:\n- Hash-based ID generation (SHA256 + base36, adaptive 4-6 chars)\n- 4 dependency types (blocks, related, parent-child, discovered-from) \n- Recursive CTE algorithms for cycle detection and ready work\n- SQLite with 14 tables, WAL mode, foreign key constraints\n- JSONL import/export with content hash deduplication\n- RPC system (JSON-RPC over Unix sockets)\n- Background daemon with auto-sync\n\n**Created 13 Implementation Tasks**:\n1. rivets-x1e: Hash-based ID generation\n2. rivets-0gc: SQLite storage layer \n3. rivets-6op: Dependency system with cycle detection\n4. rivets-qeb: Ready work algorithm\n5. rivets-x51: JSONL import/export\n6. rivets-bsp: Core CLI commands\n7. rivets-xy9: Configuration system\n8. rivets-bi2: RPC system\n9. rivets-9mh: Daemon process\n10. rivets-37y: Labels and comments\n11. rivets-6tl: Filtering and queries\n12. rivets-4l2: Init command\n13. rivets-azn: Architecture documentation\n\nFull analysis documented in agent output.","acceptance_criteria":"✓ Beads codebase explored thoroughly\n✓ Core features documented (ID generation, dependencies, ready work, etc.)\n✓ Key algorithms identified (cycle detection, hash IDs, recursive CTEs)\n✓ 13 implementation tasks created in beads\n✓ Tasks cover all major beads functionality\n✓ Each task has design notes from beads source","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T21:01:56.182254064Z","updated_at":"2025-11-17T22:17:10.318274001Z","closed_at":"2025-11-17T22:17:10.318274001Z"} +{"id":"rivets-xwcy","title":"Docs: Add documentation for MAX_LABEL_LENGTH magic number","description":"In validators.rs (line 153), the constant lacks explanation:\n\n```rust\n/// Maximum length for labels\npub const MAX_LABEL_LENGTH: usize = 50;\n```\n\nPer M-DOCUMENTED-MAGIC guideline:\n> Hardcoded magic values in production code must be accompanied by a comment outlining why this value was chosen.\n\n**Fix:** Add reasoning for why 50 was chosen:\n```rust\n/// Maximum length for labels.\n///\n/// Set to 50 characters to:\n/// - Keep labels concise and readable in CLI output\n/// - Allow for descriptive multi-word labels (e.g., \"needs-security-review\")\n/// - Align with common tag/label length limits in other issue trackers\npub const MAX_LABEL_LENGTH: usize = 50;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","validators.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:45:19.530764463Z","updated_at":"2025-12-28T15:45:19.530764463Z","closed_at":null} {"id":"rivets-9d4g","title":"Fix: Use MAX_PRIORITY constant instead of hardcoded 4 in validation","description":"In domain/mod.rs, the validate() functions use hardcoded `4` instead of the `MAX_PRIORITY` constant:\n\n**Line 133 (Issue::validate):**\n```rust\nif self.priority > 4 { // Should use MAX_PRIORITY\n return Err(format!(\"Priority {} exceeds maximum of 4\", self.priority));\n}\n```\n\n**Line 336 (NewIssue::validate):**\n```rust\nif self.priority > 4 { // Should use MAX_PRIORITY\n return Err(...);\n}\n```\n\nBut the constant is defined at line 274:\n```rust\npub const MAX_PRIORITY: u8 = 4;\n```\n\n**Fix:** Replace hardcoded `4` with `MAX_PRIORITY` for consistency and maintainability.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["bug","consistency","domain"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:50:58.776992958Z","updated_at":"2025-12-28T21:28:52.791035223Z","closed_at":"2025-12-28T21:28:52.791035223Z"} +{"id":"rivets-43i","title":"Implement info command","description":"Add 'info' command to display database path, issue prefix, and system status. Should support --json flag for programmatic use. Example output: { \"database_path\": \".rivets/issues.jsonl\", \"issue_prefix\": \"rivets\" }","status":"closed","priority":1,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:39.082722928Z","updated_at":"2025-12-28T00:33:28.505227293Z","closed_at":"2025-12-28T00:33:28.505227293Z"} +{"id":"rivets-p7v","title":"Implement rivets CLI skeleton with basic command structure","description":"Create the initial implementation of the rivets CLI application including main.rs, CLI argument parsing with clap, and stub modules for commands and domain logic. Create a hello-world style application that compiles and runs as `bd`.","status":"closed","priority":1,"issue_type":"task","assignee":"Claude","labels":[],"design":"Based on rivets-kr3 design:\n- Create src/main.rs with CLI entry point\n- Create src/cli.rs with clap-based argument parsing\n- Create src/commands/ module with stub command implementations\n- Create src/domain/ module for issue tracking domain types\n- Create src/storage.rs stub for storage layer\n- Create src/config.rs stub for configuration\n- Create src/error.rs for CLI-specific errors\n- Ensure binary is named 'rivets' in Cargo.toml\n- Add basic integration test that runs the CLI","acceptance_criteria":"- Application compiles and runs as `rivets` command\n- CLI argument parsing works with clap\n- Help message displays correctly with `rivets --help`\n- All module stubs created with proper exports\n- Basic integration test runs the CLI successfully\n- `cargo test --package rivets` passes\n- `cargo run --package rivets` executes without errors","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-kr3","dep_type":"blocks"}],"created_at":"2025-11-17T21:45:33.081597765Z","updated_at":"2025-11-17T22:08:32.126908789Z","closed_at":"2025-11-17T22:08:32.126908789Z"} {"id":"rivets-q82","title":"Apply Microsoft Pragmatic Rust Guidelines to rivets crate","description":"Code review findings from analyzing the rivets crate against the Pragmatic Rust Guidelines.\n\n**Modules reviewed:**\n- cli/ (args.rs, execute.rs, mod.rs, types.rs, validators.rs)\n- commands/ (init.rs, mod.rs)\n- domain/ (mod.rs)\n- storage/in_memory/ (graph.rs, inner.rs, jsonl.rs, mod.rs, sorting.rs, trait_impl.rs)\n\nThese issues address code quality, maintainability, DRY violations, magic number documentation, and adherence to Rust best practices.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-12-28T15:36:35.212044095Z","updated_at":"2025-12-28T15:52:28.492032788Z","closed_at":null} -{"id":"rivets-7ag","title":"Add atomic append_notes operation to IssueStorage trait","description":"Address TOCTOU race condition in close command by adding atomic note appending.\n\nLocation: `execute.rs:241-263`\n\nCurrent behavior:\n- Two separate operations: `get_issue()` then `update_issue()` with appended notes\n- TOCTOU window exists between read and write\n- Documented as acceptable for single-user CLI\n\nEnhancement for multi-user scenarios:\n- Add `append_notes(&self, id: &IssueId, notes: &str)` method to `IssueStorage` trait\n- Implement atomic append in storage backends\n- Update close command to use atomic operation\n\nThis would eliminate the race condition for future concurrent access scenarios.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["pr-feedback","storage"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T17:45:08.387504813Z","updated_at":"2025-11-30T17:45:08.387504813Z","closed_at":null} -{"id":"rivets-cz85","title":"Compute module_path for symbols","description":"The schema has a `module_path` column (indexed) but symbols are stored with empty strings. See `lib.rs:624` and `lib.rs:753`.\n\nThe existing `resolve_module_path` in `resolver.rs` computes **file paths**, not **module paths** (like `crate::storage::issue`). Need to compute the actual Rust module path from the file location.\n\nThis would enable queries like \"find all symbols in module X\".","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-m4wt","dep_type":"blocks"}],"created_at":"2026-01-30T00:17:57.789103820Z","updated_at":"2026-02-01T23:54:48.228483332Z","closed_at":"2026-02-01T23:54:48.228483332Z"} -{"id":"rivets-6mi","title":"Add tracing and logging","description":"Integrate tracing for observability:\n- tracing-subscriber setup\n- Request/response logging\n- Debug logging for context and storage operations","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:39.610630301Z","updated_at":"2025-12-08T00:00:49.802877931Z","closed_at":"2025-12-08T00:00:49.802877931Z"} -{"id":"rivets-dgt","title":"Define Warning types and collection system","description":"Define the Warning enum and WarningCollector for tracking non-fatal errors during JSONL loading. This enables resilient loading that continues despite malformed data.\n\nBased on the research API design and existing LoadWarning pattern from in_memory.rs.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\n#[derive(Debug, Clone)]\npub enum Warning {\n MalformedJson { line_number: usize, error: String },\n SkippedLine { line_number: usize, reason: String },\n}\n\npub struct WarningCollector {\n warnings: Arc>>,\n}\n\nimpl WarningCollector {\n pub fn new() -> Self {\n Self {\n warnings: Arc::new(Mutex::new(Vec::new())),\n }\n }\n \n pub fn add(&self, warning: Warning) {\n self.warnings.lock().unwrap().push(warning);\n }\n \n pub fn into_warnings(self) -> Vec {\n Arc::try_unwrap(self.warnings)\n .unwrap_or_else(|arc| (*arc.lock().unwrap()).clone())\n }\n}\n```","acceptance_criteria":"- Warning enum defined with MalformedJson and SkippedLine variants\n- WarningCollector struct with thread-safe add()\n- into_warnings() method to extract collected warnings\n- Clone and Debug impls for Warning\n- Unit tests for warning collection\n- Compiles without errors","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-3r7","dep_type":"blocks"}],"created_at":"2025-11-27T23:15:49.553769651Z","updated_at":"2025-11-28T01:42:18.645501372Z","closed_at":"2025-11-28T01:42:18.645501372Z"} -{"id":"rivets-dp5","title":"Add nu-ansi-term dependency for colored output","description":"Add nu-ansi-term crate to workspace dependencies for terminal color support.\n\nThis is the foundation for all colored output features.","status":"open","priority":1,"issue_type":"task","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:29.450981296Z","updated_at":"2025-11-30T18:34:29.450981296Z","closed_at":null} -{"id":"rivets-nz52","title":"Observability & Logging","description":"Improve debugging and operational visibility: file-based logging, migrate println! to tracing, structured log fields.","status":"open","priority":3,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:31.027113094Z","updated_at":"2026-02-21T02:28:31.027113094Z","closed_at":null} -{"id":"rivets-08u","title":"Implement map transformations for JsonlQuery","description":"Add map() method to JsonlQuery for transforming records during streaming.\n\nThis enables type transformations and projections on JSONL data.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\npub struct JsonlQuery {\n predicates: Vec bool + Send + Sync>>,\n transform: Option U + Send + Sync>>,\n _phantom: PhantomData<(T, U)>,\n}\n\nimpl JsonlQuery\nwhere\n T: DeserializeOwned + 'static,\n U: 'static,\n{\n pub fn map(self, transform: F) -> JsonlQuery\n where\n F: Fn(U) -> V + Send + Sync + 'static,\n V: 'static,\n {\n JsonlQuery {\n predicates: self.predicates,\n transform: Some(Box::new(move |t| transform(\n self.transform.as_ref()\n .map(|f| f(t))\n .unwrap_or(t)\n ))),\n _phantom: PhantomData,\n }\n }\n}\n```\n\nNote: This requires refactoring JsonlQuery to support type transformations.","acceptance_criteria":"- map() method implemented\n- Supports type transformations\n- Can chain multiple map() calls\n- Transformations applied during streaming\n- Unit tests verify transformations\n- Integration test with filter + map pipeline","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-83j","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T23:17:01.111641956Z","updated_at":"2025-11-27T23:17:01.111641956Z","closed_at":null} -{"id":"rivets-e8t","title":"Implement ASCII dependency tree rendering","description":"Implement ASCII art dependency tree visualization:\n\n```\nrivets-abc \"Main Epic\"\n├── rivets-def \"Subtask 1\" [>] P1\n│ └── rivets-ghi \"Dependency\" [✓] P2\n└── rivets-jkl \"Subtask 2\" [ ] P2\n ├── rivets-mno \"Blocked by\" [X] P1\n └── rivets-pqr \"Related\" [ ] P3\n```\n\nFeatures:\n- Box-drawing characters for tree structure\n- Color-coded status and priority\n- Collapsible for large trees\n- Support for all dependency types (blocks, related, parent-child, discovered-from)","status":"open","priority":1,"issue_type":"feature","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:52.660367062Z","updated_at":"2025-11-30T18:34:52.660367062Z","closed_at":null} -{"id":"rivets-9po8","title":"Rivets MCP Server Hardening","description":"Improve rivets-mcp reliability: cache eviction, lock contention, tracing visibility, and integration test coverage.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:29.669076471Z","updated_at":"2026-02-21T02:28:29.669076471Z","closed_at":null} -{"id":"rivets-limz","title":"Audit tethys tracing log messages for structured-field consistency","description":"Project convention (per CLAUDE.md 'Structured Logging with tracing') is structured fields, not string interpolation in the message. PR #63 added some logs that bake the operation name into prose ('compute_dependencies: file not in any known crate; skipping') rather than using a structured field like operation = \"compute_dependencies\". Codebase has a mix of both styles.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Concrete examples flagged in PR #63 review\n\nThe three new trace/debug sites added in PR #63 use prose-style operation names:\n\n- resolve.rs::resolve_refs_for_file: 'File not in any known crate; skipping Pass-2-imports'\n- indexing.rs::compute_dependencies: 'compute_dependencies: file not in any known crate; skipping'\n- indexing.rs::compute_dependencies_from_stored: 'compute_dependencies_from_stored: file not in any known crate; skipping'\n\nProject convention would prefer:\n\n```rust\ndebug!(\n operation = \"compute_dependencies\",\n file = %current_file.display(),\n \"File not in any known crate; skipping\"\n);\n```\n\n## Scope\n\nThis is broader than just PR #63 sites. The whole tracing surface in tethys should be audited. CLAUDE.md provides examples of the desired pattern; grep for tracing macros and identify call sites that don't match.\n\n## Why P4\n\nCosmetic. Doesn't affect correctness. Worth doing during a quiet maintenance window, not as part of a bug fix PR.","acceptance_criteria":"- [ ] Inventory all tracing call sites in crates/tethys/src/\n- [ ] For each, classify: matches structured-field convention OR uses string interpolation\n- [ ] Update string-interpolated sites to use structured fields per CLAUDE.md\n- [ ] Bonus: add a lint or test that flags the pattern (optional)","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:40:44.092737600Z","updated_at":"2026-05-12T23:40:44.092737600Z","closed_at":null} -{"id":"rivets-pex","title":"Add progress spinners for long operations","description":"Add progress spinners for operations that may take time:\n- List command with many issues\n- Dependency tree calculation\n- Ready work calculation\n- Import/export operations\n\nShow elapsed time for operations >500ms.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:25.358698235Z","updated_at":"2025-11-30T18:35:25.358698235Z","closed_at":null} -{"id":"rivets-4im","title":"Refactor: Extract notes-building helper for close/reopen","description":"The pattern for building notes with 'Closed: {reason}' or 'Reopened: {reason}' is duplicated in execute_close (lines 431-457) and execute_reopen (lines 527-553).\n\n**Recommendation:** Extract into helper:\n```rust\nfn append_note(existing: Option<&str>, new_note: &str) -> String {\n match existing {\n Some(notes) => format!(\"{}\\n\\n{}\", notes, new_note),\n None => new_note.to_string(),\n }\n}\n```","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:29.340092547Z","updated_at":"2025-12-28T17:01:02.387365270Z","closed_at":"2025-12-28T17:01:02.387365270Z"} -{"id":"rivets-2y50","title":"tethys: compute and store content hash for indexed files","description":"Content hash is currently passed as None when indexing files (in both lib.rs and batch_writer.rs). Implement content hash computation to enable cache invalidation based on file content rather than mtime alone.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:02.264719080Z","updated_at":"2026-03-19T01:41:02.264719080Z","closed_at":null} +{"id":"rivets-0gc","title":"Design and implement storage trait abstraction","description":"Create the Storage trait abstraction that will allow multiple backend implementations (in-memory, JSONL, PostgreSQL). This is the foundation for the phased storage approach.\n\nPhase 1: Storage Trait + In-Memory Graph (MVP)\n- Define Storage trait with all required methods\n- In-memory implementation using HashMap + petgraph\n- JSONL persistence as backup/export format\n\nPhase 2: PostgreSQL with Recursive CTEs (Production)\n- PostgreSQL implementation with recursive CTEs for dependency queries\n- Migration path from in-memory to PostgreSQL\n\nThis task focuses on the trait definition and abstraction layer.\n\n## Clarifications\n\n### Session 2025-11-17\n\n- Q: Should the IssueStorage trait be async or synchronous? → A: Make trait async from start using async-trait crate (future-proof for PostgreSQL, enables non-blocking I/O)\n- Q: How should storage operations handle concurrent access in the in-memory backend? → A: Wrap in Arc> for thread-safe access (standard pattern, simple for MVP)\n- Q: How should JSONL file corruption be handled during load? → A: Skip invalid lines with warning logs, continue loading valid entries (practical, allows recovery from partial corruption)\n- Q: Which tokio runtime should the CLI use? → A: Current-thread runtime (simpler for CLI, lower overhead, easier debugging)\n- Q: What should happen to dependencies when an issue is deleted? → A: Delete issue's dependencies, fail if issue has dependents with warning (safe, prevents orphaned references)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"## Storage Trait Design\n\n### Core Trait Definition\n\n**Async trait using async-trait crate:**\n\n```rust\nuse async_trait::async_trait;\n\n#[async_trait]\npub trait IssueStorage: Send + Sync {\n // CRUD operations\n async fn create(&mut self, issue: NewIssue) -> Result;\n async fn get(&self, id: &IssueId) -> Result>;\n async fn update(&mut self, id: &IssueId, updates: IssueUpdate) -> Result;\n async fn delete(&mut self, id: &IssueId) -> Result<()>;\n \n // Dependency management\n async fn add_dependency(&mut self, from: &IssueId, to: &IssueId, dep_type: DependencyType) -> Result<()>;\n async fn remove_dependency(&mut self, from: &IssueId, to: &IssueId) -> Result<()>;\n async fn get_dependencies(&self, id: &IssueId) -> Result>;\n async fn get_dependents(&self, id: &IssueId) -> Result>;\n async fn has_cycle(&self, from: &IssueId, to: &IssueId) -> Result;\n \n // Queries\n async fn list(&self, filter: &IssueFilter) -> Result>;\n async fn ready_to_work(&self, filter: Option<&IssueFilter>) -> Result>;\n async fn blocked_issues(&self) -> Result)>>;\n \n // Batch operations for JSONL import/export\n async fn import_issues(&mut self, issues: Vec) -> Result<()>;\n async fn export_all(&self) -> Result>;\n \n // Persistence (for backends that need explicit save)\n async fn save(&self) -> Result<()>;\n}\n```\n\n**Note**: In-memory implementation will use blocking operations wrapped in `async` blocks for Phase 1. PostgreSQL will use true async I/O with sqlx in Phase 2. The save() method allows backends to persist state - InMemoryStorage saves to JSONL, PostgreSQL is a no-op.\n\n### Async Runtime Configuration\n\n**Use current-thread tokio runtime for CLI:**\n\n```rust\n// main.rs\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<()> {\n // CLI initialization and execution\n let cli = Cli::parse();\n let config = Config::load().await?;\n let storage = create_storage(config.storage_backend).await?;\n \n cli.execute(storage).await\n}\n```\n\n**Rationale**:\n- Simpler runtime for sequential CLI operations\n- Lower memory and CPU overhead\n- Easier to debug with single-threaded execution\n- Sufficient for I/O-bound storage operations\n- Can upgrade to multi-threaded if parallelism becomes necessary\n\n### Concurrency Model\n\n**Thread-safe wrapper for in-memory storage:**\n\n```rust\n// Storage implementation (not thread-safe)\nstruct InMemoryStorageInner {\n issues: HashMap,\n graph: DiGraph,\n node_map: HashMap,\n}\n\n// Thread-safe wrapper\npub type InMemoryStorage = Arc>;\n\n// CLI usage\npub struct App {\n storage: Box, // Already thread-safe via Arc>\n config: Config,\n}\n```\n\n**Rationale**: \n- Simple standard pattern for Rust CLI applications\n- Prevents data races with minimal complexity\n- Easy to test and reason about\n- PostgreSQL backend will handle concurrency at database level\n\n### Delete Semantics & Referential Integrity\n\n**Safe deletion with dependent check:**\n\n```rust\nasync fn delete(&mut self, id: &IssueId) -> Result<()> {\n // Check for dependents\n let dependents = self.get_dependents(id).await?;\n if !dependents.is_empty() {\n let dependent_ids: Vec<_> = dependents.iter()\n .map(|d| d.issue_id.to_string())\n .collect();\n return Err(Error::HasDependents {\n issue_id: id.clone(),\n dependents: dependent_ids,\n message: format!(\n \"Cannot delete {}: {} other issues depend on it. Delete or update those issues first.\",\n id, dependents.len()\n )\n });\n }\n \n // Remove all outgoing dependencies from this issue\n let dependencies = self.get_dependencies(id).await?;\n for dep in dependencies {\n self.remove_dependency(id, &dep.depends_on_id).await?;\n }\n \n // Remove issue from storage\n self.issues.remove(id);\n if let Some(node_idx) = self.node_map.remove(id) {\n self.graph.remove_node(node_idx);\n }\n \n Ok(())\n}\n```\n\n**Strategy**: \n- Fail with clear error if issue has dependents\n- Error message lists dependent issue IDs\n- Remove all outgoing dependencies automatically\n- Prevents orphaned dependency references\n- User can use `--force` flag to batch delete dependents first (future enhancement)\n\n### Error Handling & Recovery\n\n**JSONL Corruption Handling:**\n\n```rust\npub async fn load_from_jsonl(path: &Path) -> Result<(Self, Vec)> {\n let file = BufReader::new(File::open(path).await?);\n let mut storage = Self::new();\n let mut errors = Vec::new();\n \n for (line_num, line) in file.lines().enumerate() {\n match line {\n Ok(content) => {\n match serde_json::from_str::(&content) {\n Ok(issue) => {\n if let Err(e) = storage.import_issue(issue).await {\n errors.push(LoadError::ImportFailed { line: line_num, reason: e });\n log::warn!(\"Line {}: Failed to import issue: {}\", line_num, e);\n }\n }\n Err(e) => {\n errors.push(LoadError::InvalidJson { line: line_num, reason: e });\n log::warn!(\"Line {}: Invalid JSON, skipping: {}\", line_num, e);\n }\n }\n }\n Err(e) => {\n errors.push(LoadError::ReadError { line: line_num, reason: e });\n log::warn!(\"Line {}: Read error, skipping: {}\", line_num, e);\n }\n }\n }\n \n if !errors.is_empty() {\n log::warn!(\"Loaded with {} errors/warnings. {} issues imported.\", \n errors.len(), storage.issue_count());\n }\n \n Ok((storage, errors))\n}\n```\n\n**Strategy**: Skip corrupted lines, log warnings, continue loading. Returns both the loaded storage and collection of errors for caller to handle.\n\n### Backend Factory\n\n```rust\npub enum StorageBackend {\n InMemory,\n Jsonl(PathBuf),\n PostgreSQL(String), // connection string\n}\n\npub async fn create_storage(backend: StorageBackend) -> Result> {\n match backend {\n StorageBackend::InMemory => Ok(Box::new(InMemoryStorage::new())),\n StorageBackend::Jsonl(path) => {\n let (storage, errors) = InMemoryStorage::load_from_jsonl(&path).await?;\n if !errors.is_empty() {\n eprintln!(\"Warning: Loaded with {} errors. Check logs.\", errors.len());\n }\n Ok(Box::new(storage))\n }\n StorageBackend::PostgreSQL(conn_str) => {\n Ok(Box::new(PostgresStorage::new(&conn_str).await?))\n }\n }\n}\n```\n\n### Architecture Benefits\n\n1. **Testability**: Use in-memory storage for fast unit tests\n2. **Flexibility**: Easy to add new backends (SQLite, cloud storage, etc.)\n3. **Performance**: Can optimize each backend independently\n4. **Migration**: Clear path from simple to complex storage\n5. **Future-proof**: Async trait enables efficient PostgreSQL and network I/O\n6. **Thread-safety**: Arc> wrapper prevents data races\n7. **Resilience**: Graceful degradation on partial JSONL corruption\n8. **Simplicity**: Current-thread runtime reduces complexity for CLI use case\n9. **Data integrity**: Safe deletion prevents orphaned dependency references\n10. **Explicit persistence**: save() method allows control over when data is persisted","acceptance_criteria":"- Storage trait defined with async methods using async-trait in src/storage/mod.rs\n- Backend factory pattern implemented for creating storage instances (async)\n- Trait includes save() method for explicit persistence (async fn save(&self) -> Result<()>)\n- Trait documentation includes usage examples and backend comparison\n- All trait methods have clear error semantics\n- Trait is object-safe (can use Box)\n- Unit tests for trait object usage with tokio test runtime\n- Documentation examples compile and run\n- async-trait dependency added to Cargo.toml\n- tokio dependency with current_thread flavor configured in main.rs\n- JSONL load handles corruption gracefully (skip invalid lines, log warnings)\n- Load function returns both storage and error collection\n- Delete operation checks for dependents, fails with clear error message if found\n- Delete operation removes all outgoing dependencies automatically\n- Error type includes HasDependents variant with issue list","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:15:21.281073361Z","updated_at":"2025-11-18T00:26:22.170944324Z","closed_at":"2025-11-18T00:26:22.170944324Z"} +{"id":"rivets-ed9y","title":"tethys: replace Mutex with a pool; collapse get_package_coupling round-trips","description":"get_package_coupling currently does 3 sequential DB round-trips:\n 1. SELECT target package row from arch_coupling JOIN arch_packages\n 2. SELECT outgoing neighbors via fetch_neighbors(OUTGOING_SQL)\n 3. SELECT incoming neighbors via fetch_neighbors(INCOMING_SQL)\n\nThe reason 2 and 3 can't share the connection guard with 1 is the Mutex in db/mod.rs is non-reentrant — fetch_neighbors re-acquires the lock internally, so the outer guard from step 1 must drop first. This was the mutex bug caught and documented during PR 60 development.\n\nReviewer flagged in PR 60 round 5: \"If the Mutex is ever replaced with a connection pool (e.g. r2d2 or a custom pool), this could be collapsed into fewer round-trips.\"\n\nThis is architecturally the right time to address it — the connection-pool migration would benefit other code paths too (concurrent queries from MCP tools, parallel architecture phase passes, etc.).\n\nScope:\n1. Add a connection pool (r2d2-sqlite or similar) wrapping rusqlite::Connection\n2. Either re-architect get_package_coupling to hold one connection across all three queries (single transaction, isolation guarantees) OR fold all three queries into one with UNION + JOIN\n3. Remove the documented scope-block workaround in get_package_coupling","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] db/mod.rs uses a connection pool instead of Mutex\n- [ ] get_package_coupling no longer needs the explicit-drop scope block\n- [ ] Benchmark before/after for get_package_coupling on a workspace of >50 crates\n- [ ] All existing tests still pass; the mutex non-reentrancy test stays green\n- [ ] Document the pool sizing rationale in db/mod.rs","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:21:42.434028300Z","updated_at":"2026-05-11T21:21:42.434028300Z","closed_at":null} +{"id":"rivets-f5f","title":"Add error handling and MCP error responses","description":"Implement proper error handling:\n- Map rivets errors to MCP error responses\n- Structured error types for MCP\n- Helpful error messages for common failures","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:33.957289146Z","updated_at":"2025-11-29T14:41:30.508649115Z","closed_at":"2025-11-29T14:41:30.508649115Z"} +{"id":"rivets-nqbg","title":"Warn at parse time when CrateInfo bin_paths have divergent parents","description":"CrateInfo::src_root() falls back to bin_paths.first() for bin-only crates. If a crate has multiple [[bin]] entries with paths in different parent directories (e.g., src/bin/a.rs and tools/b.rs), 'first' is non-deterministic ordering-wise and the chosen src_root won't correctly resolve modules under both bins.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Scenario\n\nA Cargo.toml like:\n\n```toml\n[[bin]]\nname = \"a\"\npath = \"src/bin/a.rs\"\n\n[[bin]]\nname = \"b\"\npath = \"tools/b.rs\"\n```\n\nProduces CrateInfo with bin_paths in declaration order. src_root() picks the first one's parent (`src/bin`). If the user's code uses crate:: paths referencing modules under tools/, those won't resolve.\n\n## Empirical occurrence\n\nDoesn't exist in the rivets workspace (single-bin crates only). May exist in larger Cargo workspaces.\n\n## Proposed mitigation\n\nIn cargo.rs::parse_crate_from_manifest, after populating bin_paths, check whether all entries have the same parent directory. If not, emit a warn! with the crate name and the divergent paths. Doesn't change resolution behavior (still picks first), just surfaces the ambiguity to the operator.\n\nOptionally: store a 'has_divergent_bin_parents' flag on CrateInfo and have src_root() return None or pick by some deterministic rule (lexicographic-first parent?). Out of scope for this issue without empirical evidence of harm.\n\n## Why P4\n\nNo known harm in any workspace tethys currently indexes. Defensive observability addition.","acceptance_criteria":"- [ ] In cargo.rs::parse_crate_from_manifest, after bin_paths population, check for divergent parents\n- [ ] If divergent, log warn! with crate name and the bin paths\n- [ ] Unit test in cargo.rs::tests with a multi-bin CrateInfo where bins have different parents\n- [ ] Decide whether to also change src_root() behavior or leave it picking first","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:40:54.732817Z","updated_at":"2026-05-12T23:40:54.732817Z","closed_at":null} +{"id":"rivets-c540","title":"tethys: fuzzy fallback for --package suggestions (edit distance)","description":"tethys coupling --package uses substring match (case-insensitive) for the \"Did you mean:\" suggestions when the name isn't found. A user who types rivets_mcp (underscore) for rivets-mcp (hyphen), or rivetsmcp (no separator), gets no suggestion. cargo's own suggestion logic uses Levenshtein-distance fallback in this case.\n\nReviewer flagged this as \"known limitation, could be quality-of-life follow-up\" across multiple PR 60 review rounds.\n\nScope: extend collect_suggestions in crates/tethys/src/cli/coupling.rs to fall back to edit-distance ranking when substring match yields nothing. Use the strsim crate (already in the rust ecosystem) or a hand-rolled DP impl.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Substring match still used first (preserves current behavior for partial matches)\n- [ ] Edit-distance fallback when substring yields zero results\n- [ ] Configurable distance threshold (default ~3)\n- [ ] Unit tests covering: snake-vs-kebab mismatch, missing separator, single-char typo, very distant name yields no suggestion","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:21:28.989377900Z","updated_at":"2026-05-11T21:21:28.989377900Z","closed_at":null} +{"id":"rivets-wzgg","title":"tethys: add exhaustive-byte invariant test for PATH_PERCENT_ENCODE_SET","description":"PATH_PERCENT_ENCODE_SET in crates/tethys/src/lsp/transport.rs is a hand-maintained chain of 27 .add(b'X') calls. A typo (duplicate, missing entry) is silent — the existing format_uri_* tests only spot-check specific inputs (spaces, %20, Unicode).\n\nAdd a test that iterates 0u8..=127 and asserts: for every byte b, set.contains(b) == is_must_encode(b), where is_must_encode is the inverse RFC-3986 definition:\n !b.is_ascii_alphanumeric() && !matches!(b, b'-'|b'.'|b'_'|b'~'|b'/'|b':') || b.is_ascii_control()\n\n~10 lines, runs in microseconds, catches every list-drift bug at CI time.\n\nSurfaced by PR #64 round-3 review (type-design-analyzer).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test added to crates/tethys/src/lsp/transport.rs#tests module\n- [ ] Iterates 0..=127 and asserts set membership matches inverse definition\n- [ ] cargo nextest run -p tethys passes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:11.247389900Z","updated_at":"2026-05-13T02:25:11.247389900Z","closed_at":null} {"id":"rivets-vvp","title":"Add integration tests for new label and issue_type filters","description":"After adding label filter to list() and label/issue_type filters to ready(), add test cases to verify they work correctly.\n\nTest cases needed:\n- list() with label filter - returns only issues with matching label\n- ready() with label filter - returns only ready issues with matching label\n- ready() with issue_type filter - returns only ready issues of specified type\n- Combined filters (e.g., label + priority)\n\nThese should be added as new cases in the rstest parameterized tests.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1yi","dep_type":"blocks"},{"depends_on_id":"rivets-wfb","dep_type":"blocks"}],"created_at":"2025-11-29T23:57:51.415495689Z","updated_at":"2025-11-30T00:03:39.132309974Z","closed_at":"2025-11-30T00:03:39.132309974Z"} -{"id":"rivets-4il","title":"Create git merge driver for .automerge files","description":"Implement CLI command and git configuration for automatic CRDT merging during git merge operations.\n\nIncludes:\n- `rv merge ` subcommand\n- .gitattributes configuration for *.automerge files\n- Documentation for git merge driver setup","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:50.322609338Z","updated_at":"2025-12-23T04:45:22.910742278Z","closed_at":"2025-12-23T04:45:22.910742278Z"} -{"id":"rivets-vmb","title":"Evaluate rstest for rivets/src/domain/mod.rs and id_generation.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in domain/mod.rs and id_generation.rs.\n\nFiles:\n- crates/rivets/src/domain/mod.rs (lines ~397+)\n- crates/rivets/src/id_generation.rs (lines ~322+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:30.801627179Z","updated_at":"2025-11-29T01:30:50.015894878Z","closed_at":"2025-11-29T01:30:50.015894878Z"} -{"id":"rivets-byie","title":"Architecture analysis: package detection and coupling metrics (Ca/Ce/instability)","description":"Detect high-level project structure (packages and architectural layers) and compute coupling metrics between them. Useful for refactor planning, codebase health monitoring, and PR review.\n\n**Inspired by:** KiroGraph's architecture/coupling/package commands. The metrics (afferent coupling, efferent coupling, instability) are Robert C. Martin classics — well-established and immediately legible to seasoned engineers.\n\n**What we already have:**\n- Cargo.toml discovery via cargo::discover_crates → CrateInfo with name + path.\n- file_deps table with from_file_id → to_file_id edges.\n- Both feed naturally into per-crate rollups; no new parsing required.\n\n**What this adds:**\n- arch_packages table: id, name, path, source ('manifest'|'directory'), language, version, manifest_path.\n- arch_file_packages: file_id → package_id mapping.\n- arch_package_deps: package_id → package_id with import_count, rolled up from file_deps.\n- arch_coupling: per-package Ca, Ce, instability = Ce / (Ca + Ce).\n\nDefinitions:\n- Ca (afferent coupling): how many other packages depend on this one. Higher = more stable / load-bearing.\n- Ce (efferent coupling): how many packages this one depends on. Higher = more unstable.\n- Instability: 0 = maximally stable (everyone depends on it, it depends on nothing); 1 = maximally unstable.\n\n**Public API:**\n- Tethys::get_packages() -> Vec\n- Tethys::get_coupling_metrics(package_name: Option<&str>) -> Vec\n- Tethys::get_package_dependencies(package_id) -> Vec\n\n**CLI:**\n- tethys architecture [--format table|json]\n- tethys coupling [--package NAME] [--sort instability|ca|ce|name]\n- tethys package [--no-files]\n\n**Layer detection (lower priority, follow-up):**\nKiroGraph also classifies files into layers (api/service/data/ui/shared) by path patterns. Could be a v2; the package-level metrics are the high-value primary feature.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] Schema migration adds arch_packages, arch_file_packages, arch_package_deps, arch_coupling\n- [ ] Architecture analysis runs as a phase during indexing (only when enabled — opt-in flag)\n- [ ] CLI: tethys architecture, tethys coupling, tethys package\n- [ ] Coupling metrics correctly compute Ca, Ce, instability for the rivets workspace itself\n- [ ] MCP tool tethys_coupling and tethys_architecture (sibling rivets-o4re)\n- [ ] Documented in tethys README\n- [ ] Tests verify metrics on a multi-crate fixture workspace","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:31:51.584272600Z","updated_at":"2026-05-10T04:31:51.584272600Z","closed_at":null} -{"id":"rivets-zy0","title":"Implement streaming support for JsonlReader","description":"Implement the stream() method that returns an async Stream of deserialized records. This enables efficient processing of large JSONL files with constant memory usage.\n\nUses futures::Stream for the return type.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse futures::stream::{Stream, StreamExt};\nuse std::pin::Pin;\n\nimpl JsonlReader {\n pub fn stream(self) -> impl Stream>\n where\n T: DeserializeOwned + 'static,\n {\n futures::stream::unfold(self, |mut reader| async move {\n match reader.read_line().await {\n Ok(Some(value)) => Some((Ok(value), reader)),\n Ok(None) => None, // EOF\n Err(e) => Some((Err(e), reader)),\n }\n })\n }\n}\n```\n\nAdd futures dependency to Cargo.toml.","acceptance_criteria":"- stream() method implemented returning impl Stream>\n- Uses futures::stream::unfold for lazy evaluation\n- Properly handles EOF (returns None to terminate stream)\n- Errors propagated through stream\n- Memory usage constant regardless of file size\n- Unit tests verify streaming behavior\n- Integration test with large file (1000+ records)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-len","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:45.895377787Z","updated_at":"2025-11-28T00:40:17.811744371Z","closed_at":"2025-11-28T00:40:17.811744371Z"} -{"id":"rivets-e7fp","title":"Implement content hashing for file change detection","description":"The content_hash field exists in the schema and types but is always passed as None. Implement actual content hashing to enable content-based change detection alongside mtime. This would allow skipping re-indexing when mtime changes but content has not. Three TODO sites: lib.rs (2) and batch_writer.rs (1).","status":"closed","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Closed: Duplicate of rivets-3l14 (older, describes the same content_hash TODOs in lib.rs and batch_writer.rs).","external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-05T22:54:56.057264980Z","updated_at":"2026-05-10T04:30:14.008782Z","closed_at":"2026-05-10T04:30:14.008780200Z"} -{"id":"rivets-azn","title":"Document rivets architecture and API design","description":"Create comprehensive architecture documentation for rivets including crate structure, module organization, data flow diagrams, and public API documentation.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"Create docs/architecture.md covering:\n\n**1. System Architecture**:\n- Crate dependency graph\n- Component interaction diagram\n- Data flow (CLI → RPC → Storage → JSONL)\n\n**2. Storage Layer**:\n- Database schema diagram\n- Table relationships\n- Index strategy\n- Migration system\n\n**3. ID Generation**:\n- Hash algorithm explanation\n- Collision handling\n- Hierarchical ID format\n\n**4. Dependency System**:\n- 4 dependency types\n- Cycle detection algorithm\n- Ready work calculation\n\n**5. JSONL Sync**:\n- Export flow diagram\n- Import flow diagram\n- Conflict resolution\n\n**6. RPC Protocol**:\n- Message format\n- Operation list\n- Error handling\n\nUse mermaid diagrams for visual clarity","acceptance_criteria":"- architecture.md created in docs/\n- All major components documented\n- Mermaid diagrams included\n- Data flow clearly explained\n- API surfaces documented\n- Design decisions justified\n- Future extensibility noted","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:16:47.848274620Z","updated_at":"2025-11-28T00:37:44.421525809Z","closed_at":"2025-11-28T00:37:44.421525809Z"} -{"id":"rivets-azq","title":"Support multiple issue IDs in update/close/show commands","description":"Modify update, close, and show commands to accept multiple issue IDs. Examples:\\n- rivets update id1 id2 id3 --status in_progress\\n- rivets close id1 id2 --reason 'Batch completion'\\n- rivets show id1 id2 --json","status":"closed","priority":1,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:45.022427513Z","updated_at":"2025-12-28T00:38:18.113840018Z","closed_at":"2025-12-28T00:38:18.113840018Z"} -{"id":"rivets-s15r","title":"Implement deferred dependency resolution for circular dependencies","description":"Currently, dependency detection fails silently when the target file hasn't been indexed yet. This happens with circular dependencies (A→B, B→A) where the first-indexed file's dependency on the second file is lost.\n\n**Current behavior**: Dependencies can only be recorded to files already in the database. Files indexed earlier may not have their dependencies to later-indexed files recorded.\n\n**Proposed solution**: Instead of two full passes, queue unresolved dependencies and retry after the initial pass:\n\n1. First pass: Index all files, queue dependencies that fail to resolve (target not in DB)\n2. Resolution passes: Retry pending queue until it stops shrinking\n3. Log any remaining unresolved (external crates or truly missing files)\n\n**Convergence check**: `pending.len() < prev_count` ensures we don't loop forever.\n\n**Benefits**:\n- More efficient than re-indexing everything\n- Handles circular dependencies elegantly\n- Minimal code change to existing `index()` flow","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["enhancement","phase-2.5","tethys"],"design":"```rust\nstruct PendingDependency {\n from_file_id: i64,\n dep_path: PathBuf,\n}\n\nfn index(&mut self) -> Result {\n let files = self.discover_files()?;\n let mut pending: Vec = Vec::new();\n \n // First pass: index all files, queue failed dependencies\n for file in &files {\n self.index_file_atomic(file, &mut pending)?;\n }\n \n // Resolution passes: retry pending until stable\n let mut prev_count = pending.len() + 1;\n while !pending.is_empty() && pending.len() < prev_count {\n prev_count = pending.len();\n pending = self.resolve_pending(pending)?;\n }\n \n // Log remaining unresolved\n for p in &pending {\n debug!(from_file = p.from_file_id, dep = %p.dep_path.display(), \n \"Dependency unresolved after all passes\");\n }\n \n Ok(stats)\n}\n```","acceptance_criteria":"- [ ] `PendingDependency` struct created\n- [ ] `index_file_atomic` modified to return pending dependencies instead of silently dropping\n- [ ] `resolve_pending()` method implemented\n- [ ] Convergence loop in `index()` with shrinking check\n- [ ] Test: circular dependency (A→B, B→A) both directions detected\n- [ ] Test: convergence terminates for external crates\n- [ ] Debug logging for unresolved dependencies after all passes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-23T03:27:52.579637343Z","updated_at":"2026-01-23T04:12:37.921123478Z","closed_at":"2026-01-23T04:12:37.921123478Z"} -{"id":"rivets-j20","title":"Verify path traversal protection in rivets directory resolution","description":"Audit and verify that the rivets directory resolution logic properly handles path traversal attempts like \"../\" in user inputs. Add tests to confirm that malicious paths cannot escape the intended directory boundaries.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["pr-feedback","security"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:42.751822201Z","updated_at":"2025-11-30T04:11:42.751822201Z","closed_at":null} -{"id":"rivets-zcy","title":"Evaluate rstest for rivets/src/storage/mod.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in storage/mod.rs.\n\nFile: crates/rivets/src/storage/mod.rs (lines ~323+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:25.117428822Z","updated_at":"2025-11-29T01:29:24.941330500Z","closed_at":"2025-11-29T01:29:24.941330500Z"} +{"id":"rivets-1qp","title":"Evaluate rstest for rivets-jsonl/src/lib.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in lib.rs.\n\nFile: crates/rivets-jsonl/src/lib.rs (lines ~319+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:13.789405062Z","updated_at":"2025-11-29T01:27:19.146404160Z","closed_at":"2025-11-29T01:27:19.146404160Z"} +{"id":"rivets-kivr","title":"BatchWriter.send() should return Result instead of silently dropping data","description":"Currently `BatchWriter::send()` silently drops data when the channel is disconnected (background thread crashed), only logging an error. Callers have no way to detect data loss.\n\n**Current behavior:**\n```rust\npub fn send(&self, data: ParsedFileData) {\n if let Err(e) = self.sender.send(data) {\n error!(file = %e.0.relative_path.display(), \"Failed to send...\");\n }\n}\n```\n\n**Suggested improvement:**\n```rust\npub fn send(&self, data: ParsedFileData) -> Result<(), Error> {\n self.sender.send(data).map_err(|e| {\n Error::Internal(format!(\n \"Background writer crashed - data lost for file: {}\",\n e.0.relative_path.display()\n ))\n })\n}\n```\n\n**Impact:** Users could parse files successfully but never index them, with no indication in the final result.\n\n**Trade-off:** Returning Result adds complexity to the parallel parsing loop - need to decide whether to collect errors or fail fast.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["batch-writer","error-handling","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:06:40.382793013Z","updated_at":"2026-01-29T19:06:40.382793013Z","closed_at":null} +{"id":"rivets-e8t","title":"Implement ASCII dependency tree rendering","description":"Implement ASCII art dependency tree visualization:\n\n```\nrivets-abc \"Main Epic\"\n├── rivets-def \"Subtask 1\" [>] P1\n│ └── rivets-ghi \"Dependency\" [✓] P2\n└── rivets-jkl \"Subtask 2\" [ ] P2\n ├── rivets-mno \"Blocked by\" [X] P1\n └── rivets-pqr \"Related\" [ ] P3\n```\n\nFeatures:\n- Box-drawing characters for tree structure\n- Color-coded status and priority\n- Collapsible for large trees\n- Support for all dependency types (blocks, related, parent-child, discovered-from)","status":"open","priority":1,"issue_type":"feature","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:52.660367062Z","updated_at":"2025-11-30T18:34:52.660367062Z","closed_at":null} +{"id":"rivets-dvsw","title":"Dead-code finder: symbols with zero incoming references","description":"Find unexported / non-public symbols with no incoming references — candidates for removal. Trivial to implement on the existing schema; high developer-value signal.\n\n**Inspired by:** KiroGraph's kirograph_dead_code tool and CLI command.\n\n**Implementation:**\nSingle SQL query:\n SELECT s.* FROM symbols s\n LEFT JOIN call_edges c ON c.callee_symbol_id = s.id\n LEFT JOIN refs r ON r.symbol_id = s.id\n WHERE c.callee_symbol_id IS NULL\n AND r.symbol_id IS NULL\n AND s.visibility != 'public'\n LIMIT ?;\n\nFiltering to non-public is important — public/exported symbols may be used by consumers outside the indexed workspace, so reporting them as dead would generate false positives.\n\n**Public API:**\n- Tethys::find_dead_code(limit: usize) -> Result>\n\n**CLI:**\n- tethys dead-code [--limit N] [--json]\n\n**Caveats to document:**\n- Macro-expanded references aren't tracked, so macro-only call sites can produce false positives.\n- Trait-method dispatch through dyn Trait may not appear as a direct call edge; investigate before reporting.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] find_dead_code() API method\n- [ ] CLI subcommand tethys dead-code with --limit and --json\n- [ ] Filters out public/exported symbols\n- [ ] Documents known false-positive sources in module docs\n- [ ] MCP tool tethys_dead_code (sibling rivets-o4re)\n- [ ] Tests on a fixture with both clearly-dead and clearly-live symbols","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:33:07.744576100Z","updated_at":"2026-05-10T04:33:07.744576100Z","closed_at":null} +{"id":"rivets-5nz","title":"Add property-based tests with proptest for validators","description":"Use proptest crate to add property-based tests for CLI validators. This would help catch edge cases by generating random inputs:\n- validate_prefix: arbitrary alphanumeric strings of varying lengths\n- validate_issue_id: arbitrary prefix-suffix combinations\n- validate_title: arbitrary strings with control characters\n- validate_description: arbitrary multiline strings","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["pr-feedback","testing"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:31.127884203Z","updated_at":"2025-11-30T04:11:31.127884203Z","closed_at":null} +{"id":"rivets-1wn","title":"Extract shared filter logic from list() and ready_to_work()","description":"The filter logic in list() and ready_to_work() is nearly identical. Extract to a shared function to reduce duplication and improve maintainability.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["refactoring","storage"],"design":"Current duplication in `crates/rivets/src/storage/in_memory/trait_impl.rs`:\n- `list()`: lines 336-378\n- `ready_to_work()`: lines 411-448\n\nProposed refactoring:\n\n```rust\n/// Apply filter criteria to an issue, returning true if it matches.\nfn apply_filter(issue: &Issue, filter: &IssueFilter) -> bool {\n if let Some(status) = &filter.status {\n if &issue.status != status { return false; }\n }\n if let Some(priority) = filter.priority {\n if issue.priority != priority { return false; }\n }\n if let Some(issue_type) = &filter.issue_type {\n if &issue.issue_type != issue_type { return false; }\n }\n if let Some(assignee) = &filter.assignee {\n if issue.assignee.as_ref() != Some(assignee) { return false; }\n }\n if let Some(label) = &filter.label {\n if !issue.labels.contains(label) { return false; }\n }\n true\n}\n```\n\nUsage:\n```rust\nissues.retain(|issue| apply_filter(issue, filter));\nif let Some(limit) = filter.limit {\n issues.truncate(limit);\n}\n```\n\nThis is low priority since the current code works correctly - just a maintainability improvement.","acceptance_criteria":"- [ ] Extract shared filter predicate function\n- [ ] Update list() to use shared function\n- [ ] Update ready_to_work() to use shared function\n- [ ] All existing tests pass\n- [ ] No change in observable behavior","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-30T01:09:16.681348623Z","updated_at":"2025-11-30T01:09:16.681348623Z","closed_at":null} +{"id":"rivets-dw3","title":"Evaluate rstest for rivets-jsonl/src/writer.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in writer.rs.\n\nFile: crates/rivets-jsonl/src/writer.rs (lines ~250+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:08.122330884Z","updated_at":"2025-11-29T01:26:43.698535623Z","closed_at":"2025-11-29T01:26:43.698535623Z"} +{"id":"rivets-1p0f","title":"Store unresolved references with symbol_id = NULL","description":"Currently we skip references we can't resolve. Instead, store them with `symbol_id = NULL` and enough context to resolve later.\n\nThis requires allowing nullable symbol_id in the refs table and updating `store_references()` to handle unresolved refs.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:33.176960624Z","updated_at":"2026-01-28T17:44:36.816956542Z","closed_at":"2026-01-28T17:44:36.816956542Z"} +{"id":"rivets-o6x7","title":"Test topology mapping","description":"Map tests to the code they cover, enabling \"affected tests\" detection.\n\n**UPDATED: Medium effort - most primitives exist!**\n\n**What we already have:**\n- ✅ Index all symbols including test functions\n- ✅ Track imports via `imports` table\n- ✅ File dependency graph\n\n**What's missing:**\n1. **Test detection during parsing** (~50 lines)\n - Rust: Check for `#[test]` attribute on functions\n - C#: Check for `[Test]`, `[Fact]`, `[Theory]` attributes\n \n2. **Schema change** (~5 lines)\n - Add `is_test BOOLEAN DEFAULT FALSE` to symbols table\n - Or add `SymbolKind::Test` variant\n\n3. **Query for affected tests** (~30 lines)\n ```rust\n pub fn get_affected_tests(&self, changed_files: &[PathBuf]) -> Result> {\n // Find test symbols that import any of the changed files\n let test_symbols = self.db.get_symbols_where_is_test(true)?;\n let mut affected = Vec::new();\n for test in test_symbols {\n let test_file = self.db.get_file_by_id(test.file_id)?;\n let imports = self.db.get_imports(test.file_id)?;\n if imports.iter().any(|imp| changed_files.contains(&imp.resolved_path)) {\n affected.push(test);\n }\n }\n Ok(affected)\n }\n ```\n\n**Estimated effort: Medium (~100 lines, 2-4 hours)**\n\n**Use cases:**\n- CI: \"Which tests should I run for this change?\"\n- Coverage: \"What code has no tests?\"","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","feature","testing","tethys"],"design":null,"acceptance_criteria":null,"notes":"Most primitives exist. Main work is extracting #[test] attributes during parsing and adding a simple query.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:38.099688442Z","updated_at":"2026-01-29T23:52:28.509241068Z","closed_at":"2026-01-29T23:52:28.509241068Z"} +{"id":"rivets-4hwn","title":"Add schema versioning and migration system","description":"Current schema uses CREATE TABLE IF NOT EXISTS with no version tracking. Add a schema_version table and migration system to support future schema changes without requiring full re-indexing.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:09.089746470Z","updated_at":"2026-02-06T00:35:09.089746470Z","closed_at":null} {"id":"rivets-lwzo","title":"Interactive HTML graph export","description":"Generate a static HTML/CSS/JS bundle that visualizes the indexed graph for human exploration. Three files served from a local directory, no server required, works offline.\n\n**Inspired by:** KiroGraph's kirograph export build / start commands.\n\n**Output:**\n- .rivets/index/export/index.html — single HTML file\n- .rivets/index/export/app.css\n- .rivets/index/export/app.js — bundled with the graph data inlined as a const\n\n**Visualization features (in priority order):**\n1. Force-directed layout with color-coded nodes by kind, edge labels by kind.\n2. Click-to-focus: click a node to inspect its details (kind, file:line, signature, degree).\n3. Search box: filter nodes by name / qualified name / file path.\n4. Path mode: click two nodes to find and highlight the shortest path between them.\n5. Legend with click-to-hide for node kinds and edge kinds.\n6. Degree-threshold filter slider.\n7. Cluster mode: group nodes by directory.\n\n**Lower priority (v2):**\n- PNG export of current view.\n- Heat-map mode coloring nodes by file mtime.\n- Analytics charts (top-N most connected, kind distribution, degree histogram).\n\n**Library choice:**\n- Cytoscape.js (mature, MIT, ~300KB minified) is the best fit — covers force-directed, custom rendering, click handlers, path-finding, and clustering out of the box.\n- Alternative: D3-force directly (more code but no large dep).\n\n**Why P4 (lower priority):**\nThis is mostly orthogonal to indexing and the AI-assistant story. It's polish for human exploration. Defer until the analytical features (hotspots, surprising, coupling, etc.) are in place — the export then becomes a natural human counterpart to those CLI tools.\n\n**CLI:**\n- tethys export build [--out DIR] [--include-contains]\n- tethys export start [--out DIR] (build + open in browser)","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] tethys export build produces a self-contained directory of html/css/js\n- [ ] Force-directed layout with color-coded nodes\n- [ ] Click-to-focus and node detail panel\n- [ ] Search by name / qualified name / file\n- [ ] Shortest-path mode (depends on path-query feature, but can fall back to BFS in JS)\n- [ ] Works offline (no CDN; deps bundled)\n- [ ] Documented in tethys README","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:33:28.444873600Z","updated_at":"2026-05-10T04:33:28.444873600Z","closed_at":null} -{"id":"rivets-1v2b","title":"tethys: plumb CancellationToken through index_with_options","description":"Surfaced by silent-failure-hunter during PR #65 multi-agent review. Pre-existing gap exposed by the rivets-lcb6 fix, not a regression introduced by it.\n\nindex_with_options has NO cancellation plumbing today. Before rivets-lcb6, a user Ctrl-C mid-index left the DB with stale-but-valid file_deps from the prior run. After rivets-lcb6, the same Ctrl-C between line 140 (clear_all_file_deps) and the repopulate phases leaves file_deps EMPTY. From the user's perspective, cancellation is now strictly worse: downstream queries (get_file_dependencies, impact analysis) return [] until the next successful index.\n\nTwo issues are linked but distinct:\n1. (this issue) Plumb a CancellationToken (or std::sync::atomic::AtomicBool / tokio::sync::CancellationToken) through index_with_options. Check it at well-defined yield points: after workspace discovery, after each batch_writer.finish(), before each clear_all_X.\n2. (rivets-ml05) Wrap the clear+repopulate in a transaction so abort restores prior state.\n\nEither approach mitigates the worst-case 'empty DB after Ctrl-C' UX. Doing both gives belt + suspenders: transaction handles panics and SQLite errors; cancellation handles user-initiated abort cleanly.\n\nSuggested API surface:\n- index_with_options(&self, options: IndexOptions) → unchanged caller-facing default; internally checks an Arc\n- index_with_options_cancellable(&self, options: IndexOptions, cancel: &CancellationToken) → new variant for CLI / MCP callers\n\nSource: PR #65 multi-agent review aggregate (silent-failure-hunter Suggestion #4).","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":"* CancellationToken (or equivalent) accepted by indexing entry points\n* Cancellation checks at: post-discovery, between batches, before each clear_all_X\n* On cancel: structured warn! log noting which phase aborted\n* Test: spawn an index, cancel mid-flight, assert clean error return (not panic) and DB state is consistent with rivets-ml05 (either fully prior-state or fully cleared)","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-17T02:44:13.339026957Z","updated_at":"2026-05-17T02:44:13.339026957Z","closed_at":null} -{"id":"rivets-d06","title":"Write tests for MCP tools","description":"Integration and end-to-end tests for the MCP server:\n- Integration tests with actual MCP protocol messages\n- Multi-workspace scenario tests\n- Error response format verification\n- Full workflow tests (create -> update -> close)\n- Test with real InMemoryStorage (not mocks)","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Integration test harness for MCP protocol\n- [ ] Tests for complete issue lifecycle via MCP\n- [ ] Multi-workspace context switching tests\n- [ ] Error response format matches MCP spec\n- [ ] All tools tested with real storage backend","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:45.284084681Z","updated_at":"2025-11-30T18:09:34.327560198Z","closed_at":"2025-11-30T18:09:34.327560198Z"} -{"id":"rivets-lryg","title":"Warn when closing/reopening already closed/open issues","description":"Running `rivets close` on an already-closed issue or `rivets reopen` on an already-open issue silently succeeds. Should inform the user the issue is already in that state.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-11T22:47:34.156430368Z","updated_at":"2026-01-11T22:58:47.138653682Z","closed_at":"2026-01-11T22:58:47.138653682Z"} -{"id":"rivets-1h03","title":"Add integration tests for concurrency and filesystem edge cases","description":"PR review identified test coverage gaps: thread-local parser re-entrant calls, BatchWriter race conditions under heavy load, mutex poisoning recovery, symlink handling in file discovery, files >2GB, empty workspace scenarios, files outside workspace boundary.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:46.284076258Z","updated_at":"2026-02-06T00:54:46.284076258Z","closed_at":null} -{"id":"rivets-34i","title":"Add missing filter parameters to MCP list and ready tools","description":"The MCP tools don't expose all filter options that the storage layer supports.\n\n**Missing filters:**\n- `list()`: Missing `label` filter\n- `ready()`: Missing `label` and `issue_type` filters\n\nThe storage layer (`IssueFilter`) supports all these fields and applies them correctly. The MCP tools just need to accept and pass these additional parameters.","status":"closed","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":"**list() changes:**\nAdd `label: Option` parameter and include in IssueFilter construction.\n\n**ready() changes:**\nAdd `label: Option` and `issue_type: Option<&str>` parameters.\nValidate issue_type like list() does before passing to storage.\n\n**Server changes:**\nUpdate `ListParams` and `ReadyParams` structs in models.rs to include new fields.\nUpdate server.rs tool handlers to pass new parameters.","acceptance_criteria":"- [ ] list() accepts label parameter\n- [ ] ready() accepts label and issue_type parameters\n- [ ] MCP tool schemas updated with new parameters\n- [ ] Integration tests for new filter combinations","notes":"Progress: Core implementation complete (models, tools, server updated). Integration tests need fixing and refactoring to rstest. See child tasks: rivets-jfs, rivets-wfb, rivets-1yi, rivets-vvp","external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:45:00.665218723Z","updated_at":"2025-11-30T00:03:50.414931004Z","closed_at":"2025-11-30T00:03:50.414931004Z"} -{"id":"rivets-dve","title":"Add test-util feature for MockStorage (M-TEST-UTIL)","description":"MockStorage in storage/mod.rs (lines 284-390) should be feature-gated to allow downstream crates to use it for testing their own code that depends on the IssueStorage trait.\n\nCurrent State:\n- MockStorage is only available with #[cfg(test)]\n- Downstream crates cannot access it for their own tests\n\nImpact:\n- Makes library harder to use for external consumers who need to test code using IssueStorage\n- Forces users to write their own mocks","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["code-quality","public-api","testing"],"design":"Add a `test-util` feature to Cargo.toml:\n\n```toml\n[features]\ntest-util = []\n```\n\nMove MockStorage to public API under feature gate:\n\n```rust\n#[cfg(any(test, feature = \"test-util\"))]\npub struct MockStorage;\n\n#[cfg(any(test, feature = \"test-util\"))]\nimpl MockStorage {\n pub fn new() -> Self { Self }\n}\n```\n\nUpdate documentation to explain the feature and its usage.","acceptance_criteria":"- [ ] `test-util` feature added to Cargo.toml\n- [ ] MockStorage is `pub` and available with `feature = \"test-util\"`\n- [ ] MockStorage includes constructor and helper methods\n- [ ] Documentation updated with usage example\n- [ ] Existing tests still pass","notes":"PR #20 created: https://github.com/dwalleck/rivets/pull/20","external_ref":null,"dependencies":[],"created_at":"2025-11-27T22:48:52.858336885Z","updated_at":"2025-12-07T23:55:59.025020672Z","closed_at":"2025-12-07T23:55:59.025020672Z"} -{"id":"rivets-j2r1","title":"Type hierarchy: extract and traverse extends/implements edges","description":"Add type-hierarchy queries — walk inheritance and trait/interface implementation chains upward (base types) or downward (derived types).\n\n**Inspired by:** KiroGraph's kirograph_type_hierarchy tool.\n\n**Languages:**\n- Rust: impl X for Y, trait X: Y + Z, struct X\n- C#: class X : BaseClass, IInterface, IGeneric\n\n**Schema additions:**\n- New ReferenceKind variants: Extends, Implements (already partly modeled in ReferenceKind enum — check current state).\n- Tree-sitter extractors for both languages emit these edges during indexing.\n- Reuse the existing refs table; no new tables needed.\n\n**Why this depends on rivets-aay4:**\nFor methods, we want to walk ''method M of class X'' to its overridden version in BaseClass. That requires knowing which class a method belongs to, which is parent_symbol_id. Without it, we can do class-to-class hierarchy but not method-to-method override resolution.\n\n**Public API:**\n- Tethys::get_type_hierarchy(symbol: &str, direction: HierarchyDirection) -> Result\n- enum HierarchyDirection { Up, Down, Both }\n\n**CLI:**\n- tethys hierarchy [--direction up|down|both]","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] Rust extractor emits Extends/Implements edges for impl blocks and trait bounds\n- [ ] C# extractor emits Extends/Implements edges for class inheritance and interface lists\n- [ ] get_type_hierarchy() API method\n- [ ] CLI subcommand tethys hierarchy\n- [ ] Tests on a Rust trait fixture and a C# class-hierarchy fixture\n- [ ] MCP tool tethys_type_hierarchy (sibling rivets-o4re)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-aay4","dep_type":"blocks"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:32:18.694969900Z","updated_at":"2026-05-10T04:32:18.694969900Z","closed_at":null} {"id":"rivets-6p4","title":"Implement query execution with filtering","description":"Implement the execute() method for JsonlQuery that applies filters during streaming.\n\nThis enables filtering JSONL records without loading entire file into memory.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nimpl JsonlQuery\nwhere\n T: DeserializeOwned + 'static,\n{\n pub fn execute(\n self,\n reader: R\n ) -> impl Stream>\n where\n R: AsyncRead + Unpin,\n {\n let jsonl_reader = JsonlReader::new(reader);\n let stream = jsonl_reader.stream::();\n \n stream.filter_map(move |result| {\n let matches = match &result {\n Ok(value) => self.matches(value),\n Err(_) => true, // Pass errors through\n };\n \n async move {\n if matches {\n Some(result)\n } else {\n None\n }\n }\n })\n }\n}\n```","acceptance_criteria":"- execute() method implemented\n- Returns Stream>\n- Filters applied during streaming\n- Only matching records yielded\n- Errors passed through unchanged\n- Memory usage constant (streaming)\n- Unit tests verify filtering behavior\n- Integration test with large file","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-83j","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T23:16:55.357983235Z","updated_at":"2025-11-27T23:16:55.357983235Z","closed_at":null} -{"id":"rivets-3yxn","title":"tethys: expose --depth flag on `callers --transitive` for parity with impact","description":"PR #57 wired `--depth` through `tethys impact`, but `tethys callers --transitive` still calls `get_symbol_impact(symbol, None)` unconditionally. Users will reasonably expect the same flag on both commands.\n\nAdd a `--depth` CLI flag on `callers` and pass it through to `get_symbol_impact`.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-04-27T22:15:19.841176315Z","updated_at":"2026-04-27T22:15:19.841176315Z","closed_at":null} -{"id":"rivets-2pn","title":"Add integration tests for invalid filter values and error paths","description":"Add integration tests that exercise error handling when invalid values are passed to MCP tool filters. The validation logic exists but isn't covered by integration tests.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["mcp","testing"],"design":"Add error path tests to `crates/rivets-mcp/tests/integration.rs`:\n\n```rust\n// ============================================================================\n// Error Path Tests - Invalid Filter Values\n// ============================================================================\n\n#[tokio::test]\nasync fn test_list_invalid_status_returns_error() {\n let workspace = create_temp_workspace();\n let tools = create_tools();\n set_context(&tools, workspace.path()).await;\n\n let result = tools\n .list(Some(\"invalid_status\"), None, None, None, None, None, None)\n .await;\n\n assert!(result.is_err());\n let err = result.unwrap_err();\n assert!(matches!(err, Error::InvalidArgument { field: \"status\", .. }));\n assert!(err.to_string().contains(\"invalid_status\"));\n assert!(err.to_string().contains(\"open, in_progress, blocked, closed\"));\n}\n\n#[tokio::test]\nasync fn test_list_invalid_issue_type_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_ready_invalid_issue_type_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_create_invalid_issue_type_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_update_invalid_status_returns_error() { ... }\n\n#[tokio::test]\nasync fn test_dep_invalid_dep_type_returns_error() { ... }\n```\n\nValidation logic locations:\n- `tools.rs:32-38` - validate_status()\n- `tools.rs:40-47` - validate_issue_type()\n- `tools.rs:49-56` - validate_dep_type()","acceptance_criteria":"- [ ] Test invalid status values (e.g., \"invalid\", \"pending\", \"done\")\n- [ ] Test invalid issue_type values (e.g., \"invalid\", \"story\", \"spike\")\n- [ ] Test invalid dep_type values (e.g., \"invalid\", \"requires\", \"depends\")\n- [ ] Test invalid priority values (e.g., 5, 10, 255)\n- [ ] Verify appropriate error types are returned (InvalidArgument)\n- [ ] Verify error messages include the invalid value and valid options","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T01:13:42.028936963Z","updated_at":"2025-11-30T18:09:28.465454953Z","closed_at":"2025-11-30T18:09:28.465454953Z"} -{"id":"rivets-ghip","title":"Add error threshold to BatchWriter.write_batch() for systemic failures","description":"Currently `write_batch()` catches ALL database errors as warnings and continues. This hides systemic failures (disk full, database corruption) that will affect every subsequent file.\n\n**Current behavior:**\n```rust\nErr(e) => {\n warn!(file = %data.relative_path.display(), error = %e, \"Failed to write file\");\n}\n```\n\n**Problem:** If disk fills up after 50% of files, user sees 5000 warnings instead of one clear error.\n\n**Suggested improvement:**\n```rust\nconst MAX_CONSECUTIVE_FAILURES: usize = 5;\nlet mut consecutive_failures = 0;\n\nfor data in batch.drain(..) {\n match Self::write_single_file(db, &data) {\n Ok(_) => { consecutive_failures = 0; }\n Err(e) => {\n consecutive_failures += 1;\n if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {\n return Err(e); // Systemic failure, stop\n }\n }\n }\n}\n```\n\n**Trade-off:** Need to distinguish file-specific errors (bad data) from systemic errors (disk full, corruption).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["batch-writer","error-handling","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:06:46.053326512Z","updated_at":"2026-01-29T19:06:46.053326512Z","closed_at":null} -{"id":"rivets-83j","title":"Implement JsonlQuery builder pattern","description":"Implement the JsonlQuery struct with builder pattern for constructing query pipelines with filters and transformations.\n\nThis provides a fluent API for filtering JSONL streams during reading.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse std::marker::PhantomData;\n\npub struct JsonlQuery {\n predicates: Vec bool + Send + Sync>>,\n _phantom: PhantomData,\n}\n\nimpl JsonlQuery\nwhere\n T: DeserializeOwned + 'static,\n{\n pub fn new() -> Self {\n Self {\n predicates: Vec::new(),\n _phantom: PhantomData,\n }\n }\n \n pub fn filter(mut self, predicate: F) -> Self\n where\n F: Fn(&T) -> bool + Send + Sync + 'static,\n {\n self.predicates.push(Box::new(predicate));\n self\n }\n \n fn matches(&self, value: &T) -> bool {\n self.predicates.iter().all(|pred| pred(value))\n }\n}\n```","acceptance_criteria":"- JsonlQuery struct defined\n- new() constructor\n- filter() method adds predicates\n- Predicates stored as trait objects\n- Builder pattern allows chaining\n- Unit tests verify builder functionality\n- Compiles without errors","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-zy0","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:49.651063411Z","updated_at":"2025-11-27T23:16:49.651063411Z","closed_at":null} -{"id":"rivets-0gom","title":"tethys: resolver creates phantom cross-crate edges on shared filenames","description":"**Refined reproduction (via gilfoyle prove-it-prototype, 2026-05-11):**\n\nBuilt an independent probe (.rivets-0gom/probe.py, stdlib sqlite3) that reads tethys's file_deps table directly. Built an independent oracle (.rivets-0gom/oracle.sh, grep + Cargo.toml). Probe and oracle disagree on 149 of 170 cross-crate edges (88%).\n\nThe phantom edges are not random. Drilling into the 72 tethys -> rivets phantoms:\n- 46 of 72 (64%) target a single file: `crates/rivets/src/error.rs`\n- 9 target `mod.rs`, 6 target `color.rs`, 3 each target `args.rs` and `lib.rs`\n- All other targets are similarly common Rust filenames\n\n**The bug, sharpened:** the resolver's `use crate::::*` path resolution is NOT crate-scoped. It collapses to a workspace-wide filename lookup: any file in any crate named `.rs` is a candidate target. Every crate using common module names (`error`, `mod`, `lib`, `color`, `types`) generates phantom edges into every other crate with a file of the same name.\n\nThis is more specific than the original hypothesis. It locates the bug in `crate::` path resolution, not in cross-crate `use foo_crate::` resolution.\n\n**Artifacts committed:** `.rivets-0gom/probe.py`, `.rivets-0gom/probe2.py`, `.rivets-0gom/oracle.sh`. Re-runnable; the fix's design and tests should reuse them as the persistent oracle.\n\n**Workspace state at reproduction:**\n- PR 60 (tethys-coupling-metrics) branch at 8a3fe8a\n- Probe: 559 file_deps rows total, 170 cross-crate, 149 phantom\n- Oracle: 2 ordered pairs legitimately have cross-crate edges (rivets->rivets-jsonl, rivets-mcp->rivets); 10 ordered pairs should have ZERO edges\n\n**Implications for PR 60:**\nThe coupling metrics are computed correctly from the file_deps table. The file_deps table itself is approximately 88% noise on this workspace. The headline output of PR 60 is therefore noise plus a small amount of signal. This issue must land before any downstream consumer treats coupling output as authoritative.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] `tethys coupling` on a workspace whose crates have files with identical names (e.g. multiple `error.rs`, `mod.rs`, `types.rs`) produces edges only for actual `use foreign_crate::...` statements\n- [ ] Regression test: a 2-crate fixture where both crates have a file named `shared_name.rs` with internally-referenced symbols, and neither crate has a Cargo dep on the other, must produce Ca=Ce=0 for both\n- [ ] `tethys cycles` no longer flags Rust-legitimate sibling-module references (`crate::a` used from `b.rs` and vice versa within the same crate) as cycles\n- [ ] Coupling metrics on the rivets workspace itself match the Cargo dependency graph (tethys.Ce=0, rivets-jsonl.Ce=0)","notes":"Closed: Designed slices 1-3 shipped in PR #61 (merge commit 0d4ecf5): search_symbol_by_name_in_path_prefix (slice 1), same-crate preference in fallback_symbol_search (slice 2), ambiguity rejection via LIMIT 2 + refuse (slice 3). Post-lcb6 verification (.rivets-0gom/after-lcb6-merge.txt + .rivets-0gom/what-i-learned-post-lcb6.md) confirmed residuals are rivets-3d0s class, which was subsequently fixed in PR #67 (merge commit 7def342) — closing the resolver-correctness chain lcb6 -> 0gom -> 3d0s.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-lcb6","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-11T21:46:24.655439300Z","updated_at":"2026-05-17T23:08:54.487319350Z","closed_at":"2026-05-17T23:08:54.487318077Z"} -{"id":"rivets-o4re","title":"MCP server for tethys tools","description":"Expose tethys functionality as MCP tools for AI agent integration. AI assistants like Claude/Cursor/Copilot can then query the indexed graph instead of grepping files, dramatically cutting tool-call count and context usage on long sessions.\n\nInspired by both Drift (50+ MCP tools across 7 layers) and KiroGraph (16 MCP tools, all auto-approved). KiroGraph's design observation drives most of the value: ''Kiro literally queries the graph instead of grepping files,'' so the index becomes context the model uses for every prompt.\n\n**MVP tools (wrap existing Tethys API methods):**\n\n| Tool | Description | Underlying API |\n|------|-------------|----------------|\n| tethys_status | Index stats, language breakdown | get_stats |\n| tethys_symbol | Get symbol by qualified name | get_symbol |\n| tethys_search | Search symbols by name/kind | search_symbols |\n| tethys_callers | Who calls this symbol? | get_callers |\n| tethys_callees | What does this symbol call? | get_symbol_dependencies |\n| tethys_impact | Impact analysis for a symbol | get_symbol_impact |\n| tethys_file_impact | Impact analysis for a file | get_impact |\n| tethys_reachable | Forward/backward reachability | get_forward_reachable / get_backward_reachable |\n| tethys_cycles | Detect dependency cycles | detect_cycles |\n| tethys_panic_points | Find .unwrap()/.expect() calls | get_panic_points |\n| tethys_affected_tests | Tests affected by changed files | get_affected_tests |\n\n**v2 tools (depend on new analytics — see sibling issues):**\n\n| Tool | Description | Depends on |\n|------|-------------|------------|\n| tethys_path | Shortest path between two symbols | new path-query feature |\n| tethys_dead_code | Symbols with zero incoming refs | new dead-code feature |\n| tethys_hotspots | Most-connected symbols | new hotspots feature |\n| tethys_surprising | Non-obvious cross-file edges | new surprising-connections feature |\n| tethys_type_hierarchy | Up/down extends-implements walk | new type-hierarchy feature |\n| tethys_coupling | Per-package Ca/Ce/instability | new architecture-metrics feature |\n| tethys_diff | Graph diff vs. saved snapshot | new snapshot/diff feature |\n\n**Implementation plan:**\n- New crates/tethys-mcp crate (binary + thin lib).\n- Use rmcp (already in workspace via rivets-mcp).\n- Each tool is a thin async wrapper around a Tethys method.\n- Tools should default to auto-approved in the Claude MCP config so the model can call them freely.\n- Coexists with rivets-mcp on a single MCP host.\n\n**Benefits:**\n- AI agents query codebase structure instead of grepping files.\n- Integrates with Claude Code, Cursor, Copilot, and any MCP-compatible client.\n- Complements rivets-mcp (issue tracker) and tethys's CLI.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["drift-inspired","feature","mcp","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:26.753055176Z","updated_at":"2026-05-10T04:30:37.025624100Z","closed_at":null} -{"id":"rivets-lye5","title":"Snapshot and diff: track structural graph changes over time","description":"Save lightweight snapshots of the graph and compare them to current state. Useful before/after refactors, in CI to audit what a PR added or removed structurally, and as a baseline for regression detection.\n\n**Inspired by:** KiroGraph's snapshot save / snapshot diff / kirograph_diff MCP tool. Storage is just JSON arrays of node IDs and edge tuples; diff is set-difference, O(n) regardless of codebase size.\n\n**Implementation:**\n- Storage: .rivets/snapshots/{label}.json with serialized list of (qualified_name, kind) for nodes and (caller_qn, callee_qn, kind) for edges. JSON over native binary because they're small and human-readable.\n- Default snapshot label is the timestamp; named labels (e.g. 'pre-refactor') are first-class.\n- Diff is computed as two set differences: added = current - snapshot, removed = snapshot - current.\n- Public API:\n - Tethys::save_snapshot(label) -> Result\n - Tethys::list_snapshots() -> Result>\n - Tethys::diff_snapshot(label) -> Result\n- CLI:\n - tethys snapshot save [LABEL]\n - tethys snapshot list\n - tethys snapshot diff [LABEL] [--format full|summary|json]\n\n**CI use case:**\ngh pr create … then tethys snapshot save pre-pr; merge; tethys snapshot diff pre-pr to audit structural changes.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] save_snapshot, list_snapshots, diff_snapshot API methods\n- [ ] CLI subcommands snapshot save / list / diff\n- [ ] Snapshot file format documented (JSON schema in module-level docs)\n- [ ] Diff returns added_symbols, removed_symbols, added_edges, removed_edges\n- [ ] Default label is timestamp; named labels supported\n- [ ] Round-trip test: save → diff against unchanged graph returns zero changes","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:31:32.968546400Z","updated_at":"2026-05-10T04:31:32.968546400Z","closed_at":null} -{"id":"rivets-j9bu","title":"Tethys: Codebase Intelligence Engine","description":"Improvements inspired by analyzing the Drift codebase. Drift is a mature codebase intelligence platform that handles 10K+ file codebases efficiently.\n\nKey patterns from Drift:\n- Rayon for parallel file parsing\n- Streaming SQLite writes via MPSC channel\n- Pre-computed call graph edges\n- MCP server integration for AI agents\n- Reachability analysis for security auditing\n- Test topology for CI integration\n\nThese improvements would make tethys production-ready for large codebases.","status":"open","priority":1,"issue_type":"epic","assignee":null,"labels":["architecture","drift-inspired","performance"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-29T12:48:13.969977556Z","updated_at":"2026-02-21T02:19:23.038730450Z","closed_at":null} -{"id":"rivets-6bc","title":"Human-first UX Differentiation","description":"Position Rivets as \"equally powerful for both humans AND agents\" - addressing the gap in Beads' positioning where humans feel like secondary citizens.\n\nTarget: Solo developers and small teams using AI agents","status":"open","priority":1,"issue_type":"epic","assignee":null,"labels":["differentiation","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-30T18:34:01.044494126Z","updated_at":"2025-11-30T18:34:01.044494126Z","closed_at":null} +{"id":"rivets-lcb6","title":"tethys: file_deps table never cleared between index runs (stale resolver edges persist)","description":"Discovered during rivets-0gom fix work (gilfoyle/checkpointed-build, slice 2): the file_deps table in tethys's index DB is populated via UPSERT-only, never DELETE'd between `tethys index` invocations.\n\nSchema (crates/tethys/src/db/file_deps.rs:18):\n``\nINSERT INTO file_deps (from_file_id, to_file_id, ref_count)\nVALUES (?1, ?2, 1)\nON CONFLICT(from_file_id, to_file_id) DO UPDATE SET ref_count = ref_count + 1\n``\n\nCompare to call_edges, which IS cleared (crates/tethys/src/db/call_edges.rs:13 -- `clear_all_call_edges`, called from indexing.rs:424). file_deps has no analog.\n\n**Impact:** changes to resolver behavior aren't reflected in file_deps without manual DB wipe (`rm .rivets/index/tethys.db`). During rivets-0gom slice 2:\n- We expected the probe to show fewer phantom edges after fixing the resolver\n- The probe showed identical counts because old phantoms from previous index runs persisted\n- Manually wiping the DB revealed the fix was actually working\n\nThis makes the index non-idempotent across resolver changes. It also means anyone using `tethys coupling` on a workspace they've indexed before is getting metrics polluted by historical resolution mistakes that may no longer be reproducible.\n\n**Reproduction:**\n1. `tethys index` on a workspace\n2. Note: file_deps row count = N1\n3. Modify a source file to add a cross-crate `use` statement; rebuild target\n4. `tethys index` again\n5. file_deps row count is now N1 + (new edges); old edges are NOT cleared even if they no longer exist in the source\n\n**Likely fix:** add `clear_all_file_deps` to db/file_deps.rs, call it from index_with_options before the resolver passes run (alongside or near the existing `clear_all_call_edges` call at indexing.rs:424). file_deps's FK with `ON DELETE CASCADE` from files means stale-file deletion already cleans up; the missing piece is the wholesale reset.\n\n**Related to:** rivets-0gom (made this finding visible). Not blocking PR 60 since neither computes file_deps freshness across runs; but blocks any future feature that wants to use file_deps deterministically.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] file_deps is cleared at the start of each index_with_options invocation (or before resolver passes 1/2 begin)\n- [ ] Regression test: index twice on the same workspace; file_deps row count is identical\n- [ ] Regression test: index workspace, modify file to remove a `use` statement, re-index; the removed edge is no longer in file_deps\n- [ ] Compatible with the incremental-update path tracked in rivets-bxom (when that lands, the clear should be scoped to changed files, not workspace-wide)","notes":"Closed: Implemented in PR #65 (merge commit 94226ce). clear_all_file_deps() added in crates/tethys/src/db/file_deps.rs:11; called from index_with_options in crates/tethys/src/indexing.rs:140. Regression-fence tests at crates/tethys/tests/file_deps_idempotency.rs cover batch + streaming modes.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-12T01:04:44.609084500Z","updated_at":"2026-05-17T23:08:54.477462078Z","closed_at":"2026-05-17T23:08:54.477461016Z"} +{"id":"rivets-s15r","title":"Implement deferred dependency resolution for circular dependencies","description":"Currently, dependency detection fails silently when the target file hasn't been indexed yet. This happens with circular dependencies (A→B, B→A) where the first-indexed file's dependency on the second file is lost.\n\n**Current behavior**: Dependencies can only be recorded to files already in the database. Files indexed earlier may not have their dependencies to later-indexed files recorded.\n\n**Proposed solution**: Instead of two full passes, queue unresolved dependencies and retry after the initial pass:\n\n1. First pass: Index all files, queue dependencies that fail to resolve (target not in DB)\n2. Resolution passes: Retry pending queue until it stops shrinking\n3. Log any remaining unresolved (external crates or truly missing files)\n\n**Convergence check**: `pending.len() < prev_count` ensures we don't loop forever.\n\n**Benefits**:\n- More efficient than re-indexing everything\n- Handles circular dependencies elegantly\n- Minimal code change to existing `index()` flow","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["enhancement","phase-2.5","tethys"],"design":"```rust\nstruct PendingDependency {\n from_file_id: i64,\n dep_path: PathBuf,\n}\n\nfn index(&mut self) -> Result {\n let files = self.discover_files()?;\n let mut pending: Vec = Vec::new();\n \n // First pass: index all files, queue failed dependencies\n for file in &files {\n self.index_file_atomic(file, &mut pending)?;\n }\n \n // Resolution passes: retry pending until stable\n let mut prev_count = pending.len() + 1;\n while !pending.is_empty() && pending.len() < prev_count {\n prev_count = pending.len();\n pending = self.resolve_pending(pending)?;\n }\n \n // Log remaining unresolved\n for p in &pending {\n debug!(from_file = p.from_file_id, dep = %p.dep_path.display(), \n \"Dependency unresolved after all passes\");\n }\n \n Ok(stats)\n}\n```","acceptance_criteria":"- [ ] `PendingDependency` struct created\n- [ ] `index_file_atomic` modified to return pending dependencies instead of silently dropping\n- [ ] `resolve_pending()` method implemented\n- [ ] Convergence loop in `index()` with shrinking check\n- [ ] Test: circular dependency (A→B, B→A) both directions detected\n- [ ] Test: convergence terminates for external crates\n- [ ] Debug logging for unresolved dependencies after all passes","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-23T03:27:52.579637343Z","updated_at":"2026-01-23T04:12:37.921123478Z","closed_at":"2026-01-23T04:12:37.921123478Z"} +{"id":"rivets-bsp","title":"Implement core CLI commands (create, list, show, update, close, delete)","description":"Implement the essential CLI commands for basic issue management using clap for argument parsing. These are the most frequently used commands.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on beads cmd/bd/:\n\n**Commands**:\n1. **create**: Create new issues (interactive or flags)\n2. **list**: Query issues with filters (status, priority, assignee, labels, etc.)\n3. **show**: Display full issue details with dependencies\n4. **update**: Modify issue fields\n5. **close**: Mark issues as closed with reason\n6. **delete**: Remove issues (with cascade confirmation)\n\n**Clap Structure**:\n```rust\n#[derive(Parser)]\nenum Commands {\n Create(CreateArgs),\n List(ListArgs),\n Show(ShowArgs),\n Update(UpdateArgs),\n Close(CloseArgs),\n Delete(DeleteArgs),\n}\n```\n\n**Features**:\n- Interactive prompts for missing data\n- JSON output mode (--json flag)\n- Batch operations where applicable\n- Validation and error messages","acceptance_criteria":"- All 6 commands implemented and working\n- --help shows usage for each command\n- Filters work correctly (status, priority, etc.)\n- JSON output mode functional\n- Interactive mode for create command\n- Error messages are clear and helpful\n- Integration tests verify command behavior","notes":"## Implementation Notes (2025-11-29)\n\n### Completed Work\n\nAll 6 core CLI commands have been implemented and integrated with the storage trait:\n\n1. **create** - Creates new issues with full option support\n - Interactive prompt for title if not provided\n - Supports all fields: title, description, priority, type, assignee, labels, design, acceptance criteria\n - Dependency specification via --deps flag\n\n2. **list** - Queries issues with filters\n - Status filter with in_progress alias support\n - Priority, type, assignee, label filters\n - Sorting: priority, newest, oldest, updated\n - Limit option\n\n3. **show** - Displays full issue details\n - Shows all fields with formatted output\n - Displays dependencies and dependents\n - Supports JSON output mode\n\n4. **update** - Modifies issue fields\n - All fields updateable\n - Status transitions supported\n\n5. **close** - Marks issues as closed\n - Custom reason support\n - Updates status to closed\n\n6. **delete** - Removes issues\n - Interactive confirmation by default\n - --force flag to skip confirmation\n - Proper error on dependents\n\n### Additional Commands\n\nAlso implemented bonus commands:\n- **ready** - Find ready-to-work issues (no blockers)\n- **dep** - Dependency management (add/remove/list)\n- **blocked** - Show blocked issues\n- **stats** - Project statistics\n\n### Architecture\n\n- Created `App` struct for storage lifecycle management\n- Created `output` module for consistent formatting (text/JSON modes)\n- Commands find rivets root by searching up directory tree\n- Auto-save after mutations\n\n### Testing\n\n- 33 integration tests using rstest\n- All tests pass\n- Tests properly initialize repositories before testing","external_ref":null,"dependencies":[{"depends_on_id":"rivets-cgl","dep_type":"blocks"}],"created_at":"2025-11-17T22:16:06.835605991Z","updated_at":"2025-11-30T03:09:14.214873854Z","closed_at":"2025-11-30T03:09:14.214873854Z"} +{"id":"rivets-6u6","title":"Implement set_context and where_am_i tools","description":"Implement context management tools:\n- set_context(workspace_root) - Set workspace and discover .rivets/\n- where_am_i() - Debug: show current workspace context\n\nThese are foundational tools that all other tools depend on.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] set_context tool registers and responds correctly\n- [ ] where_am_i returns current context state\n- [ ] Error handling for missing/invalid workspace\n- [ ] Unit tests: both tools with MockStorage","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-s3o","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:16.966340774Z","updated_at":"2025-11-30T01:19:11.657351433Z","closed_at":"2025-11-30T01:19:11.657351433Z"} +{"id":"rivets-lmh","title":"Documentation and integration guide","description":"Write documentation:\n- README.md with usage instructions\n- Integration guide for Claude Code\n- API documentation for tools","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:50.949175150Z","updated_at":"2025-12-08T00:02:07.288716558Z","closed_at":"2025-12-08T00:02:07.288716558Z"} +{"id":"rivets-vpny","title":"Docs: Add rationale for MAX_BLOCKING_DEPTH constant","description":"In storage/in_memory/graph.rs (line 18), the constant has minimal documentation:\n\n```rust\n/// Maximum depth for BFS traversal in blocking detection.\n///\n/// This limit prevents infinite loops and handles extremely deep hierarchies gracefully.\nconst MAX_BLOCKING_DEPTH: usize = 50;\n```\n\nPer M-DOCUMENTED-MAGIC guideline, should explain WHY 50 was chosen:\n\n**Suggested documentation:**\n```rust\n/// Maximum depth for BFS traversal in blocking detection.\n///\n/// This limit prevents infinite loops and handles extremely deep hierarchies gracefully.\n/// Set to 50 to:\n/// - Handle practical issue hierarchies (most projects have < 10 levels)\n/// - Provide headroom for complex epics with nested sub-tasks\n/// - Cap worst-case traversal time for malformed graphs\n/// - Balance between completeness and performance\nconst MAX_BLOCKING_DEPTH: usize = 50;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","graph.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:51:04.569965544Z","updated_at":"2025-12-28T15:51:04.569965544Z","closed_at":null} +{"id":"rivets-1os9","title":"tethys: implement depth-limited transitive impact analysis","description":"The --depth flag in impact analysis is accepted but not implemented. Currently, transitive analysis always explores the full dependency graph. Implement depth limiting to control how many levels of indirection to follow.","status":"in_progress","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:06.640405407Z","updated_at":"2026-04-27T21:37:12.185509775Z","closed_at":null} +{"id":"rivets-ceg","title":"Implement CLI argument parsing with clap validation","description":"Extend the CLI skeleton (rivets-p7v) with complete argument parsing, validation, error handling, and help text generation for all commands.\n\nThis task implements the argument parsing layer that sits between the CLI entry point and command execution. Each command gets proper args structs with validation.\n\n**Commands to implement**:\n- create: --title, --description, --priority, --type, --assignee, --deps\n- list: --status, --priority, --type, --assignee, --label, --limit\n- show: , --json\n- update: , --title, --description, --status, --priority, --assignee\n- close: , --reason\n- delete: , --force\n- init: --prefix, --quiet\n- ready: --assignee, --limit, --sort\n\n**Features**:\n- Validation for all flag values (priority 0-4, valid status enum, etc.)\n- Helpful error messages with suggestions\n- Auto-generated help text via clap derives\n- Subcommand structure with shared global flags\n- Interactive prompts for missing required fields (create command)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Use clap derive API for argument parsing:\n\n```rust\nuse clap::{Parser, Subcommand, ValueEnum};\n\n#[derive(Parser)]\n#[command(name = \"rivets\")]\n#[command(author, version, about, long_about = None)]\npub struct Cli {\n /// Output in JSON format\n #[arg(long, global = true)]\n pub json: bool,\n \n #[command(subcommand)]\n pub command: Option,\n}\n\n#[derive(Subcommand)]\npub enum Commands {\n /// Initialize a new rivets repository\n Init(InitArgs),\n \n /// Create a new issue\n Create(CreateArgs),\n \n /// List issues with optional filters\n List(ListArgs),\n \n /// Show detailed issue information\n Show(ShowArgs),\n \n /// Update an existing issue\n Update(UpdateArgs),\n \n /// Close an issue\n Close(CloseArgs),\n \n /// Delete an issue\n Delete(DeleteArgs),\n \n /// Show ready-to-work issues\n Ready(ReadyArgs),\n}\n\n#[derive(Parser)]\npub struct CreateArgs {\n /// Issue title\n #[arg(short, long)]\n pub title: Option,\n \n /// Issue description\n #[arg(short, long)]\n pub description: Option,\n \n /// Priority (0-4)\n #[arg(short, long, value_parser = clap::value_parser!(u8).range(0..=4))]\n pub priority: Option,\n \n /// Issue type\n #[arg(short = 't', long)]\n pub issue_type: Option,\n \n /// Assignee\n #[arg(short, long)]\n pub assignee: Option,\n}\n\n#[derive(ValueEnum, Clone)]\npub enum IssueTypeArg {\n Bug,\n Feature,\n Task,\n Epic,\n Chore,\n}\n\n// ... more arg structs\n```\n\n**Validation**:\n- Priority range checking (0-4)\n- Issue ID format validation\n- Enum value validation via ValueEnum\n- Required vs optional field handling\n- Custom validators for complex types","acceptance_criteria":"- All CLI commands have argument structs defined\n- Clap derive attributes configured correctly\n- Priority validation enforces 0-4 range\n- Issue ID validation checks format\n- Enum arguments use ValueEnum for type safety\n- Help text auto-generated and informative\n- Error messages are clear with examples\n- Unit tests for argument parsing\n- Integration tests verify --help output\n- Invalid arguments produce helpful error messages","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p7v","dep_type":"blocks"}],"created_at":"2025-11-17T23:04:11.868192762Z","updated_at":"2025-11-28T14:22:51.079089079Z","closed_at":"2025-11-28T14:22:51.079089079Z"} +{"id":"rivets-7p54","title":"Hotspots: rank symbols by connectivity (in+out degree)","description":"Add a hotspots query that ranks symbols by total edge degree (incoming + outgoing) excluding structural edges, surfacing the most-connected code in the codebase. Useful for identifying core abstractions and high-blast-radius change points before refactoring.\n\n**Inspired by:** KiroGraph's kirograph_hotspots tool and CLI command. See crates/tethys/KIROGRAPH-COMPARISON.md.\n\n**Implementation:**\n- Pure SQL on the existing schema:\n SELECT s.id, COUNT(*) AS degree FROM symbols s\n JOIN (SELECT caller_symbol_id AS sid FROM call_edges\n UNION ALL\n SELECT callee_symbol_id AS sid FROM call_edges\n UNION ALL\n SELECT symbol_id AS sid FROM refs WHERE symbol_id IS NOT NULL\n UNION ALL\n SELECT in_symbol_id AS sid FROM refs WHERE in_symbol_id IS NOT NULL) e\n ON e.sid = s.id\n GROUP BY s.id ORDER BY degree DESC LIMIT ?\n- Public API: Tethys::get_hotspots(limit) -> Vec\n- CLI: tethys hotspots [--limit N] [--json]","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] get_hotspots() public API method on Tethys\n- [ ] CLI subcommand 'tethys hotspots' with --limit and --json flags\n- [ ] Returns each entry with in_degree, out_degree, total_degree\n- [ ] Excludes structural/contains edges (none currently exist, so noop, but documented)\n- [ ] Unit tests on a fixture workspace\n- [ ] Integration tested on the rivets workspace itself","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:30:50.185175300Z","updated_at":"2026-05-10T04:30:50.185175300Z","closed_at":null} +{"id":"rivets-n3w","title":"Add smart suggestions after operations","description":"Show context-aware suggestions after operations:\n\n```\n$ rivets close rivets-abc\nClosed issue: rivets-abc\n\nTip: rivets-def is now unblocked. Run 'rivets ready' to see available work.\n```\n\nAlso show warnings for risky operations:\n```\n$ rivets update rivets-abc --status closed\nWarning: Use 'rivets close' instead for proper close workflow.\nContinue anyway? [y/N]:\n```","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:24.182937809Z","updated_at":"2025-11-30T18:36:24.182937809Z","closed_at":null} +{"id":"rivets-xvlw","title":"tethys: --rebuild flag fails on schema changes","description":"When the database schema changes (e.g., adding new columns like `is_test`), running `tethys index --rebuild` fails because:\n\n1. The existing database file is preserved\n2. `CREATE TABLE IF NOT EXISTS` doesn't add new columns to existing tables\n3. New indexes referencing the missing column fail with \"no such column\" error\n\n**Error observed:**\n```\nerror: database error: no such column: is_test in \nCREATE INDEX IF NOT EXISTS idx_symbols_is_test ON symbols(is_test) WHERE is_test = 1;\n```\n\n**Workaround:** Manually delete `.rivets/index/tethys.db` before running with `--rebuild`.","status":"open","priority":2,"issue_type":"bug","assignee":null,"labels":["database","tethys"],"design":"Options to consider:\n\n1. **Delete and recreate** (simplest): Have `--rebuild` delete the database file entirely before creating a fresh one. This is semantically correct since \"rebuild\" implies starting fresh.\n\n2. **Schema migration**: Add version tracking and migration logic to handle schema evolution. More complex but preserves data when possible.\n\n3. **Hybrid**: Check schema version on open, delete and recreate if incompatible.\n\nRecommended: Option 1 for now (delete on rebuild), with option 3 as future enhancement.","acceptance_criteria":"- [ ] `tethys index --rebuild` works correctly after schema changes\n- [ ] No manual deletion of database required\n- [ ] Clear user feedback when schema is incompatible","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T02:42:14.111981937Z","updated_at":"2026-01-30T02:42:14.111981937Z","closed_at":null} +{"id":"rivets-0tj5","title":"IndexErrorKind semantic mismatch: Config→IoError, Internal→DatabaseError","description":"The From<&Error> for IndexErrorKind mapping has semantic mismatches: Error::Config maps to IoError (config errors aren't I/O failures) and Error::Internal maps to DatabaseError (mutex poisoning isn't a DB error). Consider adding InfrastructureError variant or renaming existing variants. Flagged in PR #55 review.","status":"open","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:16:11.217907736Z","updated_at":"2026-03-20T22:16:11.217907736Z","closed_at":null} +{"id":"rivets-7ag","title":"Add atomic append_notes operation to IssueStorage trait","description":"Address TOCTOU race condition in close command by adding atomic note appending.\n\nLocation: `execute.rs:241-263`\n\nCurrent behavior:\n- Two separate operations: `get_issue()` then `update_issue()` with appended notes\n- TOCTOU window exists between read and write\n- Documented as acceptable for single-user CLI\n\nEnhancement for multi-user scenarios:\n- Add `append_notes(&self, id: &IssueId, notes: &str)` method to `IssueStorage` trait\n- Implement atomic append in storage backends\n- Update close command to use atomic operation\n\nThis would eliminate the race condition for future concurrent access scenarios.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["pr-feedback","storage"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T17:45:08.387504813Z","updated_at":"2025-11-30T17:45:08.387504813Z","closed_at":null} +{"id":"rivets-len","title":"Implement JsonlReader::read_line() method","description":"Implement the read_line() async method that reads a single JSONL line and deserializes it into type T.\n\nThis is the foundational read operation that all other reading functionality builds on.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse serde::de::DeserializeOwned;\nuse tokio::io::AsyncBufReadExt;\n\nimpl JsonlReader {\n pub async fn read_line(&mut self) -> Result> {\n let mut line = String::new();\n let bytes_read = self.reader.read_line(&mut line).await?;\n \n if bytes_read == 0 {\n return Ok(None); // EOF\n }\n \n self.line_number += 1;\n \n let trimmed = line.trim();\n if trimmed.is_empty() {\n // Skip empty lines, read next\n return self.read_line().await;\n }\n \n let value: T = serde_json::from_str(trimmed)\n .map_err(|e| Error::Json(e))?;\n \n Ok(Some(value))\n }\n}\n```","acceptance_criteria":"- read_line() method implemented\n- Returns Result> (None for EOF)\n- Increments line_number on each line\n- Skips empty lines\n- Deserializes JSON using serde_json\n- Proper error handling for malformed JSON\n- Unit tests verify reading single/multiple lines\n- Unit tests verify EOF handling","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-uo7","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:40.175422639Z","updated_at":"2025-11-28T00:05:52.845896777Z","closed_at":"2025-11-28T00:05:52.845896777Z"} +{"id":"rivets-3hs","title":"Add indicatif dependency for progress indicators","description":"Add indicatif crate to workspace dependencies for progress bars and spinners.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:19.476455174Z","updated_at":"2025-11-30T18:35:19.476455174Z","closed_at":null} +{"id":"rivets-o7o","title":"Consider splitting integration.rs by test category","description":"The integration test file has grown to 1,638 lines. Consider splitting it into separate files organized by test category for better maintainability.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["mcp","refactoring","testing"],"design":"Potential split structure for `crates/rivets-mcp/tests/`:\n\n```\ntests/\n├── common/\n│ └── mod.rs # Shared fixtures, helpers (IssueSetup, FilterParams, etc.)\n├── crud_tests.rs # Basic create/show/update/close operations\n├── list_filter_tests.rs # List filter combinations\n├── ready_filter_tests.rs # Ready filter combinations \n├── dependency_tests.rs # Dependency management tests\n├── context_tests.rs # Workspace context tests\n└── edge_case_tests.rs # Edge cases (empty results, unicode, etc.)\n```\n\nBenefits:\n- Easier to find relevant tests\n- Faster incremental compilation when modifying one category\n- Clearer test organization\n\nThis is low priority - the current single file works fine, just harder to navigate as it grows.","acceptance_criteria":"- [ ] Evaluate whether splitting improves maintainability\n- [ ] If splitting, organize tests into logical categories\n- [ ] Ensure shared test utilities are extracted to a common module\n- [ ] All tests continue to pass after reorganization","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-30T01:07:57.543771055Z","updated_at":"2025-11-30T01:07:57.543771055Z","closed_at":null} +{"id":"rivets-fxy","title":"Implement semantic color theme","description":"Implement color theme with semantic colors:\n\nStatus colors:\n- Open: Blue\n- In Progress: Yellow/Gold\n- Ready: Green (new status indicator)\n- Blocked: Red\n- Closed: Gray/dim\n\nPriority colors:\n- P0: Red (critical)\n- P1: Orange\n- P2: Yellow\n- P3: Blue\n- P4: Gray\n\nIssue type colors:\n- Bug: Red\n- Feature: Green\n- Task: Blue\n- Epic: Purple\n- Chore: Gray","status":"open","priority":1,"issue_type":"feature","assignee":null,"labels":["phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:41.241090815Z","updated_at":"2025-11-30T18:34:41.241090815Z","closed_at":null} +{"id":"rivets-iwsc","title":"Docs: Add rationale for prefix length and traversal depth constants","description":"In commands/init.rs, the constants at lines 57-63 have minimal documentation:\n\n```rust\n/// Minimum prefix length\npub const MIN_PREFIX_LENGTH: usize = 2;\n\n/// Maximum prefix length\npub const MAX_PREFIX_LENGTH: usize = 20;\n\n/// Maximum directory depth to traverse when searching for rivets root\npub const MAX_TRAVERSAL_DEPTH: usize = 256;\n```\n\nPer M-DOCUMENTED-MAGIC guideline, these should explain WHY these values were chosen:\n\n- Why 2 minimum? (single char too ambiguous?)\n- Why 20 maximum? (readability in issue IDs?)\n- Why 256 traversal depth? (matches typical filesystem limits? performance?)\n\n**Example fix:**\n```rust\n/// Minimum prefix length.\n///\n/// Set to 2 to ensure prefixes are meaningful and distinguishable.\n/// Single-character prefixes would be too ambiguous and harder to type.\npub const MIN_PREFIX_LENGTH: usize = 2;\n```","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","init.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:50:43.658996271Z","updated_at":"2025-12-28T15:50:43.658996271Z","closed_at":null} +{"id":"rivets-fwy","title":"Add skim dependency for fuzzy search","description":"Add skim crate to workspace dependencies for fuzzy finding functionality.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:06.747113999Z","updated_at":"2025-11-30T18:36:06.747113999Z","closed_at":null} +{"id":"rivets-cs4","title":"Improve tracing field visibility in rivets-mcp tools","description":"Several #[instrument] attributes in crates/rivets-mcp/src/tools.rs skip fields that could be useful for debugging (e.g., assignee, label). Consider including them in the tracing output for better observability.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["enhancement","observability"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-12-18T02:29:30.588563048Z","updated_at":"2025-12-18T02:29:30.588563048Z","closed_at":null} +{"id":"rivets-4srr","title":"Add Serialize/Deserialize derives to architecture types","description":"The architecture types added in rivets-byie (Package, PackageId, PackageSource, CouplingMetrics, CouplingSort, PackageDependency, CouplingDetail, ArchStats) don''t derive Serialize/Deserialize, while every other public type in tethys::types does (SymbolId, FileId, Language, Symbol, Reference, etc.).\n\nThe CLI's JSON output is currently hand-rolled via serde_json::json!{} macros, so this works fine for the CLI. But:\n- Library consumers (rivets-mcp, future MCP tools) can't round-trip these types through serde.\n- It's inconsistent with the crate-wide convention.\n\n**Fix:** Add #[derive(Serialize, Deserialize)] to all eight architecture types in crates/tethys/src/types.rs. PackageId should also use #[serde(transparent)] like SymbolId and FileId. Adjust the CLI json! literals to delegate to the serde derives where simpler.\n\nFound during final review of rivets-byie.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["tethys","enhancement"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T00:37:34.786653Z","updated_at":"2026-05-11T00:37:34.786653Z","closed_at":null} +{"id":"rivets-rg0","title":"Create rivets-mcp crate structure","description":"Create the rivets-mcp crate directory structure:\n- crates/rivets-mcp/src/{lib.rs, main.rs, server.rs, tools.rs, context.rs, models.rs, error.rs}\n- Add to workspace Cargo.toml\n- Configure dependencies (rmcp, schemars, etc.)","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:00.027928947Z","updated_at":"2025-11-29T03:06:21.145518766Z","closed_at":"2025-11-29T03:06:21.145518766Z"} {"id":"rivets-8j9u","title":"Add lsp-types dependency","description":"Add `lsp-types = \"0.97\"` to Cargo.toml for LSP protocol types.\n\nThis provides all the types we need: InitializeParams, GotoDefinitionParams, Location, etc.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:31.060353746Z","updated_at":"2026-01-28T23:51:18.097164710Z","closed_at":"2026-01-28T23:51:18.097164710Z"} -{"id":"rivets-1qp","title":"Evaluate rstest for rivets-jsonl/src/lib.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in lib.rs.\n\nFile: crates/rivets-jsonl/src/lib.rs (lines ~319+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:13.789405062Z","updated_at":"2025-11-29T01:27:19.146404160Z","closed_at":"2025-11-29T01:27:19.146404160Z"} -{"id":"rivets-gut","title":"Implement rivets done workflow command","description":"Add `rivets done` command for completing work:\n\n```\n$ rivets done rivets-abc\nClosing rivets-abc...\nAdd completion notes (optional, Ctrl+D to finish):\n> Fixed authentication flow, added token refresh.\n> PR: #42\n\nClosed: rivets-abc\n - Added close reason\n - Status: in_progress -> closed\n\n2 issues are now unblocked:\n - rivets-def Add OAuth support\n - rivets-ghi User profile update\n```","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","workflow"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:56.117458877Z","updated_at":"2025-11-30T18:37:56.117458877Z","closed_at":null} -{"id":"rivets-puyl","title":"Add reference kind tracking to call_edges table","description":"The `call_edges` table currently only tracks caller→callee relationships with call counts, but loses information about the **type** of reference (call, import, type usage, etc.).\n\n**Current limitation:**\n- `get_callers` returns `reference_kinds: vec![ReferenceKind::Call]` as a hardcoded default\n- The original join-based query used `GROUP_CONCAT(kind)` to aggregate actual reference kinds\n- The `parse_reference_kinds` helper function was removed as dead code\n\n**Why this matters:**\n- Impact analysis may want to distinguish \"function calls\" from \"type references\"\n- Security audits may care about data flow (calls) vs type dependencies (imports)\n- Callers could filter by reference kind for more targeted queries\n\n**Proposed enhancement:**\n```sql\nALTER TABLE call_edges ADD COLUMN kinds TEXT;\n-- Store as comma-separated or JSON array: \"call,call,import\"\n```\n\nOr normalize with a separate table:\n```sql\nCREATE TABLE call_edge_kinds (\n caller_symbol_id INTEGER NOT NULL,\n callee_symbol_id INTEGER NOT NULL,\n kind TEXT NOT NULL,\n count INTEGER DEFAULT 1,\n PRIMARY KEY (caller_symbol_id, callee_symbol_id, kind)\n);\n```\n\n**Implementation notes:**\n- Update `populate_call_edges` SQL to GROUP BY kind as well\n- Restore `parse_reference_kinds` function or use JSON\n- Update `get_callers`/`get_callees` to return actual kinds\n\n**Trade-off:**\n- More storage and slightly more complex queries\n- But provides richer information for analysis","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["call-edges","enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:34:49.093333401Z","updated_at":"2026-01-29T19:34:49.093333401Z","closed_at":null} -{"id":"rivets-8mze","title":"Expand language support: Python, TypeScript, Go","description":"Add language extractors for the three highest-leverage languages beyond Rust and C#. KiroGraph supports 17 languages; tethys can borrow the same tree-sitter grammars.\n\n**Why these three first:**\n- Python: huge ecosystem; tree-sitter-python is mature; many Rust crates wrap Python.\n- TypeScript: covers frontend; necessary for full-stack repos and any project using Node tooling. Grammar is tree-sitter-typescript (also covers .tsx via tree-sitter-tsx).\n- Go: well-defined module system; grammar is small and stable; popular in infrastructure code.\n\n**Approach:**\nTethys's languages/ module is already structured around per-language extractors (common.rs, csharp.rs, rust.rs, tree_sitter_utils.rs). Each new language gets its own file plus grammar dependency in Cargo.toml.\n\nEach extractor needs to handle:\n- Symbol extraction (functions, classes/structs, methods, types)\n- Visibility (Python lacks formal modifiers; convention is leading underscore)\n- Imports / module resolution (Python: import / from; TS: import { } from; Go: import \"path\")\n- Test detection (Python: pytest test_*, unittest.TestCase; TS: describe/it; Go: func TestXxx)\n- Reference extraction (call expressions, type annotations)\n\n**Dependencies to add:**\n- tree-sitter-python = ''0.23''\n- tree-sitter-typescript = ''0.23''\n- tree-sitter-go = ''0.23''\n\n**Suggested split:**\nThis issue is an epic — break into one child issue per language so they can ship incrementally:\n- Python language support\n- TypeScript language support\n- Go language support\n\nEach child should add: grammar dep, extractor module, common test fixture under crates/tethys/tests/fixtures//, and integration tests. The tree-sitter parsing infrastructure is already done — adding a language is mostly a query-pattern translation exercise.","status":"open","priority":2,"issue_type":"epic","assignee":null,"labels":["tethys","kirograph-inspired","languages"],"design":null,"acceptance_criteria":"- [ ] Three child issues created (Python, TypeScript, Go), each independently shippable\n- [ ] Each language reaches feature parity with the Rust extractor: symbols, refs, imports, test detection\n- [ ] Integration test on a representative open-source repo per language\n- [ ] tethys index detects language correctly by file extension\n- [ ] README updated with new language list","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:32:34.601715100Z","updated_at":"2026-05-10T04:32:34.601715100Z","closed_at":null} -{"id":"rivets-7v2","title":"Add interactive prompts for missing required fields","description":"When creating issues without required fields, prompt interactively:\n\n```\n$ rivets create\nTitle: Fix authentication bug\nPriority [2]: 1\nType [task]: bug\nCreated issue: rivets-abc\n```\n\nSkip prompts when --json flag is set (for agent compatibility).","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:36.843222609Z","updated_at":"2025-11-30T18:35:36.843222609Z","closed_at":null} -{"id":"rivets-qawf","title":"tethys: push name/Ca/Ce sort to SQL ORDER BY (keep instability in Rust)","description":"get_coupling_metrics (architecture.rs) fetches all rows from arch_coupling unsorted, then sorts in Rust. This was a deliberate choice for the Instability variant — the formula lives only in CouplingMetrics::instability() and can't be replicated in SQL without duplicating logic. For the other three sort variants (Name, Afferent, Efferent) the choice was made by consistency, not necessity.\n\nReviewer flagged in PR 60 round 7: \"At scale (monorepos with hundreds of crates) the full fetch + in-memory sort could become noticeable... consider adding a follow-up issue for DB-side sorting of the non-instability variants (name, Ca, Ce) if performance ever becomes a concern.\"\n\nTrigger condition: a workspace with hundreds of crates where get_coupling_metrics becomes a measurable hot path.\n\nScope:\n- Add ORDER BY clauses to the query in get_coupling_metrics for Name/Ca/Ce variants\n- Keep the Rust-side sort for Instability (formula stays in one place)\n- The current secondary sort (by package name) for ties stays in the SQL ORDER BY\n\nNote: SQLite COLLATE NOCASE may be wanted for the Name variant to match the locale-insensitive lexicographic sort Rust's String::cmp provides today.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] get_coupling_metrics emits ORDER BY for CouplingSort::Name, ::Afferent, ::Efferent\n- [ ] Instability sort still happens in Rust (formula stays in one place)\n- [ ] Tie-breaking by name remains deterministic across variants\n- [ ] Benchmark before/after on a synthetic >200-crate workspace","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:21:54.741507500Z","updated_at":"2026-05-11T21:21:54.741507500Z","closed_at":null} -{"id":"rivets-c0p","title":"Convert resilient_loading.rs to rstest","description":"Extract common fixtures for temp file creation. Parameterize error frequency tests with #[case].\n\nFile: crates/rivets-jsonl/tests/resilient_loading.rs","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Temp file creation extracted to fixture\n- Error frequency tests parameterized with #[case]\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:25.053725065Z","updated_at":"2025-11-29T02:09:12.153724641Z","closed_at":"2025-11-29T02:09:12.153724641Z"} -{"id":"rivets-wao","title":"Implement rivets pick workflow command","description":"Add `rivets pick` command for interactive work claiming:\n\n```\n$ rivets pick\nShowing 5 ready issues sorted by priority:\n\n1. [P0] rivets-abc Fix critical auth bug @unassigned\n2. [P1] rivets-def Update OAuth tokens @unassigned \n3. [P2] rivets-ghi Add unit tests @unassigned\n\nPick issue to work on (1-3, or 'q' to quit): 1\n\nStarting work on rivets-abc...\n - Status: open -> in_progress\n - Assignee: -> alice (you)\n\nReady to work! Run 'rivets done rivets-abc' when complete.\n```","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","workflow"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:50.467716127Z","updated_at":"2025-11-30T18:37:50.467716127Z","closed_at":null} -{"id":"rivets-ry6","title":"Adopt rstest for test improvement","description":"Adopt the rstest crate to improve test code quality and reduce duplication. rstest provides fixtures, parameterized tests, and matrix testing capabilities that will eliminate 20-30% of test code duplication.","status":"closed","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- rstest dependency added to both crates\n- High-impact test files converted to use rstest fixtures and parameterization\n- All tests pass after conversion\n- Measurable reduction in test code lines","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T00:55:47.850281116Z","updated_at":"2025-11-29T02:11:34.462327431Z","closed_at":"2025-11-29T02:11:34.462327431Z"} -{"id":"rivets-y9x","title":"Add storage metrics to rv stats command","description":"Monitor Automerge document growth and history depth.\n\nAdd to `rv stats` output:\n- Document file size (bytes)\n- Number of changes in history\n- Number of actors (unique editors)\n- Last compaction date (if applicable)\n- Comparison to equivalent JSONL size\n\nThis helps users understand storage overhead and decide when compaction is needed.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T21:49:43.181865075Z","updated_at":"2025-12-23T04:45:34.661097069Z","closed_at":"2025-12-23T04:45:34.661097069Z"} +{"id":"rivets-vmb","title":"Evaluate rstest for rivets/src/domain/mod.rs and id_generation.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in domain/mod.rs and id_generation.rs.\n\nFiles:\n- crates/rivets/src/domain/mod.rs (lines ~397+)\n- crates/rivets/src/id_generation.rs (lines ~322+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:30.801627179Z","updated_at":"2025-11-29T01:30:50.015894878Z","closed_at":"2025-11-29T01:30:50.015894878Z"} +{"id":"rivets-0jv","title":"Consider path canonicalization in find_rivets_root for symlink support","description":"The find_rivets_root function traverses parent directories to find a .rivets directory. If the start path contains symlinks, this might behave unexpectedly since it operates on the logical path rather than the resolved physical path.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["enhancement","filesystem"],"design":"Current implementation:\\n```rust\\nlet mut current = start_dir.to_path_buf();\\n```\\n\\nPotential enhancement:\\n```rust\\nlet mut current = start_dir.canonicalize().ok()?.to_path_buf();\\n```\\n\\nTrade-offs:\\n- Pro: Correctly handles symlinked directories\\n- Con: canonicalize() fails if path doesn't exist\\n- Con: Adds complexity for edge case\\n\\nOptions:\\n1. Try canonicalize, fall back to original path on failure\\n2. Document current behavior as intentional\\n3. Add optional `follow_symlinks` parameter\\n\\nCurrent implementation is reasonable for most use cases. This is a low-priority enhancement for symlink-heavy workflows.","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T02:54:20.690844991Z","updated_at":"2025-11-30T02:54:20.690844991Z","closed_at":null} +{"id":"rivets-fk9","title":"Research and design JSONL library architecture","description":"Design a standalone Rust library for efficient JSONL (JSON Lines) operations that can be used by rivets and potentially other projects. The library should handle reading, writing, querying, and streaming JSONL data structures.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Research key features needed:\n- Efficient line-by-line reading and writing\n- Streaming support for large files\n- Querying/filtering capabilities\n- Schema validation (optional)\n- Error handling for malformed JSON\n- Memory-efficient operations\n- Concurrent access patterns\n\nConsider existing Rust JSONL libraries and what gaps exist. Determine if we build from scratch or extend existing solutions.","acceptance_criteria":"- Document listing key features and API design\n- Comparison of existing Rust JSONL libraries (evaluated on: performance benchmarks, API ergonomics, maintenance status, license compatibility, feature completeness)\n- Decision matrix showing trade-offs\n- Decision on whether to build new library or extend existing one\n- Performance requirements documented (targets: streaming 100MB file <1s, memory usage <10MB regardless of file size)\n- Public API surface designed with examples\n- Compatibility with serde ecosystem verified","notes":"## Research Complete (2025-11-27)\n\nComprehensive research documented in `docs/rivets-jsonl-research.md`.\n\n**Key Findings**:\n- Evaluated 4 existing Rust JSONL libraries (jsonl, serde-jsonlines, json-lines, json-stream)\n- None fully meet rivets' needs (async-first, streaming queries, resilient loading)\n- **Decision**: Build custom implementation borrowing proven patterns\n\n**Performance Targets**:\n- 100MB file in <1s (read)\n- <10MB memory regardless of file size\n- Match or exceed current in_memory.rs baseline\n\n**API Design**:\n- Async-native with tokio\n- Extension traits for ergonomics\n- Streaming via futures::Stream\n- Resilient loading with warning collection\n- Query/filter during stream\n\n**Next**: Implement Phase 1 (Core Read/Write)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-cr9","dep_type":"blocks"}],"created_at":"2025-11-17T21:08:44.178736229Z","updated_at":"2025-11-27T23:01:29.152853238Z","closed_at":"2025-11-27T23:01:29.152853238Z"} +{"id":"rivets-aay4","title":"Populate parent_symbol_id for nested symbols","description":"The schema supports `parent_symbol_id` for hierarchical symbols (e.g., method inside struct) but it's always `None`. See `lib.rs:632` and `lib.rs:761`.\n\nPopulating this would enable queries like:\n- \"Show all methods of struct X\"\n- \"Find all nested types in module Y\"\n- Symbol hierarchy navigation","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:15.487249815Z","updated_at":"2026-01-30T00:18:15.487249815Z","closed_at":null} +{"id":"rivets-j9bu","title":"Tethys: Codebase Intelligence Engine","description":"Improvements inspired by analyzing the Drift codebase. Drift is a mature codebase intelligence platform that handles 10K+ file codebases efficiently.\n\nKey patterns from Drift:\n- Rayon for parallel file parsing\n- Streaming SQLite writes via MPSC channel\n- Pre-computed call graph edges\n- MCP server integration for AI agents\n- Reachability analysis for security auditing\n- Test topology for CI integration\n\nThese improvements would make tethys production-ready for large codebases.","status":"open","priority":1,"issue_type":"epic","assignee":null,"labels":["architecture","drift-inspired","performance"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-29T12:48:13.969977556Z","updated_at":"2026-02-21T02:19:23.038730450Z","closed_at":null} +{"id":"rivets-kz8j","title":"Add clippy missing_errors_doc documentation","description":"Remove the allow(clippy::missing_errors_doc) suppression and add proper error documentation to public methods. Currently deferred to avoid churn during initial development.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:14.455287073Z","updated_at":"2026-02-06T00:35:14.455287073Z","closed_at":null} {"id":"rivets-3nx","title":"Implement undo/redo system","description":"Add transaction history for undo/redo:\n\n- Store operation log in `.rivets/history.jsonl`\n- Each entry: `{timestamp, operation, before_state, after_state}`\n- Keep last 50 operations\n\nCommands:\n- `rivets undo` - Revert last mutation\n- `rivets history` - Show recent operations\n\nNote: Lower priority since git provides a safety net.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:38:07.706908268Z","updated_at":"2025-11-30T18:38:07.706908268Z","closed_at":null} -{"id":"rivets-0vd","title":"Implement dep tree visualization","description":"Add 'dep tree' subcommand to display dependency tree for an issue.\\n\\nShould show both upstream dependencies (what this issue depends on) and downstream dependents (what depends on this issue) in a tree format.\\n\\nExample: rivets dep tree proj-abc","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:24:02.683285758Z","updated_at":"2025-12-28T00:50:58.475771946Z","closed_at":"2025-12-28T00:50:58.475771946Z"} -{"id":"rivets-oeu5","title":"Add --lsp flag to CLI commands","description":"Add `--lsp` flag to: index, callers, impact\n\nBehavior:\n- If --lsp and LSP available: use LSP\n- If --lsp and LSP missing: error and halt with helpful message\n- No flag: tree-sitter only\n\nError message should include installation instructions for rust-analyzer.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-nwwm","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:50.158866500Z","updated_at":"2026-01-29T00:31:18.920171795Z","closed_at":"2026-01-29T00:31:18.920171795Z"} -{"id":"rivets-bz5","title":"Implement InMemoryStorage with petgraph for dependency graph","description":"Create the InMemoryStorage implementation using HashMap for issues and petgraph for the dependency graph. This is the first storage backend and enables fast MVP development.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on storage trait from rivets-0gc, implement in-memory backend:\n\n**Structure (following rivets-0gc architecture):**\n\n```rust\n// Private inner storage (not thread-safe)\nstruct InMemoryStorageInner {\n issues: HashMap,\n graph: DiGraph, // petgraph\n node_map: HashMap,\n}\n\n// Public thread-safe type alias\npub type InMemoryStorage = Arc>;\n```\n\n**Async Trait Implementation (blocking operations in async methods):**\n\n```rust\n#[async_trait]\nimpl IssueStorage for InMemoryStorage {\n async fn create(&mut self, new: NewIssue) -> Result {\n let mut inner = self.lock().await;\n let id = generate_hash_id(&new); // Uses rivets-x1e\n let issue = Issue::from(new, id.clone());\n inner.issues.insert(id.clone(), issue.clone());\n let node = inner.graph.add_node(id.clone());\n inner.node_map.insert(id.clone(), node);\n Ok(issue)\n }\n \n async fn has_cycle(&self, from: &IssueId, to: &IssueId) -> Result {\n let inner = self.lock().await;\n // Use petgraph's has_path_connecting\n let from_node = inner.node_map.get(from)\n .ok_or_else(|| Error::IssueNotFound(from.clone()))?;\n let to_node = inner.node_map.get(to)\n .ok_or_else(|| Error::IssueNotFound(to.clone()))?;\n Ok(algo::has_path_connecting(&inner.graph, *to_node, *from_node, None))\n }\n \n async fn ready_to_work(&self, filter: Option<&IssueFilter>) -> Result> {\n let inner = self.lock().await;\n // Graph traversal to find issues with no blocking dependencies\n // Filter by status=open or in_progress\n // Check all incoming edges for blocking dependencies\n // Return issues with no blockers\n // ... implementation\n }\n}\n```\n\n**Key Implementation Notes:**\n- Use `Arc::new(Mutex::new(InMemoryStorageInner { ... }))` for initialization\n- Lock mutex for all operations: `let inner = self.lock().await;`\n- All petgraph and HashMap operations are blocking (acceptable for MVP)\n- Graph operations use petgraph::algo for cycle detection and traversal\n- Maintain node_map synchronization when adding/removing issues\n\n**Dependencies**: `petgraph = \\\"0.6\\\"` for graph algorithms, `async-trait` for trait implementation, `tokio` for Mutex","acceptance_criteria":"- InMemoryStorage implements all trait methods\n- Uses petgraph for dependency graph\n- Cycle detection working with graph algorithms\n- Ready work calculation via graph traversal\n- All CRUD operations functional\n- Unit tests for all operations\n- Benchmark: 1000 issues in <10ms","notes":"## Clarifications\n\n### Session 2025-11-17\n\n- Q: The task design shows `pub struct InMemoryStorage` but rivets-0gc specifies `InMemoryStorageInner` wrapped in `Arc>`. Which approach should this task implement? → A: Follow rivets-0gc: implement InMemoryStorageInner + Arc> wrapper (ensures thread safety, matches architecture from trait design)\n- Q: The task shows synchronous trait methods (`fn create_issue`) but rivets-0gc defines async trait methods (`async fn create`). How should InMemoryStorage implement the trait? → A: Use blocking operations in async methods (simple for MVP, matches Phase 1 approach from rivets-0gc)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-06w","dep_type":"blocks"},{"depends_on_id":"rivets-0gc","dep_type":"blocks"},{"depends_on_id":"rivets-x1e","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:41.594099121Z","updated_at":"2025-11-18T02:58:07.492885986Z","closed_at":"2025-11-18T02:58:07.492885986Z"} +{"id":"rivets-1h03","title":"Add integration tests for concurrency and filesystem edge cases","description":"PR review identified test coverage gaps: thread-local parser re-entrant calls, BatchWriter race conditions under heavy load, mutex poisoning recovery, symlink handling in file discovery, files >2GB, empty workspace scenarios, files outside workspace boundary.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:46.284076258Z","updated_at":"2026-02-06T00:54:46.284076258Z","closed_at":null} +{"id":"rivets-6ep","title":"Investigate lock contention in multi-workspace MCP scenarios","description":"The current `Arc>` design serializes all storage access across workspaces. For typical single-workspace usage this is fine, but could become a bottleneck with many concurrent MCP requests to different workspaces.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["future","optimization","performance"],"design":"**Current behavior:**\n- Every tool call acquires context read lock to get storage\n- Lock ordering (release context before storage) is correct but adds overhead\n\n**Potential optimization:**\nReplace with lock-free workspace lookup using `DashMap`:\n```rust\nstorage_cache: Arc>>>>\n```\n\nThis allows concurrent reads from different workspaces without contention. Only `set_context` would need exclusive access.\n\n**Locations:**\n- `tools.rs:96-100`\n- `context.rs:32`","acceptance_criteria":"- [ ] Profile with realistic multi-workspace workload to confirm contention exists\n- [ ] If confirmed, implement DashMap-based approach\n- [ ] Benchmark before/after to verify improvement\n- [ ] No regression in single-workspace performance","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T04:28:36.847262094Z","updated_at":"2025-11-29T04:28:36.847262094Z","closed_at":null} +{"id":"rivets-j2r1","title":"Type hierarchy: extract and traverse extends/implements edges","description":"Add type-hierarchy queries — walk inheritance and trait/interface implementation chains upward (base types) or downward (derived types).\n\n**Inspired by:** KiroGraph's kirograph_type_hierarchy tool.\n\n**Languages:**\n- Rust: impl X for Y, trait X: Y + Z, struct X\n- C#: class X : BaseClass, IInterface, IGeneric\n\n**Schema additions:**\n- New ReferenceKind variants: Extends, Implements (already partly modeled in ReferenceKind enum — check current state).\n- Tree-sitter extractors for both languages emit these edges during indexing.\n- Reuse the existing refs table; no new tables needed.\n\n**Why this depends on rivets-aay4:**\nFor methods, we want to walk ''method M of class X'' to its overridden version in BaseClass. That requires knowing which class a method belongs to, which is parent_symbol_id. Without it, we can do class-to-class hierarchy but not method-to-method override resolution.\n\n**Public API:**\n- Tethys::get_type_hierarchy(symbol: &str, direction: HierarchyDirection) -> Result\n- enum HierarchyDirection { Up, Down, Both }\n\n**CLI:**\n- tethys hierarchy [--direction up|down|both]","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] Rust extractor emits Extends/Implements edges for impl blocks and trait bounds\n- [ ] C# extractor emits Extends/Implements edges for class inheritance and interface lists\n- [ ] get_type_hierarchy() API method\n- [ ] CLI subcommand tethys hierarchy\n- [ ] Tests on a Rust trait fixture and a C# class-hierarchy fixture\n- [ ] MCP tool tethys_type_hierarchy (sibling rivets-o4re)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-aay4","dep_type":"blocks"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:32:18.694969900Z","updated_at":"2026-05-10T04:32:18.694969900Z","closed_at":null} +{"id":"rivets-g25","title":"Implement reopen command","description":"Add 'reopen' command to reopen closed issues. Should support multiple issue IDs and --reason flag. Example: rivets reopen id1 id2 --reason 'Reopening for further work'","status":"closed","priority":1,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:33.144141194Z","updated_at":"2025-12-28T00:30:58.643711846Z","closed_at":"2025-12-28T00:30:58.643711846Z"} +{"id":"rivets-7nko","title":"Consider StorageResult type alias if helper pattern expands","description":"From PR #34 review: Consider adding a type alias for Result if more helper functions emerge that use this pattern. Currently only save_or_record_failure uses it, so not needed yet.\n\n```rust\ntype StorageResult = Result;\n```\n\nTrigger: Add this when a second helper function needs the same type signature.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T16:53:22.966528697Z","updated_at":"2025-12-28T16:53:22.966528697Z","closed_at":null} +{"id":"rivets-6tl","title":"Implement issue filtering and query system","description":"Implement the comprehensive filtering system for querying issues by status, priority, type, assignee, labels, dates, and text search.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads IssueFilter struct and query builder:\n\n**Filter Dimensions**:\n- **Status**: open, in_progress, blocked, closed\n- **Priority**: 0-4, ranges (P0-P2), min/max\n- **Type**: bug, feature, task, epic, chore\n- **Assignee**: specific user, empty\n- **Labels**: AND (all required), OR (any match)\n- **Dates**: created_after/before, updated_after/before, closed_after/before\n- **Text**: title_contains, description_contains, notes_contains\n- **Flags**: empty_description, no_assignee, no_labels\n- **IDs**: specific list\n\n**Query Builder Pattern**:\n```rust\nIssueFilter::builder()\n .status(vec![Status::Open, Status::InProgress])\n .priority_range(0..=2)\n .labels_all(vec![\"bug\", \"urgent\"])\n .created_after(date)\n .limit(100)\n .build()\n```\n\n**SQL Generation**:\n- Dynamic WHERE clause construction\n- Parameterized queries (SQL injection safe)\n- Index-friendly query plans","acceptance_criteria":"- All filter dimensions functional\n- Complex multi-filter queries work\n- Query builder API type-safe\n- SQL generation correct and efficient\n- Performance acceptable (1000 issues in <50ms)\n- Unit tests for edge cases\n- Integration tests for complex filters","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:47.522142136Z","updated_at":"2025-11-17T22:16:47.522142136Z","closed_at":null} +{"id":"rivets-3q4","title":"Add throughput benchmarks","description":"Create throughput benchmarks to verify \"100MB file in <1s\" read and write targets.\n\nMeasures actual I/O performance with realistic data.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create benches/throughput_benchmarks.rs:\n\n```rust\nuse criterion::{criterion_group, criterion_main, Criterion, Throughput};\n\nfn throughput_benchmarks(c: &mut Criterion) {\n let mut group = c.benchmark_group(\"throughput\");\n \n // Read benchmarks\n group.throughput(Throughput::Bytes(100 * 1024 * 1024)); // 100MB\n group.bench_function(\"read_100mb\", |b| {\n b.iter(|| {\n // Stream read 100MB JSONL file\n });\n });\n \n // Write benchmarks\n group.throughput(Throughput::Bytes(100 * 1024 * 1024));\n group.bench_function(\"write_100mb\", |b| {\n b.iter(|| {\n // Write 100MB JSONL file\n });\n });\n \n // Atomic write benchmarks\n group.throughput(Throughput::Bytes(100 * 1024 * 1024));\n group.bench_function(\"write_100mb_atomic\", |b| {\n b.iter(|| {\n // Atomic write 100MB\n });\n });\n}\n```\n\nTargets from research:\n- Read: 100MB in <1s (>100MB/s)\n- Write: 100MB in <1.5s (>66MB/s)","acceptance_criteria":"- Throughput benchmarks created\n- Read 100MB benchmark passes <1s target\n- Write 100MB benchmark passes <1.5s target\n- Atomic write performance measured\n- Results documented in README\n- cargo bench runs successfully","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-t0k","dep_type":"blocks"}],"created_at":"2025-11-27T23:17:52.519008590Z","updated_at":"2025-11-27T23:17:52.519008590Z","closed_at":null} +{"id":"rivets-l8ur","title":"PR #60 polish backlog: cleanup items deferred from review rounds","description":"Collection of low-priority cleanup items surfaced during PR #60's third review round. Each is real but represents diminishing returns relative to the work already done; bundle them for a future hygiene pass.\n\n**Items:**\n\n1. Remove the now-redundant ''suggestions_capped_at_five'' test in cli/coupling.rs — the new ''suggestions_capped_at_five_returns_first_five_by_input_order'' is a strict superset. Both currently exist and assert the same length on identical inputs.\n\n2. Add an ''add_file'' helper to ''package_coupling_tests'' (it already exists in ''repopulate_architecture_tests''). The new sort-order test in particular repeats the inline ''upsert_file'' pattern 7 times.\n\n3. Strengthen ''get_package_coupling_returns_err_for_corrupt_target_source'': currently asserts only ''is_err()''. Add assertions that the error is the expected variant (''Error::Internal'') and that the message contains the corrupt source value (''totally-bogus'') so a future change that drops diagnostic context would fail the test.\n\n4. Corrupt-source tests use hardcoded package ''id=1,2,3'' and rely on ''PRAGMA ignore_check_constraints'' silently succeeding. Add a verification ''SELECT'' after the corrupt INSERT to confirm the bad row landed, so a silent PRAGMA failure produces a clear diagnostic at the injection point instead of a confusing downstream assertion mismatch. Optionally switch the inserts to use ''INSERT ... RETURNING id'' or query back ids rather than hardcoding.\n\n5. The ''completed_path_writes_nothing_to_stdout'' assertion message embeds the ''rivets-tuph'' ticket reference. Move it to a ''// TODO(rivets-tuph)'' comment above the assertion so the failure message stays clean and the editorial intent stays out of test output.\n\n6. Tighten the ''#[expect(clippy::cast_precision_loss, reason = ...)]'' annotation on ''CouplingMetrics::instability'' to explicitly say ''the annotated cast is ; the numerator uses which is infallible widening, exempt from the lint.''\n\n7. Add a reciprocal cross-reference: ''Package.name'' field doc should point at ''PackageId'' (''recommended stable identity; see [] for why the numeric id is unsuitable for that purpose''). Currently the ''PackageId → name'' direction is documented but not the reverse.\n\n8. The ''none_writes_nothing'' assertion is bare ''assert!(buf.is_empty())'' with no contract message. Add an explanation so a failure tells a future maintainer what the ''None'' arm represents.\n\nDiscovered during third-round PR #60 review (the 6 specialist reviewers ran on commits a4949bb..HEAD).","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["tethys","polish","test-quality"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T05:20:38.446776300Z","updated_at":"2026-05-11T05:20:38.446776300Z","closed_at":null} +{"id":"rivets-2uv","title":"Evaluate rstest for rivets-jsonl/src/warning.rs and atomic.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in warning.rs and atomic.rs.\n\nFiles:\n- crates/rivets-jsonl/src/warning.rs (lines ~392+)\n- crates/rivets-jsonl/src/atomic.rs (lines ~209+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:19.453806850Z","updated_at":"2025-11-29T01:28:55.618500868Z","closed_at":"2025-11-29T01:28:55.618500868Z"} +{"id":"rivets-4iz","title":"Add ratatui and crossterm dependencies for TUI","description":"Add TUI dependencies to rivets crate:\n- ratatui = \"0.29\" (TUI framework)\n- crossterm = \"0.28\" (cross-platform terminal backend)","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:55.243494646Z","updated_at":"2025-11-30T18:36:55.243494646Z","closed_at":null} +{"id":"rivets-kln","title":"Add dialoguer dependency for interactive prompts","description":"Add dialoguer crate to workspace dependencies for interactive CLI prompts.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["phase-1b","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:35:31.001190538Z","updated_at":"2025-11-30T18:35:31.001190538Z","closed_at":null} +{"id":"rivets-n08s","title":"Prompt for prefix in `rivets init` when not provided","description":"When running `rivets init` without a prefix argument, prompt users to enter one interactively (similar to how we prompt when creating an issue).","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-11T22:47:22.022380598Z","updated_at":"2026-01-11T22:54:29.052167180Z","closed_at":"2026-01-11T22:54:29.052167180Z"} +{"id":"rivets-778r","title":"Complete C# unsafe modifier and generic parameter extraction","description":"Two TODO sites in languages/csharp.rs: line 1036 (detect unsafe modifier) and line 1038 (extract type parameters/generics). Currently hardcoded to false/None.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:48.487835208Z","updated_at":"2026-02-06T00:54:48.487835208Z","closed_at":null} +{"id":"rivets-41t","title":"Improve JSON output consistency across commands","description":"Review and potentially standardize JSON output patterns across CLI commands.\n\nCurrently there are two approaches:\n- `output.rs:260-266`: Uses `IssueDetails` struct with `#[serde(flatten)]` to embed issue fields\n- `execute.rs:320-323`: Creates inline `serde_json::json!()` objects for delete command\n\nBoth approaches work, but consider whether to standardize on one pattern for better consistency and maintainability. Options:\n1. Use dedicated structs with `#[serde(flatten)]` everywhere (more type-safe)\n2. Use inline `json!()` macros everywhere (more flexible)\n3. Keep mixed approach where appropriate (current state)\n\nThis is a low-priority code quality improvement.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["code-quality","pr-feedback"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T17:42:48.770546385Z","updated_at":"2025-11-30T17:42:48.770546385Z","closed_at":null} {"id":"rivets-1dcc","title":"Adopt rstest for parameterized tests in tethys","description":"PR review recommends adopting rstest for parameterized tests per CLAUDE.md guidelines. Current tests are well-structured but many could be consolidated with rstest case attributes. Low priority - functional tests already exist.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:05.545352705Z","updated_at":"2026-02-06T00:35:05.545352705Z","closed_at":null} -{"id":"rivets-aic","title":"Add version field to RivetsConfig for migration support","description":"Add a version field to config.yaml to support future config format migrations. This allows the application to detect older config formats and migrate them automatically.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["config","migration"],"design":"Add a version field to RivetsConfig:\\n\\n```yaml\\nversion: 1\\nissue-prefix: proj\\nstorage:\\n backend: memory\\n data_file: .rivets/issues.jsonl\\n```\\n\\nImplementation:\\n1. Add `version: u32` field to RivetsConfig (default to 1)\\n2. On load, check version and apply migrations if needed\\n3. Migrations should be idempotent and well-tested\\n4. Consider semver for version if schema changes significantly\\n\\nThis enables:\\n- Backwards compatibility with older configs\\n- Automatic migration on first run after upgrade\\n- Clear error messages for unsupported versions","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5an","dep_type":"related"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-30T02:51:51.756750966Z","updated_at":"2025-11-30T02:51:51.756750966Z","closed_at":null} -{"id":"rivets-len","title":"Implement JsonlReader::read_line() method","description":"Implement the read_line() async method that reads a single JSONL line and deserializes it into type T.\n\nThis is the foundational read operation that all other reading functionality builds on.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse serde::de::DeserializeOwned;\nuse tokio::io::AsyncBufReadExt;\n\nimpl JsonlReader {\n pub async fn read_line(&mut self) -> Result> {\n let mut line = String::new();\n let bytes_read = self.reader.read_line(&mut line).await?;\n \n if bytes_read == 0 {\n return Ok(None); // EOF\n }\n \n self.line_number += 1;\n \n let trimmed = line.trim();\n if trimmed.is_empty() {\n // Skip empty lines, read next\n return self.read_line().await;\n }\n \n let value: T = serde_json::from_str(trimmed)\n .map_err(|e| Error::Json(e))?;\n \n Ok(Some(value))\n }\n}\n```","acceptance_criteria":"- read_line() method implemented\n- Returns Result> (None for EOF)\n- Increments line_number on each line\n- Skips empty lines\n- Deserializes JSON using serde_json\n- Proper error handling for malformed JSON\n- Unit tests verify reading single/multiple lines\n- Unit tests verify EOF handling","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-uo7","dep_type":"blocks"}],"created_at":"2025-11-27T23:14:40.175422639Z","updated_at":"2025-11-28T00:05:52.845896777Z","closed_at":"2025-11-28T00:05:52.845896777Z"} -{"id":"rivets-i8qn","title":"Add entry_point_file() / src_root() accessors on CrateInfo","description":"Inline expressions in tethys resolver code leak CrateInfo internals (lib_path -> bin_paths fallback chain, src/ hardcode, hyphen->underscore normalization). Encapsulate behind accessors on the type.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":"## Suggested accessors\n\nThree accessors on `CrateInfo` that consolidate currently-leaked behavior:\n\n1. **`entry_point_file(&self) -> Option`** - returns `lib_path` joined onto `path`, falling back to first `bin_paths` entry, `None` if neither set. Replaces the `or_else` chain in `resolver.rs` single-segment arm (~4 lines -> 1).\n\n2. **`src_root(&self) -> PathBuf`** - returns the crate's source root directory. Should NOT be implemented as `self.path.join(\"src\")` - that's the bug being fixed by rivets-6aoc/rivets-34tv. The correct implementation derives from `lib_path.parent()` with hardcoded `src/` only as the None fallback. Introducing this accessor IS the rivets-6aoc fix; this issue and rivets-6aoc should likely be done together.\n\n3. **`module_name(&self) -> Cow`** - returns the name normalized for Rust module paths (hyphens->underscores). Marginal win (one use site today), but pre-pays the design tax if more callers need to compare crate names to module path heads.\n\n## Discovered during\n\nPR #62 round-4 review feedback from type-design-analyzer agent. Ratings reported: encapsulation 3/5, invariant expression 2/5, usefulness 3/5, enforcement 2/5.\n\n## Out-of-scope justification for PR #62\n\n`src_root()` would relocate the `src/` hardcode to the type without fixing it - misleading encapsulation. Best done in/with rivets-6aoc where the implementation is the actual fix. `module_name()` is marginal at a single use site. `entry_point_file()` alone is real value but was deferred to keep PR #62 narrowly scoped (its tests already lock down the inline behavior; the accessor refactor is a clean follow-up).","acceptance_criteria":"- [ ] entry_point_file() accessor on CrateInfo with doc-comment explaining fallback order\n- [ ] resolver.rs single-segment arm uses the accessor\n- [ ] src_root() accessor introduced as part of rivets-6aoc work (lib_path-derived implementation, not hardcoded src/)\n- [ ] module_name() accessor only if a second call site appears; otherwise drop\n- [ ] Tests cover all three accessors' contracts","notes":"Closed: Fixed in PR #70 (commit 33639ac). Added CrateInfo::entry_point_file() accessor with 4 unit tests; replaced inline lib_path/bin_paths fallback chain in resolver.rs single-segment arm. CrateInfo::src_root() pre-existed from rivets-6aoc. module_name() accessor dropped per conditional acceptance #4 (only one CrateInfo.name normalization call site exists).","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gom","dep_type":"blocks"},{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-12T21:47:17.334291900Z","updated_at":"2026-05-18T18:51:03.262363Z","closed_at":"2026-05-18T18:51:03.262360900Z"} -{"id":"rivets-ycaq","title":"Tethys: Resolver correctness and coverage","description":"Umbrella for the resolver-correctness work that determines how trustworthy tethys's cross-file/cross-crate edge data is for downstream consumers (coupling metrics, callers, cycles, impact analysis).\n\nEmpirical baseline (rivets workspace, no LSP, 2026-05-13):\n - 21,777 total refs extracted\n - 6,675 resolved (30.7%) — 2,937 same-file (Pass 1), 3,738 cross-file (Pass 2)\n - 15,102 unresolved (69.3%) — ~12,962 are stdlib/external (Pass-3 LSP territory), 2,140 match a name that exists somewhere in workspace symbols\n\nTwo structural facts shape the goal:\n 1. Most unresolved refs are intrinsically out of reach for tethys's workspace-only model — names like expect, unwrap, Vec, Option. Only LSP (Pass 3) can resolve these.\n 2. The 3,738 'resolved cross-file' number includes known phantom edges. rivets-0gom's probe found 88% of cross-crate file_deps edges were phantom; rivets-3d0s identified residual phantom edges from stdlib symbol-name collisions.\n\n'100% resolution' has three readings:\n - Every ref resolved by tethys alone: NOT achievable (stdlib/external out of scope by design)\n - Every workspace-legitimate ref resolved correctly: ACHIEVABLE via this epic\n - Every ref resolved including external: achievable with --lsp once correctness chain is fixed (PR #64 closed rivets-714v unblocking Pass 3 on Windows multi-crate)\n\nSequenced children:\n - rivets-lcb6 (file_deps never cleared between runs) — measurement prerequisite\n - rivets-0gom (crate:: path resolution is workspace-wide filename lookup) — highest-impact correctness\n - rivets-3d0s (stdlib/external symbol-name collisions) — residual phantom cleanup\n - [Pass-2 short-circuit, this epic] — coverage gap from imports.is_empty() early return\n - rivets-i8qn + rivets-6jxv — design-tax cleanup, lock in 6aoc/34tv fix at the type level\n\nProjection after completion: workspace-internal resolution correctness approaches 100%; overall rate (with --lsp on) climbs from 30.7% to 75–90% as Pass 3 fills stdlib/external; residual gap is rust-analyzer's own limits (macro-generated identifiers, trait-impl ambiguities).","status":"closed","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] rivets-lcb6 closed (file_deps cleared between index runs)\n- [ ] rivets-0gom closed (crate:: path resolution scoped to caller's crate)\n- [ ] rivets-3d0s closed (no phantom edges from stdlib symbol-name collisions)\n- [ ] Pass-2 short-circuit-on-no-imports issue closed\n- [ ] rivets-i8qn + rivets-6jxv closed (CrateInfo accessors, deduped crate_root_for_file helper)\n- [ ] Re-measured baseline on rivets workspace shows: workspace cross-crate edges match Cargo dep graph; cross-file resolved count is stable across two consecutive index runs; phantom-edge rate < 1%","notes":"Closed: All 6 acceptance criteria met. Closed children: rivets-lcb6 (#65), rivets-0gom (#66), rivets-3d0s (#67), rivets-dn35 (#69), rivets-i8qn (#70), rivets-6jxv (#71). Measurement artifact: .rivets-ycaq/measurement-2026-05-17.md (#68) shows phantom-edge rate 0.00% on rivets workspace (was 88% pre-fix), cross-crate file_deps reduced 74 -> 8 (all 8 corroborated by use imports), idempotent across consecutive --rebuild runs. Refs-level phantoms (202 cross-crate FORBIDDEN-pair refs) intentionally preserved per design-v3 K-hybrid claims C5/C6 — the fix contains phantom propagation at the file_deps consumer boundary, not in the resolver itself. Adjacent resolver work (rivets-044i qualified-path gap discovered mid-epic, plus 22 other open resolver issues for perf/coverage/features) lives outside ycaq's scope.","external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:48:14.592810300Z","updated_at":"2026-05-18T05:17:25.966500900Z","closed_at":"2026-05-18T05:17:25.966493100Z"} -{"id":"rivets-pae","title":"Block ANSI escape sequences in title validation","description":"Explicitly block ANSI escape sequences in validate_title() to prevent terminal manipulation attacks. Current validation blocks control characters 0x00-0x1F and 0x7F-0x9F, but ANSI sequences starting with ESC (0x1B) followed by [ could still be used for terminal manipulation.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["pr-feedback","security"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-p9oz","dep_type":"parent-child"}],"created_at":"2025-11-30T04:11:36.937708941Z","updated_at":"2025-11-30T04:11:36.937708941Z","closed_at":null} -{"id":"rivets-yyri","title":"Refactor: Extract shared validation logic between Issue and NewIssue","description":"In domain/mod.rs, `Issue::validate()` (lines 118-138) and `NewIssue::validate()` (lines 321-344) have similar validation logic:\n\n**Issue::validate():**\n```rust\npub fn validate(&self) -> Result<(), String> {\n let trimmed_title = self.title.trim();\n if trimmed_title.is_empty() { return Err(...); }\n if self.title.len() > MAX_TITLE_LENGTH { return Err(...); }\n if self.priority > 4 { return Err(...); }\n Ok(())\n}\n```\n\n**NewIssue::validate():**\n```rust\npub fn validate(&self) -> Result<(), String> {\n let trimmed_title = self.title.trim();\n if trimmed_title.is_empty() { return Err(...); }\n if trimmed_title.len() > MAX_TITLE_LENGTH { return Err(...); }\n if self.priority > 4 { return Err(...); }\n Ok(())\n}\n```\n\n**Recommendation:** Extract to a helper function:\n```rust\nfn validate_title_and_priority(title: &str, priority: u8) -> Result<(), String> {\n let trimmed = title.trim();\n if trimmed.is_empty() { return Err(\"Title cannot be empty\".to_string()); }\n if trimmed.len() > MAX_TITLE_LENGTH { return Err(...); }\n if priority > MAX_PRIORITY { return Err(...); }\n Ok(())\n}\n```\n\nNote: Also addresses the hardcoded `4` vs `MAX_PRIORITY` constant issue.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","domain","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:50:52.984657028Z","updated_at":"2025-12-28T20:57:06.989347788Z","closed_at":"2025-12-28T20:57:06.989347788Z"} -{"id":"rivets-4j2","title":"Update MCP server for CLI parity","description":"Update rivets-mcp to expose new CLI functionality as MCP tools.\n\nRequired new tools:\n- reopen: Reopen closed issues (mirrors CLI reopen command)\n- stale: Find issues not updated recently\n- label_add: Add labels to issues\n- label_remove: Remove labels from issues \n- label_list: List labels for an issue\n- label_list_all: List all labels in use across issues\n\nEnhancements:\n- Enhance where_am_i or add info tool with issue prefix, config details\n- Consider dep_tree tool for dependency visualization\n\nNote: Multi-ID support is optional for MCP since agents can call tools multiple times.\n\nThis should be done after the corresponding CLI commands are implemented.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-cej","dep_type":"blocks"},{"depends_on_id":"rivets-g25","dep_type":"blocks"},{"depends_on_id":"rivets-i2w","dep_type":"blocks"},{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:25:51.377507037Z","updated_at":"2025-12-28T05:16:27.368028795Z","closed_at":"2025-12-28T05:16:27.368028795Z"} -{"id":"rivets-4hwn","title":"Add schema versioning and migration system","description":"Current schema uses CREATE TABLE IF NOT EXISTS with no version tracking. Add a schema_version table and migration system to support future schema changes without requiring full re-indexing.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:09.089746470Z","updated_at":"2026-02-06T00:35:09.089746470Z","closed_at":null} -{"id":"rivets-kivr","title":"BatchWriter.send() should return Result instead of silently dropping data","description":"Currently `BatchWriter::send()` silently drops data when the channel is disconnected (background thread crashed), only logging an error. Callers have no way to detect data loss.\n\n**Current behavior:**\n```rust\npub fn send(&self, data: ParsedFileData) {\n if let Err(e) = self.sender.send(data) {\n error!(file = %e.0.relative_path.display(), \"Failed to send...\");\n }\n}\n```\n\n**Suggested improvement:**\n```rust\npub fn send(&self, data: ParsedFileData) -> Result<(), Error> {\n self.sender.send(data).map_err(|e| {\n Error::Internal(format!(\n \"Background writer crashed - data lost for file: {}\",\n e.0.relative_path.display()\n ))\n })\n}\n```\n\n**Impact:** Users could parse files successfully but never index them, with no indication in the final result.\n\n**Trade-off:** Returning Result adds complexity to the parallel parsing loop - need to decide whether to collect errors or fail fast.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["batch-writer","error-handling","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:06:40.382793013Z","updated_at":"2026-01-29T19:06:40.382793013Z","closed_at":null} -{"id":"rivets-k3mv","title":"Integrate LSP into index --lsp","description":"When `--lsp` flag is set during indexing:\n\n1. After Pass 2, identify still-unresolved references\n2. For each, call LSP goto_definition to find the target\n3. Update reference with resolved symbol_id\n\nLSP is spawned lazily, kept alive for batch queries, shutdown on completion.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-7uw0","dep_type":"blocks"},{"depends_on_id":"rivets-oeu5","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:56.529556093Z","updated_at":"2026-01-29T00:57:33.035836992Z","closed_at":"2026-01-29T00:57:33.035836992Z"} -{"id":"rivets-l56","title":"Replace #[allow] with #[expect] (M-LINT-OVERRIDE-EXPECT)","description":"storage/mod.rs:233 uses `#[allow(dead_code)]` on the PostgreSQL enum variant, violating the M-LINT-OVERRIDE-EXPECT guideline.\n\nCurrent Code (line 233):\n```rust\n#[allow(dead_code)]\nPostgreSQL(String),\n```\n\nIssues:\n- #[allow] silently suppresses warnings\n- No explanation for why the warning is acceptable\n- Will continue to suppress warning even if PostgreSQL gets implemented\n\nGuideline Requirement:\n- Use #[expect] with reason to document intent\n- Fails loudly if expectation becomes invalid","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["code-quality","lints"],"design":"Replace #[allow] with #[expect] and add reason:\n\n```rust\n#[expect(dead_code, reason = \"PostgreSQL backend not yet implemented (tracked in TODO)\")]\nPostgreSQL(String),\n```\n\nThis:\n- Documents why the variant is currently unused\n- Will cause a warning if PostgreSQL gets implemented but attribute isn't removed\n- Makes the temporary nature explicit","acceptance_criteria":"- [ ] #[allow(dead_code)] replaced with #[expect]\n- [ ] Reason documented explaining temporary nature\n- [ ] Code compiles without warnings\n- [ ] All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T22:49:04.233675714Z","updated_at":"2025-11-27T22:49:04.233675714Z","closed_at":null} +{"id":"rivets-9mh","title":"Implement daemon with auto-sync and RPC server","description":"Implement the background daemon process that provides auto-export (dirty issues to JSONL), auto-import (JSONL to SQLite), and serves RPC requests.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads internal/daemon/:\n\n**Daemon Responsibilities**:\n1. Auto-export with debouncing (5s default)\n2. Auto-import on JSONL mtime changes\n3. RPC server for CLI commands\n4. PID file management\n5. Graceful shutdown on signals\n\n**Lifecycle**:\n- Auto-start on first CLI command (if not running)\n- Background detached process\n- Socket cleanup on exit\n- Lock file prevents multiple daemons\n\n**Rust Stack**:\n- `tokio` runtime for async event loop\n- `notify` crate for file watching (optional)\n- `signal-hook` for signal handling\n- `daemonize` crate for backgrounding\n\n**Features**:\n- Debounced exports (collect changes, flush after timeout)\n- File watcher for imports\n- Health monitoring\n- Log rotation","acceptance_criteria":"- Daemon starts in background\n- Auto-export triggers after changes\n- Auto-import on JSONL modification\n- RPC server responds to requests\n- PID file created and managed\n- Graceful shutdown on SIGTERM\n- Multiple daemon prevention\n- Integration tests verify auto-sync behavior","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:07.330511078Z","updated_at":"2025-11-17T22:16:07.330511078Z","closed_at":null} +{"id":"rivets-3gey","title":"Reachability analysis (forward/backward data flow)","description":"Add data flow analysis capabilities for security auditing.\n\n**UPDATED: Lower effort than originally estimated - primitives already exist!**\n\n**Existing primitives we can wrap:**\n- `get_callers(symbol)` - direct callers (graph/sql.rs:332)\n- `get_callees(symbol)` - direct callees (graph/sql.rs:368)\n- `get_dependents(file)` - files that depend on this file\n- `get_dependencies(file)` - files this file depends on\n- `get_dependency_chain(from, to)` - path between two files\n\n**What's needed:** ~50 lines of recursive wrapper code:\n\n```rust\npub fn get_reachable(&self, symbol: &str, max_depth: usize) -> Result> {\n let mut visited = HashSet::new();\n let mut results = Vec::new();\n let mut queue = VecDeque::new();\n \n queue.push_back((symbol_id, vec![], 0));\n \n while let Some((current, path, depth)) = queue.pop_front() {\n if depth > max_depth || visited.contains(¤t) {\n continue;\n }\n visited.insert(current);\n \n // Use existing get_callees() primitive\n for callee in self.symbol_graph.get_callees(current)? {\n let mut new_path = path.clone();\n new_path.push(callee.symbol.clone());\n results.push(ReachablePath { target: callee.symbol, path: new_path.clone(), depth: depth + 1 });\n queue.push_back((callee.symbol.id, new_path, depth + 1));\n }\n }\n Ok(results)\n}\n```\n\n**Use cases:**\n- Forward: \"What can this function access?\" (BFS on get_callees)\n- Backward: \"Who can reach this?\" (BFS on get_callers)\n\n**Estimated effort: Low (~50 lines, 1-2 hours)**","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","feature","security","tethys"],"design":null,"acceptance_criteria":null,"notes":"Primitives already exist - just needs BFS wrapper. Removed dependency on call_edges table since we can use existing get_callers/get_callees.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-gvmh","dep_type":"blocks"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:32.479277091Z","updated_at":"2026-01-29T20:15:21.061394775Z","closed_at":"2026-01-29T20:15:21.061394775Z"} {"id":"rivets-014n","title":"Refactor: Extract shared filter logic between list() and ready_to_work()","description":"In storage/in_memory/trait_impl.rs, the filter logic in `list()` (lines 342-378) and `ready_to_work()` (lines 413-451) is nearly identical:\n\n**list() filter:**\n```rust\n.filter(|issue| {\n if let Some(status) = &filter.status {\n if &issue.status != status { return false; }\n }\n if let Some(priority) = filter.priority {\n if issue.priority != priority { return false; }\n }\n if let Some(issue_type) = &filter.issue_type {\n if &issue.issue_type != issue_type { return false; }\n }\n if let Some(assignee) = &filter.assignee {\n if issue.assignee.as_ref() != Some(assignee) { return false; }\n }\n if let Some(label) = &filter.label {\n if !issue.labels.contains(label) { return false; }\n }\n true\n})\n```\n\n**ready_to_work() has the exact same logic.**\n\n**Recommendation:** Extract to a helper:\n```rust\nfn matches_filter(issue: &Issue, filter: &IssueFilter) -> bool {\n if let Some(status) = &filter.status {\n if &issue.status != status { return false; }\n }\n // ... rest of filter logic\n true\n}\n```\n\nThen use in both methods:\n```rust\n.filter(|issue| matches_filter(issue, filter))\n```","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["DRY","refactor","trait_impl.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:51:16.121637806Z","updated_at":"2025-12-28T20:57:12.887646398Z","closed_at":"2025-12-28T20:57:12.887646398Z"} -{"id":"rivets-p4n","title":"Implement TUI dependency graph view","description":"Create dependency graph visualization in TUI:\n- ASCII art graph of issue dependencies\n- Color-coded nodes by status\n- Navigate graph with keyboard\n- Show dependency types (blocks, related, parent-child, discovered-from)\n- Highlight current issue and its connections","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:12.590091097Z","updated_at":"2025-11-30T18:37:12.590091097Z","closed_at":null} -{"id":"rivets-9e3u","title":"Style: Standardize use statement placement in execute.rs","description":"Some functions have imports at the top of the function body, others inline. This is inconsistent.\n\n**Example of current inconsistency:**\n```rust\npub async fn execute_init(args: &InitArgs) -> Result<()> {\n use crate::commands::init; // ← Inside function\n ...\n}\n```\n\n**Options:**\n1. Move all module-level imports to the top of the file\n2. Keep scoped imports but document the convention\n\n**Recommendation:** Evaluate which approach is preferred for this codebase and apply consistently.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["execute.rs","style"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:55.496327128Z","updated_at":"2025-12-28T15:37:55.496327128Z","closed_at":null} -{"id":"rivets-sxl","title":"Add integration test for set_workspace cache eviction","description":"The `evict_oldest()` method is tested in isolation, but the actual eviction trigger in `set_workspace()` (lines 104-107) has no test coverage. The existing `test_cache_eviction` uses `set_test_workspace()` which bypasses eviction logic.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["coverage","testing"],"design":"Create an async integration test that:\n1. Creates `MAX_CACHED_WORKSPACES + 1` real temp directories with `.rivets/` structure\n2. Calls `set_workspace()` for each\n3. Verifies the cache stays at the limit and oldest entry is evicted\n\n```rust\n#[tokio::test]\nasync fn test_set_workspace_evicts_when_full() {\n let temps: Vec = (0..=MAX_CACHED_WORKSPACES)\n .map(|_| TempDir::new().unwrap())\n .collect();\n \n // Initialize .rivets/ in each\n for temp in &temps {\n std::fs::create_dir(temp.path().join(\".rivets\")).unwrap();\n }\n \n let mut context = Context::new();\n \n // Fill cache to limit\n for temp in &temps[..MAX_CACHED_WORKSPACES] {\n context.set_workspace(temp.path()).await.unwrap();\n }\n assert_eq!(context.cache_size(), MAX_CACHED_WORKSPACES);\n \n let first_workspace = temps[0].path().canonicalize().unwrap();\n \n // Add one more - should trigger eviction\n context.set_workspace(temps[MAX_CACHED_WORKSPACES].path()).await.unwrap();\n \n assert_eq!(context.cache_size(), MAX_CACHED_WORKSPACES);\n assert!(!context.storage_cache.contains_key(&first_workspace));\n}\n```\n\n**Location:** `context.rs` tests module","acceptance_criteria":"- [ ] Integration test exercises actual `set_workspace()` eviction path\n- [ ] Test uses real temp directories (not `set_test_workspace`)\n- [ ] Verifies cache size stays at `MAX_CACHED_WORKSPACES`\n- [ ] Verifies oldest workspace is removed from cache","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T04:32:31.990408907Z","updated_at":"2025-11-29T04:32:31.990408907Z","closed_at":null} -{"id":"rivets-044i","title":"tethys: qualified refs from import-less files don't resolve (e.g., helper::do_thing or crate_name::Type)","description":"Discovered while writing the rivets-dn35 regression test (tests/pass2_no_imports.rs).\n\nEven after rivets-dn35 removes the imports.is_empty() short-circuit in Pass 2, qualified refs from import-less files still don't resolve. Example:\n\n // crate_a/src/lib.rs (NO use statements)\n mod helper;\n pub fn entry() { helper::do_thing_q(); }\n\n // crate_a/src/helper.rs\n pub fn do_thing_q() {}\n\ntry_resolve_reference takes the is_qualified=true path:\n 1. explicit imports lookup: 'helper' not in (empty) explicit_imports -> miss\n 2. glob imports: empty list -> skip\n 3. fallback: get_symbol_by_qualified_name('helper::do_thing_q')\n -> looks for symbols.qualified_name = 'helper::do_thing_q'\n -> free functions are stored with qualified_name = name (no module prefix)\n -> miss\n\nSame shape with workspace-crate prefix (e.g. crate_a::widget::Widget from a sibling file) also fails for the same reason.","status":"open","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":"Two candidate fixes:\n\n(a) In try_resolve_reference's qualified branch (when explicit imports miss), invoke resolver::resolve_module_path on the path segments. That function already handles crate::, self::, super::, and workspace-crate prefixes — returning a module file path. Then call resolve_symbol_in_module on the result.\n\n(b) Walk the symbol's file's module hierarchy: derive an implicit 'crate::SAME_MODULE_PATH::ref_name' from the caller's file location and try get_symbol_by_qualified_name with that.\n\n(a) is more general (handles workspace-crate-prefix style too). (b) is narrower (only same-crate sibling-module shape).\n\nRisk: option (a) adds a new resolution path. If it succeeds where the existing fallback would have returned None, the resolver becomes more permissive — could introduce phantom resolutions analogous to rivets-0gom's un-ambiguation drift. Mitigation: the K-hybrid filter (PR #67) catches cross-crate phantoms at file_deps; the new path could be scoped to crate_root anchors (crate::, self::, super::, own workspace-crate-name) to avoid pulling in unrelated crates.","acceptance_criteria":"- [ ] Test: qualified call helper::do_thing_q() from import-less src/lib.rs (mod helper;) resolves to crate_a/src/helper.rs::do_thing_q\n- [ ] Test: workspace-crate-prefix call crate_a::widget::Widget::new() from import-less integration test resolves\n- [ ] No regression in rivets-3d0s K-hybrid file_deps phantom rate (re-run .rivets-ycaq/probe_phantom_rate.py: must remain 0.00%)\n- [ ] No new ambiguity violations (re-run .rivets-0gom/probe.py Section 3)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ycaq","dep_type":"discovered-from"}],"created_at":"2026-05-18T00:20:09.088221500Z","updated_at":"2026-05-18T00:20:09.088221500Z","closed_at":null} -{"id":"rivets-xi4","title":"Add rstest dependency to both crates","description":"Add rstest = \"0.26\" to dev-dependencies in both workspace crates.\n\nFiles to modify:\n- crates/rivets/Cargo.toml\n- crates/rivets-jsonl/Cargo.toml","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- rstest = \"0.26\" added to dev-dependencies in both Cargo.toml files\n- cargo build succeeds\n- cargo test succeeds (no changes to tests yet)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T00:55:59.605072624Z","updated_at":"2025-11-29T02:07:41.450391325Z","closed_at":"2025-11-29T02:07:41.450391325Z"} -{"id":"rivets-vabg","title":"Streaming SQLite writes during indexing","description":"Write to SQLite immediately after parsing each file instead of accumulating data in memory.\n\n**Current behavior:**\n- Parse all files, accumulate symbols/references in memory\n- Write to SQLite after parsing completes\n- Memory usage: O(n) where n = codebase size\n\n**Drift's pattern:**\n- Parse file → immediately send to background writer thread\n- Writer batches 100 files per transaction\n- Memory usage: O(1) constant regardless of codebase size\n\n**Benefits:**\n- Handle 10K+ file codebases without OOM\n- Progressive results (can query partial index)\n- Better crash recovery (data already persisted)\n\n**Implementation:**\n```rust\nstruct BatchWriter {\n sender: Sender,\n handle: JoinHandle,\n}\n\nimpl BatchWriter {\n fn new(db_path: PathBuf, batch_size: usize) -> Self;\n fn send(&self, batch: FileBatch) -> Result<()>;\n fn finish(self) -> Result;\n}\n```\n\nThis pairs with rayon parallelization - parsing threads send to writer thread.","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:15.225967341Z","updated_at":"2026-01-29T19:04:45.646948276Z","closed_at":"2026-01-29T19:04:45.646948276Z"} -{"id":"rivets-cjz","title":"Replace println! with tracing::info! in CLI commands","description":"The execute_init function uses println! for user output. Consider using tracing::info! for more flexible logging that can be configured at runtime.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["cli","observability"],"design":"Replace direct println! calls in CLI command execution with tracing macros:\\n\\n- Use tracing::info! for success messages\\n- Use tracing::warn! for warnings\\n- Use tracing::error! for errors\\n\\nThis enables:\\n- Structured logging\\n- Log level filtering\\n- Integration with observability tools\\n- Consistent logging across all commands","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-0dw","dep_type":"related"},{"depends_on_id":"rivets-nz52","dep_type":"parent-child"}],"created_at":"2025-11-30T02:48:28.716762764Z","updated_at":"2025-11-30T02:48:28.716762764Z","closed_at":null} -{"id":"rivets-4dw","title":"rivets-mcp: Rust MCP Server for Rivets","description":"Create a Rust-native MCP server that exposes rivets issue tracking functionality to AI assistants like Claude. This is a port of the beads-mcp Python server to Rust.\n\nKey decisions:\n- Library: rmcp v0.8.1 (official Rust MCP SDK)\n- Structure: New rivets-mcp crate in workspace\n- Storage: Direct IssueStorage trait access\n- Transport: stdio only (MVP)","status":"closed","priority":1,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Core MCP server implementation complete with all 10 tools functional. Remaining work is testing enhancements (rivets-8fe, rivets-2pn) and optional refactoring (rivets-o7o, rivets-1wn).","external_ref":null,"dependencies":[],"created_at":"2025-11-29T01:15:28.493045843Z","updated_at":"2025-12-08T00:02:28.855597106Z","closed_at":"2025-12-08T00:02:28.855597106Z"} -{"id":"rivets-mb5x","title":"Docs: Add documentation for MAX_VISUAL_DEPTH magic number","description":"Per M-DOCUMENTED-MAGIC guideline, the constant at line 855 lacks explanation:\n\n```rust\nconst MAX_VISUAL_DEPTH: usize = 10;\n```\n\n**Guideline states:**\n> Hardcoded magic values in production code must be accompanied by a comment outlining why this value was chosen.\n\n**Fix:**\n```rust\n/// Maximum indentation depth for tree visualization.\n/// Set to 10 to prevent extremely wide output on terminals while\n/// still showing meaningful hierarchy for most dependency trees.\nconst MAX_VISUAL_DEPTH: usize = 10;\n```","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["M-DOCUMENTED-MAGIC","docs","execute.rs"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:35.134166836Z","updated_at":"2025-12-28T15:37:35.134166836Z","closed_at":null} -{"id":"rivets-84i","title":"Add Automerge sync protocol support","description":"Implement generate_sync_message and receive_sync_message for future P2P and server sync capabilities.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:55.978563339Z","updated_at":"2025-12-23T04:45:40.567472509Z","closed_at":"2025-12-23T04:45:40.567472509Z"} -{"id":"rivets-2c5","title":"Optimize ready_to_work filter pre-application in find_blocked_issues","description":"Investigate potential optimization to apply IssueFilter early in find_blocked_issues() to reduce the number of issues checked for blocking status.\n\nCurrently, find_blocked_issues checks ALL non-closed issues in Phase 1, then ready_to_work applies the user filter afterward. For selective filters, this does unnecessary work.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["optimization","performance"],"design":"**Current Flow:**\n1. `find_blocked_issues` checks all non-closed issues\n2. `ready_to_work` filters out blocked issues\n3. User filter applied last\n\n**Proposed Optimization:**\nPass filter hint to `find_blocked_issues` to skip checking issues that won't be in final result.\n\n**Correctness Constraint:**\nCannot skip issues that might be transitive blockers via ParentChild. If Parent A (priority=1) is blocked, Child C (priority=2) must also be marked blocked even if filter.priority=2 would skip A.\n\n**Safe Optimization Approaches:**\n1. Only apply filter in Phase 1 for \"leaf-only\" filters (labels, assignee) that don't affect blocking propagation\n2. Lazy per-issue blocking check - trade BFS O(n+e) for O(filtered × edges), better only for very selective filters\n3. Two-pass: first find all potentially blocking parents, then filter\n\n**When to Implement:**\nProfile ready_to_work with large datasets (10k+ issues) to determine if optimization is worthwhile.","acceptance_criteria":"- [ ] Profile ready_to_work performance with 10k+ issues\n- [ ] Determine if optimization provides measurable benefit\n- [ ] If implementing, ensure correctness with parent-child transitive blocking\n- [ ] Add benchmark tests for ready_to_work with various filter selectivity levels","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-28T21:22:06.362877015Z","updated_at":"2025-11-28T21:22:06.362877015Z","closed_at":null} -{"id":"rivets-qeb","title":"Implement ready work algorithm with recursive blocking","description":"Implement the ready work detection algorithm that finds unblocked issues by recursively propagating blocks through parent-child hierarchies using CTEs.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on beads ready.go, adapted for Phase 1 (in-memory + petgraph):\n\n**Algorithm** (Phase 1 - petgraph):\n1. Find directly blocked issues (via 'blocks' deps to open/in_progress issues)\n2. Recursively propagate blockage through parent-child deps (depth limit 50)\n3. Exclude all blocked issues from results\n\n**Phase 1 Implementation**:\n```rust\nimpl InMemoryStorage {\n fn find_ready(&self, filter: Option<&IssueFilter>) -> Result> {\n use petgraph::Direction;\n \n // Find all blocked issues\n let mut blocked = HashSet::new();\n \n // Direct blocks: issues with blocking dependencies\n for (id, issue) in &self.issues {\n if issue.status == Status::Closed {\n continue;\n }\n \n for dep in &issue.dependencies {\n if dep.dep_type == DependencyType::Blocks {\n let blocker = self.issues.get(&dep.depends_on_id)?;\n if blocker.status == Status::Open || blocker.status == Status::InProgress {\n blocked.insert(id.clone());\n }\n }\n }\n }\n \n // Transitive blocking via parent-child (BFS with depth limit)\n let mut to_process: VecDeque<(IssueId, usize)> = blocked.iter()\n .map(|id| (id.clone(), 0))\n .collect();\n \n while let Some((id, depth)) = to_process.pop_front() {\n if depth >= 50 { continue; }\n \n // Find children (issues that depend on this one via parent-child)\n let node = self.node_map.get(&id)?;\n for edge in self.graph.edges_directed(*node, Direction::Incoming) {\n if edge.weight() == &DependencyType::ParentChild {\n let child_node = edge.source();\n let child_id = &self.graph[child_node];\n if blocked.insert(child_id.clone()) {\n to_process.push_back((child_id.clone(), depth + 1));\n }\n }\n }\n }\n \n // Filter out blocked issues\n let mut ready: Vec = self.issues.values()\n .filter(|issue| {\n issue.status != Status::Closed \n && !blocked.contains(&issue.id)\n })\n .cloned()\n .collect();\n \n // Apply additional filter if provided\n if let Some(filter) = filter {\n ready = self.apply_filter(ready, filter)?;\n }\n \n // Sort by policy (hybrid default)\n self.sort_by_policy(&mut ready, SortPolicy::Hybrid);\n \n Ok(ready)\n }\n}\n```\n\n**Sort Policies**:\n- `hybrid` (default): Recent (48h) by priority, older by age\n- `priority`: P0→P1→P2→P3→P4 strict\n- `oldest`: Creation date ascending\n\n**Phase 3 (PostgreSQL)**: Will use recursive CTEs for blocking propagation (see design notes)","acceptance_criteria":"- Ready issues query excludes blocked work using petgraph\n- Recursive parent-child blocking via BFS traversal\n- All 3 sort policies implemented\n- Depth limit (50) prevents infinite loops\n- Performance: <10ms for 1000 issues\n- Unit tests for complex blocking scenarios\n- Integration test with various dependency graphs","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:15:21.592668862Z","updated_at":"2025-11-28T19:38:24.156494963Z","closed_at":"2025-11-28T19:38:24.156494963Z"} -{"id":"rivets-uyg","title":"Implement read_jsonl_resilient() convenience function","description":"Implement the read_jsonl_resilient() convenience function that reads an entire JSONL file into a Vec while collecting warnings.\n\nThis provides an easy-to-use API for resilient loading from file paths.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse std::path::Path;\nuse tokio::fs::File;\nuse futures::StreamExt;\n\npub async fn read_jsonl_resilient(\n path: P\n) -> Result<(Vec, Vec)>\nwhere\n T: DeserializeOwned + 'static,\n P: AsRef,\n{\n let file = File::open(path).await?;\n let reader = JsonlReader::new(file);\n let (stream, collector) = reader.stream_resilient();\n \n let values: Vec = stream.collect().await;\n let warnings = collector.into_warnings();\n \n Ok((values, warnings))\n}\n```","acceptance_criteria":"- read_jsonl_resilient() function implemented\n- Returns Result<(Vec, Vec)>\n- Reads entire file into memory\n- Collects all warnings\n- Integration test with corrupted JSONL file\n- Verifies warnings contain correct line numbers","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-8yl","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:01.004393308Z","updated_at":"2025-11-28T03:25:03.360959904Z","closed_at":"2025-11-28T03:25:03.360959904Z"} -{"id":"rivets-jfs","title":"Fix remaining list/ready call compilation errors in integration tests","description":"After adding label/issue_type parameters to list() and ready() tools, some integration test calls still have wrong parameter counts.\n\nRemaining ready() calls to fix (need 6 params: limit, priority, issue_type, assignee, label, workspace_root):\n- Line 988: `.ready(None, Some(0), None, None)` - priority filter\n- Line 1035: `.ready(None, None, Some(\"alice\".to_string()), None)` - assignee filter \n- Line 1057: `.ready(Some(2), None, None, None)` - limit filter\n\nThese need issue_type and label parameters added.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:57:34.115965496Z","updated_at":"2025-11-30T00:00:52.311358389Z","closed_at":"2025-11-30T00:00:52.311358389Z"} -{"id":"rivets-ck11","title":"tethys: slice 1 tests missed Windows path-separator divergence; harden either contract or callee","description":"Discovered during rivets-0gom fix work (gilfoyle/checkpointed-build, slice 2): a Windows-specific path-normalization bug existed in the slice 2 implementation that was not caught by slice 1's unit tests, despite slice 1 explicitly testing path-prefix behavior.\n\n**The miss:**\n`search_symbol_by_name_in_path_prefix` was tested in slice 1 with synthetic file paths inserted via `Path::new(\"crate_a/src/lib.rs\")`. On Windows, those literals contain forward slashes and are stored that way in the DB. Slice 1's tests all passed.\n\nIn slice 2, the resolver computes the prefix from `CrateInfo::path` (canonical absolute Windows path, e.g. `\\\\?\\C:\\...\\crates\\rivets`) via `relative_path` and `to_string_lossy`. The result on Windows is `crates\\rivets` (native BACKslashes). Without normalization, the LIKE query in slice 1's function returned zero rows for every prefix on every Windows system, silently breaking the entire fix.\n\nThe integration gate (probe vs oracle) caught it. The unit tests did not.\n\n**Why the gap exists:**\nslice 1's stress fixture happened to be path-shape-uniform — both `insert_file` calls used forward-slash literals, so the function's normalization assumption was never challenged. A correctly designed adversarial fixture would have used at least one backslash path (representing what real Windows callers might pass).\n\n**Two follow-up options:**\n\n1. Add a Windows-specific regression test to slice 1's unit tests that calls `search_symbol_by_name_in_path_prefix` with a backslash-containing prefix and asserts None (i.e., document that the function requires forward-slash prefixes). This pins the contract but doesn't prevent caller bugs.\n\n2. Make `search_symbol_by_name_in_path_prefix` defensively normalize its prefix argument. This shifts the contract: \"any path-ish string is acceptable; we'll forward-slash it internally.\" Lower caller burden, slight performance cost (one string allocation).\n\nI lean (2). The single call site in fallback_symbol_search ALSO normalizes; that's duplication. Centralizing in the DB-layer function is the right place.\n\n**Broader lesson for gilfoyle/budgeted-plan:** the \"stress fixture\" rule should specifically require coverage of platform-divergent value shapes when the function deals with paths, strings with multibyte chars, or anything that has OS-level non-determinism. Worth a skill-level update.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Decide: option 1 (test pinning) or option 2 (defensive normalization)\n- [ ] If option 2: `search_symbol_by_name_in_path_prefix` accepts both `crates\\rivets` and `crates/rivets` and returns identical results on Windows\n- [ ] Regression test: same function call with backslash prefix and forward-slash prefix produces identical Symbol output on Windows\n- [ ] gilfoyle/budgeted-plan stress-fixture rule augmented with a \"platform-divergent value shapes\" sub-rule for path/string args","notes":"Closed: Resolved by in-function normalization in search_symbol_by_name_in_path_prefix (PR #61 round 2)","external_ref":null,"dependencies":[],"created_at":"2026-05-12T01:05:06.350162900Z","updated_at":"2026-05-12T02:45:41.003064500Z","closed_at":"2026-05-12T02:45:41.003001700Z"} -{"id":"rivets-utkl","title":"Add tracing::debug to needs_update() placeholder","description":"needs_update() always returns Ok(true). Add a debug\\! log saying 'change detection not yet implemented, assuming stale' so automated callers can diagnose why re-indexing always runs. Flagged in PR #55 review.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:17:52.888212060Z","updated_at":"2026-03-20T22:17:52.888212060Z","closed_at":null} -{"id":"rivets-9o82","title":"Add gated integration tests for LSP","description":"Add #[ignore] tests that require rust-analyzer installed:\n\n- lsp_resolves_trait_method_call\n- lsp_resolves_method_on_inferred_type\n- lsp_index_increases_resolution_count\n\nRun with: `cargo test --ignored`","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-1dza","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:29:15.445807729Z","updated_at":"2026-01-29T02:01:00.845691700Z","closed_at":"2026-01-29T02:01:00.845691700Z"} -{"id":"rivets-6aoc","title":"Hardcoded crate_root in resolve_cross_file_references breaks multi-crate workspaces","description":"resolve.rs:66 uses self.workspace_root.join(\"src\") which is wrong for multi-crate workspaces. Should use per-crate src_root from self.crates: Vec. Flagged in PR #55 review.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"## Cross-reference (2026-05-12)\n\nRelated to [rivets-v465](rivets-v465) (open, in PR #62) — a sibling multi-crate-resolver bug. v465 fixes `use other_crate::Foo` paths (workspace-crate name at `path[0]`); this issue covers `use crate::Foo` paths where the caller's own crate_root is hardcoded to `workspace_root/src` instead of per-crate src.\n\n**Confirmed `src/` hardcoding sites on `fix/rivets-v465-import-resolver-leak` HEAD:**\n- `crates/tethys/src/resolve.rs:66` (called out in PR #55 review per this issue)\n- `crates/tethys/src/indexing.rs:857` and `:1023` (also covered by [rivets-34tv](rivets-34tv))\n- `crates/tethys/src/resolver.rs:61` (new site introduced by the v465 fix's workspace-crate arm — same bug class)\n\nThese should be fixed together with v465 for full multi-crate-resolver coverage. The `self.crates()` accessor and `&[CrateInfo]` plumbing introduced in v465 PR #62 are reusable infrastructure for this fix.\n\nClosed: Merged in PR #63 (commit 9a39bd5)","external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:16:15.036553022Z","updated_at":"2026-05-13T00:29:50.838403200Z","closed_at":"2026-05-13T00:29:50.838401700Z"} -{"id":"rivets-p7v","title":"Implement rivets CLI skeleton with basic command structure","description":"Create the initial implementation of the rivets CLI application including main.rs, CLI argument parsing with clap, and stub modules for commands and domain logic. Create a hello-world style application that compiles and runs as `bd`.","status":"closed","priority":1,"issue_type":"task","assignee":"Claude","labels":[],"design":"Based on rivets-kr3 design:\n- Create src/main.rs with CLI entry point\n- Create src/cli.rs with clap-based argument parsing\n- Create src/commands/ module with stub command implementations\n- Create src/domain/ module for issue tracking domain types\n- Create src/storage.rs stub for storage layer\n- Create src/config.rs stub for configuration\n- Create src/error.rs for CLI-specific errors\n- Ensure binary is named 'rivets' in Cargo.toml\n- Add basic integration test that runs the CLI","acceptance_criteria":"- Application compiles and runs as `rivets` command\n- CLI argument parsing works with clap\n- Help message displays correctly with `rivets --help`\n- All module stubs created with proper exports\n- Basic integration test runs the CLI successfully\n- `cargo test --package rivets` passes\n- `cargo run --package rivets` executes without errors","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-kr3","dep_type":"blocks"}],"created_at":"2025-11-17T21:45:33.081597765Z","updated_at":"2025-11-17T22:08:32.126908789Z","closed_at":"2025-11-17T22:08:32.126908789Z"} -{"id":"rivets-41t","title":"Improve JSON output consistency across commands","description":"Review and potentially standardize JSON output patterns across CLI commands.\n\nCurrently there are two approaches:\n- `output.rs:260-266`: Uses `IssueDetails` struct with `#[serde(flatten)]` to embed issue fields\n- `execute.rs:320-323`: Creates inline `serde_json::json!()` objects for delete command\n\nBoth approaches work, but consider whether to standardize on one pattern for better consistency and maintainability. Options:\n1. Use dedicated structs with `#[serde(flatten)]` everywhere (more type-safe)\n2. Use inline `json!()` macros everywhere (more flexible)\n3. Keep mixed approach where appropriate (current state)\n\nThis is a low-priority code quality improvement.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["code-quality","pr-feedback"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T17:42:48.770546385Z","updated_at":"2025-11-30T17:42:48.770546385Z","closed_at":null} -{"id":"rivets-dhs","title":"Add query performance benchmarks","description":"Create benchmarks to measure query/filter performance and verify the \"1M records in <5s\" target from research.\n\nUses criterion for benchmarking.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create benches/query_benchmarks.rs:\n\n```rust\nuse criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};\nuse rivets_jsonl::*;\n\nfn query_benchmarks(c: &mut Criterion) {\n let mut group = c.benchmark_group(\"query\");\n \n // Benchmark: filter 1M records\n group.bench_function(\"filter_1m_records\", |b| {\n b.iter(|| {\n // ... benchmark code\n });\n });\n \n // Benchmark: filter + map pipeline\n group.bench_function(\"filter_map_pipeline\", |b| {\n b.iter(|| {\n // ... benchmark code\n });\n });\n \n // Benchmark: complex multi-predicate filter\n group.bench_function(\"complex_filter\", |b| {\n b.iter(|| {\n // ... benchmark code\n });\n });\n}\n\ncriterion_group!(benches, query_benchmarks);\ncriterion_main!(benches);\n```\n\nAdd criterion as dev-dependency.","acceptance_criteria":"- Benchmarks created in benches/\n- Measures filter performance\n- Measures map performance \n- Measures complex query pipelines\n- 1M records benchmark runs\n- Results documented in comments\n- cargo bench runs successfully","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-08u","dep_type":"blocks"},{"depends_on_id":"rivets-6p4","dep_type":"blocks"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-27T23:17:06.817308399Z","updated_at":"2025-11-27T23:17:06.817308399Z","closed_at":null} -{"id":"rivets-81c","title":"Implement IssueDocument type for Automerge","description":"Create the core IssueDocument struct that wraps AutoCommit and provides methods for issue CRUD operations on the document.\n\nIncludes:\n- Add autosurgeon Reconcile/Hydrate derives to domain types (Issue, Dependency, IssueStatus, IssueType, DependencyType)\n- Create IssueDocument wrapper struct\n- Implement hydrate/reconcile methods for document ↔ struct conversion","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:27.719020626Z","updated_at":"2025-12-23T04:44:59.580445491Z","closed_at":"2025-12-23T04:44:59.580445491Z"} +{"id":"rivets-ry6","title":"Adopt rstest for test improvement","description":"Adopt the rstest crate to improve test code quality and reduce duplication. rstest provides fixtures, parameterized tests, and matrix testing capabilities that will eliminate 20-30% of test code duplication.","status":"closed","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- rstest dependency added to both crates\n- High-impact test files converted to use rstest fixtures and parameterization\n- All tests pass after conversion\n- Measurable reduction in test code lines","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T00:55:47.850281116Z","updated_at":"2025-11-29T02:11:34.462327431Z","closed_at":"2025-11-29T02:11:34.462327431Z"} +{"id":"rivets-8zq","title":"Refactor stringly-typed errors to structured error enums","description":"Improve error handling by replacing stringly-typed error variants with structured enums for better type safety, pattern matching, and debugging.\n\nCurrent issues identified:\n1. `anyhow::anyhow!()` used in domain layer (cli/execute.rs:60) - loses type info\n2. `Config(String)` catch-all in rivets/src/error.rs\n3. `Storage(String)` catch-all in rivets/src/error.rs\n4. `Mcp(String)` unused variant in rivets-mcp/src/error.rs\n5. Error context lost when InvalidFormat → Storage(msg) in jsonl.rs:140-144","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["error-handling","refactor","type-safety"],"design":"Replace generic string variants with structured error enums:\n\n```rust\n// ConfigError enum\n#[derive(Debug, Error)]\npub enum ConfigError {\n #[error(\"Invalid prefix: {0}\")]\n InvalidPrefix(String),\n #[error(\"Parse error: {0}\")]\n Parse(String),\n #[error(\"IO error: {0}\")]\n Io(#[from] std::io::Error),\n}\n\n// StorageError enum\n#[derive(Debug, Error)]\npub enum StorageError {\n #[error(\"JSONL error: {0}\")]\n Jsonl(#[from] rivets_jsonl::Error),\n #[error(\"Validation failed: {0}\")]\n Validation(String),\n #[error(\"ID generation failed: {0}\")]\n IdGeneration(String),\n}\n\n// Add ValidationError variant for common case\n#[error(\"Validation error: {field}: {reason}\")]\nValidation { field: &'static str, reason: String }\n```\n\nFiles to modify:\n- crates/rivets/src/error.rs\n- crates/rivets/src/cli/execute.rs\n- crates/rivets/src/commands/init.rs\n- crates/rivets/src/storage/in_memory/trait_impl.rs\n- crates/rivets/src/storage/in_memory/jsonl.rs\n- crates/rivets-mcp/src/error.rs","acceptance_criteria":"- [ ] Replace `anyhow::anyhow!()` with typed error in cli/execute.rs:60\n- [ ] Create ConfigError enum and update Config variant to use it\n- [ ] Create StorageError enum and update Storage variant to use it\n- [ ] Add ValidationError variant for validation failures\n- [ ] Remove unused Mcp(String) variant from rivets-mcp\n- [ ] Update all call sites constructing Config(String) and Storage(String)\n- [ ] Preserve InvalidFormat semantic instead of downgrading to Storage\n- [ ] All tests pass\n- [ ] No new compiler warnings","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-12-01T04:49:52.900240044Z","updated_at":"2025-12-01T04:49:52.900240044Z","closed_at":null} +{"id":"rivets-i2w","title":"Implement label subcommand","description":"Add 'label' subcommand with add/remove/list/list-all actions. Should support multiple issue IDs.\\n\\nExamples:\\n- rivets label add id1 id2 urgent --json\\n- rivets label remove id1 bug --json\\n- rivets label list id1 --json\\n- rivets label list-all --json","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:50.950734701Z","updated_at":"2025-12-28T00:42:19.285302354Z","closed_at":"2025-12-28T00:42:19.285302354Z"} {"id":"rivets-1wa","title":"Implement query tools (ready, list, show, blocked)","description":"Implement issue query tools:\n- ready(limit, priority, assignee) - Find unblocked tasks via ready_to_work()\n- list(status, priority, type, assignee, limit) - List with filters\n- show(issue_id) - Full issue details with dependencies\n- blocked() - Get blocked issues with their blockers","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] ready tool returns unblocked issues\n- [ ] list tool supports all filter parameters\n- [ ] show tool returns full issue with dependencies\n- [ ] blocked tool returns blocked issues with blockers\n- [ ] Unit tests: each tool with MockStorage (happy path + edge cases)","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-6u6","dep_type":"blocks"},{"depends_on_id":"rivets-vdi","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:22.628369922Z","updated_at":"2025-11-30T01:19:17.301204975Z","closed_at":"2025-11-30T01:19:17.301204975Z"} -{"id":"rivets-ealr","title":"tethys: detect actual crate root from Cargo.toml instead of assuming src/","description":"Two places in lib.rs assume crate root is workspace_root/src/. This breaks for crates with non-standard main/lib locations. Needs Cargo.toml parsing to detect the actual crate root.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"Closed: Duplicate of rivets-34tv (same hardcoded workspace_root/src/ assumption at the same lib.rs sites). rivets-6aoc remains open as it covers a separate code path in resolve.rs.","external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:03.504824959Z","updated_at":"2026-05-10T04:30:16.386071800Z","closed_at":"2026-05-10T04:30:16.386070Z"} -{"id":"rivets-v465","title":"tethys: import resolver leaks legitimate cross-crate refs to unscoped fallback","description":"Discovered as the root-cause blocker for rivets-3d0s during gilfoyle/checkpointed-build (PR #61 follow-up branch fix/rivets-3d0s-stdlib-symbol-pollution).\n\n**Symptom:** ~80% of legitimate cross-crate refs in Cargo-dep-allowed pairs are being resolved through the unscoped workspace-wide fallback (search_unique_symbol_by_name) instead of via Pass 2's explicit/glob import resolution.\n\n**Quantified on the rivets workspace (post-rivets-0gom, pre-rivets-3d0s):**\n - Total cross-crate refs: 294\n - In FORBIDDEN pairs (phantoms): 174\n - In ALLOWED pairs (legitimate): 120\n - Of those 120 legit_cross, **95 reach unscoped fallback** (would be demoted by an extended rivets-3d0s-style audit)\n - Breakdown of leaks: ~50 function calls, ~48 method calls, ~6 enum_variant constructors\n\n**Why this is a bigger bug than rivets-3d0s:**\nSame code path (unscoped fallback) is currently doing BOTH legitimate cross-crate resolution (when imports leak) and phantom resolution (when no real import exists). Any audit/filter that tries to demote phantom resolutions at this level hits 80% of legitimate refs too. The rivets-3d0s audit-and-demote design is blocked on this — without fixing the import resolver, no kind-compatibility rule can distinguish the two populations.\n\n**Reproduction:**\nOn branch fix/rivets-3d0s-stdlib-symbol-pollution (or main, post rivets-0gom):\n 1. tethys index --rebuild (re-index rivets workspace)\n 2. uv run --no-project --python 3.13 -- python .rivets-3d0s/audit_simulation.py\n - With EXTENDED=True at the top of the script\n 3. Observe: 'legit-cross demoted: 95' — these are the leak signal.\n\n**Discovered during:** gilfoyle/checkpointed-build of slice 1+2 of rivets-3d0s. The audit landed; probe showed 174 -> 164 phantoms (5.7% reduction vs predicted 58.6%). Root cause analysis: the audit's narrowing of candidate sets converts previously-ambiguous names into unique matches, creating new method/function phantoms. Extending the audit to cover call->method via unscoped (Option A) was probed and shown to have an 80% false-positive rate on ALLOWED pairs. Slice 1+2 was reverted.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":"**Investigation entry points:**\n\n1. **crates/tethys/src/resolve.rs::resolve_via_explicit_import** — does it correctly handle all of:\n - 'use rivets::storage::Storage;' (named import, multi-segment path)\n - 'use rivets::storage::*;' (glob import)\n - 'use rivets::storage;' followed by 'storage::Storage' (partial-path use)\n - Re-exports ('pub use crate::storage::Storage;' in lib.rs)\n - Alias imports ('use rivets::storage::Storage as S;')\n\n2. **crates/tethys/src/resolve.rs::resolve_via_glob_import** — same checks for glob.\n\n3. **crates/tethys/src/languages/rust.rs** — does the extractor capture imports correctly? Maybe imports are extracted but with wrong source_module strings.\n\n4. **The 'method' bucket specifically:** even with imports of the TYPE, method calls like 'storage.create_issue()' may bypass import resolution because tree-sitter doesn't know that 'storage's type came from an import. This may require special handling — possibly the right answer is 'don't try; let LSP handle it' (which routes back to rivets-714v).\n\n**Likely findings:**\n- The 50 function calls are probably failing on multi-segment paths or re-exports\n- The 48 method calls likely need type-info (LSP) — separately tracked as rivets-714v\n- The 6 enum_variant constructors likely need qualified-name handling (e.g., 'Error::Variant' resolving when only 'Error' is imported)\n\n**Suggested approach:**\nDo gilfoyle/prove-it-prototype on this issue. Build a probe that:\n 1. Lists the 95 leaked refs by (caller_file, ref_name, target_file)\n 2. For each, inspects the caller_file's imports to determine what SHOULD have matched\n 3. Classifies failures: 're-export', 'multi-segment-path', 'glob-import-miss', 'method-needs-type-info', 'qualified-name-construct'\n\nThen design fixes per category. Some may be cheap (re-export support); some may be expensive (type-info-via-LSP).","acceptance_criteria":"- [ ] Categorize the 95 (or current count) leaks by failure class (re-export miss, multi-segment, glob, method-needs-type-info, etc.)\n- [ ] For at least the top 2 failure classes, design + implement fixes\n- [ ] On the rivets workspace post-fix: legit_cross refs via unscoped drops by >= 50% (95 -> <= 47)\n- [ ] No regression in same-crate or import-based resolution counts\n- [ ] After this lands, re-attempt rivets-3d0s: the extended audit (Option A) should have a manageable false-positive rate (<= 10 ALLOWED-pair demotions)","notes":"\n\n## Checkpointed-build outcome (2026-05-12)\n\nBranch: fix/rivets-v465-import-resolver-leak. Status: slices 1+2 IMPLEMENTED; slice 3 verification done; ACTUAL impact much smaller than design predicted.\n\n**What slice 1+2 actually fixes:** the small-but-real case where a leaked cross-crate ref's bare name matches an explicit workspace-crate import (`use rivets::storage::in_memory::new_in_memory_storage` + bare `new_in_memory_storage()`). resolve_module_path now correctly handles `path[0] == workspace_crate_name`.\n\n**Empirical impact on rivets workspace:** 6 refs migrate from Pass-2-fallback to Pass-2-imports. Pre-fix all 279 cross-crate refs (105 ALLOWED + 174 FORBIDDEN) went through fallback; post-fix 273 still do.\n\n**Why so small:** the refining probe (.rivets-v465/refine_c5_upper_bound.py) revealed that only 5 of 105 ALLOWED-pair leaks have a `ref_name`-matching workspace import. The other 100 are method-on-imported-type calls (`use rivets::Storage; s.create_issue()`) — the type is imported but the method has no import of its own. Pass 2's name-matching resolver fundamentally cannot catch these without type information.\n\n**Re-scoped acceptance criteria:** original \"≥50% fallback reduction\" replaced with \"≥5 refs migrate\" (achievable; 6 measured). The dominant population (100 method-on-imported-type leaks + 174 FORBIDDEN phantoms) now depends on rivets-714v (LSP integration for multi-crate workspaces).\n\n**Diagnostic artifacts retained on the branch:**\n- .rivets-v465/probe.py - initial leak categorization\n- .rivets-v465/check_same_crate_ambiguity.py - same-crate-vs-cross split \n- .rivets-v465/check_pass_provenance.py - resolved ref counts\n- .rivets-v465/source_module_shapes.py - cheapest falsifier (workspace-crate imports shape)\n- .rivets-v465/refine_c5_upper_bound.py - design-correcting probe\n- .rivets-v465/after-fix-counts.txt - final empirical snapshot\n- .rivets-v465/falsifiable-design.md - includes \"Re-design rationale\" section explaining the C5/C6 revision\n\n**Implication for rivets-3d0s:** still blocked. The fallback still handles ~99% of cross-crate resolution post-fix, so any rule at the fallback level (like rivets-3d0s's audit-and-demote) would still produce massive false positives. Audit-and-demote becomes viable only after rivets-714v migrates the method-on-imported-type calls out of fallback.\n\n## Related-issues discovery (2026-05-12, after PR #62 opened)\n\nProcess miss: should have searched the rivets tracker before filing rivets-v465. Two open issues describe the same multi-crate-resolver code class from different angles:\n\n- **[rivets-6aoc](rivets-6aoc)** (P2, open): \"resolve.rs:66 uses `self.workspace_root.join('src')` which is wrong for multi-crate workspaces. Should use per-crate src_root from `self.crates: Vec`. Flagged in PR #55 review.\"\n- **[rivets-34tv](rivets-34tv)** (P2, open): \"Two sites in lib.rs (~1089 and ~1516) still hardcode workspace_root/src/ as crate root in `compute_dependencies` and `resolve_unresolved_references`.\"\n- **[rivets-m4wt](rivets-m4wt)** (P1, closed Feb): \"Parse Cargo.toml to detect actual crate root.\" `CrateInfo` discovery was added but applied incompletely — the three hardcoded sites above survived.\n\n**rivets-v465's relationship to these:** distinct bug, same code class. v465 fixes `path[0]==workspace_crate_name` matching (e.g., `use rivets::Foo`). 6aoc/34tv fix the caller's own `crate_root` being hardcoded (affects `use crate::Foo` resolution for non-root crates in multi-crate workspaces). Both are needed for full multi-crate support; neither subsumes the other.\n\n**Hardcoded `workspace_root.join(\"src\")` still present (as of HEAD of rivets-v465 branch):**\n- `crates/tethys/src/resolve.rs:66` (resolve_cross_file_references)\n- `crates/tethys/src/indexing.rs:857` (compute_dependencies first call site)\n- `crates/tethys/src/indexing.rs:1023` (compute_dependencies second call site)\n\nEmpirical implication: my v465 measurements showed 5971 same-crate refs resolved, but that count is masked by Pass-1 same-file resolution + Pass-2 fallback. Pass-2 cross-file same-crate refs (`use crate::Foo` where `crate` means the caller's actual crate, not `workspace_root/src`) are likely ALSO failing in non-root crates due to 6aoc/34tv. A combined fix would compound the value.\n\nClosed: Merged in PR #62 (commit c1af978)","external_ref":null,"dependencies":[],"created_at":"2026-05-12T04:18:20.703569200Z","updated_at":"2026-05-12T22:01:15.506283400Z","closed_at":"2026-05-12T22:01:15.506276900Z"} -{"id":"rivets-s3o","title":"Implement context management module","description":"Implement workspace context management:\n- Workspace detection (walk up to find .rivets/)\n- Path canonicalization\n- Per-workspace storage instance management\n- Context state storage (Arc>)","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Workspace detection walks up to find .rivets/\n- [ ] Path canonicalization handles symlinks\n- [ ] Storage instances cached per workspace\n- [ ] Unit tests: discover_workspace, set_workspace, storage caching","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-cfx","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:11.298145823Z","updated_at":"2025-11-29T14:41:24.606759038Z","closed_at":"2025-11-29T14:41:24.606759038Z"} -{"id":"rivets-tri","title":"Find domain name for Rivets","description":"Research and select an appropriate domain name for the Rivets project. Use the domain name brainstormer skill to generate and evaluate potential domain options that align with the project's identity as a Rust-based project tracking system.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":"## Domain Name Research for Rivets (Updated 2025-11-30)\n\n### Project Context\n- Rivets is a Rust-based issue tracking system using JSONL storage\n- CLI-first developer tool\n- Human-readable, version-control friendly\n- Alternative to traditional issue trackers\n\n---\n\n## DECISION: rivets.rs\n\nSelected **rivets.rs** as the official domain for the Rivets project.\n\n### Why This Choice\n- Perfect alignment with Rust ecosystem (.rs = Rust file extension)\n- Follows precedent of other Rust projects (docs.rs, lib.rs, crates.io)\n- Short, memorable, and brandable\n- Available via Serbian registrars (ISTanCo, EuroDNS, etc.)\n- ~€10-18/year pricing\n\n### Registration Notes\n- .rs domains available from: ISTanCo (istanco.rs), SkyHosting, EuroDNS, Gandi\n- No local presence required - open to worldwide registration\n- Note: Some corporate firewalls may block .rs TLD (Serbia)\n\n---\n\n## Research Summary (for reference)\n\n### Domains Checked - TAKEN\n- rivets.dev (Frontier.sh)\n- rivets.io (for sale)\n- rivets.com\n- getrivets.com\n- rivet.dev (Rivet stateful backends)\n\n### Alternative Options Considered\n- gorivets.dev, rivethq.dev, rivetcli.com (potentially available)\n- rivets.rs (SELECTED)","external_ref":null,"dependencies":[],"created_at":"2025-12-01T04:00:32.666023401Z","updated_at":"2025-12-07T23:36:51.172920061Z","closed_at":"2025-12-07T23:36:51.172920061Z"} -{"id":"rivets-0srv","title":"Add rayon for parallel file parsing","description":"Use rayon to parallelize file parsing during indexing. Currently tethys processes files sequentially, using only one CPU core.\n\n**Drift's pattern:**\n```rust\n// Parallel parsing with rayon\nlet results: Vec<_> = files\n .par_iter()\n .filter_map(|file| process_file(file).ok())\n .collect();\n```\n\n**Key considerations:**\n- Parse in parallel, write to SQLite sequentially (SQLite doesn't like concurrent writes)\n- Use MPSC channel to send parsed data to a background writer thread\n- Batch SQLite writes (100 files per transaction) to amortize overhead\n- Use atomic counters for thread-safe progress tracking\n\n**Expected improvement:**\n- 1,000 files: ~2-3s → ~0.5-1s\n- 10,000 files: ~20-30s → ~3-5s\n- Full CPU utilization instead of single core\n\n**Implementation steps:**\n1. Add `rayon = \"1.10\"` to Cargo.toml\n2. Create `BatchWriter` struct with MPSC channel\n3. Refactor `index_workspace` to use `par_iter()` for Pass 1\n4. Keep Pass 2 (import resolution) and Pass 3 (LSP) sequential","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"},{"depends_on_id":"rivets-vabg","dep_type":"related"}],"created_at":"2026-01-29T12:48:28.808680006Z","updated_at":"2026-01-29T17:55:56.037736620Z","closed_at":"2026-01-29T17:55:56.037736620Z"} -{"id":"rivets-l66","title":"Implement JSONL persistence for InMemoryStorage","description":"Add save/load functionality to InMemoryStorage to persist data to JSONL files. This provides durability for the in-memory backend.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Extend InMemoryStorage with async persistence (matches rivets-0gc async trait pattern):\n\n**Async Implementation:**\n\n```rust\nimpl InMemoryStorage {\n pub async fn load_from_jsonl(path: &Path) -> Result<(Self, Vec)> {\n use tokio::fs::File;\n use tokio::io::{AsyncBufReadExt, BufReader};\n \n let file = BufReader::new(File::open(path).await?);\n let storage = Arc::new(Mutex::new(InMemoryStorageInner::new()));\n let mut warnings = Vec::new();\n let mut lines = file.lines();\n \n // First pass: import all issues (without dependencies)\n while let Some(line) = lines.next_line().await? {\n match serde_json::from_str::(&line) {\n Ok(issue) => {\n let mut inner = storage.lock().await;\n let id = issue.id.clone();\n inner.issues.insert(id.clone(), issue);\n let node = inner.graph.add_node(id.clone());\n inner.node_map.insert(id, node);\n }\n Err(e) => {\n warnings.push(LoadWarning::MalformedJson { line, error: e });\n log::warn!(\"Skipping malformed JSON: {}\", e);\n }\n }\n }\n \n // Second pass: add dependency edges with orphan/cycle detection\n let mut inner = storage.lock().await;\n for issue in inner.issues.values() {\n for dep in &issue.dependencies {\n // Check if dependency target exists\n if !inner.issues.contains_key(&dep.depends_on_id) {\n warnings.push(LoadWarning::OrphanedDependency {\n from: issue.id.clone(),\n to: dep.depends_on_id.clone(),\n });\n log::warn!(\"Skipping orphaned dependency: {} -> {}\", \n issue.id, dep.depends_on_id);\n continue;\n }\n \n // Check for cycles before adding edge\n if inner.has_cycle_internal(&issue.id, &dep.depends_on_id)? {\n warnings.push(LoadWarning::CircularDependency {\n from: issue.id.clone(),\n to: dep.depends_on_id.clone(),\n });\n log::warn!(\"Skipping circular dependency: {} -> {}\", \n issue.id, dep.depends_on_id);\n continue;\n }\n \n // Add edge to graph\n inner.add_dependency_edge_internal(dep)?;\n }\n }\n \n Ok((storage, warnings))\n }\n \n pub async fn save_to_jsonl(&self, path: &Path) -> Result<()> {\n use tokio::fs::File;\n use tokio::io::{AsyncWriteExt, BufWriter};\n \n // Atomic write: temp file + rename\n let temp_path = path.with_extension(\"tmp\");\n let file = BufWriter::new(File::create(&temp_path).await?);\n \n let inner = self.lock().await;\n for issue in inner.issues.values() {\n let json = serde_json::to_string(issue)?;\n file.write_all(json.as_bytes()).await?;\n file.write_all(b\"\\n\").await?; // Newline-delimited\n }\n file.flush().await?;\n \n // Atomic rename\n tokio::fs::rename(&temp_path, path).await?;\n Ok(())\n }\n}\n```\n\n**Error Handling:**\n- Malformed JSON: Skip line, log warning, continue loading\n- Orphaned dependencies: Skip edge, log warning, import issue without that dependency\n- Circular dependencies: Skip edge, log warning, prevent cycle creation\n- Returns (Storage, Vec) to communicate warnings to caller\n\n**File format**: Standard JSONL (newline-delimited JSON)\n**Atomicity**: Write to temp file, then rename (atomic on POSIX systems)\n**Dependencies**: tokio with fs feature for async file I/O","acceptance_criteria":"- load_from_jsonl reads and reconstructs storage\n- save_to_jsonl writes all issues\n- Dependency graph correctly rebuilt on load\n- Atomic writes (temp file + rename)\n- Round-trip test: save → load → verify\n- Handles large files (streaming read)\n- Error handling for malformed JSON","notes":"## Clarifications\n\n### Session 2025-11-17\n\n- Q: The design shows synchronous methods (`pub fn load_from_jsonl`, `pub fn save_to_jsonl`) but rivets-0gc defines an async storage trait. Should these persistence methods be async? → A: Make persistence methods async (matches async trait pattern from rivets-0gc, enables non-blocking file I/O for large JSONL files)\n- Q: During JSONL import, what should happen if an issue references dependencies that don't exist in the file (orphaned dependency references)? → A: Skip orphaned dependencies with warning (allows partial recovery from corrupted/incomplete files, resilient approach)\n- Q: If the JSONL file contains issues that form circular dependencies, how should load_from_jsonl handle this? → A: Detect and reject circular dependencies during import (ensures loaded data has no cycles, maintains data integrity)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-bz5","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:41.842178005Z","updated_at":"2025-11-27T22:55:23.560905657Z","closed_at":"2025-11-27T22:55:23.560905657Z"} -{"id":"rivets-067","title":"Create rivets-automerge crate scaffold","description":"Set up the new crate with Cargo.toml, basic module structure, and dependencies.\n\nDependencies to add:\n- autosurgeon = \"0.9\"\n- automerge = \"0.7\"","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:22.019758791Z","updated_at":"2025-12-23T04:44:53.700012982Z","closed_at":"2025-12-23T04:44:53.700012982Z"} -{"id":"rivets-6qm","title":"Implement fuzzy search for issue selection","description":"Add fuzzy search for issue ID selection:\n\n```\n$ rivets show\n> auth\n rivets-abc Fix authentication bug\n rivets-def Add OAuth support\n rivets-ghi Auth token refresh\n```\n\nUse skim library for the fuzzy matching UI.\nAlso support label and assignee autocomplete.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:12.633939684Z","updated_at":"2025-11-30T18:36:12.633939684Z","closed_at":null} +{"id":"rivets-jfs","title":"Fix remaining list/ready call compilation errors in integration tests","description":"After adding label/issue_type parameters to list() and ready() tools, some integration test calls still have wrong parameter counts.\n\nRemaining ready() calls to fix (need 6 params: limit, priority, issue_type, assignee, label, workspace_root):\n- Line 988: `.ready(None, Some(0), None, None)` - priority filter\n- Line 1035: `.ready(None, None, Some(\"alice\".to_string()), None)` - assignee filter \n- Line 1057: `.ready(Some(2), None, None, None)` - limit filter\n\nThese need issue_type and label parameters added.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:57:34.115965496Z","updated_at":"2025-11-30T00:00:52.311358389Z","closed_at":"2025-11-30T00:00:52.311358389Z"} +{"id":"rivets-fr9b","title":"tethys: track parent_symbol_id for nested symbols","description":"When indexing symbols, parent_symbol_id is always set to None. Implement proper parent symbol tracking so nested symbols (e.g., methods inside structs/impls) have their parent relationship recorded.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:02.074384054Z","updated_at":"2026-03-19T01:41:02.074384054Z","closed_at":null} +{"id":"rivets-06w","title":"Implement core domain types (Issue, Dependency, Filter)","description":"Create the core domain types that represent issues, dependencies, filters, and related structures. These are used across all storage implementations and the CLI.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Create domain module in rivets crate (src/domain/mod.rs):\n\n**Module Structure**:\n```\nsrc/domain/\n├── mod.rs # Public exports\n├── issue.rs # Issue, NewIssue, IssueUpdate\n├── dependency.rs # Dependency, DependencyType\n├── filter.rs # IssueFilter, builder\n└── types.rs # IssueId, Priority, Status, IssueType\n```\n\n**Core Types**:\n```rust\n// Issue representation\npub struct Issue {\n pub id: IssueId,\n pub title: String,\n pub description: String,\n pub design: Option,\n pub acceptance_criteria: Option,\n pub notes: Option,\n pub status: Status,\n pub priority: Priority,\n pub issue_type: IssueType,\n pub assignee: Option,\n pub created_at: DateTime,\n pub updated_at: DateTime,\n pub closed_at: Option>,\n pub labels: Vec,\n pub dependencies: Vec,\n}\n\npub struct NewIssue { /* builder fields */ }\npub struct IssueUpdate { /* optional fields */ }\n\n// Enums\npub enum Status { Open, InProgress, Blocked, Closed }\npub enum IssueType { Bug, Feature, Task, Epic, Chore }\npub enum DependencyType { Blocks, Related, ParentChild, DiscoveredFrom }\n\n// Filters\npub struct IssueFilter {\n pub status: Option>,\n pub priority: Option>,\n pub assignee: Option,\n pub labels: Option>,\n // ... all filter dimensions\n}\n\n// Newtypes for type safety\npub struct IssueId(String);\npub struct Priority(u8); // 0-4\n```\n\n**Serde support**: All types derive Serialize/Deserialize for JSONL","acceptance_criteria":"- All core types defined\n- Serde derives for serialization\n- Builder patterns for NewIssue, IssueFilter\n- Validation logic (priority 0-4, ID format)\n- Type safety with newtypes\n- Unit tests for validation\n- Documentation with examples","notes":"Implement as src/domain/ module in rivets crate, NOT a separate rivets-core crate. This keeps the domain types co-located with the CLI application for now. Can be extracted to separate crate later if needed for library reuse.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-0gc","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:41.346359643Z","updated_at":"2025-11-18T01:25:08.707227678Z","closed_at":"2025-11-18T01:25:08.707227678Z"} +{"id":"rivets-3911","title":"Refactor lib.rs into focused modules (<500 lines each)","description":"lib.rs is 3,299 lines, exceeding the project guideline of <500 lines per file. Split into focused modules: indexing.rs (Phase 1a/1b), file_ops.rs (discovery and metadata), dependency_resolution.rs (Pass 2 cross-file), lsp_integration.rs (Pass 3). PR review flagged this as critical.","status":"open","priority":2,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:46.087604018Z","updated_at":"2026-02-06T00:54:46.087604018Z","closed_at":null} +{"id":"rivets-exy4","title":"Auto-sync via dirty-marker + deferred flush","description":"Add a low-overhead pattern for keeping the tethys index fresh during active editing: file-save writes a tiny dirty marker (cheap, no parse), and the actual re-sync runs at session boundaries (e.g. agent stop, pre-prompt).\n\n**Inspired by:** KiroGraph's hook system. The pattern avoids per-keystroke overhead while keeping the graph fresh enough that AI assistants always see current code structure.\n\n**Two-phase flow:**\n\nPhase 1 — mark dirty (cheap):\n- A new tethys mark-dirty subcommand writes /.rivets/index/dirty.txt (one path per line, append-only).\n- O(1) — no parsing, no SQL writes, suitable for per-save invocation from an editor or hook.\n\nPhase 2 — sync if dirty (deferred):\n- A new tethys sync-if-dirty subcommand reads dirty.txt, re-indexes only the listed files (incremental), and clears the marker.\n- Suitable for invocation at agent stop, pre-prompt, or other session boundaries.\n\n**Hook integration:**\n- Claude Code: PostToolUse hook on Edit/Write writes via mark-dirty; Stop hook runs sync-if-dirty.\n- Editors: file-save hooks invoke mark-dirty.\n- CI: a pre-test step runs sync-if-dirty to ensure the index reflects HEAD.\n\n**Why this depends on rivets-q8qw and rivets-3l14:**\n- rivets-q8qw (incremental updates): without it, the sync re-indexes the whole workspace, defeating the deferred-flush optimization.\n- rivets-3l14 (content hash): mtime alone is not a reliable change indicator (touch updates mtime without changing content). Content hash makes ''actually changed'' the trigger.\n\n**Public API:**\n- Tethys::mark_dirty(paths: &[Path]) -> Result<()>\n- Tethys::sync_if_dirty() -> Result","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] mark_dirty and sync_if_dirty API methods\n- [ ] CLI subcommands tethys mark-dirty PATH... and tethys sync-if-dirty [--quiet]\n- [ ] Dirty marker format documented (newline-separated relative paths, dedup on read)\n- [ ] sync-if-dirty is a no-op when marker is absent or empty\n- [ ] Race-safe: mark-dirty appends with lock; sync-if-dirty reads-then-truncates atomically\n- [ ] Example Claude Code hook config in tethys README\n- [ ] Tests cover: empty marker, deleted file in marker, concurrent mark + sync","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-3l14","dep_type":"blocks"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"},{"depends_on_id":"rivets-q8qw","dep_type":"blocks"}],"created_at":"2026-05-10T04:32:52.798004500Z","updated_at":"2026-05-10T04:32:52.798004500Z","closed_at":null} +{"id":"rivets-djr","title":"Evaluate rstest for rivets/src/cli.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in cli.rs.\n\nFile: crates/rivets/src/cli.rs (lines ~873+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:36.464605033Z","updated_at":"2025-11-29T01:31:25.950082366Z","closed_at":"2025-11-29T01:31:25.950082366Z"} +{"id":"rivets-dee","title":"Convert in_memory_resilient_loading.rs to rstest","description":"Extract JSON construction into fixtures. Parameterize multi-issue loading tests.\n\nFile: crates/rivets/tests/in_memory_resilient_loading.rs","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- JSON construction fixtures created\n- Multi-issue loading tests parameterized\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:36.382550325Z","updated_at":"2025-11-29T02:10:43.723213826Z","closed_at":"2025-11-29T02:10:43.723213826Z"} +{"id":"rivets-cfx","title":"Implement MCP server with stdio transport","description":"Implement the basic MCP server skeleton using rmcp:\n- main.rs entry point with tokio runtime\n- ServerHandler implementation\n- stdio transport setup\n- Graceful shutdown handling","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"**Tool Registration Pattern:**\nUse rmcp's tool registration with handler functions:\n```rust\nserver\n .tool(\"set_context\", |params| {\n // Deserialize params, call tools.set_context()\n })\n .tool(\"ready\", |params| {\n // Call tools.ready()\n })\n // ... etc\n```\n\n**Schema Generation:**\nUse the `JsonSchema` derives in `models.rs` to auto-generate tool input/output schemas. rmcp can use these for MCP tool discovery.\n\n**Server Structure:**\n```rust\npub struct RivetsServer {\n context: Arc>,\n tools: Tools,\n}\n\nimpl ServerHandler for RivetsServer {\n // Tool dispatch\n}\n```\n\n**Transport:**\n- stdio only for MVP\n- Use rmcp's stdio transport utilities","acceptance_criteria":"- [ ] MCP server starts with stdio transport\n- [ ] ServerHandler implementation compiles\n- [ ] Graceful shutdown on SIGINT/SIGTERM\n- [ ] Unit tests: server creation, basic lifecycle","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"},{"depends_on_id":"rivets-rg0","dep_type":"blocks"}],"created_at":"2025-11-29T01:16:05.632602712Z","updated_at":"2025-11-29T05:29:56.252223505Z","closed_at":"2025-11-29T05:29:56.252223505Z"} +{"id":"rivets-bz5","title":"Implement InMemoryStorage with petgraph for dependency graph","description":"Create the InMemoryStorage implementation using HashMap for issues and petgraph for the dependency graph. This is the first storage backend and enables fast MVP development.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Based on storage trait from rivets-0gc, implement in-memory backend:\n\n**Structure (following rivets-0gc architecture):**\n\n```rust\n// Private inner storage (not thread-safe)\nstruct InMemoryStorageInner {\n issues: HashMap,\n graph: DiGraph, // petgraph\n node_map: HashMap,\n}\n\n// Public thread-safe type alias\npub type InMemoryStorage = Arc>;\n```\n\n**Async Trait Implementation (blocking operations in async methods):**\n\n```rust\n#[async_trait]\nimpl IssueStorage for InMemoryStorage {\n async fn create(&mut self, new: NewIssue) -> Result {\n let mut inner = self.lock().await;\n let id = generate_hash_id(&new); // Uses rivets-x1e\n let issue = Issue::from(new, id.clone());\n inner.issues.insert(id.clone(), issue.clone());\n let node = inner.graph.add_node(id.clone());\n inner.node_map.insert(id.clone(), node);\n Ok(issue)\n }\n \n async fn has_cycle(&self, from: &IssueId, to: &IssueId) -> Result {\n let inner = self.lock().await;\n // Use petgraph's has_path_connecting\n let from_node = inner.node_map.get(from)\n .ok_or_else(|| Error::IssueNotFound(from.clone()))?;\n let to_node = inner.node_map.get(to)\n .ok_or_else(|| Error::IssueNotFound(to.clone()))?;\n Ok(algo::has_path_connecting(&inner.graph, *to_node, *from_node, None))\n }\n \n async fn ready_to_work(&self, filter: Option<&IssueFilter>) -> Result> {\n let inner = self.lock().await;\n // Graph traversal to find issues with no blocking dependencies\n // Filter by status=open or in_progress\n // Check all incoming edges for blocking dependencies\n // Return issues with no blockers\n // ... implementation\n }\n}\n```\n\n**Key Implementation Notes:**\n- Use `Arc::new(Mutex::new(InMemoryStorageInner { ... }))` for initialization\n- Lock mutex for all operations: `let inner = self.lock().await;`\n- All petgraph and HashMap operations are blocking (acceptable for MVP)\n- Graph operations use petgraph::algo for cycle detection and traversal\n- Maintain node_map synchronization when adding/removing issues\n\n**Dependencies**: `petgraph = \\\"0.6\\\"` for graph algorithms, `async-trait` for trait implementation, `tokio` for Mutex","acceptance_criteria":"- InMemoryStorage implements all trait methods\n- Uses petgraph for dependency graph\n- Cycle detection working with graph algorithms\n- Ready work calculation via graph traversal\n- All CRUD operations functional\n- Unit tests for all operations\n- Benchmark: 1000 issues in <10ms","notes":"## Clarifications\n\n### Session 2025-11-17\n\n- Q: The task design shows `pub struct InMemoryStorage` but rivets-0gc specifies `InMemoryStorageInner` wrapped in `Arc>`. Which approach should this task implement? → A: Follow rivets-0gc: implement InMemoryStorageInner + Arc> wrapper (ensures thread safety, matches architecture from trait design)\n- Q: The task shows synchronous trait methods (`fn create_issue`) but rivets-0gc defines async trait methods (`async fn create`). How should InMemoryStorage implement the trait? → A: Use blocking operations in async methods (simple for MVP, matches Phase 1 approach from rivets-0gc)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-06w","dep_type":"blocks"},{"depends_on_id":"rivets-0gc","dep_type":"blocks"},{"depends_on_id":"rivets-x1e","dep_type":"blocks"}],"created_at":"2025-11-17T22:41:41.594099121Z","updated_at":"2025-11-18T02:58:07.492885986Z","closed_at":"2025-11-18T02:58:07.492885986Z"} +{"id":"rivets-j5e7","title":"Feature: Implement --no-assignee flag for explicit unassignment","description":"In args.rs (lines 149-156), there's a TODO for implementing a --no-assignee flag:\n\n```rust\n/// New assignee\n///\n/// Note: To unassign, use `--no-assignee` flag instead. Clap does not\n/// support empty strings (\"\") as argument values by default.\n///\n/// TODO: Implement --no-assignee flag for explicit unassignment\n#[arg(short, long)]\npub assignee: Option,\n```\n\n**Problem:** Users cannot unassign an issue via the CLI because clap doesn't accept empty strings.\n\n**Solution:** Add a `--no-assignee` boolean flag that explicitly clears the assignee field when updating an issue.\n\n**Implementation notes:**\n- Add `#[arg(long, conflicts_with = \"assignee\")]` for the new flag\n- In execute_update, check if no_assignee is true and set assignee to Some(None) to clear it","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["args.rs","cli","enhancement"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:42:13.371513935Z","updated_at":"2025-12-28T21:37:19.178243668Z","closed_at":"2025-12-28T21:37:19.178243668Z"} +{"id":"rivets-kr3","title":"Design rivets project structure with two-crate architecture","description":"Plan the overall project structure splitting rivets into two crates: a JSONL library (general-purpose) and the rivets CLI application (issue tracking). Define workspace layout, dependencies, and inter-crate relationships.","status":"closed","priority":1,"issue_type":"task","assignee":"Claude","labels":[],"design":"## Workspace Design\n\n### Directory Structure\n```\nrivets/\n├── Cargo.toml # Workspace config (resolver=3)\n├── crates/\n│ ├── rivets-jsonl/ # JSONL library crate\n│ │ ├── src/ (lib.rs, reader.rs, writer.rs, query.rs, stream.rs, error.rs)\n│ │ ├── tests/, benches/, examples/\n│ │ └── Cargo.toml\n│ └── rivets/ # CLI application crate\n│ ├── src/ (main.rs, cli.rs, commands/, domain/, storage.rs, config.rs)\n│ ├── tests/\n│ └── Cargo.toml\n├── docs/\n└── target/ # Shared build output\n```\n\n### Separation of Concerns\n\n**rivets-jsonl**: Generic JSONL operations (read, write, stream, query), no domain knowledge\n**rivets**: Issue tracking domain (CLI, commands, business logic), uses rivets-jsonl for storage\n\n### Dependencies\n- One-way: rivets → rivets-jsonl\n- Shared deps via [workspace.dependencies]: serde, serde_json, thiserror, anyhow, clap\n- rivets-jsonl internally referenced via { workspace = true }\n\n### Naming\n- Library: rivets-jsonl (crates.io)\n- Binary: rivets (crates.io), installs 'rivets' command\n- Modules: snake_case, one primary type per file\n\n### Testing\n- Unit: #[cfg(test)] modules\n- Integration: crates/*/tests/\n- Benchmarks: crates/rivets-jsonl/benches/\n- Doc tests: /// examples in public APIs\n\n### Future Extensibility\nWorkspace supports: rivets-tui, rivets-server, rivets-web, rivets-sync","acceptance_criteria":"- Cargo.toml workspace configuration designed\n- Directory structure planned and documented\n- Clear separation of concerns between crates defined\n- Naming conventions established\n- Inter-crate dependency strategy documented","notes":"Design complete with comprehensive workspace structure. Ready for implementation. Directory structure uses flat 'crates/' layout per Rust best practices. Resolver 3 for modern dependency resolution. Clean separation between generic JSONL library and domain-specific CLI application.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-cr9","dep_type":"blocks"}],"created_at":"2025-11-17T21:08:45.002094002Z","updated_at":"2025-11-17T21:47:19.541529556Z","closed_at":"2025-11-17T21:43:27.115215646Z"} {"id":"rivets-itz7","title":"Store imports during indexing (Rust + C#)","description":"During file indexing, extract import statements and store them in the imports table.\n\nFor Rust: parse `use` statements via existing `extract_use_statements()`\nFor C#: parse `using` directives via existing `extract_using_directives()`\n\nMap both to the common imports table format.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-lxbg","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:26.855812694Z","updated_at":"2026-01-28T17:49:54.996717800Z","closed_at":"2026-01-28T17:49:54.996717800Z"} -{"id":"rivets-4srr","title":"Add Serialize/Deserialize derives to architecture types","description":"The architecture types added in rivets-byie (Package, PackageId, PackageSource, CouplingMetrics, CouplingSort, PackageDependency, CouplingDetail, ArchStats) don''t derive Serialize/Deserialize, while every other public type in tethys::types does (SymbolId, FileId, Language, Symbol, Reference, etc.).\n\nThe CLI's JSON output is currently hand-rolled via serde_json::json!{} macros, so this works fine for the CLI. But:\n- Library consumers (rivets-mcp, future MCP tools) can't round-trip these types through serde.\n- It's inconsistent with the crate-wide convention.\n\n**Fix:** Add #[derive(Serialize, Deserialize)] to all eight architecture types in crates/tethys/src/types.rs. PackageId should also use #[serde(transparent)] like SymbolId and FileId. Adjust the CLI json! literals to delegate to the serde derives where simpler.\n\nFound during final review of rivets-byie.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["tethys","enhancement"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T00:37:34.786653Z","updated_at":"2026-05-11T00:37:34.786653Z","closed_at":null} -{"id":"rivets-ed9y","title":"tethys: replace Mutex with a pool; collapse get_package_coupling round-trips","description":"get_package_coupling currently does 3 sequential DB round-trips:\n 1. SELECT target package row from arch_coupling JOIN arch_packages\n 2. SELECT outgoing neighbors via fetch_neighbors(OUTGOING_SQL)\n 3. SELECT incoming neighbors via fetch_neighbors(INCOMING_SQL)\n\nThe reason 2 and 3 can't share the connection guard with 1 is the Mutex in db/mod.rs is non-reentrant — fetch_neighbors re-acquires the lock internally, so the outer guard from step 1 must drop first. This was the mutex bug caught and documented during PR 60 development.\n\nReviewer flagged in PR 60 round 5: \"If the Mutex is ever replaced with a connection pool (e.g. r2d2 or a custom pool), this could be collapsed into fewer round-trips.\"\n\nThis is architecturally the right time to address it — the connection-pool migration would benefit other code paths too (concurrent queries from MCP tools, parallel architecture phase passes, etc.).\n\nScope:\n1. Add a connection pool (r2d2-sqlite or similar) wrapping rusqlite::Connection\n2. Either re-architect get_package_coupling to hold one connection across all three queries (single transaction, isolation guarantees) OR fold all three queries into one with UNION + JOIN\n3. Remove the documented scope-block workaround in get_package_coupling","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] db/mod.rs uses a connection pool instead of Mutex\n- [ ] get_package_coupling no longer needs the explicit-drop scope block\n- [ ] Benchmark before/after for get_package_coupling on a workspace of >50 crates\n- [ ] All existing tests still pass; the mutex non-reentrancy test stays green\n- [ ] Document the pool sizing rationale in db/mod.rs","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-11T21:21:42.434028300Z","updated_at":"2026-05-11T21:21:42.434028300Z","closed_at":null} -{"id":"rivets-i2w","title":"Implement label subcommand","description":"Add 'label' subcommand with add/remove/list/list-all actions. Should support multiple issue IDs.\\n\\nExamples:\\n- rivets label add id1 id2 urgent --json\\n- rivets label remove id1 bug --json\\n- rivets label list id1 --json\\n- rivets label list-all --json","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:50.950734701Z","updated_at":"2025-12-28T00:42:19.285302354Z","closed_at":"2025-12-28T00:42:19.285302354Z"} -{"id":"rivets-hefr","title":"Scope SQLite busy_timeout based on read vs write workload","description":"The current Index::open sets a 30-second busy_timeout for all operations. This was chosen to handle concurrent test parallelism (rivets-byie's architecture phase writes interact with tests sharing the rivets workspace DB).\n\n**The concern:** if a user runs an interactive command like 'tethys coupling' while a background 'tethys index' is running, the read-side query could silently block for up to 30s before its first SQL operation succeeds. With WAL mode, true read queries don't block on writes, but the schema/pragma writes in Index::open itself can. The user sees no feedback — just a long pause.\n\n**Possible fix shapes:**\n- Set a short busy_timeout (500ms or 1s) by default and override it inside indexing operations only.\n- Add an Index::open_read_only or similar that uses a shorter timeout for query-only commands.\n- Surface a 'waiting for lock' message after 2-3s if the operation is still blocked.\n\n**Why deferred:** the concurrent-interactive scenario is rare in practice (most users run one tethys command at a time). The 30s timeout is conservative but safe. Worth tuning if anyone reports a real UX issue.\n\nDiscovered during second-round PR #60 review.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["tethys","enhancement","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T05:03:24.255671800Z","updated_at":"2026-05-11T05:03:24.255671800Z","closed_at":null} -{"id":"rivets-778r","title":"Complete C# unsafe modifier and generic parameter extraction","description":"Two TODO sites in languages/csharp.rs: line 1036 (detect unsafe modifier) and line 1038 (extract type parameters/generics). Currently hardcoded to false/None.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-02-06T00:54:48.487835208Z","updated_at":"2026-02-06T00:54:48.487835208Z","closed_at":null} -{"id":"rivets-h3mv","title":"Surprising connections: cross-file edges scored by file-path distance","description":"Find non-obvious cross-file connections — direct edges between symbols in structurally distant files. High-score pairs indicate unexpected coupling worth investigating.\n\n**Inspired by:** KiroGraph's kirograph_surprising tool. The clever insight: rank cross-file edges by path_distance(source_file, target_file) * edge_kind_weight to surface coupling that a human reviewer would never spot in casual inspection.\n\n**Scoring:**\n- Path distance: number of directory hops between source and target file paths.\n- Edge kind weights (suggested initial values): calls=1.0, references=0.8, type_of=0.7, instantiates=0.7, imports=0.3 (low — imports are expected).\n- Score = path_distance * weight; return top-N unique source-target pairs.\n\n**Why this depends on rivets-puyl:**\nThe call_edges table currently has no kind column. Surprising-connection scoring needs to weight different edge kinds differently, so we need rivets-puyl (Add reference kind tracking to call_edges table) to land first. Without it, we can only score on path distance, which is much weaker signal.\n\n**Implementation:**\n- Public API: Tethys::find_surprising_connections(limit) -> Vec\n- CLI: tethys surprising [--limit N] [--json]","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] find_surprising_connections() API method\n- [ ] CLI subcommand with --limit and --json\n- [ ] Path-distance helper: count distinct directory components between two relative paths\n- [ ] Edge-kind weights configurable or at least documented as constants\n- [ ] Filters out same-file edges\n- [ ] Unit tests on a multi-directory fixture workspace","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"},{"depends_on_id":"rivets-puyl","dep_type":"blocks"}],"created_at":"2026-05-10T04:31:04.459957300Z","updated_at":"2026-05-10T04:31:04.459957300Z","closed_at":null} -{"id":"rivets-ql0","title":"Implement rivets tui subcommand","description":"Add `rivets tui` subcommand that launches the terminal UI.\n\nCreate module structure:\n- src/tui/mod.rs - TUI module entry\n- src/tui/app.rs - Application state (Elm-like Model-View-Update)\n- src/tui/ui/ - View components\n- src/tui/widgets/ - Reusable widgets\n\nUse ratatui with crossterm backend.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:01.065594800Z","updated_at":"2025-11-30T18:37:01.065594800Z","closed_at":null} -{"id":"rivets-q3z","title":"Consider lazy storage initialization","description":"Currently `set_workspace()` creates the storage instance immediately. Consider lazy initialization where storage is only created on first actual use.\n\nThis could reduce startup overhead for multi-workspace scenarios where not all workspaces are actively used.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["future","optimization","performance"],"design":"**Current behavior:**\n- `set_workspace()` calls `create_storage()` immediately\n- Storage is cached but created eagerly\n\n**Potential optimization:**\nStore workspace config without initializing storage until first tool call needs it:\n\n```rust\nenum CachedWorkspace {\n Pending { db_path: PathBuf },\n Initialized { storage: Arc>> },\n}\n```\n\n**Trade-offs:**\n- Pro: Faster `set_context` calls\n- Pro: Less memory for unused workspaces\n- Con: First tool call per workspace has latency\n- Con: More complex code\n\n**When to consider:**\nProfile first - may not matter for typical usage patterns.","acceptance_criteria":"- [ ] Profile to determine if eager initialization is actually a bottleneck\n- [ ] If needed, implement lazy initialization pattern\n- [ ] Ensure first-use latency is acceptable\n- [ ] No regression for single-workspace usage","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-29T04:41:25.388695600Z","updated_at":"2025-11-29T04:41:25.388695600Z","closed_at":null} -{"id":"rivets-7cji","title":"Implement depth-limited transitive analysis","description":"The `--depth` flag in `tethys impact` is accepted but ignored (with a warning). See `cli/impact.rs:20-27`.\n\nShould limit how many levels of indirection to follow in transitive dependency analysis. Current behavior is full graph traversal.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["cli","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:27.215489041Z","updated_at":"2026-01-30T00:18:27.215489041Z","closed_at":null} -{"id":"rivets-g25","title":"Implement reopen command","description":"Add 'reopen' command to reopen closed issues. Should support multiple issue IDs and --reason flag. Example: rivets reopen id1 id2 --reason 'Reopening for further work'","status":"closed","priority":1,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:33.144141194Z","updated_at":"2025-12-28T00:30:58.643711846Z","closed_at":"2025-12-28T00:30:58.643711846Z"} -{"id":"rivets-rndz","title":"Update get_callers/get_symbol_impact to use resolved refs","description":"The graph operations should now pick up cross-file references since they're stored with proper symbol_id after Pass 2.\n\nVerify that:\n- `get_callers()` returns callers from other files\n- `get_symbol_impact()` shows transitive dependents across files","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4tev","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:45.836727912Z","updated_at":"2026-01-28T18:10:27.543232953Z","closed_at":"2026-01-28T18:10:27.543232953Z"} -{"id":"rivets-2wp","title":"Create architecture document and diagrams for rivets","description":"Based on research from JSONL library design and project structure planning, create comprehensive architecture documentation with diagrams showing component relationships, data flow, and system design.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Create comprehensive documentation covering:\n\n**1. High-Level System Architecture**:\n- System architecture diagram showing all components/crates\n- Component interaction diagrams (crate dependency graph)\n- Key design decisions and rationale\n- Technology choices (libraries, patterns, etc.)\n- Future extensibility considerations\n\n**2. Data Flow**:\n- JSONL data flow through system (CLI → RPC → Storage → JSONL)\n- How JSONL operations work across components\n- Module organization within each crate\n\n**3. Storage Layer**:\n- Database schema diagram\n- Table relationships\n- Index strategy\n- Migration system\n\n**4. ID Generation System**:\n- Hash algorithm explanation\n- Collision handling strategy\n- Hierarchical ID format\n\n**5. Dependency System**:\n- 4 dependency types explained\n- Cycle detection algorithm\n- Ready work calculation logic\n\n**6. JSONL Sync**:\n- Export flow diagram\n- Import flow diagram\n- Conflict resolution strategy\n\n**7. RPC Protocol**:\n- Message format specification\n- Operation list\n- Error handling approach\n\nUse mermaid diagrams for visual clarity throughout. Format as ADR or similar structured format.","acceptance_criteria":"- Architecture document created at docs/architecture.md\n- System architecture diagram showing all crates and components\n- Crate dependency graph with clear separation of concerns\n- Data flow diagram showing JSONL operations (CLI → RPC → Storage → JSONL)\n- Database schema diagram with table relationships\n- ID generation algorithm documented with collision handling\n- Dependency system explained (4 types, cycle detection, ready work)\n- JSONL sync flows documented (export/import with conflict resolution)\n- RPC protocol specification (message format, operations, errors)\n- All diagrams in mermaid format (maintainable and version-controllable)\n- Design decisions justified with rationale\n- Future extensibility considerations noted\n- Document integrates findings from rivets-fk9 and rivets-kr3 research","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-fk9","dep_type":"blocks"},{"depends_on_id":"rivets-kr3","dep_type":"blocks"}],"created_at":"2025-11-17T21:08:45.698527855Z","updated_at":"2025-11-28T00:44:22.287918117Z","closed_at":"2025-11-28T00:44:22.287918117Z"} -{"id":"rivets-53zq","title":"Test polish for workspace-crate resolver (PR #62 follow-up)","description":"Three low-priority test polish items surfaced in PR #62 reviews 4 and 5. None are correctness issues; PR #62 was merged with the core fix and the items below tracked here.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Items\n\n### F1 — Hoist `use crate::types::CrateInfo;` to mod tests scope\n\n`use crate::types::CrateInfo;` appears inline inside multiple test functions in `crates/tethys/src/resolver.rs::tests` (e.g., `resolves_workspace_crate_via_new_arm`, `single_segment_falls_back_to_bin_when_lib_path_absent`, `single_segment_returns_none_when_no_entry_point`). The `workspace_with_crates` helper in the same module references it too. Hoisting to a single `use` at the top of `mod tests` would clean up the duplication.\n\nTrivial; ~5 lines removed.\n\n### F2 — Test that lib_path takes priority when both lib_path and bin_paths are set\n\nThe single-segment workspace-crate arm uses:\n\n```rust\ntarget.lib_path\n .as_ref()\n .or_else(|| target.bin_paths.first().map(|(_, p)| p))\n```\n\nCurrently tested:\n- `lib_path: Some(...), bin_paths: []` (single_segment_workspace_crate_resolves_to_entry_point_file)\n- `lib_path: None, bin_paths: [bin]` (single_segment_falls_back_to_bin_when_lib_path_absent)\n- `lib_path: None, bin_paths: []` (single_segment_returns_none_when_no_entry_point)\n\nNOT directly tested: `lib_path: Some(lib), bin_paths: [bin]` should resolve to lib_path, not the bin. The `or_else` ordering implies this but it isn't locked down.\n\nAdd a test constructing a `CrateInfo` with both populated, asserting the resolved path ends with the lib_path file.\n\n### F3 — Prefix-of-crate-name negative test\n\nAsserts that a use-path head that is a strict prefix of a workspace crate name (e.g., `riv` when the workspace has `rivets`) does NOT match. The current `find(|c| c.name.replace('-', \"_\") == head)` uses `==` not `starts_with`, so this is correct by inspection — but an explicit test documents the intentional non-match for future readers.\n\nLow priority; documents semantics already obvious from the code.\n\n## Why one issue, not three\n\nThese are all small (10-line tests or smaller) and all touch the same test module in `resolver.rs`. A single follow-up PR addressing all three is cleaner than three separate ones.","acceptance_criteria":"- [ ] F1: `use crate::types::CrateInfo;` hoisted to mod tests scope; duplicate inline imports removed\n- [ ] F2: New test constructing CrateInfo with both lib_path and bin_paths populated; asserts lib_path wins\n- [ ] F3: New test asserting a use-path head that is a prefix of a workspace crate name returns None\n- [ ] cargo nextest run -p tethys passes\n- [ ] cargo clippy + cargo fmt clean","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T21:57:36.396522300Z","updated_at":"2026-05-12T21:57:36.396522300Z","closed_at":null} -{"id":"rivets-dw3","title":"Evaluate rstest for rivets-jsonl/src/writer.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in writer.rs.\n\nFile: crates/rivets-jsonl/src/writer.rs (lines ~250+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:08.122330884Z","updated_at":"2025-11-29T01:26:43.698535623Z","closed_at":"2025-11-29T01:26:43.698535623Z"} -{"id":"rivets-t0k","title":"Add comprehensive tests for resilient loading","description":"Create comprehensive tests for Phase 2 resilient loading functionality.\n\nTests should verify warning collection, error recovery, and integration with in_memory storage.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Test categories:\n\n**Warning collection tests**:\n- Collect warnings for malformed JSON\n- Collect warnings for skipped lines\n- Warning contains correct line numbers\n- Multiple warnings collected\n\n**Resilient streaming tests**:\n- stream_resilient() continues on errors\n- stream_resilient() yields only valid records\n- Mixed valid/invalid records handled correctly\n\n**Integration tests**:\n- read_jsonl_resilient() with corrupted file\n- in_memory::load_from_jsonl with warnings\n- Verify existing in_memory tests still pass","acceptance_criteria":"- Unit tests for warning collection\n- Unit tests for stream_resilient()\n- Integration tests for read_jsonl_resilient()\n- Integration tests for in_memory integration\n- All tests pass\n- Test coverage >80% for Phase 2 code","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4q2","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:12.327924224Z","updated_at":"2025-11-28T07:16:42.034792534Z","closed_at":"2025-11-28T07:16:42.034792534Z"} +{"id":"rivets-zcy","title":"Evaluate rstest for rivets/src/storage/mod.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in storage/mod.rs.\n\nFile: crates/rivets/src/storage/mod.rs (lines ~323+)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:25.117428822Z","updated_at":"2025-11-29T01:29:24.941330500Z","closed_at":"2025-11-29T01:29:24.941330500Z"} +{"id":"rivets-dr0b","title":"Refactor: Add structured logging per M-LOG-STRUCTURED","description":"The code uses raw println!/eprintln! instead of structured logging:\n\n```rust\neprintln!(\"Warning: Failed to reload after save error: {}\", reload_err);\n```\n\nPer M-LOG-STRUCTURED guideline, should use a logging crate with named properties:\n\n```rust\ntracing::warn!(\n name: \"storage.reload.failed\",\n error = %reload_err,\n \"Failed to reload after save error: {{error}}\"\n);\n```\n\n**Scope:** Review all eprintln! calls in execute.rs and convert to structured tracing events.","status":"closed","priority":3,"issue_type":"task","assignee":null,"labels":["M-LOG-STRUCTURED","execute.rs","refactor"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-q82","dep_type":"parent-child"}],"created_at":"2025-12-28T15:37:40.909394132Z","updated_at":"2025-12-28T17:26:55.645651081Z","closed_at":"2025-12-28T17:26:55.645651081Z"} +{"id":"rivets-sxl","title":"Add integration test for set_workspace cache eviction","description":"The `evict_oldest()` method is tested in isolation, but the actual eviction trigger in `set_workspace()` (lines 104-107) has no test coverage. The existing `test_cache_eviction` uses `set_test_workspace()` which bypasses eviction logic.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["coverage","testing"],"design":"Create an async integration test that:\n1. Creates `MAX_CACHED_WORKSPACES + 1` real temp directories with `.rivets/` structure\n2. Calls `set_workspace()` for each\n3. Verifies the cache stays at the limit and oldest entry is evicted\n\n```rust\n#[tokio::test]\nasync fn test_set_workspace_evicts_when_full() {\n let temps: Vec = (0..=MAX_CACHED_WORKSPACES)\n .map(|_| TempDir::new().unwrap())\n .collect();\n \n // Initialize .rivets/ in each\n for temp in &temps {\n std::fs::create_dir(temp.path().join(\".rivets\")).unwrap();\n }\n \n let mut context = Context::new();\n \n // Fill cache to limit\n for temp in &temps[..MAX_CACHED_WORKSPACES] {\n context.set_workspace(temp.path()).await.unwrap();\n }\n assert_eq!(context.cache_size(), MAX_CACHED_WORKSPACES);\n \n let first_workspace = temps[0].path().canonicalize().unwrap();\n \n // Add one more - should trigger eviction\n context.set_workspace(temps[MAX_CACHED_WORKSPACES].path()).await.unwrap();\n \n assert_eq!(context.cache_size(), MAX_CACHED_WORKSPACES);\n assert!(!context.storage_cache.contains_key(&first_workspace));\n}\n```\n\n**Location:** `context.rs` tests module","acceptance_criteria":"- [ ] Integration test exercises actual `set_workspace()` eviction path\n- [ ] Test uses real temp directories (not `set_test_workspace`)\n- [ ] Verifies cache size stays at `MAX_CACHED_WORKSPACES`\n- [ ] Verifies oldest workspace is removed from cache","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T04:32:31.990408907Z","updated_at":"2025-11-29T04:32:31.990408907Z","closed_at":null} +{"id":"rivets-40p","title":"Add NO_COLOR environment variable support","description":"Support NO_COLOR environment variable per https://no-color.org/\n\nWhen NO_COLOR is set (to any value), disable all colored output. This ensures compatibility with:\n- CI/CD pipelines\n- Log aggregation systems\n- Users with accessibility needs\n- Piped output\n\nAlso auto-detect if stdout is a TTY and disable colors if not.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["accessibility","phase-1a","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:34:58.392325579Z","updated_at":"2025-11-30T18:34:58.392325579Z","closed_at":null} +{"id":"rivets-wao","title":"Implement rivets pick workflow command","description":"Add `rivets pick` command for interactive work claiming:\n\n```\n$ rivets pick\nShowing 5 ready issues sorted by priority:\n\n1. [P0] rivets-abc Fix critical auth bug @unassigned\n2. [P1] rivets-def Update OAuth tokens @unassigned \n3. [P2] rivets-ghi Add unit tests @unassigned\n\nPick issue to work on (1-3, or 'q' to quit): 1\n\nStarting work on rivets-abc...\n - Status: open -> in_progress\n - Assignee: -> alice (you)\n\nReady to work! Run 'rivets done rivets-abc' when complete.\n```","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":["phase-4","workflow"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:50.467716127Z","updated_at":"2025-11-30T18:37:50.467716127Z","closed_at":null} +{"id":"rivets-83j","title":"Implement JsonlQuery builder pattern","description":"Implement the JsonlQuery struct with builder pattern for constructing query pipelines with filters and transformations.\n\nThis provides a fluent API for filtering JSONL streams during reading.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\nuse std::marker::PhantomData;\n\npub struct JsonlQuery {\n predicates: Vec bool + Send + Sync>>,\n _phantom: PhantomData,\n}\n\nimpl JsonlQuery\nwhere\n T: DeserializeOwned + 'static,\n{\n pub fn new() -> Self {\n Self {\n predicates: Vec::new(),\n _phantom: PhantomData,\n }\n }\n \n pub fn filter(mut self, predicate: F) -> Self\n where\n F: Fn(&T) -> bool + Send + Sync + 'static,\n {\n self.predicates.push(Box::new(predicate));\n self\n }\n \n fn matches(&self, value: &T) -> bool {\n self.predicates.iter().all(|pred| pred(value))\n }\n}\n```","acceptance_criteria":"- JsonlQuery struct defined\n- new() constructor\n- filter() method adds predicates\n- Predicates stored as trait objects\n- Builder pattern allows chaining\n- Unit tests verify builder functionality\n- Compiles without errors","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-zy0","dep_type":"blocks"}],"created_at":"2025-11-27T23:16:49.651063411Z","updated_at":"2025-11-27T23:16:49.651063411Z","closed_at":null} {"id":"rivets-uo7","title":"Define core JsonlReader and JsonlWriter types","description":"Define the core async types for JsonlReader and JsonlWriter with proper generic parameters and trait bounds. This establishes the foundation for all read/write operations.\n\nBased on research API design:\n- JsonlReader\n- JsonlWriter\n- Both wrap BufReader/BufWriter for buffering","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":["phase-1","rivets-jsonl"],"design":"```rust\nuse tokio::io::{AsyncRead, AsyncWrite, BufReader, BufWriter};\n\npub struct JsonlReader {\n reader: BufReader,\n line_number: usize,\n}\n\nimpl JsonlReader {\n pub fn new(reader: R) -> Self {\n Self {\n reader: BufReader::new(reader),\n line_number: 0,\n }\n }\n}\n\npub struct JsonlWriter {\n writer: BufWriter,\n}\n\nimpl JsonlWriter {\n pub fn new(writer: W) -> Self {\n Self {\n writer: BufWriter::new(writer),\n }\n }\n}\n```\n\nAdd tokio dependency with features = [\"io-util\", \"fs\"].","acceptance_criteria":"- JsonlReader struct defined with generic R: AsyncRead + Unpin\n- JsonlWriter struct defined with generic W: AsyncWrite + Unpin\n- Both have new() constructors\n- BufReader/BufWriter used for buffering\n- Line number tracking in JsonlReader\n- Compiles without errors\n- Basic struct tests pass","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-27T23:14:00.581453653Z","updated_at":"2025-11-27T23:46:18.409886819Z","closed_at":"2025-11-27T23:46:18.409886819Z"} -{"id":"rivets-6yc","title":"Evaluate lock holding duration in MCP tools","description":"Review the lock holding patterns in rivets-mcp tools to ensure we're not holding locks longer than necessary.\n\nContext from PR review:\n- Current pattern: Acquire context read lock, then acquire storage write lock for operations\n- Potential optimization: Clone the Arc early and release the context lock immediately\n- Trade-off: Earlier lock release vs additional Arc clone overhead\n\nEvaluation should consider:\n1. Whether the current lock ordering prevents deadlocks (context -> storage)\n2. If cloning Arc early provides meaningful concurrency benefits\n3. Actual contention patterns in typical MCP usage (single client vs multiple)\n4. Whether read locks on context could be held across storage operations safely","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["concurrency","performance","rivets-mcp"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T07:29:38.632289389Z","updated_at":"2025-11-29T07:29:38.632289389Z","closed_at":null} -{"id":"rivets-cs4","title":"Improve tracing field visibility in rivets-mcp tools","description":"Several #[instrument] attributes in crates/rivets-mcp/src/tools.rs skip fields that could be useful for debugging (e.g., assignee, label). Consider including them in the tracing output for better observability.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["enhancement","observability"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-12-18T02:29:30.588563048Z","updated_at":"2025-12-18T02:29:30.588563048Z","closed_at":null} -{"id":"rivets-x51","title":"Implement JSONL import/export system","description":"Implement the JSONL import/export system for syncing between SQLite and JSONL files, with content hash deduplication, dirty tracking, and conflict resolution.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads importer (internal/importer/):\n\n**Export Flow**:\n1. Query dirty_issues table\n2. Serialize issues with embedded deps, labels, comments\n3. Compute content hash for deduplication\n4. Write to issues.jsonl (newline-delimited)\n5. Clear dirty flags\n\n**Import Flow**:\n1. Read JSONL line-by-line\n2. Parse and validate each issue\n3. Content hash comparison for conflict detection\n4. Upsert to SQLite\n5. Handle orphan dependencies (resurrect/skip/fail)\n\n**Features**:\n- Incremental dirty tracking\n- Content hash prevents duplicate writes\n- Prefix validation and migration\n- Orphan handling policies\n- External ref duplicate detection\n\n**Rust Implementation**:\n- Use serde_json for parsing\n- Streaming reader for large files\n- Batch inserts for performance","acceptance_criteria":"- Export writes issues to JSONL format\n- Import reads JSONL into SQLite\n- Content hash deduplication working\n- Dirty tracking identifies changed issues\n- Orphan handling implemented\n- Conflict resolution via content hash\n- Performance: 1000 issues in <1 second\n- Integration tests for round-trip consistency","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:16:06.672016354Z","updated_at":"2025-11-17T23:02:47.394020398Z","closed_at":"2025-11-17T23:02:47.394020398Z"} -{"id":"rivets-nsli","title":"tethys: test format_uri percent-encodes literal % in paths","description":"PATH_PERCENT_ENCODE_SET includes .add(b'%'), so a literal % in a path encodes as %25. No test exercises this. A regression removing .add(b'%') would produce a URI that parses but resolves to the wrong file (or double-decodes downstream).\n\nRare in Rust source paths but load-bearing for the RFC 3986 round-trip claim.\n\nSurfaced by PR #64 round-3 review (pr-test-analyzer).","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Test added to crates/tethys/src/lsp/transport.rs#tests\n- [ ] Input path contains a literal % character\n- [ ] Asserts the % is encoded as %25 in the resulting URI","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:25:23.849650300Z","updated_at":"2026-05-13T02:25:23.849650300Z","closed_at":null} -{"id":"rivets-37y","title":"Implement labels and comments system","description":"Implement label tagging and commenting functionality for issues, with many-to-many relationships and filtering support.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads labels.go and comments functionality:\n\n**Labels**:\n- Many-to-many relationship (issues ↔ labels)\n- Operations: add, remove, list labels on issue\n- Query by labels (AND / OR semantics)\n- Label management (create, delete, rename)\n\n**Comments**:\n- One-to-many (issue → comments)\n- Fields: author, created_at, content\n- Operations: add comment, list comments\n- Threaded comments (optional future)\n\n**Schema**:\n```sql\nlabels (\n id INTEGER PRIMARY KEY,\n issue_id TEXT NOT NULL,\n label TEXT NOT NULL,\n UNIQUE(issue_id, label)\n)\n\ncomments (\n id INTEGER PRIMARY KEY,\n issue_id TEXT NOT NULL,\n author TEXT,\n created_at TIMESTAMP,\n content TEXT NOT NULL\n)\n```\n\n**CLI Commands**:\n- `rivets label add `\n- `rivets label remove `\n- `rivets comment add `\n- `rivets comment list `","acceptance_criteria":"- Label add/remove operations work\n- Comment add/list operations work\n- Query issues by labels (--label, --labels-any)\n- Labels embedded in JSONL export\n- Comments embedded in JSONL export\n- Foreign key CASCADE on issue deletion\n- Unit tests for label/comment CRUD","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:47.355222934Z","updated_at":"2025-11-17T22:16:47.355222934Z","closed_at":null} -{"id":"rivets-j3a","title":"Evaluate rstest for rivets-jsonl/src/reader.rs inline tests","description":"Evaluate and apply rstest improvements to inline unit tests in reader.rs.\n\nFile: crates/rivets-jsonl/src/reader.rs (lines ~448-568)","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- Inline tests evaluated for rstest opportunities\n- Parameterization applied where beneficial\n- All tests pass","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"}],"created_at":"2025-11-29T01:19:02.499885060Z","updated_at":"2025-11-29T01:25:34.366773480Z","closed_at":"2025-11-29T01:25:34.366773480Z"} -{"id":"rivets-0tj5","title":"IndexErrorKind semantic mismatch: Config→IoError, Internal→DatabaseError","description":"The From<&Error> for IndexErrorKind mapping has semantic mismatches: Error::Config maps to IoError (config errors aren't I/O failures) and Error::Internal maps to DatabaseError (mutex poisoning isn't a DB error). Consider adding InfrastructureError variant or renaming existing variants. Flagged in PR #55 review.","status":"open","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-20T22:16:11.217907736Z","updated_at":"2026-03-20T22:16:11.217907736Z","closed_at":null} -{"id":"rivets-epmj","title":"Discriminated NotFound variants in tethys::Error (e.g., PackageNotFound)","description":"Currently tethys::Error::NotFound(String) carries an ad-hoc payload format like 'package: foo', 'file: bar', 'symbol: baz'. The string content is a de facto protocol for distinguishing what was not found, but nothing in the type enforces or documents it.\n\nThis becomes a problem when rivets-mcp adds a tethys_coupling tool. If MCP needs to react differently to 'package not found' vs 'file not found' (e.g., suggest re-indexing for the former, suggest checking the path for the latter), it has to parse the string payload — a brittle implicit contract.\n\n**Fix:** Introduce discriminated variants on tethys::Error:\n- PackageNotFound(String)\n- (existing NotFound(String) stays as catch-all for legacy callers)\n\nUpdate get_package_coupling and cli/coupling.rs::run_detail_to to use PackageNotFound.\n\n**Why deferred:** Low urgency today because (a) no MCP consumer of get_package_coupling exists yet, (b) the string format isn't being parsed anywhere, (c) main.rs renders both variants the same way. The right time to fix is just before rivets-o4re wires up tethys_coupling.\n\nDiscovered during the durability review pass on PR #60.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":["tethys","enhancement","error-handling"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-byie","dep_type":"parent-child"}],"created_at":"2026-05-11T02:47:33.676226Z","updated_at":"2026-05-11T02:47:33.676226Z","closed_at":null} -{"id":"rivets-8zq","title":"Refactor stringly-typed errors to structured error enums","description":"Improve error handling by replacing stringly-typed error variants with structured enums for better type safety, pattern matching, and debugging.\n\nCurrent issues identified:\n1. `anyhow::anyhow!()` used in domain layer (cli/execute.rs:60) - loses type info\n2. `Config(String)` catch-all in rivets/src/error.rs\n3. `Storage(String)` catch-all in rivets/src/error.rs\n4. `Mcp(String)` unused variant in rivets-mcp/src/error.rs\n5. Error context lost when InvalidFormat → Storage(msg) in jsonl.rs:140-144","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":["error-handling","refactor","type-safety"],"design":"Replace generic string variants with structured error enums:\n\n```rust\n// ConfigError enum\n#[derive(Debug, Error)]\npub enum ConfigError {\n #[error(\"Invalid prefix: {0}\")]\n InvalidPrefix(String),\n #[error(\"Parse error: {0}\")]\n Parse(String),\n #[error(\"IO error: {0}\")]\n Io(#[from] std::io::Error),\n}\n\n// StorageError enum\n#[derive(Debug, Error)]\npub enum StorageError {\n #[error(\"JSONL error: {0}\")]\n Jsonl(#[from] rivets_jsonl::Error),\n #[error(\"Validation failed: {0}\")]\n Validation(String),\n #[error(\"ID generation failed: {0}\")]\n IdGeneration(String),\n}\n\n// Add ValidationError variant for common case\n#[error(\"Validation error: {field}: {reason}\")]\nValidation { field: &'static str, reason: String }\n```\n\nFiles to modify:\n- crates/rivets/src/error.rs\n- crates/rivets/src/cli/execute.rs\n- crates/rivets/src/commands/init.rs\n- crates/rivets/src/storage/in_memory/trait_impl.rs\n- crates/rivets/src/storage/in_memory/jsonl.rs\n- crates/rivets-mcp/src/error.rs","acceptance_criteria":"- [ ] Replace `anyhow::anyhow!()` with typed error in cli/execute.rs:60\n- [ ] Create ConfigError enum and update Config variant to use it\n- [ ] Create StorageError enum and update Storage variant to use it\n- [ ] Add ValidationError variant for validation failures\n- [ ] Remove unused Mcp(String) variant from rivets-mcp\n- [ ] Update all call sites constructing Config(String) and Storage(String)\n- [ ] Preserve InvalidFormat semantic instead of downgrading to Storage\n- [ ] All tests pass\n- [ ] No new compiler warnings","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-12-01T04:49:52.900240044Z","updated_at":"2025-12-01T04:49:52.900240044Z","closed_at":null} -{"id":"rivets-fk9","title":"Research and design JSONL library architecture","description":"Design a standalone Rust library for efficient JSONL (JSON Lines) operations that can be used by rivets and potentially other projects. The library should handle reading, writing, querying, and streaming JSONL data structures.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":[],"design":"Research key features needed:\n- Efficient line-by-line reading and writing\n- Streaming support for large files\n- Querying/filtering capabilities\n- Schema validation (optional)\n- Error handling for malformed JSON\n- Memory-efficient operations\n- Concurrent access patterns\n\nConsider existing Rust JSONL libraries and what gaps exist. Determine if we build from scratch or extend existing solutions.","acceptance_criteria":"- Document listing key features and API design\n- Comparison of existing Rust JSONL libraries (evaluated on: performance benchmarks, API ergonomics, maintenance status, license compatibility, feature completeness)\n- Decision matrix showing trade-offs\n- Decision on whether to build new library or extend existing one\n- Performance requirements documented (targets: streaming 100MB file <1s, memory usage <10MB regardless of file size)\n- Public API surface designed with examples\n- Compatibility with serde ecosystem verified","notes":"## Research Complete (2025-11-27)\n\nComprehensive research documented in `docs/rivets-jsonl-research.md`.\n\n**Key Findings**:\n- Evaluated 4 existing Rust JSONL libraries (jsonl, serde-jsonlines, json-lines, json-stream)\n- None fully meet rivets' needs (async-first, streaming queries, resilient loading)\n- **Decision**: Build custom implementation borrowing proven patterns\n\n**Performance Targets**:\n- 100MB file in <1s (read)\n- <10MB memory regardless of file size\n- Match or exceed current in_memory.rs baseline\n\n**API Design**:\n- Async-native with tokio\n- Extension traits for ergonomics\n- Streaming via futures::Stream\n- Resilient loading with warning collection\n- Query/filter during stream\n\n**Next**: Implement Phase 1 (Core Read/Write)","external_ref":null,"dependencies":[{"depends_on_id":"rivets-cr9","dep_type":"blocks"}],"created_at":"2025-11-17T21:08:44.178736229Z","updated_at":"2025-11-27T23:01:29.152853238Z","closed_at":"2025-11-27T23:01:29.152853238Z"} -{"id":"rivets-ans","title":"Convert roundtrip.rs to rstest","description":"Replace 11 similar roundtrip tests with parameterized #[rstest] test. Create fixture for buffer setup pattern.\n\nFile: crates/rivets-jsonl/tests/roundtrip.rs\nEstimated reduction: ~150 lines → ~50 lines\n\nThis is the highest-impact conversion - the buffer setup pattern is repeated 11 times.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- All roundtrip tests converted to use #[rstest] with #[case] parameterization\n- Buffer setup extracted to fixture or helper\n- All tests pass\n- Significant line count reduction achieved","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-ry6","dep_type":"parent-child"},{"depends_on_id":"rivets-xi4","dep_type":"blocks"}],"created_at":"2025-11-29T00:56:19.405900112Z","updated_at":"2025-11-29T02:08:24.172376961Z","closed_at":"2025-11-29T02:08:24.172376961Z"} -{"id":"rivets-dn35","title":"tethys: Pass 2 short-circuits on import-less files, leaving workspace-internal refs unresolved","description":"crates/tethys/src/resolve.rs::resolve_refs_for_file returns early when imports.is_empty(), so any unresolved reference in a file with no use statements never reaches Pass-2 import/fallback resolution.\n\nCLAUDE.md flags this explicitly in the 'Tethys resolver internals' section but it's not tracked.\n\nAffected input shape: files that legitimately reference workspace symbols without a use statement — typically:\n - test files using fully-qualified paths (crate_name::Type)\n - mod.rs files that re-export via pub use only\n - integration test harnesses\n\nImpact on resolution coverage: unknown without measurement. The early-return is a speed optimization that traded correctness for cycles; the import-less case may have been judged rare when it shipped, but on the rivets workspace itself it's worth quantifying.\n\nReproduction: index the rivets workspace, then query for refs where r.symbol_id IS NULL AND r.file_id IN (SELECT id FROM files WHERE NOT EXISTS (SELECT 1 FROM imports WHERE file_id = files.id)) AND r.reference_name IN (SELECT name FROM symbols). Any non-zero result is a Pass-2 miss this short-circuit caused.\n\nLikely fix: drop the imports.is_empty() short-circuit entirely, or replace with a per-ref check (don't skip the whole file, just skip refs whose name can't resolve without an import context). Pass 2's fallback search_symbol_by_name path doesn't actually need imports — only the import-resolution path does.","status":"closed","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Probe SQL above returns 0 rows on the rivets workspace\n- [ ] Regression test: a fixture file with no use statements but a fully-qualified workspace reference (e.g., crate_target::Widget) resolves correctly\n- [ ] resolve_refs_for_file no longer returns Ok(()) early when imports.is_empty()\n- [ ] No regression in resolution time on the rivets workspace (measure via tethys index timing)","notes":"Closed: Fixed in PR #69 (commit 21e82e6). Removed the imports.is_empty() short-circuit in resolve_refs_for_file so import-less files now reach fallback_symbol_search. Locked in by tests/pass2_no_imports.rs. Acceptance criterion #2 (qualified-ref test) was partially met: the unqualified-fallback case works; the qualified-path case is a separate gap filed as rivets-044i.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-13T02:48:03.363815600Z","updated_at":"2026-05-18T18:51:03.202003600Z","closed_at":"2026-05-18T18:51:03.202000400Z"} -{"id":"rivets-5an","title":"Add config file validation on load","description":"When loading config.yaml in commands, validate that manually-edited values (like prefix) are still valid. Currently, if a user edits the config file to have an invalid prefix, it won't be caught until issue creation.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["config","validation"],"design":"Add validation in RivetsConfig::load():\\n\\n1. After deserializing, call validate_prefix() on issue_prefix\\n2. Validate storage.backend is a known value\\n3. Validate storage.data_file path is reasonable\\n\\nReturn helpful error messages indicating which field is invalid and what the requirements are.","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-30T02:48:34.570179736Z","updated_at":"2025-11-30T02:48:34.570179736Z","closed_at":null} +{"id":"rivets-1u7n","title":"Enforce parent-child dependency type is epic-to-task","description":"Help text says parent-child dependency is \"Hierarchical - epic to task\" but we don't actually enforce this. Either enforce the constraint or update the help text.","status":"open","priority":2,"issue_type":"bug","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2026-01-11T22:47:15.926596806Z","updated_at":"2026-01-11T22:47:15.926596806Z","closed_at":null} +{"id":"rivets-aic","title":"Add version field to RivetsConfig for migration support","description":"Add a version field to config.yaml to support future config format migrations. This allows the application to detect older config formats and migrate them automatically.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["config","migration"],"design":"Add a version field to RivetsConfig:\\n\\n```yaml\\nversion: 1\\nissue-prefix: proj\\nstorage:\\n backend: memory\\n data_file: .rivets/issues.jsonl\\n```\\n\\nImplementation:\\n1. Add `version: u32` field to RivetsConfig (default to 1)\\n2. On load, check version and apply migrations if needed\\n3. Migrations should be idempotent and well-tested\\n4. Consider semver for version if schema changes significantly\\n\\nThis enables:\\n- Backwards compatibility with older configs\\n- Automatic migration on first run after upgrade\\n- Clear error messages for unsupported versions","acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5an","dep_type":"related"},{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-30T02:51:51.756750966Z","updated_at":"2025-11-30T02:51:51.756750966Z","closed_at":null} +{"id":"rivets-oeu5","title":"Add --lsp flag to CLI commands","description":"Add `--lsp` flag to: index, callers, impact\n\nBehavior:\n- If --lsp and LSP available: use LSP\n- If --lsp and LSP missing: error and halt with helpful message\n- No flag: tree-sitter only\n\nError message should include installation instructions for rust-analyzer.","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-nwwm","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:50.158866500Z","updated_at":"2026-01-29T00:31:18.920171795Z","closed_at":"2026-01-29T00:31:18.920171795Z"} +{"id":"rivets-syau","title":"Path queries: shortest path between two symbols","description":"Find the shortest connection between any two symbols, traversing all edge kinds (calls, refs, imports). Answers ''how is X connected to Y?'' for code review and impact analysis.\n\n**Inspired by:** KiroGraph's kirograph_path tool and CLI command. Tethys already has get_dependency_chain for files; the same BFS over the union of edge kinds gives symbol-to-symbol path-finding.\n\n**Implementation:**\n- BFS shortest-path search over the union of call_edges and refs (grouped by symbol_id, in_symbol_id).\n- Public API:\n - Tethys::get_symbol_path(from: &str, to: &str) -> Result>>\n - PathStep { symbol, edge_kind, line, file }\n- CLI: tethys path [--format text|json]\n- Resolve symbol names with the same fuzzy matcher as tethys search; prefer real-symbol matches over file/import nodes.\n- Output shows each hop with file:line and the edge kind used.\n\n**Edge cases:**\n- No path exists → return None / clear CLI message.\n- Symbol not found → standard NotFound error.\n- Self-path (from == to) → empty path with clear semantics.\n\n**Test plan:**\nBuild a fixture workspace with three modules a → b → c plus an unrelated d, and verify path(a, c) returns the two-hop path while path(a, d) returns None.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["tethys","kirograph-inspired","feature"],"design":null,"acceptance_criteria":"- [ ] get_symbol_path() public API method\n- [ ] CLI subcommand tethys path FROM TO\n- [ ] BFS over union of call_edges and refs\n- [ ] Returns each hop with file:line and edge kind\n- [ ] Disconnected symbols return None / friendly CLI message\n- [ ] MCP tool tethys_path (sibling rivets-o4re)\n- [ ] Tests cover: connected, disconnected, self-path, fuzzy name resolution","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-05-10T04:32:05.780014500Z","updated_at":"2026-05-10T04:32:05.780014500Z","closed_at":null} +{"id":"rivets-hzul","title":"`rivets update` with no field arguments should error","description":"Currently `rivets update ISSUE-1` succeeds silently even when no fields are updated. Should fail with a message explaining what arguments can be used.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":["cli","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-01-11T22:47:28.086230193Z","updated_at":"2026-01-11T22:55:43.729972424Z","closed_at":"2026-01-11T22:55:43.729972424Z"} +{"id":"rivets-nz52","title":"Observability & Logging","description":"Improve debugging and operational visibility: file-based logging, migrate println! to tracing, structured log fields.","status":"open","priority":3,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:31.027113094Z","updated_at":"2026-02-21T02:28:31.027113094Z","closed_at":null} +{"id":"rivets-y9x","title":"Add storage metrics to rv stats command","description":"Monitor Automerge document growth and history depth.\n\nAdd to `rv stats` output:\n- Document file size (bytes)\n- Number of changes in history\n- Number of actors (unique editors)\n- Last compaction date (if applicable)\n- Comparison to equivalent JSONL size\n\nThis helps users understand storage overhead and decide when compaction is needed.","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T21:49:43.181865075Z","updated_at":"2025-12-23T04:45:34.661097069Z","closed_at":"2025-12-23T04:45:34.661097069Z"} {"id":"rivets-3af","title":"Extract sorting logic into reusable functions","description":"Consider extracting sorting logic from execute.rs into reusable functions.\n\nCurrently:\n- `execute.rs:140-156`: List command has inline sorting logic for `SortOrderArg`\n- Ready command uses `SortPolicy` with different sorting semantics\n\nThe list command sorting is clear and correct, but if sorting needs to be reused elsewhere, extracting it into a dedicated module or helper functions would improve maintainability.\n\nPotential approach:\n- Create a `sorting` module with functions for different sort strategies\n- Unify `SortOrderArg` and `SortPolicy` if they share common patterns\n- Keep domain-specific sorting in the storage layer where appropriate","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":["code-quality","pr-feedback"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2025-11-30T17:44:03.996137048Z","updated_at":"2025-11-30T17:44:03.996137048Z","closed_at":null} -{"id":"rivets-cr9","title":"Research beads project and create tasks to implement the project in rust as rivets","description":"We are going to convert the project beads to rust and call it rivets. You should have context to the beads project. Create all needed research and development tasks that you become aware of in beads so that we can begin development","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":[],"design":"## Beads Research Complete\n\nConducted comprehensive analysis of beads codebase (~19,243 LOC storage layer alone). \n\n**Key Findings**:\n- Hash-based ID generation (SHA256 + base36, adaptive 4-6 chars)\n- 4 dependency types (blocks, related, parent-child, discovered-from) \n- Recursive CTE algorithms for cycle detection and ready work\n- SQLite with 14 tables, WAL mode, foreign key constraints\n- JSONL import/export with content hash deduplication\n- RPC system (JSON-RPC over Unix sockets)\n- Background daemon with auto-sync\n\n**Created 13 Implementation Tasks**:\n1. rivets-x1e: Hash-based ID generation\n2. rivets-0gc: SQLite storage layer \n3. rivets-6op: Dependency system with cycle detection\n4. rivets-qeb: Ready work algorithm\n5. rivets-x51: JSONL import/export\n6. rivets-bsp: Core CLI commands\n7. rivets-xy9: Configuration system\n8. rivets-bi2: RPC system\n9. rivets-9mh: Daemon process\n10. rivets-37y: Labels and comments\n11. rivets-6tl: Filtering and queries\n12. rivets-4l2: Init command\n13. rivets-azn: Architecture documentation\n\nFull analysis documented in agent output.","acceptance_criteria":"✓ Beads codebase explored thoroughly\n✓ Core features documented (ID generation, dependencies, ready work, etc.)\n✓ Key algorithms identified (cycle detection, hash IDs, recursive CTEs)\n✓ 13 implementation tasks created in beads\n✓ Tasks cover all major beads functionality\n✓ Each task has design notes from beads source","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T21:01:56.182254064Z","updated_at":"2025-11-17T22:17:10.318274001Z","closed_at":"2025-11-17T22:17:10.318274001Z"} -{"id":"rivets-dhxo","title":"tethys: streaming compute_all_dependencies re-inserts file_deps from orphan files (deleted-from-disk but still in DB)","description":"Streaming-mode indexing (IndexOptions::with_streaming()) calls compute_all_dependencies (crates/tethys/src/indexing.rs:943) which iterates self.db.list_all_files() — the FULL set of files in the DB, including files that have been deleted from disk since their last index. For each such orphan, it loads the stale stored imports + refs and calls compute_dependencies_from_stored, which re-inserts file_deps rows with the orphan''s file_id as from_file_id.\n\nDownstream queries (coupling Ce/Ca, callers, cycles, impact analysis) then see the orphan as a real source of cross-file edges, producing phantom contributions.\n\n**Asymmetries — which paths are affected:**\n\n| Path | Status |\n|---|---|\n| tethys index --rebuild (any mode) | Not affected — db.reset() wipes DB; orphans gone |\n| tethys index non-streaming (default) | Not affected — compute_dependencies runs per disk-file in the parse loop; orphans never seen |\n| tethys index streaming | **Affected** |\n\n**Root cause:** index_with_options has no orphan-cleanup pass. db/files.rs:145-146 only DELETEs symbols/imports when an EXISTING file is RE-indexed (still on disk). Files deleted from disk are never processed, so no DELETE fires for them. FileChange::Deleted is detected in reindex.rs:122 but only by get_stale_files() (observation API, not called from indexing).\n\n**How discovered:** Gemini code review on PR #65 flagged \"compute_all_dependencies may re-calculate dependencies for stale entries (files in DB but deleted from disk).\" Initial round-1 verdict was \"reject (not a real bug)\" on cascade-cleanup grounds; pressure-test revealed the cascade only applies to re-indexed files, not orphans. See .rivets-lcb6/review-decisions-round-1.md for the corrected verdict.\n\n**Not caused by rivets-lcb6.** Pre-existing in streaming mode. rivets-lcb6''s clear_all_file_deps even partially mitigates by wiping file_deps each run, but compute_all_dependencies immediately re-inserts from orphans.\n\n**Likely fix shape:** before compute_all_dependencies runs, delete file rows whose disk path doesn''t resolve (and let FK cascades clean up dependents). Either:\n1. New cleanup_orphan_files that classifies via the same staleness check as reindex.rs::classify_indexed_file and DELETEs orphan rows, called from index_with_options before resolver/dep passes.\n2. Filter compute_all_dependencies to skip files whose path doesn''t exist on disk (lighter touch, leaves orphan rows in files table).\n\nOption 1 is cleaner — once orphan files are gone from the DB, all downstream queries are correct without per-callsite filtering.","status":"open","priority":3,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Regression test: index a 2-file workspace in streaming mode, delete one file from disk, re-index (no --rebuild). Verify (a) the orphan no longer appears in list_all_files, OR (b) file_deps contains no rows with from_file_id == orphan's id.\n- [ ] Equivalent test for non-streaming mode confirms the bug doesn't manifest there (regression fence).\n- [ ] No perf regression: indexing a workspace with N files and 0 orphans takes the same time before and after.","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-13T03:26:07.176733800Z","updated_at":"2026-05-13T03:26:37.855664300Z","closed_at":null} -{"id":"rivets-lcb6","title":"tethys: file_deps table never cleared between index runs (stale resolver edges persist)","description":"Discovered during rivets-0gom fix work (gilfoyle/checkpointed-build, slice 2): the file_deps table in tethys's index DB is populated via UPSERT-only, never DELETE'd between `tethys index` invocations.\n\nSchema (crates/tethys/src/db/file_deps.rs:18):\n``\nINSERT INTO file_deps (from_file_id, to_file_id, ref_count)\nVALUES (?1, ?2, 1)\nON CONFLICT(from_file_id, to_file_id) DO UPDATE SET ref_count = ref_count + 1\n``\n\nCompare to call_edges, which IS cleared (crates/tethys/src/db/call_edges.rs:13 -- `clear_all_call_edges`, called from indexing.rs:424). file_deps has no analog.\n\n**Impact:** changes to resolver behavior aren't reflected in file_deps without manual DB wipe (`rm .rivets/index/tethys.db`). During rivets-0gom slice 2:\n- We expected the probe to show fewer phantom edges after fixing the resolver\n- The probe showed identical counts because old phantoms from previous index runs persisted\n- Manually wiping the DB revealed the fix was actually working\n\nThis makes the index non-idempotent across resolver changes. It also means anyone using `tethys coupling` on a workspace they've indexed before is getting metrics polluted by historical resolution mistakes that may no longer be reproducible.\n\n**Reproduction:**\n1. `tethys index` on a workspace\n2. Note: file_deps row count = N1\n3. Modify a source file to add a cross-crate `use` statement; rebuild target\n4. `tethys index` again\n5. file_deps row count is now N1 + (new edges); old edges are NOT cleared even if they no longer exist in the source\n\n**Likely fix:** add `clear_all_file_deps` to db/file_deps.rs, call it from index_with_options before the resolver passes run (alongside or near the existing `clear_all_call_edges` call at indexing.rs:424). file_deps's FK with `ON DELETE CASCADE` from files means stale-file deletion already cleans up; the missing piece is the wholesale reset.\n\n**Related to:** rivets-0gom (made this finding visible). Not blocking PR 60 since neither computes file_deps freshness across runs; but blocks any future feature that wants to use file_deps deterministically.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] file_deps is cleared at the start of each index_with_options invocation (or before resolver passes 1/2 begin)\n- [ ] Regression test: index twice on the same workspace; file_deps row count is identical\n- [ ] Regression test: index workspace, modify file to remove a `use` statement, re-index; the removed edge is no longer in file_deps\n- [ ] Compatible with the incremental-update path tracked in rivets-bxom (when that lands, the clear should be scoped to changed files, not workspace-wide)","notes":"Closed: Implemented in PR #65 (merge commit 94226ce). clear_all_file_deps() added in crates/tethys/src/db/file_deps.rs:11; called from index_with_options in crates/tethys/src/indexing.rs:140. Regression-fence tests at crates/tethys/tests/file_deps_idempotency.rs cover batch + streaming modes.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-ycaq","dep_type":"parent-child"}],"created_at":"2026-05-12T01:04:44.609084500Z","updated_at":"2026-05-17T23:08:54.477462078Z","closed_at":"2026-05-17T23:08:54.477461016Z"} -{"id":"rivets-lxbg","title":"Add imports table to schema","description":"Add `imports` table to track what each file imports:\n\n```sql\nCREATE TABLE imports (\n file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n symbol_name TEXT NOT NULL,\n source_module TEXT NOT NULL,\n alias TEXT,\n PRIMARY KEY (file_id, symbol_name, source_module)\n);\nCREATE INDEX idx_imports_file ON imports(file_id);\nCREATE INDEX idx_imports_symbol ON imports(symbol_name);\n```\n\nUpdate db.rs with methods to insert/query imports.","status":"closed","priority":1,"issue_type":"task","assignee":"claude","labels":["phase1","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:27:20.547791398Z","updated_at":"2026-01-28T17:38:55.129203575Z","closed_at":"2026-01-28T17:38:55.129203575Z"} -{"id":"rivets-43i","title":"Implement info command","description":"Add 'info' command to display database path, issue prefix, and system status. Should support --json flag for programmatic use. Example output: { \"database_path\": \".rivets/issues.jsonl\", \"issue_prefix\": \"rivets\" }","status":"closed","priority":1,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:39.082722928Z","updated_at":"2025-12-28T00:33:28.505227293Z","closed_at":"2025-12-28T00:33:28.505227293Z"} -{"id":"rivets-o7o","title":"Consider splitting integration.rs by test category","description":"The integration test file has grown to 1,638 lines. Consider splitting it into separate files organized by test category for better maintainability.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["mcp","refactoring","testing"],"design":"Potential split structure for `crates/rivets-mcp/tests/`:\n\n```\ntests/\n├── common/\n│ └── mod.rs # Shared fixtures, helpers (IssueSetup, FilterParams, etc.)\n├── crud_tests.rs # Basic create/show/update/close operations\n├── list_filter_tests.rs # List filter combinations\n├── ready_filter_tests.rs # Ready filter combinations \n├── dependency_tests.rs # Dependency management tests\n├── context_tests.rs # Workspace context tests\n└── edge_case_tests.rs # Edge cases (empty results, unicode, etc.)\n```\n\nBenefits:\n- Easier to find relevant tests\n- Faster incremental compilation when modifying one category\n- Clearer test organization\n\nThis is low priority - the current single file works fine, just harder to navigate as it grows.","acceptance_criteria":"- [ ] Evaluate whether splitting improves maintainability\n- [ ] If splitting, organize tests into logical categories\n- [ ] Ensure shared test utilities are extracted to a common module\n- [ ] All tests continue to pass after reorganization","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-30T01:07:57.543771055Z","updated_at":"2025-11-30T01:07:57.543771055Z","closed_at":null} -{"id":"rivets-q8qw","title":"Implement incremental index updates","description":"Currently `update()` in `lib.rs:2095-2104` re-indexes everything. The schema stores `mtime_ns` and `size_bytes` which could enable proper incremental updates:\n\n1. Query files from DB\n2. Compare mtime/size with filesystem\n3. Only re-parse changed files\n4. Remove deleted files from index\n\nThis would significantly improve performance for large codebases.","status":"open","priority":2,"issue_type":"feature","assignee":null,"labels":["performance","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-gkt2","dep_type":"related"},{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:03.674170427Z","updated_at":"2026-01-30T00:18:03.674170427Z","closed_at":null} -{"id":"rivets-7qb","title":"Memory profiling and optimization","description":"Profile memory usage of rivets-jsonl and optimize to meet the <10MB target regardless of file size.\n\nUses memory profiling tools and techniques to verify streaming guarantees.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Memory profiling approach:\n\n1. **Create test files**:\n - 1MB (1K records)\n - 10MB (10K records)\n - 100MB (100K records)\n - 1GB (1M records)\n\n2. **Profile streaming operations**:\n ```rust\n #[cfg(test)]\n mod memory_tests {\n use std::alloc::{GlobalAlloc, Layout, System};\n use std::sync::atomic::{AtomicUsize, Ordering};\n \n struct TrackingAllocator;\n \n static ALLOCATED: AtomicUsize = AtomicUsize::new(0);\n \n unsafe impl GlobalAlloc for TrackingAllocator {\n unsafe fn alloc(&self, layout: Layout) -> *mut u8 {\n ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);\n System.alloc(layout)\n }\n \n unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {\n ALLOCATED.fetch_sub(layout.size(), Ordering::SeqCst);\n System.dealloc(ptr, layout);\n }\n }\n }\n ```\n\n3. **Optimization targets**:\n - Buffer size tuning\n - Reduce allocations in hot paths\n - Reuse String buffers where possible\n\n4. **Verification**:\n - Peak memory usage <10MB for all file sizes\n - Document peak memory in README","acceptance_criteria":"- Memory profiling tests created\n- Tested with 1MB, 10MB, 100MB, 1GB files\n- Peak memory <10MB for all sizes\n- Buffer sizes optimized\n- Allocations minimized in hot paths\n- Results documented","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"},{"depends_on_id":"rivets-t0k","dep_type":"blocks"}],"created_at":"2025-11-27T23:17:46.767467084Z","updated_at":"2025-11-27T23:17:46.767467084Z","closed_at":null} -{"id":"rivets-aay4","title":"Populate parent_symbol_id for nested symbols","description":"The schema supports `parent_symbol_id` for hierarchical symbols (e.g., method inside struct) but it's always `None`. See `lib.rs:632` and `lib.rs:761`.\n\nPopulating this would enable queries like:\n- \"Show all methods of struct X\"\n- \"Find all nested types in module Y\"\n- Symbol hierarchy navigation","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:15.487249815Z","updated_at":"2026-01-30T00:18:15.487249815Z","closed_at":null} -{"id":"rivets-rg0","title":"Create rivets-mcp crate structure","description":"Create the rivets-mcp crate directory structure:\n- crates/rivets-mcp/src/{lib.rs, main.rs, server.rs, tools.rs, context.rs, models.rs, error.rs}\n- Add to workspace Cargo.toml\n- Configure dependencies (rmcp, schemars, etc.)","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-4dw","dep_type":"parent-child"}],"created_at":"2025-11-29T01:16:00.027928947Z","updated_at":"2025-11-29T03:06:21.145518766Z","closed_at":"2025-11-29T03:06:21.145518766Z"} -{"id":"rivets-4iz","title":"Add ratatui and crossterm dependencies for TUI","description":"Add TUI dependencies to rivets crate:\n- ratatui = \"0.29\" (TUI framework)\n- crossterm = \"0.28\" (cross-platform terminal backend)","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:55.243494646Z","updated_at":"2025-11-30T18:36:55.243494646Z","closed_at":null} -{"id":"rivets-e3j","title":"Implement TUI Kanban board view","description":"Create Kanban board view for TUI with columns:\n- Open\n- In Progress\n- Blocked\n- Closed\n\nFeatures:\n- Issue cards with title, priority, assignee\n- Move issues between columns\n- Color-coded by priority\n- Scrollable columns","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-3","tui"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:37:06.816842488Z","updated_at":"2025-11-30T18:37:06.816842488Z","closed_at":null} -{"id":"rivets-v21s","title":"Implement cycle detection for file dependencies","description":"Implement the `detect_cycles()` method that currently returns \"not implemented\".\n\n**What already exists:**\n- ✅ `FileGraphOps::detect_cycles()` trait method (graph/mod.rs:88)\n- ✅ `FileGraphOps::detect_cycles_involving(file_id)` trait method\n- ✅ `tethys cycles` CLI command (cli/cycles.rs)\n- ✅ `file_dependencies` table with all edges\n- ❌ Actual implementation (returns error at graph/sql.rs:318)\n\n**Implementation approach (~100 lines):**\n\nOption A: **Tarjan's SCC** (finds all cycles efficiently)\n```rust\nfn detect_cycles(&self) -> Result> {\n // Standard Tarjan's algorithm for strongly connected components\n // Any SCC with size > 1 is a cycle\n}\n```\n\nOption B: **Simple DFS** (easier to understand)\n```rust\nfn detect_cycles(&self) -> Result> {\n let mut cycles = Vec::new();\n let mut visited = HashSet::new();\n let mut rec_stack = HashSet::new();\n \n for file_id in self.get_all_file_ids()? {\n self.dfs_find_cycles(file_id, &mut visited, &mut rec_stack, &mut cycles)?;\n }\n Ok(cycles)\n}\n```\n\n**The CLI already handles output:**\n```rust\n// cli/cycles.rs already formats output nicely:\n// Cycle 1: a.rs -> b.rs -> c.rs -> a.rs\n```\n\n**Estimated effort: Low-Medium (~100 lines, 2-3 hours)**\n\nJust need to implement the algorithm - all the plumbing exists.","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":["drift-inspired","feature","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:53:28.542509377Z","updated_at":"2026-01-29T15:00:39.567609143Z","closed_at":"2026-01-29T15:00:39.567609143Z"} -{"id":"rivets-rg8","title":"Implement AutomergeStorage: IssueStorage trait","description":"Implement the full IssueStorage trait for AutomergeStorage, including all CRUD, dependency, and query methods.\n\nIncludes:\n- AutomergeStorage struct with in-memory cache (HashMap + petgraph)\n- Implement rebuild_dependency_graph() for post-hydration graph construction\n- All IssueStorage trait methods\n- Wire into storage factory and config (StorageBackend::Automerge variant)","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T19:10:33.301178192Z","updated_at":"2025-12-23T04:45:05.425292273Z","closed_at":"2025-12-23T04:45:05.425292273Z"} -{"id":"rivets-1wn","title":"Extract shared filter logic from list() and ready_to_work()","description":"The filter logic in list() and ready_to_work() is nearly identical. Extract to a shared function to reduce duplication and improve maintainability.","status":"open","priority":3,"issue_type":"chore","assignee":null,"labels":["refactoring","storage"],"design":"Current duplication in `crates/rivets/src/storage/in_memory/trait_impl.rs`:\n- `list()`: lines 336-378\n- `ready_to_work()`: lines 411-448\n\nProposed refactoring:\n\n```rust\n/// Apply filter criteria to an issue, returning true if it matches.\nfn apply_filter(issue: &Issue, filter: &IssueFilter) -> bool {\n if let Some(status) = &filter.status {\n if &issue.status != status { return false; }\n }\n if let Some(priority) = filter.priority {\n if issue.priority != priority { return false; }\n }\n if let Some(issue_type) = &filter.issue_type {\n if &issue.issue_type != issue_type { return false; }\n }\n if let Some(assignee) = &filter.assignee {\n if issue.assignee.as_ref() != Some(assignee) { return false; }\n }\n if let Some(label) = &filter.label {\n if !issue.labels.contains(label) { return false; }\n }\n true\n}\n```\n\nUsage:\n```rust\nissues.retain(|issue| apply_filter(issue, filter));\nif let Some(limit) = filter.limit {\n issues.truncate(limit);\n}\n```\n\nThis is low priority since the current code works correctly - just a maintainability improvement.","acceptance_criteria":"- [ ] Extract shared filter predicate function\n- [ ] Update list() to use shared function\n- [ ] Update ready_to_work() to use shared function\n- [ ] All existing tests pass\n- [ ] No change in observable behavior","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-30T01:09:16.681348623Z","updated_at":"2025-11-30T01:09:16.681348623Z","closed_at":null} -{"id":"rivets-aaw","title":"Add compact vs detailed view modes","description":"Add view modes for issue listing:\n\nCompact (default): Single line per issue\n```\n[ ] rivets-abc Fix auth bug P1 @alice\n[>] rivets-def Add OAuth P2\n```\n\nDetailed (--detailed/-d): Multi-line with description preview\n```\n[ ] rivets-abc Fix authentication bug\n P1 | task | @alice | urgent, backend\n Authentication fails when token expires...\n```","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:30.019940230Z","updated_at":"2025-11-30T18:36:30.019940230Z","closed_at":null} -{"id":"rivets-1zz","title":"Add conflict logging for Automerge merges","description":"Even though CRDTs auto-resolve conflicts, users should be aware when merges happen.\n\nImplement:\n- Log when merge() is called and changes are integrated\n- Track which fields had concurrent modifications\n- Optional: Store merge events in a separate log file or document metadata\n- Surface merge history in `rv log` or similar command","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5vz","dep_type":"blocks"}],"created_at":"2025-11-30T21:49:37.307979170Z","updated_at":"2025-12-23T04:45:28.797678904Z","closed_at":"2025-12-23T04:45:28.797678904Z"} -{"id":"rivets-kz8j","title":"Add clippy missing_errors_doc documentation","description":"Remove the allow(clippy::missing_errors_doc) suppression and add proper error documentation to public methods. Currently deferred to avoid churn during initial development.","status":"open","priority":4,"issue_type":"chore","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-5hvt","dep_type":"parent-child"}],"created_at":"2026-02-06T00:35:14.455287073Z","updated_at":"2026-02-06T00:35:14.455287073Z","closed_at":null} -{"id":"rivets-bi2","title":"Implement RPC system for daemon communication","description":"Implement the RPC server/client system using Unix domain sockets (or named pipes on Windows) for CLI-daemon communication with JSON-RPC protocol.","status":"open","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads internal/rpc/:\n\n**Architecture**:\n- Server runs in daemon process\n- Client in each CLI invocation\n- Transport: Unix sockets (Linux/macOS), Named pipes (Windows)\n- Protocol: JSON-RPC over newline-delimited messages\n\n**Operations Exposed**:\n- Full CRUD (create, read, update, delete)\n- Queries (list, ready, blocked, stats)\n- Import/export triggers\n- Health checks\n\n**Rust Stack**:\n- `tokio` for async I/O\n- `serde_json` for JSON-RPC\n- `interprocess` crate for IPC\n- Custom error types for RPC failures\n\n**Features**:\n- Version compatibility checks\n- Connection timeouts\n- Graceful shutdown\n- Request/response correlation","acceptance_criteria":"- RPC server accepts connections\n- RPC client can call server methods\n- JSON-RPC protocol implemented\n- All CRUD operations exposed\n- Health check endpoint works\n- Connection errors handled gracefully\n- Concurrent requests supported\n- Integration tests for RPC round-trips","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-sx8j","dep_type":"parent-child"}],"created_at":"2025-11-17T22:16:07.159548647Z","updated_at":"2025-11-17T22:16:07.159548647Z","closed_at":null} -{"id":"rivets-dzn8","title":"Consolidate three copies of workspace_with_files test helper","description":"Three independent copies of the workspace_with_files test fixture helper exist in the tethys integration test suite. PR #63 (rivets-6aoc) had to update all three with the same Cargo.toml auto-write logic; future test-infrastructure changes will keep paying this triplicate cost.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Current state\n\nThree near-identical `workspace_with_files` functions:\n\n1. `crates/tethys/tests/common/mod.rs` (shared, intended canonical location)\n2. `crates/tethys/tests/indexing.rs` (local copy, 77 callers in this one file)\n3. `crates/tethys/tests/test_topology.rs` (local copy)\n\nAll three now (post-PR #63) have similar auto-write-default-Cargo.toml logic with slight string differences:\n- common writes \"test_workspace\"\n- indexing writes \"test_workspace\"\n- test_topology writes \"test_topology\"\n\n## Folds in S2 from PR #63 review\n\nThe Cargo.toml exact-string match in the helpers (`files.iter().any(|(p, _)| *p == \"Cargo.toml\")`) is fragile: a caller passing \"./Cargo.toml\" would trigger a silent double-write. Normalize via `Path::file_name()` once during consolidation.\n\n## Proposed work\n\n1. Make `tests/common/mod.rs::workspace_with_files` the canonical helper.\n2. Delete the local copies in `tests/indexing.rs` and `tests/test_topology.rs`.\n3. Update all callers in those files to `use common::workspace_with_files;` (add `mod common;`).\n4. Normalize Cargo.toml detection in the canonical helper via `Path::file_name()` rather than exact-string comparison.\n5. Verify all integration tests still pass.","acceptance_criteria":"- [ ] Single workspace_with_files in tests/common/mod.rs\n- [ ] indexing.rs and test_topology.rs use the common helper via mod common\n- [ ] Cargo.toml detection uses Path::file_name(), not exact-string match\n- [ ] cargo nextest run -p tethys passes\n- [ ] cargo clippy + cargo fmt clean","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:40:36.828342600Z","updated_at":"2026-05-12T23:40:36.828342600Z","closed_at":null} -{"id":"rivets-o6x7","title":"Test topology mapping","description":"Map tests to the code they cover, enabling \"affected tests\" detection.\n\n**UPDATED: Medium effort - most primitives exist!**\n\n**What we already have:**\n- ✅ Index all symbols including test functions\n- ✅ Track imports via `imports` table\n- ✅ File dependency graph\n\n**What's missing:**\n1. **Test detection during parsing** (~50 lines)\n - Rust: Check for `#[test]` attribute on functions\n - C#: Check for `[Test]`, `[Fact]`, `[Theory]` attributes\n \n2. **Schema change** (~5 lines)\n - Add `is_test BOOLEAN DEFAULT FALSE` to symbols table\n - Or add `SymbolKind::Test` variant\n\n3. **Query for affected tests** (~30 lines)\n ```rust\n pub fn get_affected_tests(&self, changed_files: &[PathBuf]) -> Result> {\n // Find test symbols that import any of the changed files\n let test_symbols = self.db.get_symbols_where_is_test(true)?;\n let mut affected = Vec::new();\n for test in test_symbols {\n let test_file = self.db.get_file_by_id(test.file_id)?;\n let imports = self.db.get_imports(test.file_id)?;\n if imports.iter().any(|imp| changed_files.contains(&imp.resolved_path)) {\n affected.push(test);\n }\n }\n Ok(affected)\n }\n ```\n\n**Estimated effort: Medium (~100 lines, 2-4 hours)**\n\n**Use cases:**\n- CI: \"Which tests should I run for this change?\"\n- Coverage: \"What code has no tests?\"","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","labels":["drift-inspired","feature","testing","tethys"],"design":null,"acceptance_criteria":null,"notes":"Most primitives exist. Main work is extracting #[test] attributes during parsing and adding a simple query.","external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T12:49:38.099688442Z","updated_at":"2026-01-29T23:52:28.509241068Z","closed_at":"2026-01-29T23:52:28.509241068Z"} -{"id":"rivets-fr9b","title":"tethys: track parent_symbol_id for nested symbols","description":"When indexing symbols, parent_symbol_id is always set to None. Implement proper parent symbol tracking so nested symbols (e.g., methods inside structs/impls) have their parent relationship recorded.","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-03-19T01:41:02.074384054Z","updated_at":"2026-03-19T01:41:02.074384054Z","closed_at":null} -{"id":"rivets-5hvt","title":"Backlog","description":"Lower-priority and speculative items. Revisit periodically to promote or close.","status":"open","priority":4,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-02-21T02:28:32.353090414Z","updated_at":"2026-02-21T02:28:32.353090414Z","closed_at":null} -{"id":"rivets-cej","title":"Implement stale command","description":"Add 'stale' command to find issues not updated recently.\\n\\nFlags:\\n- --days N (default: 30)\\n- --status (filter by status)\\n- --limit N\\n- --json\\n\\nExample: rivets stale --days 30 --status in_progress --json","status":"closed","priority":2,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-v3s","dep_type":"parent-child"}],"created_at":"2025-12-28T00:23:56.751583711Z","updated_at":"2025-12-28T00:44:18.799743031Z","closed_at":"2025-12-28T00:44:18.799743031Z"} -{"id":"rivets-nkjd","title":"tethys: resolve_super_path uses filesystem-walk semantics, diverges from Rust super:: scoping","description":"tethys's `resolver::resolve_super_path` interprets `super::X` from a file at `src/parent/child.rs` as a filesystem grandparent walk: `current_file.parent().parent()` then joins X, reaching `src/X.rs`. Rust's actual semantics: `super::X` from `mod parent::child` refers to `crate::parent::X`, which lives at `src/parent/X.rs` (or methods on the parent module file).\n\nConcrete example, surfaced during rivets-044i (PR #74):\n\n src/lib.rs -> mod parent;\n src/parent.rs -> mod child; pub mod sibling;\n src/parent/child.rs:\n pub fn entry() {\n super::sibling::foo(); // Rust: src/parent/sibling.rs ; tethys: src/sibling.rs\n }\n\nThe new `self_and_super_paths_resolve_via_as_written` test in pass2_qualified_paths.rs documents this divergence (fixture lays sibling at the location tethys's resolver expects) but doesn't fix it.\n\nImpact: any qualified ref using `super::X::method()` style from a depth-2+ file resolves to the wrong file via the qualified_module_fallback path (or fails to resolve at all if the wrong-level file doesn't exist). The use-statement form `use super::X;` goes through a different code path (Pass-2 explicit imports) and may have the same issue — needs verification.\n\nSurfaced by claude-code-review #4 on PR #74 during round-3 review-feedback assessment, with the explicit suggestion to file a tracker entry rather than leave the divergence documented-only.","status":"open","priority":4,"issue_type":"bug","assignee":null,"labels":[],"design":"Two candidate fixes:\n\n(a) Build a module-tree map at index time: for each `.rs` adjacent to a `/` directory, record that the .rs file is the parent-module file of the subdirectory's contents. Then `resolve_super_path` walks the module tree instead of the filesystem.\n\n(b) Special-case the parent-module pattern in resolve_super_path: when `current_file.parent()` contains a sibling `.rs` (e.g. for `src/parent/child.rs`, check whether `src/parent.rs` exists), treat that .rs file as the parent module and resolve `super::X` relative to *its* directory (which is `src/parent/`, not `src/`).\n\nOption (b) is narrower and matches the common-case Rust idiom (parent.rs + parent/ pattern). Option (a) handles edge cases like `parent/mod.rs` + sibling files. Either way, `resolve_self_path` may need parallel review since it's the inverse semantics question.\n\nRisk: any test or workspace currently relying on the filesystem-walk semantics breaks. The rivets workspace itself doesn't seem to use this shape (otherwise PR #74's rivets-workspace re-index would have surfaced differently).","acceptance_criteria":"- [ ] resolve_super_path returns `src/parent/X.rs` for ref `super::X` from `src/parent/child.rs` when `src/parent.rs` exists\n- [ ] Test in pass2_qualified_paths.rs (or new test file) pins the Rust-spec semantics\n- [ ] Existing rivets workspace re-index post-fix: no phantom-rate regression (claim 7 of rivets-3d0s fence holds)\n- [ ] Update self_and_super_paths_resolve_via_as_written's docstring to remove the divergence-documentation language (or replace with a comment that says \"this is what Rust does, this is what we do, and they now agree\")\n- [ ] resolve_self_path reviewed for parallel issues; either confirmed correct or follow-up issue filed","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-19T01:04:21.198957500Z","updated_at":"2026-05-19T01:04:21.198957500Z","closed_at":null} +{"id":"rivets-bjdn","title":"Pre-compute crate-path lookup map for large workspaces","description":"The per-file crate_root lookup added by rivets-6aoc (cargo::get_crate_for_file, called in resolve.rs, indexing.rs x2, and resolver.rs) does an O(crates) linear scan with Path::starts_with per file. At rivets-workspace scale (4 crates) and typical Cargo workspaces (<50 crates) this is comfortably within the planned budget. At >50 crates AND if Pass-2 wall-time becomes non-trivial in profiling, swap to a pre-computed lookup map for O(log crates) or O(1) per-file dispatch.","status":"open","priority":4,"issue_type":"task","assignee":null,"labels":[],"design":"## Current implementation\n\n`crates/tethys/src/cargo.rs`:\n```rust\npub fn get_crate_for_file<'a>(file_path: &Path, crates: &'a [CrateInfo]) -> Option<&'a CrateInfo> {\n crates.iter()\n .filter(|c| file_path.starts_with(&c.path))\n .max_by_key(|c| c.path.components().count())\n}\n```\n\nCalled per file from:\n- `resolve.rs::resolve_refs_for_file` (Pass-2-imports cross-file ref resolution)\n- `indexing.rs::compute_dependencies` (dep-graph from extracted refs)\n- `indexing.rs::compute_dependencies_from_stored` (dep-graph from stored refs)\n- `resolver.rs::resolve_module_path` workspace-crate arm (target crate's src_root lookup)\n\nCost: O(files x crates) per indexing pass, dominated by Path::starts_with comparisons. For rivets (118 files x 4 crates = ~470 ops per pass) negligible.\n\n## Trigger condition\n\nTwo conditions must both hold before this optimization is worth the complexity:\n1. A real workspace appears with >50 crates being indexed by tethys\n2. Profiling shows the linear scan is non-trivial wall-time relative to DB I/O and parsing\n\nUntil then, the simple linear scan reads more clearly and avoids premature optimization.\n\n## Proposed mitigation when triggered\n\nPre-compute a `BTreeMap` once in `Tethys::new` (after `discover_crates` returns) and store it as a field on `Tethys`. Per-file lookups become O(log crates) via `range(..file_path).next_back()` (longest-prefix match via reverse-iteration on the sorted map).\n\nAlternative: `HashMap` with each crate's *exact* path as the key. Per-file dispatch then iterates parent directories of the file (O(path depth) which is ~10-20 for typical Rust files) checking membership. Avoids the longest-prefix complication.\n\n## Discovered during\n\nrivets-6aoc / rivets-34tv PR. Plan-phase budget analysis (`.rivets-6aoc/plan.md` slice 2 \"Justification\") admitted the linear scan exceeds the 10^6 ops budget at extreme scale (50k files x 100 crates = 5x10^6), justified inline by:\n1. Inner op is microsecond-scale (Path::starts_with on canonicalized paths)\n2. Dominated by SQL queries on `imports` and `symbols` tables that fire per-file regardless\n3. Acceptable trade-off until profiling demonstrates the cost matters\n\n## Out-of-scope justification for the rivets-6aoc PR\n\nPremature optimization. The simple linear scan is correct, readable, and tests pass. The mitigation path is documented and reusable when needed.","acceptance_criteria":"- [ ] Decision point: a workspace with >50 crates AND profiling shows the get_crate_for_file linear scan is >5% of indexing wall-time\n- [ ] Pre-computed BTreeMap or HashMap of (path -> CrateInfo) stored on Tethys\n- [ ] All 4 call sites in resolve.rs / indexing.rs / resolver.rs use the pre-computed map\n- [ ] Existing get_crate_for_file behavior tests still pass (longest-prefix match, prefix-of-name disambiguation)\n- [ ] Hyperfine measurement on the triggering workspace shows the optimization actually helps","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-05-12T23:18:23.735449800Z","updated_at":"2026-05-12T23:18:23.735449800Z","closed_at":null} +{"id":"rivets-h1va","title":"Implement LspClient with JSON-RPC transport","description":"Create `src/lsp/mod.rs` with:\n\n- `LspClient` struct (process handle, stdin/stdout, request_id)\n- `send_request()` - generic JSON-RPC request\n- `read_response()` - parse Content-Length header + JSON body\n- `start()` - spawn process, do initialize handshake\n- `shutdown()` - graceful shutdown\n\nUse lsp-types for all protocol types, just implement the transport (~100 lines).","status":"closed","priority":2,"issue_type":"task","assignee":"claude","labels":["lsp","phase2","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-8j9u","dep_type":"blocks"},{"depends_on_id":"rivets-zk2q","dep_type":"parent-child"}],"created_at":"2026-01-28T17:28:37.453815533Z","updated_at":"2026-01-29T00:00:09.362735526Z","closed_at":"2026-01-29T00:00:09.362735526Z"} +{"id":"rivets-x51","title":"Implement JSONL import/export system","description":"Implement the JSONL import/export system for syncing between SQLite and JSONL files, with content hash deduplication, dirty tracking, and conflict resolution.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"Based on beads importer (internal/importer/):\n\n**Export Flow**:\n1. Query dirty_issues table\n2. Serialize issues with embedded deps, labels, comments\n3. Compute content hash for deduplication\n4. Write to issues.jsonl (newline-delimited)\n5. Clear dirty flags\n\n**Import Flow**:\n1. Read JSONL line-by-line\n2. Parse and validate each issue\n3. Content hash comparison for conflict detection\n4. Upsert to SQLite\n5. Handle orphan dependencies (resurrect/skip/fail)\n\n**Features**:\n- Incremental dirty tracking\n- Content hash prevents duplicate writes\n- Prefix validation and migration\n- Orphan handling policies\n- External ref duplicate detection\n\n**Rust Implementation**:\n- Use serde_json for parsing\n- Streaming reader for large files\n- Batch inserts for performance","acceptance_criteria":"- Export writes issues to JSONL format\n- Import reads JSONL into SQLite\n- Content hash deduplication working\n- Dirty tracking identifies changed issues\n- Orphan handling implemented\n- Conflict resolution via content hash\n- Performance: 1000 issues in <1 second\n- Integration tests for round-trip consistency","notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-17T22:16:06.672016354Z","updated_at":"2025-11-17T23:02:47.394020398Z","closed_at":"2025-11-17T23:02:47.394020398Z"} +{"id":"rivets-ycaq","title":"Tethys: Resolver correctness and coverage","description":"Umbrella for the resolver-correctness work that determines how trustworthy tethys's cross-file/cross-crate edge data is for downstream consumers (coupling metrics, callers, cycles, impact analysis).\n\nEmpirical baseline (rivets workspace, no LSP, 2026-05-13):\n - 21,777 total refs extracted\n - 6,675 resolved (30.7%) — 2,937 same-file (Pass 1), 3,738 cross-file (Pass 2)\n - 15,102 unresolved (69.3%) — ~12,962 are stdlib/external (Pass-3 LSP territory), 2,140 match a name that exists somewhere in workspace symbols\n\nTwo structural facts shape the goal:\n 1. Most unresolved refs are intrinsically out of reach for tethys's workspace-only model — names like expect, unwrap, Vec, Option. Only LSP (Pass 3) can resolve these.\n 2. The 3,738 'resolved cross-file' number includes known phantom edges. rivets-0gom's probe found 88% of cross-crate file_deps edges were phantom; rivets-3d0s identified residual phantom edges from stdlib symbol-name collisions.\n\n'100% resolution' has three readings:\n - Every ref resolved by tethys alone: NOT achievable (stdlib/external out of scope by design)\n - Every workspace-legitimate ref resolved correctly: ACHIEVABLE via this epic\n - Every ref resolved including external: achievable with --lsp once correctness chain is fixed (PR #64 closed rivets-714v unblocking Pass 3 on Windows multi-crate)\n\nSequenced children:\n - rivets-lcb6 (file_deps never cleared between runs) — measurement prerequisite\n - rivets-0gom (crate:: path resolution is workspace-wide filename lookup) — highest-impact correctness\n - rivets-3d0s (stdlib/external symbol-name collisions) — residual phantom cleanup\n - [Pass-2 short-circuit, this epic] — coverage gap from imports.is_empty() early return\n - rivets-i8qn + rivets-6jxv — design-tax cleanup, lock in 6aoc/34tv fix at the type level\n\nProjection after completion: workspace-internal resolution correctness approaches 100%; overall rate (with --lsp on) climbs from 30.7% to 75–90% as Pass 3 fills stdlib/external; residual gap is rust-analyzer's own limits (macro-generated identifiers, trait-impl ambiguities).","status":"closed","priority":2,"issue_type":"epic","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] rivets-lcb6 closed (file_deps cleared between index runs)\n- [ ] rivets-0gom closed (crate:: path resolution scoped to caller's crate)\n- [ ] rivets-3d0s closed (no phantom edges from stdlib symbol-name collisions)\n- [ ] Pass-2 short-circuit-on-no-imports issue closed\n- [ ] rivets-i8qn + rivets-6jxv closed (CrateInfo accessors, deduped crate_root_for_file helper)\n- [ ] Re-measured baseline on rivets workspace shows: workspace cross-crate edges match Cargo dep graph; cross-file resolved count is stable across two consecutive index runs; phantom-edge rate < 1%","notes":"Closed: All 6 acceptance criteria met. Closed children: rivets-lcb6 (#65), rivets-0gom (#66), rivets-3d0s (#67), rivets-dn35 (#69), rivets-i8qn (#70), rivets-6jxv (#71). Measurement artifact: .rivets-ycaq/measurement-2026-05-17.md (#68) shows phantom-edge rate 0.00% on rivets workspace (was 88% pre-fix), cross-crate file_deps reduced 74 -> 8 (all 8 corroborated by use imports), idempotent across consecutive --rebuild runs. Refs-level phantoms (202 cross-crate FORBIDDEN-pair refs) intentionally preserved per design-v3 K-hybrid claims C5/C6 — the fix contains phantom propagation at the file_deps consumer boundary, not in the resolver itself. Adjacent resolver work (rivets-044i qualified-path gap discovered mid-epic, plus 22 other open resolver issues for perf/coverage/features) lives outside ycaq's scope.","external_ref":null,"dependencies":[],"created_at":"2026-05-13T02:48:14.592810300Z","updated_at":"2026-05-18T05:17:25.966500900Z","closed_at":"2026-05-18T05:17:25.966493100Z"} +{"id":"rivets-6qm","title":"Implement fuzzy search for issue selection","description":"Add fuzzy search for issue ID selection:\n\n```\n$ rivets show\n> auth\n rivets-abc Fix authentication bug\n rivets-def Add OAuth support\n rivets-ghi Auth token refresh\n```\n\nUse skim library for the fuzzy matching UI.\nAlso support label and assignee autocomplete.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["phase-2","ux"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-6bc","dep_type":"parent-child"}],"created_at":"2025-11-30T18:36:12.633939684Z","updated_at":"2025-11-30T18:36:12.633939684Z","closed_at":null} +{"id":"rivets-7qr","title":"Fix limit parameter not working in list and ready tools","description":"The `limit` parameter in `list()` and `ready()` tools is not being applied. Tests create 5 issues, request limit=2, but get 5 results back.\n\nFailing tests:\n- `test_list_with_limit` - expected 2, got 5\n- `test_ready_with_limit` - expected 2, got 5\n\nFound during rivets-vdi implementation.","status":"closed","priority":2,"issue_type":"bug","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[],"created_at":"2025-11-29T23:40:57.038151966Z","updated_at":"2025-11-29T23:42:49.399935566Z","closed_at":"2025-11-29T23:42:49.399935566Z"} +{"id":"rivets-7cji","title":"Implement depth-limited transitive analysis","description":"The `--depth` flag in `tethys impact` is accepted but ignored (with a warning). See `cli/impact.rs:20-27`.\n\nShould limit how many levels of indirection to follow in transitive dependency analysis. Current behavior is full graph traversal.","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["cli","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-30T00:18:27.215489041Z","updated_at":"2026-01-30T00:18:27.215489041Z","closed_at":null} +{"id":"rivets-o3s8","title":"Stream load_indexed_map to reduce needs_update memory footprint","description":"needs_update currently loads all DB rows into a HashMap before walking the disk. For very large workspaces this is the dominant memory cost; deleted-detection requires the full DB enumeration so this cannot be fully eliminated, but a streaming cursor or per-file SELECT approach could reduce peak memory at the cost of either a second pass for deletes or O(N) round-trips. Likely needs a benchmark first to confirm any pain point justifies the complexity.","status":"open","priority":4,"issue_type":"feature","assignee":null,"labels":[],"design":null,"acceptance_criteria":"- [ ] Benchmark current memory and latency on a 10k-file workspace - [ ] Decide on streaming-cursor vs per-file SELECT based on benchmark - [ ] Update needs_update implementation, retain consistency tests - [ ] Update needs_update docstring to reflect new memory profile","notes":null,"external_ref":null,"dependencies":[],"created_at":"2026-04-27T23:13:41.272913367Z","updated_at":"2026-04-27T23:13:41.272913367Z","closed_at":null} +{"id":"rivets-zkb","title":"Evaluate cache eviction strategy for workspace storage","description":"The current workspace storage uses FIFO eviction when the cache limit (8 workspaces) is reached. Consider whether LRU eviction would be more appropriate for typical usage patterns.\n\nContext from PR review:\n- Current implementation: FIFO eviction via `workspace_order.remove(0)`\n- Alternative: LRU eviction (move recently-used workspaces to end of queue)\n- Trade-off: FIFO is simpler but may evict frequently-used workspaces\n\nEvaluation should consider:\n1. Typical agent workflow patterns (do they frequently switch between workspaces?)\n2. Memory/performance impact of LRU tracking\n3. Whether the current cache limit (8) is appropriate","status":"open","priority":3,"issue_type":"task","assignee":null,"labels":["performance","rivets-mcp"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-9po8","dep_type":"parent-child"}],"created_at":"2025-11-29T07:29:32.898224105Z","updated_at":"2025-11-29T07:29:32.898224105Z","closed_at":null} +{"id":"rivets-puyl","title":"Add reference kind tracking to call_edges table","description":"The `call_edges` table currently only tracks caller→callee relationships with call counts, but loses information about the **type** of reference (call, import, type usage, etc.).\n\n**Current limitation:**\n- `get_callers` returns `reference_kinds: vec![ReferenceKind::Call]` as a hardcoded default\n- The original join-based query used `GROUP_CONCAT(kind)` to aggregate actual reference kinds\n- The `parse_reference_kinds` helper function was removed as dead code\n\n**Why this matters:**\n- Impact analysis may want to distinguish \"function calls\" from \"type references\"\n- Security audits may care about data flow (calls) vs type dependencies (imports)\n- Callers could filter by reference kind for more targeted queries\n\n**Proposed enhancement:**\n```sql\nALTER TABLE call_edges ADD COLUMN kinds TEXT;\n-- Store as comma-separated or JSON array: \"call,call,import\"\n```\n\nOr normalize with a separate table:\n```sql\nCREATE TABLE call_edge_kinds (\n caller_symbol_id INTEGER NOT NULL,\n callee_symbol_id INTEGER NOT NULL,\n kind TEXT NOT NULL,\n count INTEGER DEFAULT 1,\n PRIMARY KEY (caller_symbol_id, callee_symbol_id, kind)\n);\n```\n\n**Implementation notes:**\n- Update `populate_call_edges` SQL to GROUP BY kind as well\n- Restore `parse_reference_kinds` function or use JSON\n- Update `get_callers`/`get_callees` to return actual kinds\n\n**Trade-off:**\n- More storage and slightly more complex queries\n- But provides richer information for analysis","status":"open","priority":3,"issue_type":"feature","assignee":null,"labels":["call-edges","enhancement","tethys"],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-j9bu","dep_type":"parent-child"}],"created_at":"2026-01-29T19:34:49.093333401Z","updated_at":"2026-01-29T19:34:49.093333401Z","closed_at":null} +{"id":"rivets-dgt","title":"Define Warning types and collection system","description":"Define the Warning enum and WarningCollector for tracking non-fatal errors during JSONL loading. This enables resilient loading that continues despite malformed data.\n\nBased on the research API design and existing LoadWarning pattern from in_memory.rs.","status":"closed","priority":1,"issue_type":"task","assignee":null,"labels":[],"design":"```rust\n#[derive(Debug, Clone)]\npub enum Warning {\n MalformedJson { line_number: usize, error: String },\n SkippedLine { line_number: usize, reason: String },\n}\n\npub struct WarningCollector {\n warnings: Arc>>,\n}\n\nimpl WarningCollector {\n pub fn new() -> Self {\n Self {\n warnings: Arc::new(Mutex::new(Vec::new())),\n }\n }\n \n pub fn add(&self, warning: Warning) {\n self.warnings.lock().unwrap().push(warning);\n }\n \n pub fn into_warnings(self) -> Vec {\n Arc::try_unwrap(self.warnings)\n .unwrap_or_else(|arc| (*arc.lock().unwrap()).clone())\n }\n}\n```","acceptance_criteria":"- Warning enum defined with MalformedJson and SkippedLine variants\n- WarningCollector struct with thread-safe add()\n- into_warnings() method to extract collected warnings\n- Clone and Debug impls for Warning\n- Unit tests for warning collection\n- Compiles without errors","notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-3r7","dep_type":"blocks"}],"created_at":"2025-11-27T23:15:49.553769651Z","updated_at":"2025-11-28T01:42:18.645501372Z","closed_at":"2025-11-28T01:42:18.645501372Z"} +{"id":"rivets-1yi","title":"Refactor ready filter tests to use rstest with struct-based test cases","description":"Replace individual ready filter tests with a single rstest parameterized test using a ReadyFilterCase struct.\n\nCurrent tests to consolidate:\n- test_ready_with_priority_filter\n- test_ready_with_assignee_filter\n- test_ready_with_limit\n- test_ready_excludes_blocked\n\nNew structure:\n```rust\nstruct ReadyFilterCase {\n name: &'static str,\n limit: Option,\n priority: Option,\n issue_type: Option<&'static str>,\n assignee: Option<&'static str>,\n label: Option<&'static str>,\n expected_count: usize,\n}\n```","status":"closed","priority":2,"issue_type":"task","assignee":null,"labels":[],"design":null,"acceptance_criteria":null,"notes":null,"external_ref":null,"dependencies":[{"depends_on_id":"rivets-jfs","dep_type":"blocks"}],"created_at":"2025-11-29T23:57:45.611773476Z","updated_at":"2025-11-30T00:03:33.242895114Z","closed_at":"2025-11-30T00:03:33.242895114Z"} From 66fde6ed7453a481580d35c525009554d4dd80b9 Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Mon, 18 May 2026 22:27:27 -0500 Subject: [PATCH 2/5] test(tethys): refs cascade fence on call removal (rivets-wsix slice 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock in design claim C1: removing a function-body call from a file's source produces exactly the expected row removals in `refs` after re-index, via the `refs.in_symbol_id REFERENCES symbols(id) ON DELETE CASCADE` chain triggered by the per-file `DELETE FROM symbols WHERE file_id` in `files.rs::upsert_file`. The wsix audit (see `.rivets-wsix/what-i-learned.md`) found that this cascade chain is the actual safety mechanism for refs re-index correctness — not the `clear_all_X` pattern lcb6 established. This test pins that mechanism. Stress fixture: starting `entry()` calls `helper::a()`, `helper::b()`, and `helper::c()`. Mutation removes the MIDDLE call. Defeats a hypothetical head-only / tail-only cascade bug: the assertions check specific surviving/removed refs by name, not just total count. TDD-inversion: commenting out files.rs:145 `DELETE FROM symbols WHERE file_id` produces refs_post=5 instead of 2 (3 stale + 2 new). The test's `assert_eq!(refs_post, 2)` fails with descriptive output. Confirms the fence is non-vacuous per the v1.0.3 falsifier-non-vacuity self-review rule. Per-gate verification: - `cargo nextest run -p tethys --test reindex_cascade` passes (~110ms) - `cargo clippy -p tethys --tests --all-features -- -D warnings` clean - `cargo fmt --check` clean - TDD-inversion fails the test as predicted --- crates/tethys/tests/reindex_cascade.rs | 118 +++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 crates/tethys/tests/reindex_cascade.rs diff --git a/crates/tethys/tests/reindex_cascade.rs b/crates/tethys/tests/reindex_cascade.rs new file mode 100644 index 0000000..f64e657 --- /dev/null +++ b/crates/tethys/tests/reindex_cascade.rs @@ -0,0 +1,118 @@ +//! Regression fences for rivets-wsix: cascade-correctness across re-index runs. +//! +//! The wsix audit (`.rivets-wsix/what-i-learned.md`) found that re-index +//! correctness for `refs`, `attributes`, and `symbols` relies on the schema's +//! `ON DELETE CASCADE` chain rooted at `symbols(id)`, not the `clear_all_X` +//! pattern lcb6 established for `file_deps` and `call_edges`. These tests +//! lock that cascade behavior in so a future schema change (e.g., a cascade +//! FK silently relaxed to `SET NULL`) is caught in CI. + +use rusqlite::Connection; + +mod common; +use common::{open_db, workspace_with_files}; + +/// Count refs in `src/lib.rs` whose resolved symbol name matches `s.name` +/// against the provided list of names. Returns total across all names. +fn count_lib_refs_by_target_names(conn: &Connection, names: &[&str]) -> i64 { + let placeholders = names.iter().map(|_| "?").collect::>().join(","); + let sql = format!( + "SELECT COUNT(*) FROM refs r + JOIN files f ON f.id = r.file_id + JOIN symbols s ON s.id = r.symbol_id + WHERE f.path = 'src/lib.rs' AND s.name IN ({placeholders})" + ); + let params_vec: Vec<&dyn rusqlite::ToSql> = + names.iter().map(|n| n as &dyn rusqlite::ToSql).collect(); + conn.query_row(&sql, params_vec.as_slice(), |row| row.get(0)) + .expect("count refs by target names") +} + +/// Pin claim C1: removing a function-body call from a file's source produces +/// exactly the expected row removals in `refs` after re-index, via the +/// `refs.in_symbol_id REFERENCES symbols(id) ON DELETE CASCADE` chain +/// triggered by the per-file `DELETE FROM symbols WHERE file_id = ?1` in +/// `files.rs::upsert_file`. +/// +/// Stress shape (middle-removal): starting `entry()` calls `helper::a()`, +/// `helper::b()`, and `helper::c()`. After mutation we remove the MIDDLE +/// call. This defeats a hypothetical head-only or tail-only cascade bug: +/// the assertion is not just "count decreased" but "the specific b-ref is +/// gone and a/c-refs remain." +#[test] +fn refs_cascade_on_call_removal() { + let (dir, mut tethys) = workspace_with_files(&[ + ( + "Cargo.toml", + r#" +[package] +name = "wsix_refs" +version = "0.0.0" +edition = "2021" +"#, + ), + ( + "src/lib.rs", + r" +mod helper; + +pub fn entry() { + helper::a(); + helper::b(); + helper::c(); +} +", + ), + ( + "src/helper.rs", + r" +pub fn a() {} +pub fn b() {} +pub fn c() {} +", + ), + ]); + + tethys.index().expect("initial index should succeed"); + + let refs_pre = count_lib_refs_by_target_names(&open_db(&tethys), &["a", "b", "c"]); + assert_eq!( + refs_pre, 3, + "fixture should produce 3 cross-file call refs (a, b, c)" + ); + + // Mutate source: remove the MIDDLE call. New content changes the file's + // content_hash, which forces tethys to re-process this file. + std::fs::write( + dir.path().join("src/lib.rs"), + r" +mod helper; + +pub fn entry() { + helper::a(); + helper::c(); +} +", + ) + .expect("rewrite src/lib.rs"); + + tethys.index().expect("re-index should succeed"); + + let conn = open_db(&tethys); + let refs_post = count_lib_refs_by_target_names(&conn, &["a", "b", "c"]); + let b_refs = count_lib_refs_by_target_names(&conn, &["b"]); + let a_refs = count_lib_refs_by_target_names(&conn, &["a"]); + + assert_eq!( + refs_post, 2, + "expected 2 refs (a, c) after removing helper::b() — got {refs_post}" + ); + assert_eq!( + b_refs, 0, + "ref to helper::b() must be cascade-deleted after source removal — got {b_refs}" + ); + assert_eq!( + a_refs, 1, + "ref to helper::a() must survive cascade (it's still in source) — got {a_refs}" + ); +} From eb1762d6788323a49236125eb3a57cd1ec057e92 Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Mon, 18 May 2026 22:29:32 -0500 Subject: [PATCH 3/5] =?UTF-8?q?test(tethys):=20symbols=E2=86=92attributes?= =?UTF-8?q?=20cascade=20fence=20(rivets-wsix=20slice=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock in design claim C2: removing an attributed symbol from source cascade-deletes both the symbol AND its `attributes` rows via `attributes.symbol_id REFERENCES symbols(id) ON DELETE CASCADE`. Stress fixture: two attributed functions `target` and `keep`. Remove `target` from source. Defeats the "cascade too aggressive" bug class — if a regression cascade-cleared all of the file's attributes instead of just the removed symbol's, the `keep_attrs_post == keep_attrs_pre` assertion would catch it. TDD-inversion: relaxing the cascade FK to `ON DELETE NO ACTION` in schema.rs causes the symbol DELETE to fail entirely (FK constraint violation), and the test's `target_sym_post == 0` assertion fires. The fence catches the schema drift at one assertion level or another. Also adds two small helper fns (`count_attrs_for_symbol`, `count_symbols_by_name`) to keep individual test functions under clippy's 100-line limit. Per-gate verification: - `cargo nextest run -p tethys --test reindex_cascade` 2/2 pass - `cargo clippy -p tethys --tests --all-features -- -D warnings` clean - `cargo fmt --check` clean - TDD-inversion (cascade FK relaxed) fails the test as predicted --- crates/tethys/tests/reindex_cascade.rs | 100 +++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/crates/tethys/tests/reindex_cascade.rs b/crates/tethys/tests/reindex_cascade.rs index f64e657..66ede01 100644 --- a/crates/tethys/tests/reindex_cascade.rs +++ b/crates/tethys/tests/reindex_cascade.rs @@ -116,3 +116,103 @@ pub fn entry() { "ref to helper::a() must survive cascade (it's still in source) — got {a_refs}" ); } + +/// Count attribute rows whose owning symbol has the given name. +fn count_attrs_for_symbol(conn: &Connection, symbol_name: &str) -> i64 { + conn.query_row( + "SELECT COUNT(*) FROM attributes a + JOIN symbols s ON s.id = a.symbol_id + WHERE s.name = ?1", + [symbol_name], + |row| row.get(0), + ) + .expect("count attrs by symbol name") +} + +/// Count symbol rows by name. +fn count_symbols_by_name(conn: &Connection, symbol_name: &str) -> i64 { + conn.query_row( + "SELECT COUNT(*) FROM symbols WHERE name = ?1", + [symbol_name], + |row| row.get(0), + ) + .expect("count symbols by name") +} + +/// Pin claim C2: removing an attributed symbol from source cascade-deletes +/// the symbol AND its `attributes` rows via +/// `attributes.symbol_id REFERENCES symbols(id) ON DELETE CASCADE`. +/// +/// Stress shape (two attributed symbols, remove one): defeats a "cascade +/// too aggressive" bug class — if the cascade clobbered all of the file's +/// attributes instead of just the removed symbol's, the `keep` assertions +/// would catch it. +#[test] +fn attributes_cascade_on_symbol_removal() { + let (dir, mut tethys) = workspace_with_files(&[ + ( + "Cargo.toml", + r#" +[package] +name = "wsix_attrs" +version = "0.0.0" +edition = "2021" +"#, + ), + ( + "src/lib.rs", + r" +#[allow(dead_code)] +pub fn target() {} + +#[allow(dead_code)] +pub fn keep() {} +", + ), + ]); + + tethys.index().expect("initial index should succeed"); + + let conn = open_db(&tethys); + let target_attrs_pre = count_attrs_for_symbol(&conn, "target"); + let keep_attrs_pre = count_attrs_for_symbol(&conn, "keep"); + assert!( + target_attrs_pre >= 1, + "fixture should index target's #[allow] attribute — got {target_attrs_pre}" + ); + assert!( + keep_attrs_pre >= 1, + "fixture should index keep's #[allow] attribute — got {keep_attrs_pre}" + ); + drop(conn); + + // Remove the `target` fn from source. + std::fs::write( + dir.path().join("src/lib.rs"), + r" +#[allow(dead_code)] +pub fn keep() {} +", + ) + .expect("rewrite src/lib.rs"); + + tethys.index().expect("re-index should succeed"); + + let conn = open_db(&tethys); + let target_sym_post = count_symbols_by_name(&conn, "target"); + let target_attrs_post = count_attrs_for_symbol(&conn, "target"); + let keep_attrs_post = count_attrs_for_symbol(&conn, "keep"); + + assert_eq!( + target_sym_post, 0, + "target symbol must be gone after source removal — got {target_sym_post}" + ); + assert_eq!( + target_attrs_post, 0, + "target's attributes must cascade-delete with the symbol — got {target_attrs_post}" + ); + assert_eq!( + keep_attrs_post, keep_attrs_pre, + "keep's attributes MUST NOT cascade-delete (cascade was too aggressive) — pre={keep_attrs_pre} post={keep_attrs_post}" + ); +} From 5041b42cbe5b27cf2e75de19bcae64e1ea2c4c53 Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Mon, 18 May 2026 22:32:29 -0500 Subject: [PATCH 4/5] test(tethys): clear_all stability fence on unchanged-source re-index (rivets-wsix slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock in design claim C3: running `Tethys::index()` twice on an unchanged workspace produces identical row counts in `call_edges` and `file_deps`, AND a stable `SUM(file_deps.ref_count)`. Catches regression of the `clear_all_X` discipline established by lcb6 (file_deps) and its sibling call_edges precedent. The `SUM(file_deps.ref_count)` assertion defeats a specific bug class the row-count check alone misses: if `clear_all_file_deps` were removed, the same dep would be detected on each run, but the `ON CONFLICT DO UPDATE SET ref_count = ref_count + 1` clause would silently increment the aggregate. Row count stays equal; ref_count sum doubles. This is exactly the original lcb6 bug. TDD-inversion: commenting out `self.db.clear_all_file_deps()` at indexing.rs:139 produces SUM(ref_count) = 1 → 2 across re-index runs. The SUM assertion fires; the row-count assertions stay quiet (correctly — they would not catch this aggregate-growth class without the SUM check). Per-gate verification: - `cargo nextest run -p tethys --test reindex_cascade` 3/3 pass - `cargo nextest run -p tethys` 639/639 pass (final integration check) - `cargo clippy -p tethys --tests --all-features -- -D warnings` clean - `cargo fmt --check` clean - TDD-inversion (clear_all_file_deps disabled) fails the SUM assertion as predicted This completes the rivets-wsix gilfoyle loop. The audit found zero new bugs (see `.rivets-wsix/what-i-learned.md`); these three regression fences lock in the existing cascade and clear_all safety mechanisms so future schema or indexing changes can't silently regress them. --- crates/tethys/tests/reindex_cascade.rs | 103 +++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/crates/tethys/tests/reindex_cascade.rs b/crates/tethys/tests/reindex_cascade.rs index 66ede01..c2df256 100644 --- a/crates/tethys/tests/reindex_cascade.rs +++ b/crates/tethys/tests/reindex_cascade.rs @@ -216,3 +216,106 @@ pub fn keep() {} "keep's attributes MUST NOT cascade-delete (cascade was too aggressive) — pre={keep_attrs_pre} post={keep_attrs_post}" ); } + +/// Snapshot of the row counts that should be invariant under unchanged-source +/// re-index when the `clear_all_X` discipline is in place. +struct ClearAllSnapshot { + call_edges: i64, + file_deps: i64, + file_deps_ref_count_sum: i64, +} + +fn snapshot_clear_all_tables(conn: &Connection) -> ClearAllSnapshot { + let call_edges = conn + .query_row("SELECT COUNT(*) FROM call_edges", [], |row| row.get(0)) + .expect("count call_edges"); + let file_deps = conn + .query_row("SELECT COUNT(*) FROM file_deps", [], |row| row.get(0)) + .expect("count file_deps"); + let file_deps_ref_count_sum = conn + .query_row( + "SELECT COALESCE(SUM(ref_count), 0) FROM file_deps", + [], + |row| row.get(0), + ) + .expect("sum file_deps.ref_count"); + ClearAllSnapshot { + call_edges, + file_deps, + file_deps_ref_count_sum, + } +} + +/// Pin claim C3: re-indexing an unchanged workspace produces stable counts in +/// `call_edges` and `file_deps`, and a stable `SUM(file_deps.ref_count)`. +/// Catches regression of the `clear_all_X` discipline (rivets-lcb6's fix +/// for `file_deps`, plus the parallel `call_edges` path). +/// +/// The `SUM(ref_count)` check defeats a specific UPSERT-aggregate-growth +/// bug class: if `clear_all_file_deps` were removed, `file_deps`'s row +/// count would not grow (the same dep is detected each run), but the +/// `ref_count` aggregate would increment via the `ON CONFLICT DO UPDATE +/// SET ref_count = ref_count + 1` clause and silently double on each +/// re-index. The row-count assertion alone would miss that. +#[test] +fn clear_all_tables_stable_under_reindex() { + let (_dir, mut tethys) = workspace_with_files(&[ + ( + "Cargo.toml", + r#" +[package] +name = "wsix_clear_all" +version = "0.0.0" +edition = "2021" +"#, + ), + ( + "src/lib.rs", + r" +mod helper; + +pub fn entry() { + helper::do_thing(); +} +", + ), + ( + "src/helper.rs", + r" +pub fn do_thing() {} +", + ), + ]); + + tethys.index().expect("first index should succeed"); + + let snap1 = snapshot_clear_all_tables(&open_db(&tethys)); + assert!( + snap1.call_edges >= 1, + "fixture should produce at least one call_edge — got {}", + snap1.call_edges + ); + assert!( + snap1.file_deps >= 1, + "fixture should produce at least one file_dep — got {}", + snap1.file_deps + ); + + // Re-index with no source change. + tethys.index().expect("second index should succeed"); + + let snap2 = snapshot_clear_all_tables(&open_db(&tethys)); + + assert_eq!( + snap1.call_edges, snap2.call_edges, + "call_edges count must not grow across unchanged-source re-index" + ); + assert_eq!( + snap1.file_deps, snap2.file_deps, + "file_deps count must not grow across unchanged-source re-index" + ); + assert_eq!( + snap1.file_deps_ref_count_sum, snap2.file_deps_ref_count_sum, + "SUM(file_deps.ref_count) must not grow (UPSERT-aggregate fence: lcb6)" + ); +} From ce386483d24bacb4a00318142ad2667d502d9f74 Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Mon, 18 May 2026 22:48:57 -0500 Subject: [PATCH 5/5] test(tethys): address PR #75 review-feedback round 1 (rivets-wsix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 1 of review feedback. Per the `.rivets-wsix/review-decisions-round-1.md` decision log: - **Accept (claim 1):** added explicit `c_refs == 1` assertion in `refs_cascade_on_call_removal`. Previously the third surviving ref was implicit (derivable from `refs_post − a_refs − b_refs = 2 − 1 − 0 = 1`). Spelling it out defends against future mutations to `count_lib_refs_by_target_names`'s IN-clause that would silently break that arithmetic. Symmetric with the existing `a_refs`/`b_refs` pattern. - **Reject (claim 2):** `count_lib_refs_by_target_names` joins on `r.symbol_id` (callee) rather than `r.in_symbol_id` (cascade trigger). The reviewer themselves noted this is a future-maintainer note, not actionable. The indirection is intentional — the regression fence verifies the *effect* of the cascade (refs disappear), not the trigger mechanism. Changing to `r.in_symbol_id` would weaken the fence by testing how the cascade fires instead of whether it produces the correct end state. - **Reject (claim 3):** plan.md / impl divergence on `filetime` vs. content-hash detection. Per CLAUDE.md "Issue diagnostic directories" convention, plan files are point-in-time, not maintained. `checkpointed-build` explicitly permits implementer deviation when the budget and oracle still hold; this is exactly that case. --- .rivets-wsix/review-decisions-round-1.md | 31 ++++++++++++++++++++++++ crates/tethys/tests/reindex_cascade.rs | 9 +++++++ 2 files changed, 40 insertions(+) create mode 100644 .rivets-wsix/review-decisions-round-1.md diff --git a/.rivets-wsix/review-decisions-round-1.md b/.rivets-wsix/review-decisions-round-1.md new file mode 100644 index 0000000..5fb0887 --- /dev/null +++ b/.rivets-wsix/review-decisions-round-1.md @@ -0,0 +1,31 @@ +# PR #75 review-feedback decisions — round 1 + +## Reviewers +- **claude-review** (GitHub Action bot, run 26074529893) +- **gemini-code-assist** (GitHub bot) + +## Per-finding table + +| # | Finding (one line) | Reviewer | Category | Verified? | Decision | Note | +|---|---|---|---|---|---|---| +| 1 | `refs_cascade_on_call_removal` doesn't explicitly assert `c_refs == 1` (only `a_refs == 1` and `b_refs == 0`) — surviving-ref coverage is asymmetric | claude-review | Polish | Yes (read test at `reindex_cascade.rs:101-117`; `c_refs` derivable from `refs_post − a_refs − b_refs = 2 − 1 − 0 = 1` but not asserted) | **Accept** | 4-line addition; matches the existing `a_refs`/`b_refs` pattern. Strengthens against a future-mutation bug class where someone adds a name to the IN-clause and breaks the arithmetic. | +| 2 | `count_lib_refs_by_target_names` joins on `r.symbol_id` (callee), but the cascade fires on `r.in_symbol_id` (container) — indirect oracle | claude-review | Design (informational) | Yes (read SQL at `reindex_cascade.rs:17-29`; reviewer themselves notes this is fine in practice because the fixture resolves cleanly) | **Reject** | The reviewer explicitly classified this as a future-maintainer note, not actionable. The indirection is *the point* — the test verifies the *effect* of the cascade (refs disappear), not the trigger mechanism, which is the right level for a regression fence. Changing to `r.in_symbol_id` would actually weaken the fence by testing the trigger instead of the consequence. | +| 3 | `plan.md` mentions `filetime` / `std::thread::sleep` but the impl uses content-hash change detection (no `filetime` dep) | claude-review | Polish (doc drift) | Yes (`.rivets-wsix/plan.md` does reference filetime; implementation in `reindex_cascade.rs` writes new content, no filetime usage) | **Reject (intentional)** | Per CLAUDE.md "Issue diagnostic directories" convention: "Point-in-time, not maintained. Probe scripts query whatever schema/state was current when the fix was developed; they will go stale and that's expected." The plan documents the design *as drafted*; the implementation legitimately took a simpler path during build, which is exactly what `checkpointed-build` permits ("the implementer is permitted to deviate if the deviation keeps the slice within budget and the oracle passing"). Leaving the plan as-is preserves the historical record of "we considered filetime, picked content-hash instead." | +| 4 | (no findings) | gemini-code-assist | n/a | n/a | n/a | "I have no feedback to provide as there were no review comments." | + +## Statistics + +- Findings: 3 actionable + 1 explicit no-comment = 4 total +- Accept: 1 +- Modify: 0 +- Reject: 2 (with rationale, no deferral — both are conscious design choices, not "do later") + +Per the skill's red-flag check on "six findings, six accepts": healthy reject rate +(2/3 of actionable findings rejected with explicit rationale). Indicates per-finding +verification actually happened. + +## Outcome + +- Applied: claim 1 (`c_refs == 1` assertion). +- Documentation: this file. +- No new tracker issues filed — no deferred work in this round. diff --git a/crates/tethys/tests/reindex_cascade.rs b/crates/tethys/tests/reindex_cascade.rs index c2df256..5aa017f 100644 --- a/crates/tethys/tests/reindex_cascade.rs +++ b/crates/tethys/tests/reindex_cascade.rs @@ -102,6 +102,7 @@ pub fn entry() { let refs_post = count_lib_refs_by_target_names(&conn, &["a", "b", "c"]); let b_refs = count_lib_refs_by_target_names(&conn, &["b"]); let a_refs = count_lib_refs_by_target_names(&conn, &["a"]); + let c_refs = count_lib_refs_by_target_names(&conn, &["c"]); assert_eq!( refs_post, 2, @@ -115,6 +116,14 @@ pub fn entry() { a_refs, 1, "ref to helper::a() must survive cascade (it's still in source) — got {a_refs}" ); + // Explicit symmetric coverage of the third surviving ref. `c_refs` is + // derivable from the prior three assertions today, but spelling it out + // defends against future mutations to `count_lib_refs_by_target_names`'s + // IN-clause that would silently break that arithmetic. + assert_eq!( + c_refs, 1, + "ref to helper::c() must survive cascade (it's still in source) — got {c_refs}" + ); } /// Count attribute rows whose owning symbol has the given name.