Repository navigation
Migrate diagnostics to nodespace CLI subcommand (closes #1139) - #1150
Conversation
malibio
left a comment
There was a problem hiding this comment.
Code Review: PR #1150 — Migrate diagnostics to nodespace CLI subcommand
Review Type: Initial review
Branch: worktree-issue-1139-cli-diagnostics
Verdict: Approve with minor improvements suggested
Requirements Check (Issue #1139)
| Acceptance Criterion | Status | Notes |
|---|---|---|
commands/diagnostics.rs deleted from Tauri app, all refs removed |
✅ | Confirmed via grep — no orphan references. |
nodespace diagnostics CLI subcommand in packages/cli |
✅ | packages/cli/src/commands/diagnostics.rs |
| Reports: db path, existence, size, total/root node count, recent IDs | ✅ | Plus schema count + --json bonus |
| Output human-readable (table/labeled lines) | ✅ | Labeled lines, mirrors issue spec |
cargo build succeeds |
✅ | cargo build -p nodespace-cli clean locally |
bun run quality:fix passes |
✅ | Frontend test for diagnostic-logger.ts passes (16/16) |
cargo clippy --all-targets -- -D warnings -D clippy::unused-async |
✅ | Verified clean |
Findings
🟡 Important
1. recent_node_ids label is misleading — packages/cli/src/commands/diagnostics.rs:94-102
The field is documented as "recent" in the proto, output, and JSON key, but the implementation takes the first N from an unordered QueryNodesSimple result with no ORDER BY semantics. The inline comment honestly acknowledges this, but the user-visible label does not. A developer running nodespace diagnostics reasonably expects "most recent" to mean "most recently created" — getting the first N from an unspecified iteration order is a different (and arguably useless) signal for debugging persistence. Rationale: an honest diagnostic tool should not lie about what it reports.
Suggested fix: Rename to sample_node_ids (output, JSON key, struct field) until QueryNodesSimple exposes an ordered variant; or filter all_nodes by created_at descending in-memory before taking N. The second fix is one line and reuses data already in hand:
let mut sorted = all_nodes.clone();
sorted.sort_by(|a, b| b.created_at.cmp(&a.created_at));
let recent_node_ids: Vec<String> = sorted.iter().take(RECENT_LIMIT).map(|n| n.id.clone()).collect();This is O(n log n) on a bounded n ≤ 100k — diagnostics is not a hot path, so the cost is fine.
2. directory_size swallows IO errors silently — packages/cli/src/commands/diagnostics.rs:124-143
fs::read_dir errors, entry.metadata() failures, and unknown file types all return 0 with no signal. The errors: Vec<String> field exists on the report exactly to surface this kind of partial failure, but directory_size has no way to push into it. For a diagnostics tool whose users are debugging "why are my nodes not persisting" issues, silently reporting Database size: 0.00 B when the real cause is a permissions error is exactly the wrong behavior. Rationale: the principle of error visibility — a debug tool should report its own failure modes, not hide them.
Suggested fix: Have directory_size return (u64, Vec<String>) or take &mut Vec<String> to accumulate errors; report them through the existing errors field.
3. Symlink handling is correct but undocumented — packages/cli/src/commands/diagnostics.rs:124-143
Verified: DirEntry::file_type() does NOT follow symlinks on Unix (uses lstat), so the implementation cannot loop infinitely on a symlinked directory. However, this also means symlinked files are silently excluded from the size total. For RocksDB this is almost certainly fine (no symlinks expected), but a one-line // comment explaining the choice ("symlinks are intentionally skipped to avoid follow loops; RocksDB stores no symlinks") would prevent a future maintainer from "fixing" this back to fs::metadata() (which DOES follow symlinks and would reintroduce the loop risk).
🟢 Suggestions
4. QUERY_LIMIT = 100_000 silently truncates with no signal — packages/cli/src/commands/diagnostics.rs:19, 88
If a database exceeds 100k nodes, total_node_count becomes meaningless without the user knowing. Diagnostics is precisely the tool that should be honest about this. Suggested fix: if all_nodes.len() == QUERY_LIMIT as usize, push a warning into errors: format!("Result truncated at QUERY_LIMIT={QUERY_LIMIT}; counts may be undercounts"). Cheap, defensive, no behavior change for normal cases.
5. Serial RPC calls — packages/cli/src/commands/diagnostics.rs:69, 104
query_nodes_simple and get_all_schemas are independent and could be tokio::try_join!'d. This is a developer tool over localhost gRPC, so the latency savings are negligible (low ms) and the current code is more readable. Not worth changing unless there's another reason to touch the file. Nit.
6. bind_addr in main.rs left behind — packages/daemon/src/main.rs:28-33
Now that resolve_db_path() is hoisted to lib.rs, the symmetric bind_addr() helper remains in main.rs. Not a bug — bind_addr is genuinely binary-only — but worth a future thought if a settings RPC ever needs to expose the bind address too.
CLAUDE.md Compliance
- ✅ No
#[allow(dead_code)],#[deprecated], or lint suppressions introduced. - ✅ Comments explain "why" not "what" throughout the new file (e.g., the rationale for
QUERY_LIMIT, theparent_idfilter explanation, the symlink-skipping behavior is implicit). - ✅ Old code deleted cleanly — no orphaned types, helpers, or tests. The
getDatabaseDiagnostics/testNodePersistencehelpers and their tests indiagnostic-logger.tswere correctly removed alongside the Tauri commands they wrapped. The two now-broken panel tabs ("Database", "Persistence Test") were correctly removed; the "Call Logs" tab stays becausebackend-adapter.tsstill useswithDiagnosticLoggingto wrap 18 Tauri calls. This is the right call under the "delete deprecated code immediately" policy — leaving the tabs would have left broken UI calling deleted commands. - ✅ The
CreateNodeRequestdrive-by fix (11 sites) is purely mechanical — adds two empty-string fields to literal struct constructions that proto required after #1147. Verified: no behavioral implications, the proto fields are optional inputs. Bundling this into the diagnostics PR is defensible because without itcargo build -p nodespace-clifails on a fresh main clone, blocking testability of this PR. Acceptable scope inclusion. - ✅ The two
cargo fmt-only changes (packages/agent/src/pty/manager.rs,packages/daemon/src/services/node_service.rs) are noise but harmless. Including them is consistent with runningrust:quality:fixand not selectively reverting unrelated formatting.
Architecture & Design
- ✅ Hoisting
resolve_db_path()intonodespace-daemon's library surface (rather than going through a not-yet-merged settings RPC) is the correct call. The CLI is documented as a "stateless gRPC client" but in practice, for an inherently colocated developer tool that introspects on-disk state, reading the same env var the daemon reads is simpler and has no failure modes the RPC path wouldn't share. The function's docstring explicitly calls out this dual-purpose use. Whennodespacedeventually moves off-host (if ever), the CLI's diagnostics will need to switch to a settings RPC, but per CLAUDE.md ("no migration strategies, implement final architecture") that's a problem for the day it actually happens, not now. - ✅ The
serde_json::json!()macro usage for output is consistent withpackages/cli/src/output.rs's existing style. No inconsistency concern.
Testing Strategy
- 🟡 Coverage gap: only
format_sizeanddirectory_size_handles_missing_pathhave unit tests. The risky surface —collect()against a live gRPC channel, root-vs-non-root counting, error-path accumulation — has zero tests despite the establishedspawn_test_daemon()pattern inpackages/cli/tests/cli_integration.rsmaking this cheap. Suggested test: spawn the in-process daemon, create one root and two child nodes, runcollect(), asserttotal_node_count == 3,root_node_count == 1,recent_node_ids.len() == 3. That's ~15 lines and would have caught (for example) a future regression in theparent_idfilter or a swap ofRECENT_LIMITvsQUERY_LIMIT. Not blocking, but the gap is disproportionate to the risk.
Recommendation
Approve with minor improvements. The PR is a clear net improvement: it correctly relocates a feature that lived in the wrong process (the Tauri side could not access ADR-031 daemon state), cleans up all orphaned code, and includes a defensible drive-by that unblocks fresh-clone testability. The findings above are quality refinements, not blockers — the "recent" label honesty and silent-IO-error issues are the highest-priority follow-ups and could be addressed in a small follow-up PR or in-PR before merge per the author's preference.
🤖 Generated with Claude Code
…tegration test Resolves the 🟡 Important and 🟢 Suggestion findings from PR #1150's review. - Sort all_nodes by created_at DESC before slicing for recent_node_ids, so the user-visible "recent" label means most-recently-created rather than first-in-iteration-order (review finding #1). - Have directory_size accumulate IO errors into the report's `errors` field instead of silently returning 0 on read_dir / file_type / metadata failures. A permissions error reporting "Database size: 0.00 B" was precisely the failure mode an operator running `nodespace diagnostics` needs to see (review finding #2). - Document the symlink-skipping behavior so a future "fix" doesn't reintroduce the loop risk that `fs::metadata` would (review finding #3). - Surface a warning when QueryNodesSimple returns exactly QUERY_LIMIT rows — the only signal we have that counts may be undercounts (review finding #4). - Drop root_node_count from the report. The daemon's query_nodes_simple handler returns NodeData with parent_id=None for every row because parentage is a graph edge, not a column — the field is unrecoverable from the current RPC surface and reporting it would be flat-out wrong (uncovered by the new integration test). A `GetRoots` RPC or extending NodeListResponse with parent ids is needed before this can come back. - Split collect() out of run() with an explicit db_path parameter so the integration test can point at a tempdir-backed daemon without the env-var dance resolve_db_path() requires (review testing gap). - Add `diagnostics_collect_reports_counts_and_recency` integration test that spawns the in-process daemon, captures a baseline, seeds three nodes with timestamp gaps, and asserts +3 total + newest-first ordering + no error accumulation. This is the test that caught the root-count bug above. - Add unit test exercising the happy path of directory_size on a nested tempdir tree, plus updated the missing-path test to assert the error is now surfaced through &mut Vec<String>. Skipped suggestions: - #5 (serial RPC calls): reviewer explicitly called this a nit on localhost gRPC and noted current code is more readable. Latency savings (low ms) don't justify the readability cost. - #6 (bind_addr hoist): reviewer noted "not a bug" — bind_addr is binary-only and there's no caller in the CLI that needs it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review feedback addressedPushed
Bug uncovered by the new integration testThe new test caught that There is no honest way to compute root count from the current RPC surface. I dropped This means the original issue's "Reports… root node count" acceptance criterion now requires a follow-up issue rather than being met by this PR. Flagging explicitly — happy to file the follow-up if reviewers agree this scope split is correct. The alternative (adding a Test status
Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: I made a non-trivial behavior change (removing Run |
Code Review: PR #1150 — Re-Review (round 2)Review Type: Re-Review (second pass) Prior-Review Status Table
New Findings (Round 2)🟡 ImportantR1. Dropping Issue #1139 lists "Reports: database path, existence, size, total node count, root node count, recent node IDs" as an acceptance criterion. This PR no longer satisfies that bullet. I spent the most time on this question and I think the call is right, but it should not pass review without an explicit follow-up. The author's analysis is verifiable and correct:
Three options were available:
CLAUDE.md's "implement final architecture directly, skip intermediate steps" argues for (2), but it also says "Make breaking changes without hesitation" and "delete deprecated code immediately". Reporting a known-wrong number violates the principle of an honest diagnostic tool more severely than missing one row violates the acceptance criterion. For a developer-debugging surface, omission with an inline comment explaining why is strictly better than a lie. The inline comment at lines 102–108 explicitly names the future fix path, so a Action requested: File a follow-up issue ("Add 🟢 SuggestionsR2. Lexicographic sort on I dug into this because RFC3339 string-sort is a notorious foot-gun. Verified empirically: chrono's
If anyone ever migrates this to a different time type or adds a second serialization path with // String sort is sound only because all created_at values come from
// DateTime<Utc>::to_rfc3339() — same timezone (+00:00) and ASCII
// '+' < digits/'.' makes whole-second values sort before sub-second.Nit, not blocking. The integration test would catch a regression but only if timestamps happen to straddle a whole-second boundary. R3. New The author flagged this and asked whether a R4. Integration test's 20ms sleep margin is adequate. — The concern: on a slow CI runner, could Architecture & Consistency
Build & Test Verification
RecommendationApprove. All three 🟡 Important and the truncation 🟢 from round 1 are correctly addressed. The dropped Before merge, please: file the 🤖 Generated with Claude Code |
R2 from PR #1150's re-review: clarify why string-comparison of `created_at` is equivalent to chronological order. Single serialization path in node_to_proto + consistent `+00:00` suffix make it safe today; a second path (e.g. `Z` suffix) would break it silently. Comment names the fix path so a future maintainer can spot the trap. R1 (file GetRoots follow-up) tracked as #1153. R3 / R4 from re-review: no change needed (reviewer's own answer — pub surface is consistent with existing CLI test pattern; 20ms sleep is fine on microsecond-precision system clocks). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-review feedback addressedPushed
Verified Re-Review DecisionDecision: NO RE-REVIEW NEEDED Rationale: Only a 7-line doc comment was added in this round; no logic changes, no API surface changes, no test changes. The follow-up issue (#1153) is administrative tracking and does not affect this PR's code. Ready for merge (subject to #1153 being filed, which it is). |
Implements `nodespace diagnostics` and removes the corresponding Tauri command surface. Under ADR-031, diagnostic data lives in nodespaced, not the Tauri process — so a CLI subcommand is the correct surface for the intended audience (developers debugging persistence issues). CLI subcommand reports: database path, existence, size, total node count, root node count, schema count, recent node IDs. Honors --json for machine-readable output. Reuses the daemon's gRPC client (`QueryNodesSimple`, `GetAllSchemas`); the database path is resolved by a new `resolve_db_path()` helper hoisted from `main.rs` into the daemon's library surface so the CLI and daemon agree on the location without a settings RPC round-trip. Tauri side: deletes `commands/diagnostics.rs` and the two `#[tauri::command]` handlers (`get_database_diagnostics`, `test_node_persistence`). The developer-only diagnostic panel (Ctrl+Shift+D) loses the now-broken "Database" and "Persistence Test" tabs but keeps its Call Logs view, which is independent of the deleted backend commands. The corresponding `getDatabaseDiagnostics` / `testNodePersistence` helpers and their tests in `diagnostic-logger.ts` are removed; `withDiagnosticLogging` and the log buffer remain (used by `backend-adapter.ts` to wrap 18 Tauri calls). Drive-by: fix `CreateNodeRequest` literal sites in the CLI's `node create` command and the cli/daemon integration tests that #1147 left missing the new `id` and `insert_after_node_id` proto fields. Without this, `cargo build -p nodespace-cli` and `cargo test` on both test crates failed on a fresh clone of main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tegration test Resolves the 🟡 Important and 🟢 Suggestion findings from PR #1150's review. - Sort all_nodes by created_at DESC before slicing for recent_node_ids, so the user-visible "recent" label means most-recently-created rather than first-in-iteration-order (review finding #1). - Have directory_size accumulate IO errors into the report's `errors` field instead of silently returning 0 on read_dir / file_type / metadata failures. A permissions error reporting "Database size: 0.00 B" was precisely the failure mode an operator running `nodespace diagnostics` needs to see (review finding #2). - Document the symlink-skipping behavior so a future "fix" doesn't reintroduce the loop risk that `fs::metadata` would (review finding #3). - Surface a warning when QueryNodesSimple returns exactly QUERY_LIMIT rows — the only signal we have that counts may be undercounts (review finding #4). - Drop root_node_count from the report. The daemon's query_nodes_simple handler returns NodeData with parent_id=None for every row because parentage is a graph edge, not a column — the field is unrecoverable from the current RPC surface and reporting it would be flat-out wrong (uncovered by the new integration test). A `GetRoots` RPC or extending NodeListResponse with parent ids is needed before this can come back. - Split collect() out of run() with an explicit db_path parameter so the integration test can point at a tempdir-backed daemon without the env-var dance resolve_db_path() requires (review testing gap). - Add `diagnostics_collect_reports_counts_and_recency` integration test that spawns the in-process daemon, captures a baseline, seeds three nodes with timestamp gaps, and asserts +3 total + newest-first ordering + no error accumulation. This is the test that caught the root-count bug above. - Add unit test exercising the happy path of directory_size on a nested tempdir tree, plus updated the missing-path test to assert the error is now surfaced through &mut Vec<String>. Skipped suggestions: - #5 (serial RPC calls): reviewer explicitly called this a nit on localhost gRPC and noted current code is more readable. Latency savings (low ms) don't justify the readability cost. - #6 (bind_addr hoist): reviewer noted "not a bug" — bind_addr is binary-only and there's no caller in the CLI that needs it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
R2 from PR #1150's re-review: clarify why string-comparison of `created_at` is equivalent to chronological order. Single serialization path in node_to_proto + consistent `+00:00` suffix make it safe today; a second path (e.g. `Z` suffix) would break it silently. Comment names the fix path so a future maintainer can spot the trap. R1 (file GetRoots follow-up) tracked as #1153. R3 / R4 from re-review: no change needed (reviewer's own answer — pub surface is consistent with existing CLI test pattern; 20ms sleep is fine on microsecond-precision system clocks). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104fa89 to
f2fe94c
Compare
* Migrate diagnostics to nodespace CLI subcommand (closes #1139) Implements `nodespace diagnostics` and removes the corresponding Tauri command surface. Under ADR-031, diagnostic data lives in nodespaced, not the Tauri process — so a CLI subcommand is the correct surface for the intended audience (developers debugging persistence issues). CLI subcommand reports: database path, existence, size, total node count, root node count, schema count, recent node IDs. Honors --json for machine-readable output. Reuses the daemon's gRPC client (`QueryNodesSimple`, `GetAllSchemas`); the database path is resolved by a new `resolve_db_path()` helper hoisted from `main.rs` into the daemon's library surface so the CLI and daemon agree on the location without a settings RPC round-trip. Tauri side: deletes `commands/diagnostics.rs` and the two `#[tauri::command]` handlers (`get_database_diagnostics`, `test_node_persistence`). The developer-only diagnostic panel (Ctrl+Shift+D) loses the now-broken "Database" and "Persistence Test" tabs but keeps its Call Logs view, which is independent of the deleted backend commands. The corresponding `getDatabaseDiagnostics` / `testNodePersistence` helpers and their tests in `diagnostic-logger.ts` are removed; `withDiagnosticLogging` and the log buffer remain (used by `backend-adapter.ts` to wrap 18 Tauri calls). Drive-by: fix `CreateNodeRequest` literal sites in the CLI's `node create` command and the cli/daemon integration tests that #1147 left missing the new `id` and `insert_after_node_id` proto fields. Without this, `cargo build -p nodespace-cli` and `cargo test` on both test crates failed on a fresh clone of main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address review: surface IO/truncation errors, sort recent IDs, add integration test Resolves the 🟡 Important and 🟢 Suggestion findings from PR #1150's review. - Sort all_nodes by created_at DESC before slicing for recent_node_ids, so the user-visible "recent" label means most-recently-created rather than first-in-iteration-order (review finding #1). - Have directory_size accumulate IO errors into the report's `errors` field instead of silently returning 0 on read_dir / file_type / metadata failures. A permissions error reporting "Database size: 0.00 B" was precisely the failure mode an operator running `nodespace diagnostics` needs to see (review finding #2). - Document the symlink-skipping behavior so a future "fix" doesn't reintroduce the loop risk that `fs::metadata` would (review finding #3). - Surface a warning when QueryNodesSimple returns exactly QUERY_LIMIT rows — the only signal we have that counts may be undercounts (review finding #4). - Drop root_node_count from the report. The daemon's query_nodes_simple handler returns NodeData with parent_id=None for every row because parentage is a graph edge, not a column — the field is unrecoverable from the current RPC surface and reporting it would be flat-out wrong (uncovered by the new integration test). A `GetRoots` RPC or extending NodeListResponse with parent ids is needed before this can come back. - Split collect() out of run() with an explicit db_path parameter so the integration test can point at a tempdir-backed daemon without the env-var dance resolve_db_path() requires (review testing gap). - Add `diagnostics_collect_reports_counts_and_recency` integration test that spawns the in-process daemon, captures a baseline, seeds three nodes with timestamp gaps, and asserts +3 total + newest-first ordering + no error accumulation. This is the test that caught the root-count bug above. - Add unit test exercising the happy path of directory_size on a nested tempdir tree, plus updated the missing-path test to assert the error is now surfaced through &mut Vec<String>. Skipped suggestions: - #5 (serial RPC calls): reviewer explicitly called this a nit on localhost gRPC and noted current code is more readable. Latency savings (low ms) don't justify the readability cost. - #6 (bind_addr hoist): reviewer noted "not a bug" — bind_addr is binary-only and there's no caller in the CLI that needs it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address re-review: document RFC3339 lexicographic sort invariant R2 from PR #1150's re-review: clarify why string-comparison of `created_at` is equivalent to chronological order. Single serialization path in node_to_proto + consistent `+00:00` suffix make it safe today; a second path (e.g. `Z` suffix) would break it silently. Comment names the fix path so a future maintainer can spot the trap. R1 (file GetRoots follow-up) tracked as #1153. R3 / R4 from re-review: no change needed (reviewer's own answer — pub surface is consistent with existing CLI test pattern; 20ms sleep is fine on microsecond-precision system clocks). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Michael Libio <malibio@Michaels-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
nodespace diagnosticsCLI subcommand reporting database path, existence, size, total node count, root node count, schema count, and recent node IDs (with--jsonmode).commands/diagnostics.rsfrom the Tauri app along with its two#[tauri::command]handlers and the now-broken "Database" / "Persistence Test" tabs in the developer panel. Call Logs tab stays.resolve_db_path()intonodespace-daemon's library surface so the CLI and daemon agree on the database location without a settings RPC.Why a CLI, not a Tauri command
Under ADR-031, diagnostic data lives in
nodespaced. The intended audience (developers debugging persistence) uses the shell, not the desktop UI — so a CLI subcommand is the correct surface, and we avoid adding a dedicated gRPC service just for debugging data.Drive-by fix
PR #1147 added
idandinsert_after_node_idfields toCreateNodeRequestin the proto but missed five call sites that still use the literal-struct form:packages/cli/src/commands/node.rs,packages/cli/tests/cli_integration.rs, and nine sites inpackages/daemon/tests/grpc_round_trip.rs. Without populating those fields, `cargo build -p nodespace-cli` and `cargo test` on both test crates fail on a fresh clone of main. Fixed here so this PR is testable; could split out if preferred.Sample output
```
NodeSpace Diagnostics
─────────────────────────────────────
Database path: /Users/me/.nodespace/daemon-db
Database exists: yes
Database size: 142.31 MB
Total nodes: 1847
Root nodes: 23
Schemas: 5
Recent node IDs: node:abc123, node:def456, ...
```
Acceptance criteria
commands/diagnostics.rsdeleted from Tauri app with all references removednodespace diagnosticsCLI subcommand implemented inpackages/cli--jsoncargo buildsucceeds (workspace)cargo clippy --all-targets -- -D warnings -D clippy::unused-asynccleanbun run test: 128 files / 3916 tests pass (baseline was 128 / 3918 — diff = 2 tests for the deleted helpers)bun run quality:fix(eslint + svelte-check) cleanNotes
Test plan
🤖 Generated with Claude Code