feat(cli): gradata projects subcommand (GRA-1238 / GH #206)#218
feat(cli): gradata projects subcommand (GRA-1238 / GH #206)#218Gradata wants to merge 2 commits into
Conversation
Adds 'gradata projects' which lists every project in ~/.gradata/projects.toml
with columns: name, brain_dir, rules count, last_correction timestamp, and
sync_status.
Schema (stdlib tomllib, no new dep):
[[projects]]
name = "my-project"
brain_dir = "/path/to/.gradata/brain"
Column semantics:
* name — project identifier from the registry
* brain_dir — path to the brain directory on disk
* rules — COUNT(*) of RULE_GRADUATED events in system.db
* last_correction — MAX(ts) of CORRECTION events, or '(never)'
* sync_status — 'ok' (brain_dir exists), 'missing' (brain_dir gone),
or 'error' (db unreadable)
Behaviour:
* Missing ~/.gradata/projects.toml → friendly hint, exit 0 (no crash)
* Malformed TOML → 'Error: malformed projects.toml (...)' + exit 1
* --json emits the same data as a JSON array of objects
Tests: tests/test_projects_command.py covers missing registry, empty registry,
single/multi project, missing brain_dir → sync_status='missing', malformed
TOML, --json shape, and a real sqlite db with RULE_GRADUATED/CORRECTION rows.
9 tests, all passing.
Layering: cmd_projects lives in cli.py (Layer 2). No new imports from Layer 0
beyond what cmd_status already uses (sqlite3, pathlib, json, tomllib stdlib).
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
🧰 Additional context used📓 Path-based instructions (1)Gradata/tests/**/*.py📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Files:
🔇 Additional comments (1)
📝 Walkthrough
WalkthroughThe PR adds a new ChangesProjects CLI subcommand
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.21.0)OpenGrep fatal error (exit code 2): �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Gradata/src/gradata/cli.py`:
- Around line 347-351: Validate each item from raw_projects before treating it
as a dict: in the loop that iterates over raw_projects (the for proj in
raw_projects block where name = proj.get(...) and brain_dir = proj.get(...)),
check isinstance(proj, dict) and if not, raise or return a clear
malformed-registry error (or skip with a logged warning) indicating the project
entry has an unexpected type; ensure the error message names the offending entry
and/or index so users can fix their registry instead of allowing an
AttributeError to propagate.
- Around line 365-377: The code currently opens a SQLite connection with
_sqlite3.connect(...) and calls con.close() only in the try path, so if a query
raises an exception the connection is leaked; modify the block around con, cur,
rules_count and last_correction so the connection is always closed — either use
a context manager (with _sqlite3.connect(str(db_path)) as con:) or ensure
con.close() is called in a finally block and guard against con being undefined
if connect() fails; update the code that uses cur.execute(...) and the except
clause to preserve existing error handling while guaranteeing con.close() runs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 81335e1b-cd6c-49ee-b216-1aff7f2bb943
📒 Files selected for processing (2)
Gradata/src/gradata/cli.pyGradata/tests/test_projects_command.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: pytest windows-latest / py3.11
- GitHub Check: pytest ubuntu-latest / py3.12
- GitHub Check: pytest ubuntu-latest / py3.11
- GitHub Check: pytest windows-latest / py3.12
- GitHub Check: pytest (py3.12)
- GitHub Check: pytest (py3.11)
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/src/**/*.py
📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Gradata/src/**/*.py: Prefersentence-transformersfor local embeddings,google-genaifor Gemini embeddings,cryptographyfor AES-GCM encrypted system.db,bm25sfor BM25 rule ranking, andmem0aifor external memory adapters — guard all optional dependency imports withtry / except ImportErrorat the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bareexcept: pass— use typed exceptions or at minimumlogger.warning(...)withexc_info=Trueto avoid silent failure in a memory product
Never import from out-of-scope sibling directories../Sprites/or../Hausgem/withingradata/*code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to../Sprites/,../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from insidegradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes
Files:
Gradata/src/gradata/cli.py
Gradata/tests/**/*.py
📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Gradata/tests/**/*.py: SetBRAIN_DIRenvironment variable viatmp_pathin conftest.py for test isolation — ensure_paths.pymodule cache refreshes when callingBrain.init()directly inside tests
Add unit tests intests/test_*.pyfor every CI push without LLM calls (deterministic); mark integration tests with@pytest.mark.integrationand skip them by default (they hit real LLM APIs)
Files:
Gradata/tests/test_projects_command.py
🔇 Additional comments (2)
Gradata/src/gradata/cli.py (1)
159-159: LGTM!Also applies to: 896-909, 1980-1983, 2323-2323
Gradata/tests/test_projects_command.py (1)
1-179: LGTM!
| raw_projects = data.get("projects", []) or [] | ||
| rows = [] | ||
| for proj in raw_projects: | ||
| name = proj.get("name", "(unnamed)") | ||
| brain_dir = proj.get("brain_dir", "") |
There was a problem hiding this comment.
Validate registry entry shape before dereferencing project fields.
raw_projects items are assumed to be dicts. A schema-invalid but parseable TOML value (e.g. projects = ["x"]) crashes with AttributeError at Line 350 instead of returning a friendly malformed-registry error.
Proposed fix
raw_projects = data.get("projects", []) or []
rows = []
for proj in raw_projects:
+ if not isinstance(proj, dict):
+ print("Error: malformed projects.toml (each [[projects]] entry must be a table)")
+ raise SystemExit(1) from None
name = proj.get("name", "(unnamed)")
brain_dir = proj.get("brain_dir", "")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| raw_projects = data.get("projects", []) or [] | |
| rows = [] | |
| for proj in raw_projects: | |
| name = proj.get("name", "(unnamed)") | |
| brain_dir = proj.get("brain_dir", "") | |
| raw_projects = data.get("projects", []) or [] | |
| rows = [] | |
| for proj in raw_projects: | |
| if not isinstance(proj, dict): | |
| print("Error: malformed projects.toml (each [[projects]] entry must be a table)") | |
| raise SystemExit(1) from None | |
| name = proj.get("name", "(unnamed)") | |
| brain_dir = proj.get("brain_dir", "") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Gradata/src/gradata/cli.py` around lines 347 - 351, Validate each item from
raw_projects before treating it as a dict: in the loop that iterates over
raw_projects (the for proj in raw_projects block where name = proj.get(...) and
brain_dir = proj.get(...)), check isinstance(proj, dict) and if not, raise or
return a clear malformed-registry error (or skip with a logged warning)
indicating the project entry has an unexpected type; ensure the error message
names the offending entry and/or index so users can fix their registry instead
of allowing an AttributeError to propagate.
| try: | ||
| con = _sqlite3.connect(str(db_path)) | ||
| cur = con.cursor() | ||
| rules_count = cur.execute( | ||
| "SELECT COUNT(*) FROM events WHERE type='RULE_GRADUATED'" | ||
| ).fetchone()[0] | ||
| row = cur.execute( | ||
| "SELECT MAX(ts) FROM events WHERE type='CORRECTION'" | ||
| ).fetchone() | ||
| last_correction = row[0] if row else None | ||
| con.close() | ||
| except (_sqlite3.OperationalError, _sqlite3.DatabaseError, OSError): | ||
| sync_status = "error" |
There was a problem hiding this comment.
Close SQLite connections on query failures.
If any query fails after connect() succeeds, execution jumps to except and con.close() is skipped. Repeated errors across many projects can leak file descriptors/locks.
Proposed fix
else:
try:
- con = _sqlite3.connect(str(db_path))
- cur = con.cursor()
- rules_count = cur.execute(
- "SELECT COUNT(*) FROM events WHERE type='RULE_GRADUATED'"
- ).fetchone()[0]
- row = cur.execute(
- "SELECT MAX(ts) FROM events WHERE type='CORRECTION'"
- ).fetchone()
- last_correction = row[0] if row else None
- con.close()
+ with _sqlite3.connect(str(db_path)) as con:
+ cur = con.cursor()
+ rules_count = cur.execute(
+ "SELECT COUNT(*) FROM events WHERE type='RULE_GRADUATED'"
+ ).fetchone()[0]
+ row = cur.execute(
+ "SELECT MAX(ts) FROM events WHERE type='CORRECTION'"
+ ).fetchone()
+ last_correction = row[0] if row else None
except (_sqlite3.OperationalError, _sqlite3.DatabaseError, OSError):
sync_status = "error"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Gradata/src/gradata/cli.py` around lines 365 - 377, The code currently opens
a SQLite connection with _sqlite3.connect(...) and calls con.close() only in the
try path, so if a query raises an exception the connection is leaked; modify the
block around con, cur, rules_count and last_correction so the connection is
always closed — either use a context manager (with
_sqlite3.connect(str(db_path)) as con:) or ensure con.close() is called in a
finally block and guard against con being undefined if connect() fails; update
the code that uses cur.execute(...) and the except clause to preserve existing
error handling while guaranteeing con.close() runs.
Windows tmp_path uses backslashes which TOML interprets as escape sequences
('Invalid hex value'). Use as_posix() to write forward slashes (works on
Windows too) and compare brain_dir via Path normalization.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
Summary
Adds
gradata projects— a table listing every project the SDK knows about from~/.gradata/projects.toml. Columns: name, brain_dir, rules count, last_correction timestamp, sync_status. Supports--jsonfor machine consumption.Schema
Uses stdlib
tomllib(Python 3.11+) — no new dependency:Column semantics
COUNT(*) FROM events WHERE type='RULE_GRADUATED'MAX(ts) FROM events WHERE type='CORRECTION'or(never)okif brain_dir exists,missingif not,errorif db unreadableFailure modes
~/.gradata/projects.toml→ friendly hint, exit 0 (no crash)Error: malformed projects.toml (...)+ exit 1sync_status=missingTest plan
tests/test_projects_command.py— 9 tests, all passing:--json)[[projects]])SystemExit(1)--jsonoutput shapeRULE_GRADUATED+CORRECTIONrows → rules count and last_correction populatedLive smoke (no registry present):
Layering check
cmd_projectslives incli.py(Layer 2). Only stdlib imports introduced:tomllib,sqlite3,pathlib,json— all already used elsewhere incli.py. No Layer 0 → 2 violation.Risk
Backwards-compatible additive change. No schema migration. New subcommand only; no existing behaviour modified. Pre-existing ruff warnings on lines 618/892 are untouched and unrelated.
Closes GRA-1238.