feat(devpulse): compass v2 P2 — drone @devpulse compass command (DPLAN-0212)

Thin command layer over the P1 storage core: add/query/stats/rate/archive/review
via drone @devpulse compass. Ratings shown in query output ([GOOD]/[BAD]/...),
--db flag for testing, auto-discovered (no devpulse.py change). 18 cmd tests,
seedgo 29/29, no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-16 03:01:29 -07:00
co-authored by Claude Opus 4.8
parent 0df8fee947
commit 0d042dcb2f
2 changed files with 699 additions and 0 deletions
+422
View File
@@ -0,0 +1,422 @@
# =================== AIPass ====================
# Name: compass.py
# Description: Compass Module — drone command for devpulse's rated decision store
# Version: 1.0.0
# Created: 2026-06-16
# Modified: 2026-06-16
# =============================================
"""
Compass Module — command routing for devpulse's rated decision store.
Compass is the truth-store of decisions: short, *rated* choices
(``good | bad | impressive | interesting``) that devpulse consults at a fork.
The rating IS the signal — repeat the good, avoid the bad. See DPLAN-0212.
This module is the thin command layer (FPLAN P2). It parses args, calls the
``compass`` storage handler (FPLAN P1), and renders results to the console.
No business logic lives here — that's the handler's job.
Subcommands:
add "context" "decision" --rating R [--note ..] [--tags a,b] [--source ..]
query "question" [--rating R] [--limit N]
stats
rate <id> <rating>
archive <id>
review
Every subcommand accepts ``--db PATH`` (passed through as ``db_path=``) for
testing and power use; omitted, it uses the real store.
Auto-discovered by devpulse.py via the handle_command() convention.
"""
from typing import List, Optional
from aipass.prax import logger
from aipass.cli.apps.modules import err_console, error, warning
from aipass.devpulse.apps.handlers import compass
from aipass.devpulse.apps.handlers.json import json_handler
console = err_console
_VALID_SUBCOMMANDS = ("add", "query", "stats", "rate", "archive", "review")
# Console colour per rating — the rating is the signal, so make it pop.
_RATING_STYLE = {
"good": "bold green",
"bad": "bold red",
"impressive": "bold magenta",
"interesting": "bold yellow",
}
HELP_TEXT = """\
[bold cyan]compass[/bold cyan] — devpulse rated decision store
[bold]Usage:[/bold]
compass add "context" "decision" --rating R [opts] Store a rated decision
compass query "question" [--rating R] [--limit N] Search (rating shown)
compass stats Counts by rating/status
compass rate <id> <rating> Re-rate a decision
compass archive <id> Archive a decision
compass review Surface one to review
compass --help Show this help
[bold]Ratings:[/bold] good | bad | impressive | interesting
[bold]Sources:[/bold] devpulse | patrick
[bold]Options (add):[/bold]
--rating R Required. One of the ratings above.
--note "..." Optional human observation.
--tags a,b,c Optional comma-separated tags.
--source S Optional. devpulse (default) or patrick.
[bold]Options (all subcommands):[/bold]
--db PATH Use an alternate SQLite store (testing / power use).
[bold]Examples:[/bold]
drone @devpulse compass add "auth fork" "chose JWT over sessions" --rating good
drone @devpulse compass query "auth" --rating good --limit 3
drone @devpulse compass stats
drone @devpulse compass rate 4 bad
drone @devpulse compass archive 4
drone @devpulse compass review
See DPLAN-0212 (design) and the compass handler (apps/handlers/compass/).
"""
def print_introspection() -> None:
"""Display module introspection info."""
console.print()
console.print("[bold cyan]compass Module[/bold cyan]")
console.print("[dim]Devpulse rated decision store. The truth-store of choices —[/dim]")
console.print("[dim]each decision rated; the rating is the signal at a fork.[/dim]")
console.print()
console.print("[yellow]Subcommands:[/yellow] [cyan]add, query, stats, rate, archive, review[/cyan]")
console.print("[dim]Run 'compass --help' for full usage.[/dim]")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""Route compass subcommands to the storage handler.
Auto-discovered by devpulse.py module loader.
Args:
command: The primary command string.
args: Additional arguments after the command.
Returns:
True if the command was handled, False otherwise.
"""
if command != "compass":
return False
if not args:
print_introspection()
return True
if args[0] in ("--help", "-h", "help"):
console.print(HELP_TEXT)
return True
subcommand = args[0]
sub_args = args[1:]
if subcommand not in _VALID_SUBCOMMANDS:
error(f"Unknown compass subcommand: {subcommand}", suggestion="Use 'compass --help' for usage")
return True
logger.info("[compass] subcommand=%s args=%s", subcommand, sub_args)
json_handler.log_operation("compass_command", {"subcommand": subcommand})
if subcommand == "add":
return _handle_add(sub_args)
if subcommand == "query":
return _handle_query(sub_args)
if subcommand == "stats":
return _handle_stats(sub_args)
if subcommand == "rate":
return _handle_rate(sub_args)
if subcommand == "archive":
return _handle_archive(sub_args)
if subcommand == "review":
return _handle_review(sub_args)
return True
# =============================================================================
# ARG PARSING HELPERS
# =============================================================================
def _extract_flag(args: List[str], flag: str) -> tuple[List[str], Optional[str]]:
"""Pull a single ``--flag VALUE`` pair out of args.
Returns the remaining args (flag + value removed) and the value (or None
if the flag was absent). Raises ValueError if the flag is given without a
following value — errors must fail loud, never silent.
"""
value: Optional[str] = None
remaining: List[str] = []
i = 0
while i < len(args):
if args[i] == flag:
if i + 1 >= len(args):
raise ValueError(f"{flag} requires a value")
value = args[i + 1]
i += 2
continue
remaining.append(args[i])
i += 1
return remaining, value
def _extract_db_path(args: List[str]) -> tuple[List[str], Optional[str]]:
"""Pull the optional ``--db PATH`` flag out of args."""
return _extract_flag(args, "--db")
def _rating_tag(rating: str) -> str:
"""Render a coloured ``[RATING]`` tag for query/review output."""
style = _RATING_STYLE.get(rating, "bold white")
return f"[{style}]\\[{(rating or '?').upper()}][/{style}]"
# =============================================================================
# SUBCOMMAND HANDLERS
# =============================================================================
def _handle_add(sub_args: List[str]) -> bool:
"""Parse and dispatch ``compass add "context" "decision" --rating R [opts]``."""
try:
rest, db_path = _extract_db_path(sub_args)
rest, rating = _extract_flag(rest, "--rating")
rest, note = _extract_flag(rest, "--note")
rest, tags = _extract_flag(rest, "--tags")
rest, source = _extract_flag(rest, "--source")
except ValueError as exc:
logger.warning("[compass] add arg-parse error: %s", exc)
error(str(exc), suggestion="Use 'compass --help' for usage")
return True
if len(rest) < 2:
error('Usage: compass add "context" "decision" --rating R [--note ..] [--tags a,b] [--source ..]')
return True
if rating is None:
error("compass add requires --rating", suggestion="One of: good | bad | impressive | interesting")
return True
context = rest[0]
decision = rest[1]
try:
new_id = compass.add_decision(
context,
decision,
rating,
note=note,
tags=tags,
source=source if source is not None else "devpulse",
db_path=db_path,
)
except ValueError as exc:
logger.warning("[compass] add rejected: %s", exc)
error(str(exc))
return True
console.print(f"[green]Added decision[/green] {_rating_tag(rating)} [bold]#{new_id}[/bold]")
console.print(f" [cyan]context:[/cyan] {context}")
console.print(f" [cyan]decision:[/cyan] {decision}")
if note:
console.print(f" [cyan]note:[/cyan] {note}")
if tags:
console.print(f" [cyan]tags:[/cyan] {tags}")
return True
def _handle_query(sub_args: List[str]) -> bool:
"""Parse and dispatch ``compass query "question" [--rating R] [--limit N]``."""
try:
rest, db_path = _extract_db_path(sub_args)
rest, rating = _extract_flag(rest, "--rating")
rest, limit_raw = _extract_flag(rest, "--limit")
except ValueError as exc:
logger.warning("[compass] query arg-parse error: %s", exc)
error(str(exc), suggestion="Use 'compass --help' for usage")
return True
if not rest:
error('Usage: compass query "question" [--rating R] [--limit N]')
return True
query_text = rest[0]
limit = 5
if limit_raw is not None:
try:
limit = int(limit_raw)
except ValueError as exc:
logger.warning("[compass] query bad --limit %r: %s", limit_raw, exc)
error(f"--limit must be an integer, got {limit_raw!r}")
return True
try:
results = compass.query_decisions(query_text, rating=rating, limit=limit, db_path=db_path)
except ValueError as exc:
logger.warning("[compass] query rejected: %s", exc)
error(str(exc))
return True
_render_query_results(query_text, rating, results)
return True
def _render_query_results(query_text: str, rating: Optional[str], results: List[dict]) -> None:
"""Render query results — rating shown prominently, most relevant first."""
filt = f" [dim](rating={rating})[/dim]" if rating else ""
console.print(f"[bold]Compass[/bold] — {len(results)} result(s) for [cyan]{query_text!r}[/cyan]{filt}")
if not results:
console.print("[dim]No matching decisions.[/dim]")
return
console.print()
for r in results:
tag = _rating_tag(r.get("rating", "?"))
console.print(f"{tag} [bold]#{r.get('id', '?')}[/bold] [dim]{r.get('created', '?')}[/dim]")
console.print(f" [cyan]context:[/cyan] {r.get('context', '')}")
console.print(f" [cyan]decision:[/cyan] {r.get('decision', '')}")
if r.get("note"):
console.print(f" [cyan]note:[/cyan] {r['note']}")
if r.get("tags"):
console.print(f" [cyan]tags:[/cyan] {r['tags']}")
meta = f"source={r.get('source', '?')} status={r.get('status', '?')} surfaced={r.get('times_surfaced', 0)}"
console.print(f" [dim]{meta}[/dim]")
console.print()
def _handle_stats(sub_args: List[str]) -> bool:
"""Dispatch ``compass stats`` and render readable counts."""
try:
rest, db_path = _extract_db_path(sub_args)
except ValueError as exc:
logger.warning("[compass] stats arg-parse error: %s", exc)
error(str(exc))
return True
if rest:
error(f"compass stats takes no positional args, got: {' '.join(rest)}")
return True
data = compass.stats(db_path=db_path)
console.print("[bold]Compass Stats[/bold]")
console.print(f" Total decisions: [bold]{data.get('total', 0)}[/bold]")
console.print(" [yellow]By rating:[/yellow]")
for rating, count in (data.get("by_rating") or {}).items():
console.print(f" {_rating_tag(rating)} {count}")
console.print(" [yellow]By status:[/yellow]")
for status, count in (data.get("by_status") or {}).items():
console.print(f" [cyan]{status:<10}[/cyan] {count}")
return True
def _handle_rate(sub_args: List[str]) -> bool:
"""Dispatch ``compass rate <id> <rating>``."""
try:
rest, db_path = _extract_db_path(sub_args)
except ValueError as exc:
logger.warning("[compass] rate arg-parse error: %s", exc)
error(str(exc))
return True
if len(rest) < 2:
error("Usage: compass rate <id> <rating>")
return True
try:
decision_id = int(rest[0])
except ValueError as exc:
logger.warning("[compass] rate bad id %r: %s", rest[0], exc)
error(f"<id> must be an integer, got {rest[0]!r}")
return True
rating = rest[1]
try:
changed = compass.rate(decision_id, rating, db_path=db_path)
except ValueError as exc:
logger.warning("[compass] rate rejected: %s", exc)
error(str(exc))
return True
if changed:
console.print(f"[green]Re-rated[/green] [bold]#{decision_id}[/bold] -> {_rating_tag(rating)}")
else:
warning(f"No decision with id {decision_id} — nothing changed.")
return True
def _handle_archive(sub_args: List[str]) -> bool:
"""Dispatch ``compass archive <id>``."""
try:
rest, db_path = _extract_db_path(sub_args)
except ValueError as exc:
logger.warning("[compass] archive arg-parse error: %s", exc)
error(str(exc))
return True
if not rest:
error("Usage: compass archive <id>")
return True
try:
decision_id = int(rest[0])
except ValueError as exc:
logger.warning("[compass] archive bad id %r: %s", rest[0], exc)
error(f"<id> must be an integer, got {rest[0]!r}")
return True
changed = compass.archive(decision_id, db_path=db_path)
if changed:
console.print(
f"[green]Archived[/green] [bold]#{decision_id}[/bold] [dim](kept as avoid-list, not deleted)[/dim]"
)
else:
warning(f"No decision with id {decision_id} — nothing changed.")
return True
def _handle_review(sub_args: List[str]) -> bool:
"""Dispatch ``compass review`` — surface one active decision to review."""
try:
rest, db_path = _extract_db_path(sub_args)
except ValueError as exc:
logger.warning("[compass] review arg-parse error: %s", exc)
error(str(exc))
return True
if rest:
error(f"compass review takes no positional args, got: {' '.join(rest)}")
return True
result = compass.review(db_path=db_path)
if result is None:
console.print("[dim]No active decisions to review.[/dim]")
return True
tag = _rating_tag(result.get("rating", "?"))
console.print(f"[bold]Compass Review[/bold] {tag} [bold]#{result.get('id', '?')}[/bold]")
console.print(f" [cyan]context:[/cyan] {result.get('context', '')}")
console.print(f" [cyan]decision:[/cyan] {result.get('decision', '')}")
if result.get("note"):
console.print(f" [cyan]note:[/cyan] {result['note']}")
if result.get("tags"):
console.print(f" [cyan]tags:[/cyan] {result['tags']}")
console.print(
f" [dim]created={result.get('created', '?')} last_reviewed={result.get('last_reviewed', '?')} "
f"surfaced={result.get('times_surfaced', 0)}[/dim]"
)
console.print("[dim]Tip: re-rate with 'compass rate <id> <rating>' or retire with 'compass archive <id>'.[/dim]")
return True
@@ -0,0 +1,277 @@
# =================== AIPass ====================
# Name: test_compass_command.py
# Description: Tests for the compass module command router (FPLAN P2)
# Version: 1.0.0
# Created: 2026-06-16
# Modified: 2026-06-16
# =============================================
"""Tests for the compass command router (FPLAN-0212 P2).
These exercise the thin command layer (``apps/modules/compass.py``) end to
end against a real temp SQLite store via the ``--db`` flag — the same path the
live ``drone @devpulse compass`` invocation takes. Everything goes through the
module entry point (``handle_command``): the round-trip (add -> query -> see
rating) is driven and asserted entirely via the command's own console output,
so the storage handler is never reached into directly.
"""
import re
from pathlib import Path
import pytest
from aipass.devpulse.apps.modules import compass as compass_cmd
@pytest.fixture
def db(tmp_path: Path) -> str:
"""A temp DB path string, passed through to the command via --db."""
return str(tmp_path / "compass_cmd_test.db")
def _output(capsys) -> str:
"""Combined stdout+stderr (err_console / error() route to stderr)."""
captured = capsys.readouterr()
return captured.out + captured.err
def _add(capsys, db, context, decision, rating, *extra) -> int:
"""Drive an add through the command entry point; return the new id.
The id is parsed from the command's own ``#<id>`` confirmation line — we
never reach into the storage handler.
"""
capsys.readouterr() # isolate this add's output
compass_cmd.handle_command("compass", ["add", context, decision, "--rating", rating, "--db", db, *extra])
out = _output(capsys)
match = re.search(r"#(\d+)", out)
assert match, f"add did not report a new id; output was: {out!r}"
return int(match.group(1))
def _query_out(capsys, db, query, *extra) -> str:
"""Run a query via the command and return its captured output (drained)."""
capsys.readouterr() # drop anything pending so we only see this query
compass_cmd.handle_command("compass", ["query", query, "--db", db, *extra])
return _output(capsys)
def _stats_out(capsys, db) -> str:
"""Run stats via the command and return its captured output (drained)."""
capsys.readouterr()
compass_cmd.handle_command("compass", ["stats", "--db", db])
return _output(capsys)
# ---------------------------------------------------------------------------
# Routing basics
# ---------------------------------------------------------------------------
def test_rejects_unrelated_command():
"""Router returns False for commands that aren't 'compass'."""
assert compass_cmd.handle_command("watchdog", []) is False
def test_no_args_shows_introspection(capsys):
"""Bare 'compass' shows introspection that mentions compass + subcommands."""
assert compass_cmd.handle_command("compass", []) is True
out = _output(capsys).lower()
assert "compass" in out
assert "add" in out and "query" in out
def test_help_flag_shows_usage(capsys):
"""--help prints usage covering every subcommand."""
assert compass_cmd.handle_command("compass", ["--help"]) is True
out = _output(capsys).lower()
assert "usage" in out
for sub in ("add", "query", "stats", "rate", "archive", "review"):
assert sub in out
def test_unknown_subcommand_errors(capsys):
"""Unknown subcommand surfaces a clean error, still returns True."""
assert compass_cmd.handle_command("compass", ["bogus"]) is True
out = _output(capsys).lower()
assert "bogus" in out or "unknown" in out
# ---------------------------------------------------------------------------
# add -> query round-trip (rating must be visible)
# ---------------------------------------------------------------------------
def test_add_then_query_shows_rating(capsys, db):
"""add stores a decision; query surfaces it with the [GOOD] rating tag."""
assert (
compass_cmd.handle_command(
"compass",
["add", "auth fork", "chose JWT over sessions", "--rating", "good", "--note", "worked", "--db", db],
)
is True
)
add_out = _output(capsys)
assert "GOOD" in add_out # rating shown on add too
assert compass_cmd.handle_command("compass", ["query", "JWT", "--db", db]) is True
q_out = _output(capsys)
assert "GOOD" in q_out # the rating tag is the whole point
assert "chose JWT over sessions" in q_out
assert "auth fork" in q_out
def test_query_rating_filter(capsys, db):
"""--rating filters query results to the matching rating only."""
compass_cmd.handle_command("compass", ["add", "ctx good", "good choice here", "--rating", "good", "--db", db])
compass_cmd.handle_command("compass", ["add", "ctx bad", "bad choice here", "--rating", "bad", "--db", db])
capsys.readouterr() # drain add output
assert compass_cmd.handle_command("compass", ["query", "choice", "--rating", "bad", "--db", db]) is True
out = _output(capsys)
assert "bad choice here" in out
assert "good choice here" not in out
def test_add_persists_to_store(capsys, db):
"""add persists to the store; a later query surfaces it with its rating."""
_add(capsys, db, "persist ctx", "persist decision", "interesting")
out = _query_out(capsys, db, "persist")
assert "INTERESTING" in out
assert "persist decision" in out
assert "1 result(s)" in out
# ---------------------------------------------------------------------------
# stats
# ---------------------------------------------------------------------------
def test_stats_reports_counts(capsys, db):
"""stats shows total plus by-rating / by-status breakdown."""
compass_cmd.handle_command("compass", ["add", "c1", "d1", "--rating", "good", "--db", db])
compass_cmd.handle_command("compass", ["add", "c2", "d2", "--rating", "bad", "--db", db])
capsys.readouterr()
assert compass_cmd.handle_command("compass", ["stats", "--db", db]) is True
out = _output(capsys).lower()
assert "total" in out
assert "2" in out
assert "good" in out and "bad" in out
assert "active" in out
# ---------------------------------------------------------------------------
# rate
# ---------------------------------------------------------------------------
def test_rate_changes_rating(capsys, db):
"""rate <id> <rating> re-rates an existing decision."""
new_id = _add(capsys, db, "rate ctx", "rate decision", "good")
assert compass_cmd.handle_command("compass", ["rate", str(new_id), "bad", "--db", db]) is True
out = _output(capsys)
assert "BAD" in out
# Confirm the new rating sticks: querying the row now shows [BAD], not [GOOD].
q_out = _query_out(capsys, db, "rate")
assert "BAD" in q_out
assert "GOOD" not in q_out
def test_rate_missing_id_warns(capsys, db):
"""rate on a non-existent id reports 'nothing changed', does not crash."""
assert compass_cmd.handle_command("compass", ["rate", "999", "good", "--db", db]) is True
out = _output(capsys).lower()
assert "999" in out and ("nothing changed" in out or "no decision" in out)
# ---------------------------------------------------------------------------
# archive
# ---------------------------------------------------------------------------
def test_archive_removes_from_query(capsys, db):
"""archive flips status; archived rows drop out of query, stats reflect it."""
new_id = _add(capsys, db, "arch ctx", "arch decision", "good")
assert compass_cmd.handle_command("compass", ["archive", str(new_id), "--db", db]) is True
out = _output(capsys).lower()
assert "archived" in out
# Archived rows no longer surface in query...
q_out = _query_out(capsys, db, "arch")
assert "0 result(s)" in q_out
assert "arch decision" not in q_out
# ...but stats still count them under archived.
s_out = _stats_out(capsys, db).lower()
assert "archived" in s_out
# ---------------------------------------------------------------------------
# review
# ---------------------------------------------------------------------------
def test_review_surfaces_a_decision(capsys, db):
"""review surfaces an active decision with its rating shown."""
_add(capsys, db, "review ctx", "review decision", "impressive")
capsys.readouterr()
assert compass_cmd.handle_command("compass", ["review", "--db", db]) is True
out = _output(capsys)
assert "IMPRESSIVE" in out
assert "review decision" in out
def test_review_empty_store(capsys, db):
"""review on an empty store reports nothing to review (no crash)."""
assert compass_cmd.handle_command("compass", ["review", "--db", db]) is True
out = _output(capsys).lower()
assert "no active" in out or "nothing" in out
# ---------------------------------------------------------------------------
# Error surfacing — must fail loud, never silent
# ---------------------------------------------------------------------------
def test_bad_rating_error_surfaces(capsys, db):
"""add with an invalid rating surfaces the handler's ValueError message."""
assert compass_cmd.handle_command("compass", ["add", "ctx", "decision", "--rating", "terrible", "--db", db]) is True
out = _output(capsys).lower()
assert "rating" in out and "terrible" in out
# Nothing should have been stored — stats reports total 0.
assert "total decisions: 0" in _stats_out(capsys, db).lower()
def test_add_requires_rating(capsys, db):
"""add without --rating fails loud."""
assert compass_cmd.handle_command("compass", ["add", "ctx", "decision", "--db", db]) is True
out = _output(capsys).lower()
assert "rating" in out
def test_add_missing_positionals(capsys, db):
"""add with too few positional args shows usage, stores nothing."""
assert compass_cmd.handle_command("compass", ["add", "only-context", "--rating", "good", "--db", db]) is True
out = _output(capsys).lower()
assert "usage" in out
assert "total decisions: 0" in _stats_out(capsys, db).lower()
def test_query_bad_limit_errors(capsys, db):
"""query with a non-integer --limit fails loud."""
assert compass_cmd.handle_command("compass", ["query", "anything", "--limit", "abc", "--db", db]) is True
out = _output(capsys).lower()
assert "limit" in out
def test_flag_without_value_errors(capsys, db):
"""A flag given without a following value fails loud (no silent swallow)."""
assert compass_cmd.handle_command("compass", ["query", "x", "--rating"]) is True
out = _output(capsys).lower()
assert "rating" in out and "value" in out