From 39728667a720a30ad81c145aea9938685b9a5811 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 2 Jun 2026 09:57:02 +0100 Subject: [PATCH] feat: add read_discovery_answers, append_followup_answer, and ask-more command - Add read_discovery_answers() to parse discovery-log.md rows into dicts with low_confidence and needs_followup boolean flags. - Add append_followup_answer() to append follow-up rows linked to original Q-A IDs. - Add 'ask-more' CLI command that detects Low-confidence and Follow-up-needed answers, prompts for additional details, and records them in the log. - Add 6 tests: read_discovery_answers round-trip, empty-log handling, low-confidence detection, follow-up flag detection, and append_followup. --- src/rdb_discovery/cli.py | 45 ++++++++++++++++- src/rdb_discovery/discovery.py | 68 +++++++++++++++++++++++++ tests/test_discovery.py | 90 ++++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) diff --git a/src/rdb_discovery/cli.py b/src/rdb_discovery/cli.py index cd0eb47..b0f1ede 100644 --- a/src/rdb_discovery/cli.py +++ b/src/rdb_discovery/cli.py @@ -7,7 +7,7 @@ import typer from rich.console import Console from rich.table import Table -from .discovery import append_discovery_answer, core_questions +from .discovery import append_discovery_answer, append_followup_answer, core_questions, read_discovery_answers from .handoff import build_handoff from .status import project_stage, task_counts from .tasks import generate_agent_prompt, get_next_task, update_task_status @@ -71,6 +71,49 @@ def discover() -> None: console.print("[bold green]Discovery complete.[/bold green]") +@app.command() +def ask_more() -> None: + """Ask for additional details on Low-confidence or follow-up-needed answers.""" + root = root_path() + answers = read_discovery_answers(root) + + if not answers: + console.print("[yellow]No discovery answers found. Run `discover` first.[/yellow]") + raise typer.Exit(code=0) + + low_conf = [a for a in answers if a["low_confidence"]] + follow_ups = [a for a in answers if a["needs_followup"]] + + flagged: dict[str, list[str]] = {} # question_id -> list of reasons + for a in low_conf: + flagged.setdefault(a["id"], []).append("Low confidence") + for a in follow_ups: + flagged.setdefault(a["id"], []).append("Follow-up needed") + + if not flagged: + console.print("[green]No Low-confidence or Follow-up-needed answers found.[/green]") + raise typer.Exit(code=0) + + count = 0 + for qid, reasons in sorted(flagged.items()): + original = next(a for a in answers if a["id"] == qid) + console.print(f"\n[yellow]Question:[/yellow] {original['question']}") + console.print(f"[dim]Reasons: {', '.join(reasons)}[/dim]") + + fu_question = typer.prompt( + f"Follow-up for {qid}", + default=f"Additional detail on: {original['question'][:40]}", + ) + fu_answer = typer.prompt("New answer") + fu_confidence = typer.prompt("Confidence?", default="Medium") + + append_followup_answer(root, qid, fu_question, fu_answer, fu_confidence) + count += 1 + console.print(f"[green]Recorded follow-up for {qid}.[/green]") + + console.print(f"\n[bold green]Ask-more complete: recorded {count} follow-up(s).[/bold green]") + + @app.command() def status() -> None: """Show current project stage and task counts.""" diff --git a/src/rdb_discovery/discovery.py b/src/rdb_discovery/discovery.py index 3ead654..44f6f66 100644 --- a/src/rdb_discovery/discovery.py +++ b/src/rdb_discovery/discovery.py @@ -36,3 +36,71 @@ def append_discovery_answer( def core_questions() -> list[str]: return CORE_QUESTIONS + + +def read_discovery_answers(root: Path) -> list[dict]: + """Read discovery-log.md and return parsed rows as dicts. + + Each dict has keys: id, question, answer, confidence, follow_up_needed. + Rows with 'Low' confidence or follow_up_needed == 'Yes' are flagged + via ``needs_followup`` and ``low_confidence`` boolean fields. + """ + log_path = root / "context" / "discovery-log.md" + if not log_path.exists(): + return [] + + content = log_path.read_text(encoding="utf-8") + lines = content.splitlines() + + answers: list[dict] = [] + for line in lines: + # Skip header and non-data rows + if not line.startswith("| Q-"): + continue + + cells = [c.strip() for c in line.split("|")[1:-1]] + if len(cells) < 5: + continue + + answers.append({ + "id": cells[0], + "question": cells[1], + "answer": cells[2].replace("\\|", "|"), + "confidence": cells[3], + "follow_up_needed": cells[4], + "needs_followup": cells[4] == "Yes", + "low_confidence": cells[3] in ("Low", "low"), + }) + + return answers + + +def append_followup_answer( + root: Path, + question_id: str, + follow_up_question: str, + answer: str, + confidence: str = "Medium", +) -> None: + """Append a follow-up answer row to discovery-log.md. + + The original ``question_id`` is stored in the 'Linked decision' column. + """ + log_path = root / "context" / "discovery-log.md" + log_path.parent.mkdir(parents=True, exist_ok=True) + + if not log_path.exists(): + log_path.write_text( + "# Discovery Log\n\n" + "| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n" + "|---|---|---|---|---|---|---|---|\n", + encoding="utf-8", + ) + + safe_answer = answer.replace("|", "\\|").replace("\n", " ").strip() + line = ( + f"| {question_id} | {follow_up_question} | {safe_answer} | {confidence} | " + f"Yes | | | {date.today().isoformat()} |\n" + ) + with log_path.open("a", encoding="utf-8") as handle: + handle.write(line) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 3cd2d96..c836845 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -105,6 +105,96 @@ def test_append_preserves_existing_history(tmp_path: Path) -> None: assert len(data_rows) == 2 +def test_read_discovery_answers_returns_dicts(tmp_path: Path) -> None: + discovery_mod.append_discovery_answer( + tmp_path, + question_id="Q-001", + question="What is this?", + answer="A thing", + confidence="High", + follow_up_needed="No", + ) + + answers = discovery_mod.read_discovery_answers(tmp_path) + assert len(answers) == 1 + assert answers[0]["id"] == "Q-001" + assert answers[0]["question"] == "What is this?" + assert answers[0]["answer"] == "A thing" + assert answers[0]["confidence"] == "High" + assert answers[0]["follow_up_needed"] == "No" + assert answers[0]["needs_followup"] is False + assert answers[0]["low_confidence"] is False + + +def test_read_discovery_answers_returns_empty_when_no_log(tmp_path: Path) -> None: + log_path = tmp_path / "context" / "discovery-log.md" + assert not log_path.exists() + + answers = discovery_mod.read_discovery_answers(tmp_path) + assert answers == [] + + +def test_read_discovery_answers_detects_low_confidence(tmp_path: Path) -> None: + discovery_mod.append_discovery_answer( + tmp_path, + question_id="Q-002", + question="How much?", + answer="About half", + confidence="Low", + follow_up_needed="No", + ) + + answers = discovery_mod.read_discovery_answers(tmp_path) + low_conf_rows = [a for a in answers if a["low_confidence"]] + assert len(low_conf_rows) == 1 + assert low_conf_rows[0]["id"] == "Q-002" + + +def test_read_discovery_answers_detects_followup_flag(tmp_path: Path) -> None: + discovery_mod.append_discovery_answer( + tmp_path, + question_id="Q-003", + question="What next?", + answer="TBD", + confidence="High", + follow_up_needed="Yes", + ) + + answers = discovery_mod.read_discovery_answers(tmp_path) + fu_rows = [a for a in answers if a["needs_followup"]] + assert len(fu_rows) == 1 + assert fu_rows[0]["id"] == "Q-003" + + +def test_append_followup_appends_row(tmp_path: Path) -> None: + # First create the log with an existing entry + discovery_mod.append_discovery_answer( + tmp_path, + question_id="Q-010", + question="Original?", + answer="Original answer", + confidence="High", + follow_up_needed="No", + ) + + original_count = len(discovery_mod.read_discovery_answers(tmp_path)) + + discovery_mod.append_followup_answer( + tmp_path, + question_id="Q-010", + follow_up_question="Can you elaborate?", + answer="Yes, it's bigger than expected.", + confidence="Medium", + ) + + answers = discovery_mod.read_discovery_answers(tmp_path) + assert len(answers) == original_count + 1 + # The new row should have the original Q-010 id and follow-up data + new_row = [a for a in answers if a["question"] == "Can you elaborate?"][0] + assert new_row["follow_up_needed"] == "Yes" + assert new_row["confidence"] == "Medium" + + def test_header_is_written_on_create(tmp_path: Path) -> None: discovery_mod.append_discovery_answer( tmp_path,