diff --git a/AGENTS.md b/AGENTS.md index de2f484..067fdcc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,21 @@ This guide is for an LLM agent that has landed in this repo and needs to help a user set up or use doxa interactively. Be command-first and keep the user in the -loop for provider, model, and source choices. +loop for provider, model, and source choices. Surface capabilities proactively -- +users often don't know what's available. + +## Capabilities at a glance + +| Goal | Command | +| --- | --- | +| State | `doxa status` · smoke test `doxa demo` · diagnose `doxa doctor` | +| Start a base | `doxa init --lens-template ` -- browse `doxa lenses list` | +| Lenses | `doxa lenses list` / `show ` / `add --from ` | +| Ingest | `doxa ingest ` | +| Hard pages | `doxa ingest --via jina\|firecrawl\|brightdata\|claude\|codex\|hermes` `[--mode browser\|extract --prompt ...] [--yolo]` | +| Answer | `doxa query "" --answer` / `--json` / `--domain ` / `--search hybrid` | +| Honest | `doxa eval` · `doxa sources list\|remove ` · `doxa domains set <0-10>` | +| Agent skill | `doxa skill install --harness claude-code\|codex\|hermes\|openclaw` | ## Detect @@ -87,7 +101,25 @@ doxa init ./doxa.yaml --yes --provider codex-cli doxa init ./fireworks.yaml --yes --provider openai-compatible --model accounts/fireworks/models/ ``` -Help the user write a lens with three fields: +### Pick a lens (do this first -- don't make the user invent one) + +A lens is the question doxa asks of every source; it shapes every belief. doxa +ships an opinionated library -- show it and choose together rather than starting +from a blank lens: + +```bash +doxa lenses list # founder-strategy, investment-memo, research-literature, ... +doxa lenses show investment-memo # inspect one first +doxa init --lens-template founder-strategy # seed a base from it +doxa lenses add my-lens --from founder-strategy # fork + tailor it to the user +``` + +`doxa init` with no `--lens-template` prompts the user to pick from the same +library interactively. Built-ins: `durable-beliefs`, `founder-strategy`, +`investment-memo`, `technical-design`, `research-literature`, `policy-analysis`, +`personal-principles`, `customer-discovery`. + +If none fit, write a custom lens with three fields: - `name`: short identifier. - `description`: what kind of beliefs to extract. @@ -107,10 +139,11 @@ For pre-fetched text or copied notes, pipe stdin and attach metadata: printf '%s' "$SOURCE_TEXT" | doxa ingest - --title "Title" --author "Author" --url "https://source.example" ``` -Show the verbatim guarantee: +Show the verbatim guarantee, and diagnose setup if anything looks off: ```bash -doxa eval +doxa eval # every quote still verbatim, every belief still linked +doxa doctor # config, storage, provider, and semantic-index readiness ``` Query it (keyword is the zero-setup default): @@ -142,6 +175,14 @@ doxa ingest --via hermes --mode browser doxa ingest --via hermes --mode extract --prompt "title, author, date as JSON" ``` +Agent fetchers run safely by default. For unattended runs on sources the user +trusts, `--yolo` enables codex `--dangerously-bypass-approvals-and-sandbox`, +hermes `--yolo`, and the `command` shell for that ingest: + +```bash +doxa ingest --via codex --yolo +``` + ```bash doxa ingest https://target.example --via jina # free, good first try for bot-walled pages doxa ingest https://target.example --via brightdata # needs BrightData env vars diff --git a/doxa/_assets/skill/SKILL.md b/doxa/_assets/skill/SKILL.md index 98c35f0..353e92d 100644 --- a/doxa/_assets/skill/SKILL.md +++ b/doxa/_assets/skill/SKILL.md @@ -1,103 +1,116 @@ --- name: doxa -description: Use when answering from or growing a custom knowledge base where every belief must be anchored to exact source quotes. +description: Use when answering from or building a custom knowledge base where every claim must be anchored to an exact source quote. Covers grounded retrieval, choosing a lens, ingesting sources (incl. hard/bot-walled pages), and keeping the base honest. --- # doxa Skill -Use the installed `doxa` CLI as the source of truth for grounded beliefs in a -custom knowledge base. No quote, no claim. +doxa is a verbatim-grounded belief base: it mines sources into linked +(belief, exact-quote) records and answers only from them. The rule, always: +**No quote, no claim.** Use the installed `doxa` CLI as the source of truth, and +proactively offer the capabilities below -- users often don't know they exist. -## Check state first +## Capabilities at a glance -```bash -doxa status -``` - -`doxa status` reports the active config, data location, and belief/quote counts. -If it shows `beliefs: 0`, the base is empty -- there is nothing to ground on, so -offer to `doxa ingest` a source instead of answering. Use `doxa sources list` to -see what has been ingested and `doxa sources remove ` to undo a mistake. +| Goal | Command | +| --- | --- | +| See where things stand | `doxa status` (config, counts, provider, semantic on/off) | +| Answer, grounded | `doxa query "" --answer` (or `--json` for tools) | +| Start a base | `doxa init --lens-template ` -- browse with `doxa lenses list` | +| Ingest a source | `doxa ingest ` | +| Fetch a hard page | `doxa ingest --via jina\|firecrawl\|brightdata\|claude\|codex\|hermes` | +| Verify / diagnose | `doxa eval` (quotes still verbatim) · `doxa doctor` (setup) | +| Manage sources | `doxa sources list` · `doxa sources remove ` | +| Semantic search | `doxa index`, then `doxa query "" --search hybrid` | -## Answer from the belief base +Run any command with `-h` for full options. -Run: +## 1. Check state first ```bash -doxa query "" --search keyword +doxa status ``` -For a final user-facing response, prefer the local answer renderer: - -```bash -doxa query "" --search keyword --answer -``` +Reports the active config, data location, and belief/quote counts. If it shows +`beliefs: 0`, the base is empty -- there is nothing to ground on, so offer to +**start a base** (below) rather than answering. -Use domain focus only when it is clearly relevant to the user's question: +## 2. Answer from the belief base ```bash -doxa query "" --search keyword --domain technical -doxa query "" --search keyword --domains policy,finance +doxa query "" --answer # readable, grounded brief +doxa query "" --json # structured records for tooling +doxa query "" --domain technical --top 10 ``` -Keyword search is the zero-setup default. If the project has configured -semantic search, `--search hybrid` may be used; if hybrid reports that the -semantic leg is unavailable and falls back to keyword, continue with the -returned keyword-grounded results. +Keyword search is the zero-setup default and covers both belief and quote text. +If the project configured semantic search, `--search hybrid` fuses semantic + +keyword; if it reports the semantic leg is unavailable and falls back to keyword, +continue with the returned keyword results. -Read the returned beliefs and verbatim quotes as the only ground truth. Answer -from those returned records. Cite or include the exact quotes when they matter. +Read the returned beliefs and verbatim quotes as the **only** ground truth. Humanize only the surrounding prose. Never alter the bytes, punctuation, -capitalization, or whitespace inside a returned quote span, and never invent a -quote, attribution, source, or belief that was not returned by the CLI. +capitalization, or whitespace inside a returned quote, and never invent a quote, +attribution, source, or belief the CLI did not return. -## Grow the belief base +## 3. Start or grow a base -When the user wants to add a trusted source, run: +### Pick a lens first (don't make the user invent one) + +A lens is the question doxa asks of every source -- it shapes every belief +extracted. doxa ships an opinionated library; show it and pick together: ```bash -doxa ingest +doxa lenses list # founder-strategy, investment-memo, research-literature, ... +doxa lenses show investment-memo # see one before using it +doxa init --lens-template founder-strategy # seed a base from it (or `doxa init` to pick interactively) +doxa lenses add my-lens --from founder-strategy # fork + customize for this user ``` -`` may be a text file, PDF, article URL, or YouTube URL depending on the -installed doxa extras. Ingestion mines beliefs through the configured provider -and drops any quote that cannot be verified as a real substring of the source. - -If the source text has already been fetched by the harness, pipe it through -stdin and attach metadata: +### Ingest sources ```bash -printf '%s' "$SOURCE_TEXT" | doxa ingest - --title "Title" --url "https://source.example" +doxa ingest ``` -For bot-protected sources, prefer a harness BrightData MCP fetch followed by -`doxa ingest -`. Direct CLI use can route through BrightData with -`doxa ingest --via brightdata` when `BRIGHTDATA_API_TOKEN` and -`BRIGHTDATA_ZONE` are set. +Quotes that aren't a verbatim substring of the source are dropped automatically. +For a hard or bot-walled page, choose a fetcher with `--via` (jina is a good free +first try), or let a browsing agent fetch it: -## Domain preferences +```bash +doxa ingest --via jina # free clean markdown +doxa ingest --via hermes --mode browser # agent renders JS, then mines +doxa ingest --via codex --mode extract --prompt "name, price, date as JSON" +``` -To inspect domain weights: +If you (the harness) already fetched the text -- e.g. via a BrightData MCP -- +pipe it in, no fetcher needed: ```bash -doxa domains +printf '%s' "$SOURCE_TEXT" | doxa ingest - --title "Title" --url "https://source.example" ``` -To adjust them when the user asks: +`--yolo` runs agent fetchers unattended (codex bypass-approvals, hermes `--yolo`, +command shell) -- use only on sources the user trusts. + +## 4. Keep the base honest + +After ingesting, verify and report what happened: ```bash -doxa domains set technical 8 -doxa domains add finance 6 -doxa domains remove creative +doxa eval # every quote still verbatim, every belief still linked +doxa doctor # config, storage, provider, and semantic-index readiness +doxa sources list # what's ingested, with belief/quote counts +doxa sources remove # undo a bad source +doxa domains set technical 8 # bias retrieval toward a topic when the user asks ``` -Domains are stored as `domain:` tags on beliefs and quotes. Retrieval also -uses `preferences.domain_aliases` so legacy plain tags such as `founders`, -`token-economics`, `trust`, or `taste` can boost and help discover the requested -domain. Domain boosts do not replace quote grounding. +Domains are stored as `domain:` tags; retrieval also matches legacy plain +tags via `preferences.domain_aliases`. Domain boosts never replace quote grounding. ## Grounding rule -A doxa quote is verbatim source text. A doxa belief is usable only when linked -to at least one returned quote. If the CLI does not return enough evidence, -state that the belief base does not contain enough grounded evidence. +A doxa quote is verbatim source text. A doxa belief is usable only when linked to +at least one returned quote. If the CLI does not return enough evidence, say the +belief base does not contain enough grounded evidence, and offer to ingest more +trusted sources -- never fill the gap from your own memory. diff --git a/doxa/cli.py b/doxa/cli.py index 940959a..c3a17a9 100644 --- a/doxa/cli.py +++ b/doxa/cli.py @@ -188,6 +188,39 @@ def _prompt_provider(default: str) -> str: return _normalize_provider(raw) +def _lens_choice(raw: str, names: list[str]) -> str | None: + """Map a picker input (number, name, or blank/'custom') to a template name, or None for custom.""" + raw = raw.strip() + if not raw or raw.lower() in {"custom", "c"}: + return None + if raw.isdigit(): + index = int(raw) - 1 + return names[index] if 0 <= index < len(names) else None + return raw if raw in names else None + + +def _prompt_lens() -> dict[str, Any]: + """Interactive lens picker. Returns a chosen template dict, or {} for a custom lens.""" + catalog = lens_catalog() + if not catalog: + return {} + names = [row["name"] for row in catalog] + width = max(len(name) for name in names) + print("\nPick a lens -- the question doxa asks of every source (this shapes every belief):") + for index, row in enumerate(catalog, start=1): + suffix = " (yours)" if row["origin"] == "user" else "" + print(f" {index}. {row['name'].ljust(width)} {row['summary']}{suffix}") + print(f" {len(catalog) + 1}. {'custom'.ljust(width)} define your own from scratch") + default_name = "durable-beliefs" if "durable-beliefs" in names else names[0] + name = _lens_choice(_prompt_text("Lens number or name", default_name), names) + if name is None: + return {} + try: + return get_lens_template(name) + except DoxaError: + return {} + + def _init_answers(args: argparse.Namespace, *, interactive: bool) -> dict[str, str]: provider_default = _normalize_provider(args.provider) provider = _prompt_provider(provider_default) if interactive else provider_default @@ -211,6 +244,8 @@ def _init_answers(args: argparse.Namespace, *, interactive: bool) -> dict[str, s base_url = _prompt_text("OpenAI-compatible base_url", base_url) template = get_lens_template(args.lens_template) if getattr(args, "lens_template", None) else {} + if interactive and not template: + template = _prompt_lens() # let the user pick from the library instead of inventing one lens_name_default = args.lens_name or template.get("name") or DEFAULT_LENS["name"] lens_description_default = args.lens or template.get("description") or DEFAULT_LENS["description"] lens_question_default = args.lens_question or template.get("question") or DEFAULT_LENS["question"] @@ -218,10 +253,9 @@ def _init_answers(args: argparse.Namespace, *, interactive: bool) -> dict[str, s lens_tags = template.get("tags") or [] if interactive: if template: - print(f"Starting from the '{args.lens_template}' lens template (edit any field below).") + print(f"\nUsing the '{template.get('name')}' lens -- press Enter to keep each field, or edit it:") else: - print("Now define the lens: what kind of beliefs should doxa mine?") - print(" (tip: start from a template -- see `doxa lenses list`.)") + print("\nDefine a custom lens: what kind of beliefs should doxa mine?") lens_name = _prompt_text("Lens name", lens_name_default) lens_description = _prompt_text("Lens description", lens_description_default) lens_question = _prompt_text("Guiding question", lens_question_default) diff --git a/doxa/guide.py b/doxa/guide.py index a64b63b..9f35b12 100644 --- a/doxa/guide.py +++ b/doxa/guide.py @@ -41,6 +41,13 @@ def d(text: str) -> str: h("Driving doxa from an AI agent? (Claude Code, Codex, Hermes ...)"), " doxa skill install --harness claude-code " + d("then just ask the agent"), "", + h("There's more than the basics -- a quick tour:"), + " doxa lenses list " + d("opinionated lenses: founder, investment, research ..."), + " doxa ingest --via jina " + d("fetch hard pages (jina/firecrawl/brightdata/agent)"), + " doxa query --answer | --json | --search hybrid " + d("brief | machine | semantic"), + " doxa eval | doxa doctor " + d("verify every quote | check your setup"), + " " + d("full tour: doxa guide"), + "", h("More"), " doxa guide " + d("guided walkthrough") + " doxa status " + d("where things stand"), " doxa --help " + d("every command") + " doxa -h " + d("help for one command"), @@ -116,6 +123,34 @@ def d(text: str) -> str: " doxa.yaml -- readable, diffable, yours. Run " + t("doxa status") + " to see counts.", "", "", + h("THE FULL TOOLBOX"), + "", + " " + t("Lenses") + " -- you don't have to invent one; start from the library:", + " doxa lenses list " + d("8 opinionated lenses + your own"), + " doxa lenses show investment-memo " + d("see one before you use it"), + " doxa init --lens-template founder-strategy", + " doxa lenses add my-lens --from founder-strategy " + d("fork + make it yours"), + "", + " " + t("Ingest anything") + " -- the URL fetcher is pluggable (--via):", + " doxa ingest --via jina " + d("free clean markdown (firecrawl/brightdata too)"), + " doxa ingest --via hermes --mode browser " + d("agent renders JS, then mines"), + " doxa ingest --via codex --mode extract --prompt \"...\"", + " doxa ingest --via codex --yolo " + d("unattended agent fetch (trusted sources)"), + " doxa ingest f.pdf | \"https://youtu.be/...\" | - " + d("PDFs, video transcripts, stdin"), + "", + " " + t("Ask, your way:"), + " doxa query \"...\" --answer " + d("readable evidence brief"), + " doxa query \"...\" --json " + d("structured, for tools/agents"), + " doxa query \"...\" --domain founders --top 10", + " doxa query \"...\" --search hybrid " + d("semantic + keyword (after doxa index)"), + "", + " " + t("Keep it honest:"), + " doxa eval " + d("every quote still verbatim, every belief still linked"), + " doxa doctor " + d("config, storage, provider, semantic readiness"), + " doxa sources list | remove " + d("see / undo what you ingested"), + " doxa domains set founders 8 " + d("bias retrieval toward a topic"), + "", + "", h("FOR AI AGENTS"), "", " doxa is built to be driven by an agent. Install the skill and your agent", diff --git a/doxa/lens.py b/doxa/lens.py index 37aca9e..6f35327 100644 --- a/doxa/lens.py +++ b/doxa/lens.py @@ -76,9 +76,15 @@ def build_extraction_prompt(config: dict[str, Any], source_meta: dict[str, str], {source_block} Rules: +- Extract EVERY distinct belief this lens surfaces in the source text. There is no + target number -- never cap the list to a round count and never pad it. Scale to the + source: a rich passage yields many beliefs, a thin one few. Mine the whole chunk and + omit nothing that fits the lens; do not stop early to keep the output short. +- Likewise mine every verbatim quote that grounds a belief -- as many as the text supports. - Every quote.quote must be copied verbatim from Source text. - Each belief must be linked by at least one quote.belief_ids entry. -- Prefer concise beliefs that state a stance, not a summary paragraph. +- Prefer concise beliefs that each state one stance, not a summary paragraph -- but split + a passage into multiple beliefs rather than dropping any; one belief per distinct claim. - Use domain: tags only when the source text clearly supports that domain classification. - conviction is a number from 0 to 1 based only on how directly the quote supports the belief. - Return strict JSON with top-level keys "beliefs" and "quotes". diff --git a/skill/SKILL.md b/skill/SKILL.md index 98c35f0..353e92d 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,103 +1,116 @@ --- name: doxa -description: Use when answering from or growing a custom knowledge base where every belief must be anchored to exact source quotes. +description: Use when answering from or building a custom knowledge base where every claim must be anchored to an exact source quote. Covers grounded retrieval, choosing a lens, ingesting sources (incl. hard/bot-walled pages), and keeping the base honest. --- # doxa Skill -Use the installed `doxa` CLI as the source of truth for grounded beliefs in a -custom knowledge base. No quote, no claim. +doxa is a verbatim-grounded belief base: it mines sources into linked +(belief, exact-quote) records and answers only from them. The rule, always: +**No quote, no claim.** Use the installed `doxa` CLI as the source of truth, and +proactively offer the capabilities below -- users often don't know they exist. -## Check state first +## Capabilities at a glance -```bash -doxa status -``` - -`doxa status` reports the active config, data location, and belief/quote counts. -If it shows `beliefs: 0`, the base is empty -- there is nothing to ground on, so -offer to `doxa ingest` a source instead of answering. Use `doxa sources list` to -see what has been ingested and `doxa sources remove ` to undo a mistake. +| Goal | Command | +| --- | --- | +| See where things stand | `doxa status` (config, counts, provider, semantic on/off) | +| Answer, grounded | `doxa query "" --answer` (or `--json` for tools) | +| Start a base | `doxa init --lens-template ` -- browse with `doxa lenses list` | +| Ingest a source | `doxa ingest ` | +| Fetch a hard page | `doxa ingest --via jina\|firecrawl\|brightdata\|claude\|codex\|hermes` | +| Verify / diagnose | `doxa eval` (quotes still verbatim) · `doxa doctor` (setup) | +| Manage sources | `doxa sources list` · `doxa sources remove ` | +| Semantic search | `doxa index`, then `doxa query "" --search hybrid` | -## Answer from the belief base +Run any command with `-h` for full options. -Run: +## 1. Check state first ```bash -doxa query "" --search keyword +doxa status ``` -For a final user-facing response, prefer the local answer renderer: - -```bash -doxa query "" --search keyword --answer -``` +Reports the active config, data location, and belief/quote counts. If it shows +`beliefs: 0`, the base is empty -- there is nothing to ground on, so offer to +**start a base** (below) rather than answering. -Use domain focus only when it is clearly relevant to the user's question: +## 2. Answer from the belief base ```bash -doxa query "" --search keyword --domain technical -doxa query "" --search keyword --domains policy,finance +doxa query "" --answer # readable, grounded brief +doxa query "" --json # structured records for tooling +doxa query "" --domain technical --top 10 ``` -Keyword search is the zero-setup default. If the project has configured -semantic search, `--search hybrid` may be used; if hybrid reports that the -semantic leg is unavailable and falls back to keyword, continue with the -returned keyword-grounded results. +Keyword search is the zero-setup default and covers both belief and quote text. +If the project configured semantic search, `--search hybrid` fuses semantic + +keyword; if it reports the semantic leg is unavailable and falls back to keyword, +continue with the returned keyword results. -Read the returned beliefs and verbatim quotes as the only ground truth. Answer -from those returned records. Cite or include the exact quotes when they matter. +Read the returned beliefs and verbatim quotes as the **only** ground truth. Humanize only the surrounding prose. Never alter the bytes, punctuation, -capitalization, or whitespace inside a returned quote span, and never invent a -quote, attribution, source, or belief that was not returned by the CLI. +capitalization, or whitespace inside a returned quote, and never invent a quote, +attribution, source, or belief the CLI did not return. -## Grow the belief base +## 3. Start or grow a base -When the user wants to add a trusted source, run: +### Pick a lens first (don't make the user invent one) + +A lens is the question doxa asks of every source -- it shapes every belief +extracted. doxa ships an opinionated library; show it and pick together: ```bash -doxa ingest +doxa lenses list # founder-strategy, investment-memo, research-literature, ... +doxa lenses show investment-memo # see one before using it +doxa init --lens-template founder-strategy # seed a base from it (or `doxa init` to pick interactively) +doxa lenses add my-lens --from founder-strategy # fork + customize for this user ``` -`` may be a text file, PDF, article URL, or YouTube URL depending on the -installed doxa extras. Ingestion mines beliefs through the configured provider -and drops any quote that cannot be verified as a real substring of the source. - -If the source text has already been fetched by the harness, pipe it through -stdin and attach metadata: +### Ingest sources ```bash -printf '%s' "$SOURCE_TEXT" | doxa ingest - --title "Title" --url "https://source.example" +doxa ingest ``` -For bot-protected sources, prefer a harness BrightData MCP fetch followed by -`doxa ingest -`. Direct CLI use can route through BrightData with -`doxa ingest --via brightdata` when `BRIGHTDATA_API_TOKEN` and -`BRIGHTDATA_ZONE` are set. +Quotes that aren't a verbatim substring of the source are dropped automatically. +For a hard or bot-walled page, choose a fetcher with `--via` (jina is a good free +first try), or let a browsing agent fetch it: -## Domain preferences +```bash +doxa ingest --via jina # free clean markdown +doxa ingest --via hermes --mode browser # agent renders JS, then mines +doxa ingest --via codex --mode extract --prompt "name, price, date as JSON" +``` -To inspect domain weights: +If you (the harness) already fetched the text -- e.g. via a BrightData MCP -- +pipe it in, no fetcher needed: ```bash -doxa domains +printf '%s' "$SOURCE_TEXT" | doxa ingest - --title "Title" --url "https://source.example" ``` -To adjust them when the user asks: +`--yolo` runs agent fetchers unattended (codex bypass-approvals, hermes `--yolo`, +command shell) -- use only on sources the user trusts. + +## 4. Keep the base honest + +After ingesting, verify and report what happened: ```bash -doxa domains set technical 8 -doxa domains add finance 6 -doxa domains remove creative +doxa eval # every quote still verbatim, every belief still linked +doxa doctor # config, storage, provider, and semantic-index readiness +doxa sources list # what's ingested, with belief/quote counts +doxa sources remove # undo a bad source +doxa domains set technical 8 # bias retrieval toward a topic when the user asks ``` -Domains are stored as `domain:` tags on beliefs and quotes. Retrieval also -uses `preferences.domain_aliases` so legacy plain tags such as `founders`, -`token-economics`, `trust`, or `taste` can boost and help discover the requested -domain. Domain boosts do not replace quote grounding. +Domains are stored as `domain:` tags; retrieval also matches legacy plain +tags via `preferences.domain_aliases`. Domain boosts never replace quote grounding. ## Grounding rule -A doxa quote is verbatim source text. A doxa belief is usable only when linked -to at least one returned quote. If the CLI does not return enough evidence, -state that the belief base does not contain enough grounded evidence. +A doxa quote is verbatim source text. A doxa belief is usable only when linked to +at least one returned quote. If the CLI does not return enough evidence, say the +belief base does not contain enough grounded evidence, and offer to ingest more +trusted sources -- never fill the gap from your own memory. diff --git a/tests/test_lenses.py b/tests/test_lenses.py index 8790972..32de411 100644 --- a/tests/test_lenses.py +++ b/tests/test_lenses.py @@ -103,3 +103,44 @@ def test_lenses_add_fork_remove_roundtrip(tmp_path, monkeypatch) -> None: def test_lenses_add_requires_description(tmp_path, monkeypatch) -> None: monkeypatch.setenv("DOXA_LENS_DIR", str(tmp_path)) assert main(["lenses", "add", "empty"]) != 0 # no --from/--file/--description + + +def test_lens_choice_resolution() -> None: + from doxa.cli import _lens_choice + + names = ["durable-beliefs", "founder-strategy", "investment-memo"] + assert _lens_choice("2", names) == "founder-strategy" + assert _lens_choice("founder-strategy", names) == "founder-strategy" + assert _lens_choice("", names) is None # blank -> custom + assert _lens_choice("custom", names) is None + assert _lens_choice("99", names) is None # out of range -> custom + assert _lens_choice("nope", names) is None # unknown name -> custom + + +def test_prompt_lens_returns_chosen_template(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("DOXA_LENS_DIR", str(tmp_path)) # built-ins only + import doxa.cli as cli + + monkeypatch.setattr(cli, "_prompt_text", lambda *a, **k: "founder-strategy") + assert cli._prompt_lens()["name"] == "founder-strategy" + monkeypatch.setattr(cli, "_prompt_text", lambda *a, **k: "custom") + assert cli._prompt_lens() == {} + + +def test_interactive_init_uses_picked_lens(tmp_path, monkeypatch) -> None: + import argparse + + import doxa.cli as cli + + monkeypatch.setenv("DOXA_LENS_DIR", str(tmp_path / "lenses")) + # provider, model, lens-pick, then Enter x3 for name/description/question + seq = iter(["codex-cli", "", "investment-memo", "", "", ""]) + # mirror real _prompt_text: empty input falls back to the shown default + monkeypatch.setattr(cli, "_prompt_text", lambda label, default="", *a, **k: (next(seq) or default)) + ns = argparse.Namespace(provider="codex-cli", model=None, api_key_env=None, base_url=None, + lens_template=None, lens_name=None, lens=None, lens_question=None) + answers = cli._init_answers(ns, interactive=True) + template = get_lens_template("investment-memo") + assert answers["lens_name"] == "investment-memo" + assert answers["lens_stances"] == template["stances"] + assert answers["lens_description"] == template["description"] diff --git a/tests/test_mine_prompt.py b/tests/test_mine_prompt.py new file mode 100644 index 0000000..2d1db97 --- /dev/null +++ b/tests/test_mine_prompt.py @@ -0,0 +1,14 @@ +"""The miner extracts everything the lens surfaces -- no assumed belief/quote count.""" +from __future__ import annotations + +from doxa.lens import build_extraction_prompt + + +def test_extraction_prompt_mines_exhaustively_without_count_cap() -> None: + config = {"lens": {"name": "t", "description": "d", "question": "q?", "stances": ["supports"], "tags": []}} + _system, user = build_extraction_prompt(config, {"title": "T"}, "some source text") + low = user.lower() + assert "every distinct belief" in low + assert "target number" in low + assert "omit nothing" in low + assert "never cap" in low diff --git a/tests/test_onboarding.py b/tests/test_onboarding.py new file mode 100644 index 0000000..f48f49d --- /dev/null +++ b/tests/test_onboarding.py @@ -0,0 +1,23 @@ +"""Discoverability: the landing, the guide, and the agent skill surface the toolbox.""" +from __future__ import annotations + +from doxa.guide import guide_text, overview_text +from doxa.resources import skill_text + + +def test_overview_points_beyond_the_basics() -> None: + out = overview_text() + assert "lenses list" in out + assert "doxa guide" in out + + +def test_guide_surfaces_the_full_toolbox() -> None: + g = guide_text() + for keyword in ["lenses list", "--lens-template", "--via", "--search hybrid", "doxa eval", "doxa doctor"]: + assert keyword in g, keyword + + +def test_skill_teaches_the_full_surface_to_agents() -> None: + s = skill_text().lower() + for keyword in ["lenses list", "--lens-template", "doxa eval", "doxa doctor", "--via", "no quote, no claim"]: + assert keyword in s, keyword